|
| 1 | +{-# LANGUAGE DataKinds #-} |
| 2 | +{-# LANGUAGE ExistentialQuantification #-} |
| 3 | +{-# LANGUAGE FlexibleContexts #-} |
| 4 | +{-# LANGUAGE InstanceSigs #-} |
| 5 | +{-# LANGUAGE OverloadedStrings #-} |
| 6 | +{-# LANGUAGE ScopedTypeVariables #-} |
| 7 | +{-# LANGUAGE TypeApplications #-} |
| 8 | +{-# LANGUAGE TypeFamilies #-} |
| 9 | + |
| 10 | +module Cardano.Wasm.Internal.Api.Tx where |
| 11 | + |
| 12 | +import Cardano.Api (FromJSON) |
| 13 | +import Cardano.Api qualified as Api |
| 14 | +import Cardano.Api.Experimental (obtainCommonConstraints) |
| 15 | +import Cardano.Api.Experimental qualified as Exp |
| 16 | +import Cardano.Api.Ledger qualified as Ledger |
| 17 | +import Cardano.Api.Plutus qualified as Shelley |
| 18 | +import Cardano.Api.Tx qualified as TxBody |
| 19 | + |
| 20 | +import Cardano.Ledger.Api qualified as Ledger |
| 21 | +import Cardano.Ledger.Binary |
| 22 | + ( Annotator |
| 23 | + , DecCBOR (decCBOR) |
| 24 | + , EncCBOR |
| 25 | + , Version |
| 26 | + , decodeFullAnnotator |
| 27 | + ) |
| 28 | +import Cardano.Wasm.Internal.ExceptionHandling (justOrError, rightOrError) |
| 29 | + |
| 30 | +import Codec.CBOR.Write qualified as CBOR |
| 31 | +import Control.Monad.Catch (Exception (displayException), MonadThrow) |
| 32 | +import Data.Aeson (ToJSON (toJSON), (.=)) |
| 33 | +import Data.Aeson qualified as Aeson |
| 34 | +import Data.Aeson.Types qualified as Aeson |
| 35 | +import Data.ByteString.Base16 qualified as Base16 |
| 36 | +import Data.ByteString.Lazy (fromStrict) |
| 37 | +import Data.Sequence.Strict qualified as StrictSeq |
| 38 | +import Data.Set qualified as Set |
| 39 | +import Data.Text qualified as Text |
| 40 | +import Data.Text.Encoding qualified as Text |
| 41 | +import GHC.Stack (HasCallStack) |
| 42 | +import Lens.Micro ((%~), (&), (.~), (<>~)) |
| 43 | + |
| 44 | +-- | Function to convert an era to its corresponding version |
| 45 | +eraToVersion :: Exp.Era era -> Version |
| 46 | +eraToVersion era = |
| 47 | + case era of |
| 48 | + Exp.ConwayEra -> Ledger.eraProtVerHigh @(Exp.LedgerEra Exp.ConwayEra) |
| 49 | + |
| 50 | +-- | Encode a value to a base16 encoded string of its CBOR representation. |
| 51 | +encodeEncCBOR :: forall era a. EncCBOR a => Exp.Era era -> a -> Text.Text |
| 52 | +encodeEncCBOR era = Text.decodeUtf8 . Base16.encode . Ledger.serialize' (eraToVersion era) |
| 53 | + |
| 54 | +-- | Decode a base16 encoded string of the CBOR representation corresponding to the CDDL. |
| 55 | +decodeDecCBOR |
| 56 | + :: forall era m a |
| 57 | + . (MonadThrow m, DecCBOR (Annotator a)) => Exp.Era era -> Text.Text -> Text.Text -> m a |
| 58 | +decodeDecCBOR era desc cbor = do |
| 59 | + cddlBS <- rightOrError $ Base16.decode $ Text.encodeUtf8 cbor |
| 60 | + rightOrError $ decodeFullAnnotator (eraToVersion era) desc decCBOR (fromStrict cddlBS) |
| 61 | + |
| 62 | +-- * @UnsignedTx@ object |
| 63 | + |
| 64 | +-- | An object representing a transaction that is being built and hasn't |
| 65 | +-- been signed yet. It abstracts over the era of the transaction. |
| 66 | +-- It is meant to be an opaque object in JavaScript API. |
| 67 | +data UnsignedTxObject |
| 68 | + = forall era. UnsignedTxObject (Exp.Era era) (Exp.UnsignedTx era) |
| 69 | + |
| 70 | +instance ToJSON UnsignedTxObject where |
| 71 | + toJSON :: UnsignedTxObject -> Aeson.Value |
| 72 | + toJSON (UnsignedTxObject era (Exp.UnsignedTx tx)) = |
| 73 | + obtainCommonConstraints era $ |
| 74 | + let encode :: forall a. EncCBOR a => a -> Text.Text |
| 75 | + encode = Text.decodeUtf8 . Base16.encode . Ledger.serialize' (eraToVersion era) |
| 76 | + in Aeson.object |
| 77 | + [ "era" .= Exp.Some era |
| 78 | + , "tx" .= encode tx |
| 79 | + ] |
| 80 | + |
| 81 | +instance FromJSON UnsignedTxObject where |
| 82 | + parseJSON :: Aeson.Value -> Aeson.Parser UnsignedTxObject |
| 83 | + parseJSON = Aeson.withObject "UnsignedTxObject" $ \o -> do |
| 84 | + Exp.Some era <- o Aeson..: "era" |
| 85 | + tx :: Text.Text <- o Aeson..: "tx" |
| 86 | + obtainCommonConstraints era $ do |
| 87 | + decodedTx <- toMonadFail $ decodeDecCBOR era "Tx" tx |
| 88 | + return $ |
| 89 | + UnsignedTxObject |
| 90 | + era |
| 91 | + (Exp.UnsignedTx decodedTx) |
| 92 | + |
| 93 | +-- | Create a new unsigned transaction object for making a Conway era transaction. |
| 94 | +newConwayTxImpl :: UnsignedTxObject |
| 95 | +newConwayTxImpl = UnsignedTxObject Exp.ConwayEra (Exp.UnsignedTx (Ledger.mkBasicTx Ledger.mkBasicTxBody)) |
| 96 | + |
| 97 | +-- | Add a simple transaction input to an unsigned transaction object. |
| 98 | +addTxInputImpl :: UnsignedTxObject -> Api.TxId -> Api.TxIx -> UnsignedTxObject |
| 99 | +addTxInputImpl (UnsignedTxObject era (Exp.UnsignedTx tx)) txId txIx = |
| 100 | + obtainCommonConstraints era $ |
| 101 | + let txIn = Api.TxIn txId txIx |
| 102 | + tx' = tx & Ledger.bodyTxL . Ledger.inputsTxBodyL %~ (<> Set.fromList [TxBody.toShelleyTxIn txIn]) |
| 103 | + in UnsignedTxObject era $ Exp.UnsignedTx tx' |
| 104 | + |
| 105 | +-- | Add a simple transaction output to an unsigned transaction object. |
| 106 | +-- It takes a destination address and an amount in lovelace. |
| 107 | +addSimpleTxOutImpl |
| 108 | + :: (HasCallStack, MonadThrow m) => UnsignedTxObject -> String -> Ledger.Coin -> m UnsignedTxObject |
| 109 | +addSimpleTxOutImpl (UnsignedTxObject era (Exp.UnsignedTx tx)) destAddr lovelaceAmount = |
| 110 | + obtainCommonConstraints era $ do |
| 111 | + destAddress <- deserialiseAddress era destAddr |
| 112 | + let sbe = Api.convert era |
| 113 | + txOut = |
| 114 | + Api.TxOut |
| 115 | + destAddress |
| 116 | + (Api.lovelaceToTxOutValue sbe lovelaceAmount) |
| 117 | + Api.TxOutDatumNone |
| 118 | + Shelley.ReferenceScriptNone |
| 119 | + shelleyTxOut = TxBody.toShelleyTxOutAny sbe txOut |
| 120 | + tx' = tx & Ledger.bodyTxL . Ledger.outputsTxBodyL %~ (<> StrictSeq.fromList [shelleyTxOut]) |
| 121 | + return $ UnsignedTxObject era $ Exp.UnsignedTx tx' |
| 122 | + where |
| 123 | + deserialiseAddress |
| 124 | + :: (HasCallStack, MonadThrow m, Exp.EraCommonConstraints era) |
| 125 | + => Exp.Era era -> String -> m (Api.AddressInEra era) |
| 126 | + deserialiseAddress _eon destAddrStr = |
| 127 | + justOrError |
| 128 | + "Couldn't deserialise destination address" |
| 129 | + $ Api.deserialiseAddress |
| 130 | + (Api.AsAddressInEra Api.asType) |
| 131 | + (Text.pack destAddrStr) |
| 132 | + |
| 133 | +-- | Set the fee for an unsigned transaction object. |
| 134 | +setFeeImpl :: UnsignedTxObject -> Ledger.Coin -> UnsignedTxObject |
| 135 | +setFeeImpl (UnsignedTxObject era (Exp.UnsignedTx tx)) fee = |
| 136 | + obtainCommonConstraints era $ |
| 137 | + let tx' = tx & Ledger.bodyTxL . Ledger.feeTxBodyL .~ fee |
| 138 | + in UnsignedTxObject era $ Exp.UnsignedTx tx' |
| 139 | + |
| 140 | +-- | Sign an unsigned transaction using a payment key. |
| 141 | +signWithPaymentKeyImpl :: UnsignedTxObject -> Api.SigningKey Api.PaymentKey -> SignedTxObject |
| 142 | +signWithPaymentKeyImpl (UnsignedTxObject era unsignedTx@(Exp.UnsignedTx tx)) signingKey = |
| 143 | + obtainCommonConstraints era $ |
| 144 | + let witness = Exp.makeKeyWitness era unsignedTx . Api.WitnessPaymentKey $ signingKey |
| 145 | + txWits = |
| 146 | + Ledger.mkBasicTxWits |
| 147 | + & Ledger.addrTxWitsL |
| 148 | + .~ Set.fromList [witness] |
| 149 | + txWithWits = |
| 150 | + obtainCommonConstraints |
| 151 | + era |
| 152 | + (tx & Ledger.witsTxL .~ txWits) |
| 153 | + in SignedTxObject |
| 154 | + era |
| 155 | + txWithWits |
| 156 | + |
| 157 | +-- * @SignedTx@ object |
| 158 | + |
| 159 | +-- | An object representing a signed transaction. |
| 160 | +data SignedTxObject |
| 161 | + = forall era. SignedTxObject (Exp.Era era) (Ledger.Tx (Exp.LedgerEra era)) |
| 162 | + |
| 163 | +instance ToJSON SignedTxObject where |
| 164 | + toJSON :: SignedTxObject -> Aeson.Value |
| 165 | + toJSON (SignedTxObject era ledgerTx) = |
| 166 | + obtainCommonConstraints era $ |
| 167 | + let encode :: forall a. EncCBOR a => a -> Text.Text |
| 168 | + encode = Text.decodeUtf8 . Base16.encode . Ledger.serialize' (eraToVersion era) |
| 169 | + in Aeson.object |
| 170 | + [ "era" .= Exp.Some era |
| 171 | + , "tx" .= encode ledgerTx |
| 172 | + ] |
| 173 | + |
| 174 | +instance FromJSON SignedTxObject where |
| 175 | + parseJSON :: HasCallStack => Aeson.Value -> Aeson.Parser SignedTxObject |
| 176 | + parseJSON = Aeson.withObject "SignedTxObject" $ \o -> do |
| 177 | + Exp.Some era <- o Aeson..: "era" |
| 178 | + let decode :: forall m a. (MonadThrow m, DecCBOR (Annotator a)) => Text.Text -> Text.Text -> m a |
| 179 | + decode desc cbor = do |
| 180 | + cddlBS <- rightOrError $ Base16.decode $ Text.encodeUtf8 cbor |
| 181 | + rightOrError $ decodeFullAnnotator (eraToVersion era) desc decCBOR (fromStrict cddlBS) |
| 182 | + tx :: Text.Text <- o Aeson..: "tx" |
| 183 | + obtainCommonConstraints era $ do |
| 184 | + decodedTx <- toMonadFail $ decode "Tx" tx |
| 185 | + return $ |
| 186 | + SignedTxObject era decodedTx |
| 187 | + |
| 188 | +-- | Add an extra signature to an already signed transaction using a payment key. |
| 189 | +alsoSignWithPaymentKeyImpl :: SignedTxObject -> Api.SigningKey Api.PaymentKey -> SignedTxObject |
| 190 | +alsoSignWithPaymentKeyImpl (SignedTxObject era tx) signingKey = |
| 191 | + obtainCommonConstraints era $ |
| 192 | + let witness = Exp.makeKeyWitness era (Exp.UnsignedTx tx) . Api.WitnessPaymentKey $ signingKey |
| 193 | + txWits = |
| 194 | + Ledger.mkBasicTxWits |
| 195 | + & Ledger.addrTxWitsL |
| 196 | + .~ Set.fromList [witness] |
| 197 | + txWithWits = |
| 198 | + obtainCommonConstraints |
| 199 | + era |
| 200 | + (tx & Ledger.witsTxL <>~ txWits) |
| 201 | + in SignedTxObject |
| 202 | + era |
| 203 | + txWithWits |
| 204 | + |
| 205 | +-- | Convert an 'Either' value to a 'MonadFail' monad. This can be useful for converting |
| 206 | +-- MonadThrow monads into Aeson Parser monads, but it loses the stack trace information. |
| 207 | +toMonadFail :: (Exception e, MonadFail m) => Either e a -> m a |
| 208 | +toMonadFail (Left e) = fail $ displayException e |
| 209 | +toMonadFail (Right a) = return a |
| 210 | + |
| 211 | +-- | Convert a signed transaction object to a base16 encoded string of its CBOR representation. |
| 212 | +toCborImpl :: SignedTxObject -> String |
| 213 | +toCborImpl (SignedTxObject era ledgerTx) = |
| 214 | + obtainCommonConstraints era $ |
| 215 | + let cborEncoding = Ledger.toCBOR ledgerTx |
| 216 | + in Text.unpack (Text.decodeUtf8 (Base16.encode (CBOR.toStrictByteString cborEncoding))) |
0 commit comments