From 32d8e75fe8bbfd32cd74eb86b6b1a8952584b418 Mon Sep 17 00:00:00 2001 From: Daniel Farrelly Date: Thu, 17 Aug 2023 00:22:50 +0100 Subject: [PATCH 1/2] Add support for generic CIP30 wallets by name --- CHANGELOG.md | 1 + src/Contract/Wallet.purs | 4 +++- src/Internal/Contract/Sign.purs | 5 +++-- src/Internal/Wallet.purs | 11 ++++++++++- src/Internal/Wallet/Cip30.js | 24 ++++++++++++++++++------ src/Internal/Wallet/Cip30Mock.purs | 28 +++++++++++++++++++++++++--- src/Internal/Wallet/Spec.purs | 4 ++++ test/Plutip/Contract.purs | 4 +++- 8 files changed, 67 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e2b6282d..b545d96c39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [HD wallet support](./doc/key-management.md) with mnemonic seed phrases ([#1498](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1498)) - Ogmios-specific functions for Local TX Monitor Ouroboros Mini-Protocol in `Contract.Backend.Ogmios` ([#1508](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1508/)) - New `mustSendChangeWithDatum` balancer constraint that adds datum to all change outputs ([#1510](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1510/)) +- Support for generic CIP-30 wallets by name ([#1524](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1524)) ### Changed diff --git a/src/Contract/Wallet.purs b/src/Contract/Wallet.purs index e6cdd7e1b9..5497a9e592 100644 --- a/src/Contract/Wallet.purs +++ b/src/Contract/Wallet.purs @@ -58,7 +58,7 @@ import Ctl.Internal.Plutus.Conversion ) import Ctl.Internal.Plutus.Conversion.Address (toPlutusAddressWithNetworkTag) import Ctl.Internal.Wallet - ( Wallet(Gero, Nami, Flint, Lode, Eternl, NuFi, Lace, KeyWallet) + ( Wallet(Gero, Nami, Flint, Lode, Eternl, NuFi, Lace, KeyWallet, GenericCip30) , WalletExtension ( NamiWallet , GeroWallet @@ -67,6 +67,7 @@ import Ctl.Internal.Wallet , LodeWallet , LaceWallet , NuFiWallet + , GenericCip30Wallet ) , apiVersion , icon @@ -107,6 +108,7 @@ import Ctl.Internal.Wallet.Spec , ConnectToLace , ConnectToEternl , ConnectToNuFi + , ConnectToGenericCip30 ) ) as X import Data.Array (head) diff --git a/src/Internal/Contract/Sign.purs b/src/Internal/Contract/Sign.purs index c8aa2e6308..5755290a81 100644 --- a/src/Internal/Contract/Sign.purs +++ b/src/Internal/Contract/Sign.purs @@ -11,7 +11,7 @@ import Ctl.Internal.Cardano.Types.Transaction as Transaction import Ctl.Internal.Contract.Monad (Contract) import Ctl.Internal.Contract.Wallet (withWallet) import Ctl.Internal.Wallet - ( Wallet(KeyWallet, Lode, Eternl, Flint, Gero, Nami, NuFi, Lace) + ( Wallet(Gero, Nami, Flint, Lode, Eternl, NuFi, Lace, GenericCip30, KeyWallet) , callCip30Wallet ) import Data.Array (fromFoldable) @@ -45,7 +45,8 @@ signTransaction tx = do liftAff $ callCip30Wallet eternl \nw -> flip nw.signTx tx Lode lode -> liftAff $ callCip30Wallet lode \nw -> flip nw.signTx tx NuFi nufi -> liftAff $ callCip30Wallet nufi \w -> flip w.signTx tx - Lace nufi -> liftAff $ callCip30Wallet nufi \w -> flip w.signTx tx + Lace lace -> liftAff $ callCip30Wallet lace \w -> flip w.signTx tx + GenericCip30 cip30 -> liftAff $ callCip30Wallet cip30 \w -> flip w.signTx tx KeyWallet kw -> liftAff do witnessSet <- (unwrap kw).signTx tx pure $ Just (tx # _witnessSet <>~ witnessSet) diff --git a/src/Internal/Wallet.purs b/src/Internal/Wallet.purs index 8d868f8e36..1d39af01f3 100644 --- a/src/Internal/Wallet.purs +++ b/src/Internal/Wallet.purs @@ -1,7 +1,7 @@ module Ctl.Internal.Wallet ( module KeyWallet , module Cip30Wallet - , Wallet(Gero, Nami, Flint, Lode, Eternl, NuFi, Lace, KeyWallet) + , Wallet(Gero, Nami, Flint, Lode, Eternl, NuFi, Lace, GenericCip30, KeyWallet) , WalletExtension ( NamiWallet , LodeWallet @@ -10,6 +10,7 @@ module Ctl.Internal.Wallet , EternlWallet , NuFiWallet , LaceWallet + , GenericCip30Wallet ) , isEternlAvailable , isGeroAvailable @@ -84,6 +85,7 @@ data Wallet | Lode Cip30Wallet | NuFi Cip30Wallet | Lace Cip30Wallet + | GenericCip30 Cip30Wallet | KeyWallet KeyWallet data WalletExtension @@ -94,6 +96,7 @@ data WalletExtension | LodeWallet | LaceWallet | NuFiWallet + | GenericCip30Wallet String mkKeyWallet :: PrivatePaymentKey -> Maybe PrivateStakeKey -> Wallet mkKeyWallet payKey mbStakeKey = KeyWallet $ privateKeysToKeyWallet @@ -119,6 +122,8 @@ mkWalletAff walletExtension = LodeWallet -> _mkLodeWalletAff NuFiWallet -> NuFi <$> mkCip30WalletAff (_enableWallet walletName) LaceWallet -> Lace <$> mkCip30WalletAff (_enableWallet walletName) + GenericCip30Wallet name' -> + GenericCip30 <$> mkCip30WalletAff (_enableWallet name') where walletName = walletExtensionToName walletExtension @@ -232,6 +237,7 @@ cip30Wallet = case _ of Lode c30 -> Just c30 NuFi c30 -> Just c30 Lace c30 -> Just c30 + GenericCip30 c30 -> Just c30 KeyWallet _ -> Nothing walletExtensionToName :: WalletExtension -> String @@ -243,6 +249,7 @@ walletExtensionToName = case _ of LodeWallet -> "LodeWallet" NuFiWallet -> "nufi" LaceWallet -> "lace" + GenericCip30Wallet name' -> name' walletToWalletExtension :: Wallet -> Maybe WalletExtension walletToWalletExtension = case _ of @@ -253,6 +260,7 @@ walletToWalletExtension = case _ of Lode _ -> Just LodeWallet NuFi _ -> Just NuFiWallet Lace _ -> Just LaceWallet + GenericCip30 _ -> Nothing KeyWallet _ -> Nothing isEnabled :: WalletExtension -> Aff Boolean @@ -311,6 +319,7 @@ actionBasedOnWallet walletAction keyWalletAction = Lode wallet -> liftAff $ callCip30Wallet wallet walletAction NuFi wallet -> liftAff $ callCip30Wallet wallet walletAction Lace wallet -> liftAff $ callCip30Wallet wallet walletAction + GenericCip30 wallet -> liftAff $ callCip30Wallet wallet walletAction KeyWallet kw -> keyWalletAction kw callCip30Wallet diff --git a/src/Internal/Wallet/Cip30.js b/src/Internal/Wallet/Cip30.js index 7414ae3070..ad274ce3d8 100644 --- a/src/Internal/Wallet/Cip30.js +++ b/src/Internal/Wallet/Cip30.js @@ -6,15 +6,27 @@ exports._getUtxos = maybe => conn => () => conn.getUtxos().then(res => (res === null ? maybe.nothing : maybe.just(res))); exports._getCollateral = maybe => conn => () => - conn.experimental - .getCollateral() - .then(utxos => - utxos !== null && utxos.length ? maybe.just(utxos) : maybe.nothing - ); + /* Notes regarding the quirks of various wallets: + + Yoroi will throw an error if no amount argument is provided, and will + break if the following expression is written as + (conn.getCollateral || conn.experimental.getCollateral)("5000000") due to + JavaScript object binding + + Typhon will throw an error if the amount argument is not a string + + Nami only provides `getCollateral` under the experimental API + */ + (typeof conn.getCollateral === "function" + ? conn.getCollateral("5000000") + : conn.experimental.getCollateral("5000000") + ).then(utxos => + utxos !== null && utxos.length ? maybe.just(utxos) : maybe.nothing + ); exports._getBalance = conn => () => conn.getBalance(); -exports._getAddresses = conn => conn.getUsedAddresses; +exports._getAddresses = conn => () => conn.getUsedAddresses(); exports._getUnusedAddresses = conn => () => conn.getUnusedAddresses(); diff --git a/src/Internal/Wallet/Cip30Mock.purs b/src/Internal/Wallet/Cip30Mock.purs index 01b803ec06..d2310e76cd 100644 --- a/src/Internal/Wallet/Cip30Mock.purs +++ b/src/Internal/Wallet/Cip30Mock.purs @@ -1,6 +1,13 @@ module Ctl.Internal.Wallet.Cip30Mock ( withCip30Mock - , WalletMock(MockFlint, MockGero, MockNami, MockLode, MockNuFi) + , WalletMock + ( MockFlint + , MockGero + , MockNami + , MockLode + , MockNuFi + , MockGenericCip30 + ) ) where import Prelude @@ -40,7 +47,14 @@ import Ctl.Internal.Types.RewardAddress ) import Ctl.Internal.Wallet ( Wallet - , WalletExtension(LodeWallet, NamiWallet, GeroWallet, FlintWallet, NuFiWallet) + , WalletExtension + ( LodeWallet + , NamiWallet + , GeroWallet + , FlintWallet + , NuFiWallet + , GenericCip30Wallet + ) , mkWalletAff ) import Ctl.Internal.Wallet.Key @@ -66,7 +80,13 @@ import Effect.Class (liftEffect) import Effect.Exception (error) import Effect.Unsafe (unsafePerformEffect) -data WalletMock = MockFlint | MockGero | MockNami | MockLode | MockNuFi +data WalletMock + = MockFlint + | MockGero + | MockNami + | MockLode + | MockNuFi + | MockGenericCip30 String -- | Construct a CIP-30 wallet mock that exposes `KeyWallet` functionality -- | behind a CIP-30 interface and uses Ogmios to submit Txs. @@ -103,6 +123,7 @@ withCip30Mock (KeyWallet keyWallet) mock contract = do MockNami -> mkWalletAff NamiWallet MockLode -> mkWalletAff LodeWallet MockNuFi -> mkWalletAff NuFiWallet + MockGenericCip30 name -> mkWalletAff (GenericCip30Wallet name) mockString :: String mockString = case mock of @@ -111,6 +132,7 @@ withCip30Mock (KeyWallet keyWallet) mock contract = do MockNami -> "nami" MockLode -> "LodeWallet" MockNuFi -> "nufi" + MockGenericCip30 name -> name type Cip30Mock = { getNetworkId :: Effect (Promise Int) diff --git a/src/Internal/Wallet/Spec.purs b/src/Internal/Wallet/Spec.purs index e8bdab7a38..8cb539eac3 100644 --- a/src/Internal/Wallet/Spec.purs +++ b/src/Internal/Wallet/Spec.purs @@ -9,6 +9,7 @@ module Ctl.Internal.Wallet.Spec , ConnectToLode , ConnectToNuFi , ConnectToLace + , ConnectToGenericCip30 ) , Cip1852DerivationPath , StakeKeyPresence(WithStakeKey, WithoutStakeKey) @@ -32,6 +33,7 @@ import Ctl.Internal.Wallet , LodeWallet , NuFiWallet , LaceWallet + , GenericCip30Wallet ) , mkKeyWallet , mkWalletAff @@ -110,6 +112,7 @@ data WalletSpec | ConnectToLode | ConnectToNuFi | ConnectToLace + | ConnectToGenericCip30 String derive instance Generic WalletSpec _ @@ -148,6 +151,7 @@ mkWalletBySpec = case _ of ConnectToLode -> mkWalletAff LodeWallet ConnectToNuFi -> mkWalletAff NuFiWallet ConnectToLace -> mkWalletAff LaceWallet + ConnectToGenericCip30 name -> mkWalletAff (GenericCip30Wallet name) -- | Create a wallet given a mnemonic phrase, account index, address index and -- | stake key presence flag. diff --git a/test/Plutip/Contract.purs b/test/Plutip/Contract.purs index 605d8a9ae8..a8c046e8bf 100644 --- a/test/Plutip/Contract.purs +++ b/test/Plutip/Contract.purs @@ -154,7 +154,7 @@ import Ctl.Internal.Wallet ( WalletExtension(NamiWallet, GeroWallet, FlintWallet, NuFiWallet) ) import Ctl.Internal.Wallet.Cip30Mock - ( WalletMock(MockNami, MockGero, MockFlint, MockNuFi) + ( WalletMock(MockNami, MockGero, MockFlint, MockNuFi, MockGenericCip30) , withCip30Mock ) import Data.Array (head, (!!)) @@ -1928,6 +1928,8 @@ suite = do withWallets distribution \alice -> do withCip30Mock alice MockNami do Cip30.contract + withCip30Mock alice (MockGenericCip30 "nami") do + Cip30.contract test "ECDSA example" do let distribution = withStakeKey privateStakeKey From 2a1894e4efe911cf7256ffe00bf02d6f3272d96c Mon Sep 17 00:00:00 2001 From: Vladimir Kalnitsky Date: Mon, 25 Sep 2023 15:58:23 +0400 Subject: [PATCH 2/2] Hardcode collateral value on the PS side, not in JS FFI --- src/Internal/Serialization/ToBytes.purs | 2 ++ src/Internal/Wallet/Cip30.js | 17 +++------------ src/Internal/Wallet/Cip30.purs | 29 ++++++++++++++++++++----- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Internal/Serialization/ToBytes.purs b/src/Internal/Serialization/ToBytes.purs index f46868ccd8..e9b14b87ee 100644 --- a/src/Internal/Serialization/ToBytes.purs +++ b/src/Internal/Serialization/ToBytes.purs @@ -32,6 +32,7 @@ import Ctl.Internal.Serialization.Types , Vkeywitness , Vkeywitnesses ) +import Ctl.Internal.Types.BigNum (BigNum) import Ctl.Internal.Types.ByteArray (ByteArray) import Ctl.Internal.Types.CborBytes (CborBytes(CborBytes)) import Untagged.Castable (class Castable) @@ -63,6 +64,7 @@ type SerializableData = Address |+| VRFKeyHash |+| Vkeywitness |+| Vkeywitnesses + |+| BigNum -- Add more as needed diff --git a/src/Internal/Wallet/Cip30.js b/src/Internal/Wallet/Cip30.js index ad274ce3d8..dd46b8800d 100644 --- a/src/Internal/Wallet/Cip30.js +++ b/src/Internal/Wallet/Cip30.js @@ -5,21 +5,10 @@ exports._getNetworkId = conn => () => conn.getNetworkId(); exports._getUtxos = maybe => conn => () => conn.getUtxos().then(res => (res === null ? maybe.nothing : maybe.just(res))); -exports._getCollateral = maybe => conn => () => - /* Notes regarding the quirks of various wallets: - - Yoroi will throw an error if no amount argument is provided, and will - break if the following expression is written as - (conn.getCollateral || conn.experimental.getCollateral)("5000000") due to - JavaScript object binding - - Typhon will throw an error if the amount argument is not a string - - Nami only provides `getCollateral` under the experimental API - */ +exports._getCollateral = maybe => conn => requiredValue => () => (typeof conn.getCollateral === "function" - ? conn.getCollateral("5000000") - : conn.experimental.getCollateral("5000000") + ? conn.getCollateral(requiredValue) + : conn.experimental.getCollateral(requiredValue) ).then(utxos => utxos !== null && utxos.length ? maybe.just(utxos) : maybe.nothing ); diff --git a/src/Internal/Wallet/Cip30.purs b/src/Internal/Wallet/Cip30.purs index 32e0dfb80a..57e05150d1 100644 --- a/src/Internal/Wallet/Cip30.purs +++ b/src/Internal/Wallet/Cip30.purs @@ -18,7 +18,7 @@ import Ctl.Internal.Cardano.Types.Transaction import Ctl.Internal.Cardano.Types.TransactionUnspentOutput ( TransactionUnspentOutput ) -import Ctl.Internal.Cardano.Types.Value (Value) +import Ctl.Internal.Cardano.Types.Value (Coin(Coin), Value) import Ctl.Internal.Deserialization.FromBytes (fromBytes, fromBytesEffect) import Ctl.Internal.Deserialization.UnspentOutput (convertValue) import Ctl.Internal.Deserialization.UnspentOutput as Deserialization.UnspentOuput @@ -36,6 +36,8 @@ import Ctl.Internal.Serialization.Address , rewardAddressBytes , rewardAddressFromAddress ) +import Ctl.Internal.Serialization.ToBytes (toBytes) +import Ctl.Internal.Types.BigNum as BigNum import Ctl.Internal.Types.ByteArray (byteArrayToHex) import Ctl.Internal.Types.CborBytes ( CborBytes @@ -44,6 +46,7 @@ import Ctl.Internal.Types.CborBytes , rawBytesAsCborBytes ) import Ctl.Internal.Types.RawBytes (RawBytes, hexToRawBytes, rawBytesToHex) +import Data.BigInt (fromInt) as BigInt import Data.Maybe (Maybe(Just, Nothing), maybe) import Data.Newtype (unwrap) import Data.Traversable (for, traverse) @@ -140,12 +143,15 @@ getWalletAddresses conn = Promise.toAffE (_getAddresses conn) <#> hexStringToAddress :: String -> Maybe Address hexStringToAddress = fromBytes <<< rawBytesAsCborBytes <=< hexToRawBytes +defaultCollateralAmount :: Coin +defaultCollateralAmount = Coin $ BigInt.fromInt 5_000_000 + -- | Get collateral using CIP-30 `getCollateral` method. -- | Throws on `Promise` rejection by wallet, returns `Nothing` if no collateral -- | is available. getCollateral :: Cip30Connection -> Aff (Maybe (Array TransactionUnspentOutput)) getCollateral conn = do - mbUtxoStrs <- toAffE $ getCip30Collateral conn + mbUtxoStrs <- toAffE $ getCip30Collateral conn defaultCollateralAmount let (mbUtxoBytes :: Maybe (Array RawBytes)) = join $ map (traverse hexToRawBytes) mbUtxoStrs @@ -238,12 +244,23 @@ foreign import _getUtxos foreign import _getCollateral :: MaybeFfiHelper -> Cip30Connection + -> String -> Effect (Promise (Maybe (Array String))) -getCip30Collateral :: Cip30Connection -> Effect (Promise (Maybe (Array String))) -getCip30Collateral conn = - _getCollateral maybeFfiHelper conn `catchError` - \_ -> throwError $ error "Wallet doesn't implement `getCollateral`." +getCip30Collateral + :: Cip30Connection -> Coin -> Effect (Promise (Maybe (Array String))) +getCip30Collateral conn requiredValue = do + bigNumValue <- maybe (throw convertError) pure + $ BigNum.fromBigInt + $ unwrap requiredValue + let requiredValueStr = byteArrayToHex $ unwrap $ toBytes bigNumValue + _getCollateral maybeFfiHelper conn requiredValueStr `catchError` + \err -> throwError $ error $ + "Failed to call `getCollateral`: " <> show err + where + convertError = + "Unable to convert CIP-30 getCollateral required value: " <> + show requiredValue foreign import _getBalance :: Cip30Connection -> Effect (Promise String)