From 1d131bf0aad4920d7edb1f749404c832028765e8 Mon Sep 17 00:00:00 2001 From: Dominik Toton Date: Tue, 28 May 2024 14:53:48 +0200 Subject: [PATCH] feat: add initial error handling --- .../catalyst_cardano_platform_interface.dart | 1 + .../lib/src/cardano_wallet.dart | 2 +- .../src/interop/catalyst_cardano_interop.dart | 34 ++- .../catalyst_cardano_wallet_proxy.dart | 283 +++++++++++++----- 4 files changed, 246 insertions(+), 74 deletions(-) diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_platform_interface/lib/catalyst_cardano_platform_interface.dart b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_platform_interface/lib/catalyst_cardano_platform_interface.dart index aab93d42e18..9c08dbe7a5b 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_platform_interface/lib/catalyst_cardano_platform_interface.dart +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_platform_interface/lib/catalyst_cardano_platform_interface.dart @@ -1,2 +1,3 @@ export 'src/cardano_wallet.dart'; export 'src/catalyst_cardano_platform.dart'; +export 'src/exceptions.dart'; diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_platform_interface/lib/src/cardano_wallet.dart b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_platform_interface/lib/src/cardano_wallet.dart index f13828bcb2d..e56cfa81050 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_platform_interface/lib/src/cardano_wallet.dart +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_platform_interface/lib/src/cardano_wallet.dart @@ -52,7 +52,7 @@ abstract interface class CardanoWallet { /// transparent to web pages using this API. If a wallet is already connected /// this function should not request access a second time, and instead just /// return the API object. - Future enable(); + Future enable({List? extensions}); } /// The full API of enabled wallet extension. diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_web/lib/src/interop/catalyst_cardano_interop.dart b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_web/lib/src/interop/catalyst_cardano_interop.dart index 54fe2c1d931..a388ef72ff7 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_web/lib/src/interop/catalyst_cardano_interop.dart +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_web/lib/src/interop/catalyst_cardano_interop.dart @@ -29,7 +29,9 @@ extension type JSCardanoWallet(JSObject _) implements JSObject { external JSPromise isEnabled(); /// See [CardanoWallet.enable]. - external JSPromise enable(); + external JSPromise enable([ + JSCipExtensions? extensions, + ]); /// Converts JS representation to pure dart representation. CardanoWallet get toDart => JSCardanoWalletProxy(this); @@ -85,11 +87,39 @@ extension type JSCardanoWalletApi(JSObject _) implements JSObject { CardanoWalletApi get toDart => JSCardanoWalletApiProxy(this); } +/// Represents wallet extensions to be activated in [JSCardanoWallet.enable]. +extension type JSCipExtensions._(JSObject _) implements JSObject { + /// An array of extensions. + external JSArray get extensions; + + /// The default constructor for [JSCipExtensions]. + external factory JSCipExtensions({JSArray extensions}); + + /// Constructs [JSCipExtensions] from dart representation. + factory JSCipExtensions.fromDart(List extensions) { + return JSCipExtensions( + extensions: extensions.map(JSCipExtension.fromDart).toList().toJS, + ); + } + + /// Converts JS representation to pure dart representation. + List get toDart => + extensions.toDart.map((e) => e.toDart).toList(); +} + /// The JS representation of the [CipExtension]. -extension type JSCipExtension(JSObject _) implements JSObject { +extension type JSCipExtension._(JSObject _) implements JSObject { /// See [JSCipExtension.cip]. external JSNumber get cip; + /// The default constructor for [JSCipExtension]. + external factory JSCipExtension({JSNumber cip}); + + /// Constructs [JSCipExtension] from dart representation. + factory JSCipExtension.fromDart(CipExtension extension) { + return JSCipExtension(cip: extension.cip.toJS); + } + /// Converts JS representation to pure dart representation. CipExtension get toDart => CipExtension(cip: cip.toDartInt); } diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_web/lib/src/interop/catalyst_cardano_wallet_proxy.dart b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_web/lib/src/interop/catalyst_cardano_wallet_proxy.dart index 8c55890be94..ab8fdaa35e2 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_web/lib/src/interop/catalyst_cardano_wallet_proxy.dart +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano_web/lib/src/interop/catalyst_cardano_wallet_proxy.dart @@ -27,12 +27,25 @@ class JSCardanoWalletProxy implements CardanoWallet { _delegate.supportedExtensions.toDart.map((e) => e.toDart).toList(); @override - Future isEnabled() => - _delegate.isEnabled().toDart.then((e) => e.toDart); + Future isEnabled() async { + try { + return await _delegate.isEnabled().toDart.then((e) => e.toDart); + } catch (ex) { + throw _mapApiException(ex) ?? _fallbackApiException(ex); + } + } @override - Future enable() => - _delegate.enable().toDart.then((e) => e.toDart); + Future enable({List? extensions}) async { + try { + final jsExtensions = + extensions != null ? JSCipExtensions.fromDart(extensions) : null; + + return await _delegate.enable(jsExtensions).toDart.then((e) => e.toDart); + } catch (ex) { + throw _mapApiException(ex) ?? _fallbackApiException(ex); + } + } } /// A wrapper around [JSCardanoWalletApi] that translates between JS/dart layers. @@ -44,78 +57,119 @@ class JSCardanoWalletApiProxy implements CardanoWalletApi { @override Future getBalance() async { - final result = await _delegate.getBalance().toDart.then((e) => e.toDart); - return Coin.fromCbor(cbor.decode(hex.decode(result))); + try { + final result = await _delegate.getBalance().toDart.then((e) => e.toDart); + return Coin.fromCbor(cbor.decode(hex.decode(result))); + } catch (ex) { + throw _mapApiException(ex) ?? _fallbackApiException(ex); + } } @override - Future> getExtensions() => _delegate - .getExtensions() - .toDart - .then((array) => array.toDart.map((item) => item.toDart).toList()); + Future> getExtensions() async { + try { + return await _delegate + .getExtensions() + .toDart + .then((array) => array.toDart.map((item) => item.toDart).toList()); + } catch (ex) { + throw _mapApiException(ex) ?? _fallbackApiException(ex); + } + } @override Future getNetworkId() async { - final result = - await _delegate.getNetworkId().toDart.then((e) => e.toDartInt); - return NetworkId.fromId(result); + try { + final result = + await _delegate.getNetworkId().toDart.then((e) => e.toDartInt); + return NetworkId.fromId(result); + } catch (ex) { + throw _mapApiException(ex) ?? _fallbackApiException(ex); + } } @override - Future getChangeAddress() => _delegate - .getChangeAddress() - .toDart - .then((e) => ShelleyAddress(hex.decode(e.toDart))); + Future getChangeAddress() async { + try { + return await _delegate + .getChangeAddress() + .toDart + .then((e) => ShelleyAddress(hex.decode(e.toDart))); + } catch (ex) { + throw _mapApiException(ex) ?? _fallbackApiException(ex); + } + } @override - Future> getRewardAddresses() { - return _delegate.getRewardAddresses().toDart.then( - (array) => array.toDart - .map((item) => ShelleyAddress(hex.decode(item.toDart))) - .toList(), - ); + Future> getRewardAddresses() async { + try { + return await _delegate.getRewardAddresses().toDart.then( + (array) => array.toDart + .map((item) => ShelleyAddress(hex.decode(item.toDart))) + .toList(), + ); + } catch (ex) { + throw _mapApiException(ex) ?? _fallbackApiException(ex); + } } @override - Future> getUnusedAddresses() { - return _delegate.getUnusedAddresses().toDart.then( - (array) => array.toDart - .map((item) => ShelleyAddress(hex.decode(item.toDart))) - .toList(), - ); + Future> getUnusedAddresses() async { + try { + return await _delegate.getUnusedAddresses().toDart.then( + (array) => array.toDart + .map((item) => ShelleyAddress(hex.decode(item.toDart))) + .toList(), + ); + } catch (ex) { + throw _mapApiException(ex) ?? _fallbackApiException(ex); + } } @override - Future> getUsedAddresses({Paginate? paginate}) { - final jsPaginate = paginate != null ? JSPaginate.fromDart(paginate) : null; + Future> getUsedAddresses({Paginate? paginate}) async { + try { + final jsPaginate = + paginate != null ? JSPaginate.fromDart(paginate) : null; - return _delegate.getUsedAddresses(jsPaginate).toDart.then( - (array) => array.toDart - .map((item) => ShelleyAddress(hex.decode(item.toDart))) - .toList(), - ); + return await _delegate.getUsedAddresses(jsPaginate).toDart.then( + (array) => array.toDart + .map((item) => ShelleyAddress(hex.decode(item.toDart))) + .toList(), + ); + } catch (ex) { + throw _mapApiException(ex) ?? + _mapPaginateException(ex) ?? + _fallbackApiException(ex); + } } @override Future> getUtxos({ Coin? amount, Paginate? paginate, - }) { - return _delegate - .getUtxos( - amount?.value.toJS, - paginate != null ? JSPaginate.fromDart(paginate) : null, - ) - .toDart - .then( - (array) => array.toDart - .map( - (item) => TransactionUnspentOutput.fromCbor( - cbor.decode(hex.decode(item.toDart)), - ), - ) - .toList(), - ); + }) async { + try { + return await _delegate + .getUtxos( + amount?.value.toJS, + paginate != null ? JSPaginate.fromDart(paginate) : null, + ) + .toDart + .then( + (array) => array.toDart + .map( + (item) => TransactionUnspentOutput.fromCbor( + cbor.decode(hex.decode(item.toDart)), + ), + ) + .toList(), + ); + } catch (ex) { + throw _mapApiException(ex) ?? + _mapPaginateException(ex) ?? + _fallbackApiException(ex); + } } @override @@ -123,35 +177,122 @@ class JSCardanoWalletApiProxy implements CardanoWalletApi { required ShelleyAddress address, required List payload, }) async { - return _delegate - .signData( - hex.encode(cbor.encode(address.toCbor())).toJS, - hex.encode(payload).toJS, - ) - .toDart - .then((e) => VkeyWitness.fromCbor(cbor.decode(hex.decode(e.toDart)))); + try { + return await _delegate + .signData( + hex.encode(cbor.encode(address.toCbor())).toJS, + hex.encode(payload).toJS, + ) + .toDart + .then((e) => VkeyWitness.fromCbor(cbor.decode(hex.decode(e.toDart)))); + } catch (ex) { + throw _mapApiException(ex) ?? + _mapDataSignException(ex) ?? + _fallbackApiException(ex); + } } @override Future signTx({ required Transaction transaction, bool partialSign = false, - }) { - final bytes = cbor.encode(transaction.toCbor()); - final hexString = hex.encode(bytes); + }) async { + try { + final bytes = cbor.encode(transaction.toCbor()); + final hexString = hex.encode(bytes); - return _delegate.signTx(hexString.toJS, partialSign.toJS).toDart.then( - (e) => TransactionWitnessSet.fromCbor( - cbor.decode(hex.decode(e.toDart)), - ), - ); + return await _delegate + .signTx(hexString.toJS, partialSign.toJS) + .toDart + .then( + (e) => TransactionWitnessSet.fromCbor( + cbor.decode(hex.decode(e.toDart)), + ), + ); + } catch (ex) { + throw _mapApiException(ex) ?? + _mapTxSignException(ex) ?? + _fallbackApiException(ex); + } } @override Future submitTx({required Transaction transaction}) async { - final bytes = cbor.encode(transaction.toCbor()); - final hexString = hex.encode(bytes); - final result = await _delegate.submitTx(hexString.toJS).toDart; - return TransactionHash.fromHex(result.toDart); + try { + final bytes = cbor.encode(transaction.toCbor()); + final hexString = hex.encode(bytes); + final result = await _delegate.submitTx(hexString.toJS).toDart; + return TransactionHash.fromHex(result.toDart); + } catch (ex) { + throw _mapApiException(ex) ?? + _mapTxSendException(ex) ?? + _fallbackApiException(ex); + } + } +} + +WalletApiException? _mapApiException(Object ex) { + final message = ex.toString(); + + if (message.contains('canceled')) { + throw WalletApiException( + code: WalletApiErrorCode.refused, + info: message, + ); + } + + if (message.contains('unsupported')) { + throw WalletApiException( + code: WalletApiErrorCode.invalidRequest, + info: message, + ); + } + + if (message.contains('account changed')) { + throw WalletApiException( + code: WalletApiErrorCode.accountChange, + info: message, + ); } + + if (message.contains('unknown')) { + throw WalletApiException( + code: WalletApiErrorCode.internalError, + info: message, + ); + } + + return null; +} + +WalletPaginateException? _mapPaginateException(Object ex) { + final message = ex.toString(); + + // TODO(dtscalac): extract maxSize from underlying JS exception + if (message.contains('page out of range')) { + return const WalletPaginateException(maxSize: -1); + } + return null; +} + +WalletDataSignException? _mapDataSignException(Object ex) { + // TODO(dtscalac): extract exception + return null; +} + +TxSignException? _mapTxSignException(Object ex) { + // TODO(dtscalac): extract exception + return null; +} + +TxSendException? _mapTxSendException(Object ex) { + // TODO(dtscalac): extract exception + return null; +} + +WalletApiException _fallbackApiException(Object ex) { + throw WalletApiException( + code: WalletApiErrorCode.invalidRequest, + info: ex.toString(), + ); }