diff --git a/packages.dhall b/packages.dhall index 64295516..72b312f9 100644 --- a/packages.dhall +++ b/packages.dhall @@ -332,7 +332,8 @@ in upstream "https://github.com/input-output-hk/purescript-cardano-wallet-client.git" "v0.0.1" - with marlowe-runtime-client = -- ./purescript-marlowe-runtime-client/spago.dhall as Location + with marlowe-runtime-client = ../purescript-marlowe-runtime-client/spago.dhall as Location + with remote-marlowe-runtime-client = mkPackage [ "aff" , "aff-promise" diff --git a/spago.dhall b/spago.dhall index 40ccc2fa..2e3bd4a2 100644 --- a/spago.dhall +++ b/spago.dhall @@ -70,6 +70,7 @@ , "spec" , "strings" , "tailrec" + , "these" , "transformers" , "tuples" , "typelevel-prelude" diff --git a/src/Component/App.purs b/src/Component/App.purs index b60bc353..dda81dc6 100644 --- a/src/Component/App.purs +++ b/src/Component/App.purs @@ -3,11 +3,10 @@ module Component.App where import Prelude import Cardano (AssetId(..), NonAdaAssets(..), nonAdaAssets) -import CardanoMultiplatformLib.Types (Bech32) import Component.Assets.Svgs (marloweLogoUrl) import Component.ConnectWallet (mkConnectWallet, walletInfo) import Component.ConnectWallet as ConnectWallet -import Component.ContractList (ModalAction(..), mkContractList) +import Component.ContractList (ModalAction(..), NotSyncedYetInserts(..), mkContractList) import Component.CreateContract (ContractJsonString) import Component.Footer (footer) import Component.Footer as Footer @@ -15,8 +14,9 @@ import Component.LandingPage (mkLandingPage) import Component.MessageHub (mkMessageBox, mkMessagePreview) import Component.Modal (Size(..), mkModal) import Component.Types (ContractInfo(..), MessageContent(Success), MessageHub(MessageHub), MkComponentMBase, WalletInfo(..)) -import Component.Types.ContractInfo (MarloweInfo(..), emptyNotSyncedYet) +import Component.Types.ContractInfo (MarloweInfo(..), NotSyncedYet(..), SomeContractInfo(..), someContractInfoFromContractCreated, someContractInfoFromContractUpdated) import Component.Types.ContractInfo as ContractInfo +import Component.Types.ContractInfoMap as ContractInfoMap import Component.Widgets (link, linkWithIcon) import Contrib.Cardano (Slotting, slotToTimestamp) import Contrib.React.Svg (svgImg) @@ -38,7 +38,6 @@ import Data.Time.Duration (Milliseconds(..)) import Data.Traversable (for, traverse) import Data.Tuple (uncurry) import Data.Tuple.Nested ((/\)) -import Debug (traceM) import Effect (Effect) import Effect.Aff (Aff, delay, forkAff, supervise) import Effect.Class (liftEffect) @@ -112,7 +111,7 @@ data DisplayOption = Default | About derive instance Eq DisplayOption -type ContractInfoMap = Map Runtime.ContractId ContractInfo +type ContractInfoMap = Map Runtime.ContractId SomeContractInfo -- On the app level we keep the previous wallet context -- so we can reuse pieces of contract info from the previous @@ -145,7 +144,7 @@ mkApp = do Runtime runtime <- asks _.runtime liftEffect $ component "App" \props -> React.do - possibleWalletInfo /\ setWalletInfo <- useState' Nothing + possibleWalletInfo /\ setWalletInfo <- useState' $ Nothing let walletInfoName = _.name <<< un WalletInfo <$> possibleWalletInfo @@ -153,22 +152,20 @@ mkApp = do possibleWalletContext /\ setWalletContext <- useState' Nothing possibleWalletContextRef <- useStateRef' possibleWalletContext - (possibleContractMap /\ contractMapInitialized) /\ updateContractMap <- useState (Nothing /\ false) + (contractInfoMap /\ contractMapInitialized) /\ updateContractInfoMap <- useState (ContractInfoMap.uninitialized slotting /\ false) - notSyncedYet /\ addToNotSyncedYet <- React.do - notSyncedYet /\ setNotSyncedYet <- React.useState Nothing - let - addContractCreated :: ContractInfo.ContractCreated -> Effect Unit - addContractCreated cc = setNotSyncedYet case _ of - Nothing -> Just $ ContractInfo.addContractCreated cc emptyNotSyncedYet - Just notSyncedYet -> Just $ ContractInfo.addContractCreated cc notSyncedYet - - -- (ContractInfo.addContractUpdated cu) - addContractUpdated :: ContractInfo.ContractUpdated -> Effect Unit - addContractUpdated cu = setNotSyncedYet case _ of - Nothing -> Just $ ContractInfo.addContractUpdated cu emptyNotSyncedYet - Just notSyncedYet -> Just $ ContractInfo.addContractUpdated cu notSyncedYet - pure (notSyncedYet /\ { addContractUpdated, addContractCreated }) + let + notSyncedYetInserts = NotSyncedYetInserts do + let + add :: ContractInfo.ContractCreated -> Effect Unit + add cc = do + updateContractInfoMap $ \(contractMap /\ initialized) -> + ContractInfoMap.insertContractCreated cc contractMap /\ initialized + + update :: ContractInfo.ContractUpdated -> Effect Unit + update cu = updateContractInfoMap $ \(contractMap /\ initialized) -> + ContractInfoMap.insertContractUpdated cu contractMap /\ initialized + { add, update } let walletCtx = un WalletContext <$> possibleWalletContext @@ -190,17 +187,15 @@ mkApp = do supervise do void $ forkAff do _ <- contractStream.getState - liftEffect $ updateContractMap \(contractMap /\ _) -> (contractMap /\ true) + liftEffect $ updateContractInfoMap \(contractMap /\ _) -> (contractMap /\ true) void $ forkAff do untilJust do - updates <- liftEffect $ contractStream.getLiveState - let - new = mkAppContractInfoMap slotting possibleWalletContext updates - liftEffect $ updateContractMap \(_ /\ initialized) -> (Just new /\ initialized) + newSynced <- liftEffect $ contractStream.getLiveState + liftEffect $ updateContractInfoMap \(contractMap /\ initialized) -> + (ContractInfoMap.updateSynced newSynced contractMap) /\ initialized delay (Milliseconds 1_000.0) pure Nothing - contractStream.start Nothing -> pure unit @@ -218,7 +213,6 @@ mkApp = do liftEffect $ setWalletContext walletContext action `catchError` \_ -> do -- FIXME: Report back (to the reporting backend) a wallet problem? - traceM "ERROR during wallet context construction" pure unit configuringWallet /\ setConfiguringWallet <- useState' false @@ -275,9 +269,7 @@ mkApp = do liftEffect $ setWalletInfo $ Just walletInfo let - possibleContracts = do - AppContractInfoMap { map: contracts } <- possibleContractMap - pure contracts + possibleContracts = Array.fromFoldable <$> ContractInfoMap.getContractsMap contractInfoMap pure $ case possibleWalletInfo of Nothing -> DOM.div {} $ @@ -364,12 +356,10 @@ mkApp = do } jsx , do - let - contractArray = Array.fromFoldable <$> possibleContracts subcomponents.contractListComponent - { possibleContracts: map ContractInfo.SyncedConractInfo <$> contractArray + { possibleContracts , contractMapInitialized - -- , notSyncedYet: notSyncedYet /\ addToNotSyncedYet + , notSyncedYetInserts , connectedWallet: possibleWalletInfo , possibleInitialModalAction: (NewContract <<< Just) <$> props.possibleInitialContract } @@ -400,46 +390,49 @@ mkApp = do , footer (Footer.Fixed false) ] -mkAppContractInfoMap :: Slotting -> Maybe WalletContext -> ContractWithTransactionsMap -> AppContractInfoMap -mkAppContractInfoMap slotting walletContext updates = do +mkAppContractInfoMap :: Slotting -> Maybe WalletContext -> Maybe ContractWithTransactionsMap -> Maybe NotSyncedYet -> Maybe AppContractInfoMap +mkAppContractInfoMap _ _ Nothing Nothing = Nothing +mkAppContractInfoMap slotting walletContext possiblySynced possiblyNotSyncedYet = do let - -- walletCtx = un WalletContext <$> walletContext - -- (usedAddresses :: Array String) = map bech32ToString $ fromMaybe [] $ _.usedAddresses <$> walletCtx - -- (tokens :: Array String) = map Cardano.assetIdToString $ fromMaybe [] $ Array.fromFoldable <<< Map.keys <<< un Cardano.Value <<< _.balance <$> walletCtx - - map = Map.catMaybes $ updates <#> \{ contract: { resource: contractHeader@(Runtime.ContractHeader { contractId, roleTokenMintingPolicyId, tags }), links: endpoints }, contractState, transactions } -> do - let - marloweInfo = do - Runtime.ContractState contractState' <- contractState - pure $ MarloweInfo - { initialContract: contractState'.initialContract - , currencySymbol: case roleTokenMintingPolicyId of - PolicyId "" -> Nothing - PolicyId policyId -> Just $ policyId - , state: contractState'.state - , currentContract: contractState'.currentContract - , initialState: V1.emptyState -- FIXME: No initial state on the API LEVEL? - , unclaimedPayouts: contractState'.unclaimedPayouts - } - Runtime.ContractHeader { contractId, block } = contractHeader - blockSlotTimestamp (Runtime.BlockHeader { slotNo }) = slotToTimestamp slotting slotNo - - createdAt :: Maybe Instant - createdAt = blockSlotTimestamp <$> block - - updatedAt :: Maybe Instant - updatedAt = do - Runtime.TxHeader tx /\ _ <- Array.head transactions - blockSlotTimestamp <$> tx.block - - pure $ ContractInfo $ - { contractId - , createdAt - , updatedAt - , endpoints - , marloweInfo - , tags - , _runtime: { contractHeader, transactions } - } - - AppContractInfoMap { walletContext, map } + ns = fromMaybe Map.empty do + NotSyncedYet { created, updated } <- possiblyNotSyncedYet + pure $ map someContractInfoFromContractCreated created + `Map.union` map someContractInfoFromContractUpdated updated + s = fromMaybe Map.empty do + synced <- possiblySynced + pure $ Map.catMaybes $ synced <#> \{ contract: { resource: contractHeader@(Runtime.ContractHeader { roleTokenMintingPolicyId, tags }), links: endpoints }, contractState, transactions } -> do + let + marloweInfo = do + Runtime.ContractState contractState' <- contractState + pure $ MarloweInfo + { initialContract: contractState'.initialContract + , currencySymbol: case roleTokenMintingPolicyId of + PolicyId "" -> Nothing + PolicyId policyId -> Just $ policyId + , state: contractState'.state + , currentContract: contractState'.currentContract + , initialState: V1.emptyState -- FIXME: No initial state on the API LEVEL? + , unclaimedPayouts: contractState'.unclaimedPayouts + } + Runtime.ContractHeader { contractId, block } = contractHeader + blockSlotTimestamp (Runtime.BlockHeader { slotNo }) = slotToTimestamp slotting slotNo + + createdAt :: Maybe Instant + createdAt = blockSlotTimestamp <$> block + + updatedAt :: Maybe Instant + updatedAt = do + Runtime.TxHeader tx /\ _ <- Array.head transactions + blockSlotTimestamp <$> tx.block + + pure $ SyncedConractInfo $ ContractInfo $ + { contractId + , createdAt + , updatedAt + , endpoints + , marloweInfo + , tags + , _runtime: { contractHeader, transactions } + } + + pure $ AppContractInfoMap { walletContext, map: ns `Map.union` s } diff --git a/src/Component/ApplyInputs.purs b/src/Component/ApplyInputs.purs index bd146175..4f536254 100644 --- a/src/Component/ApplyInputs.purs +++ b/src/Component/ApplyInputs.purs @@ -10,7 +10,8 @@ import Component.BodyLayout (descriptionLink, wrappedContentWithFooter) import Component.BodyLayout as BodyLayout import Component.InputHelper (ChoiceInput(..), DepositInput(..), NotifyInput, toIChoice, toIDeposit) import Component.MarloweYaml (marloweStateYaml, marloweYaml) -import Component.Types (MkComponentM, WalletInfo(..)) +import Component.Types (ContractInfo, MkComponentM, WalletInfo(..)) +import Component.Types.ContractInfo as ContractInfo import Component.Widgets (link) import Contrib.Data.FunctorWithIndex (mapWithIndexFlipped) import Contrib.Fetch (FetchError) @@ -30,12 +31,13 @@ import Data.BigInt.Argonaut as BigInt import Data.DateTime.Instant (instant, toDateTime, unInstant) import Data.Decimal as Decimal import Data.Either (Either(..)) +import Data.Foldable (foldr) import Data.FunctorWithIndex (mapWithIndex) import Data.Int as Int +import Data.List as List import Data.Map as Map import Data.Maybe (Maybe(..)) import Data.Newtype (un) -import Data.Nullable as Argonaut import Data.Time.Duration (Milliseconds(..), Seconds(..)) import Data.Tuple (snd) import Data.Validation.Semigroup (V(..)) @@ -45,7 +47,8 @@ import Effect.Aff (Aff) import Effect.Class (liftEffect) import Effect.Now (now) import JS.Unsafe.Stringify (unsafeStringify) -import Language.Marlowe.Core.V1.Semantics.Types (Action(..), Ada(..), Case(..), ChoiceId(..), Contract(..), Input(..), InputContent(..), Party(..), TimeInterval(..), Token(..), Value(..)) as V1 +import Language.Marlowe.Core.V1.Semantics (computeTransaction) as V1 +import Language.Marlowe.Core.V1.Semantics.Types (Action(..), Ada(..), Case(..), ChoiceId(..), Contract(..), Environment(..), Input(..), InputContent(..), Party(..), TimeInterval(..), Token(..), TransactionInput(..), TransactionOutput(..), Value(..)) as V1 import Language.Marlowe.Core.V1.Semantics.Types (Input(..)) import Marlowe.Runtime.Web.Client (ClientError, post', put') import Marlowe.Runtime.Web.Types (ContractEndpoint, ContractsEndpoint, PostContractsRequest(..), PostContractsResponseContent, PostTransactionsRequest(PostTransactionsRequest), PostTransactionsResponse, PutTransactionRequest(PutTransactionRequest), Runtime(Runtime), ServerURL, TransactionEndpoint, TransactionsEndpoint, toTextEnvelope) @@ -345,12 +348,7 @@ type NotifyFormComponentProps = mkNotifyFormComponent :: MkComponentM (NotifyFormComponentProps -> JSX) mkNotifyFormComponent = do - Runtime runtime <- asks _.runtime - cardanoMultiplatformLib <- asks _.cardanoMultiplatformLib - walletInfoCtx <- asks _.walletInfoCtx - - liftEffect $ component "ApplyInputs.NotifyFormComponent" \{ notifyInput, connectedWallet, marloweContext, onDismiss, onSuccess } -> React.do - possibleWalletContext <- useContext walletInfoCtx <#> map (un WalletContext <<< snd) + liftEffect $ component "ApplyInputs.NotifyFormComponent" \{ marloweContext, onDismiss, onSuccess } -> React.do pure $ BodyLayout.component do let body = DOOM.div_ $ @@ -442,14 +440,6 @@ data Step = Creating CreateInputStep -- | Signing (Either String PostContractsResponseContent) -- | Signed (Either ClientError PostContractsResponseContent) -type Props = - { onDismiss :: Effect Unit - , onSuccess :: TransactionEndpoint -> Effect Unit - , connectedWallet :: WalletInfo Wallet.Api - , transactionsEndpoint :: TransactionsEndpoint - , marloweContext :: Machine.MarloweContext - } - machineProps marloweContext transactionsEndpoint connectedWallet cardanoMultiplatformLib runtime = do let env = { connectedWallet, cardanoMultiplatformLib, runtime } @@ -754,6 +744,7 @@ submittingTransaction onDismiss runtimeRequest possibleRuntimeResponse = do , content: wrappedContentWithFooter body footer } +inputApplied :: Effect Unit -> JSX inputApplied onNext = do let body = DOM.div { className: "row" } @@ -815,6 +806,15 @@ showPossibleErrorAndDismiss title description body onDismiss errors = do , content: wrappedContentWithFooter body' footer } +type Props = + { onDismiss :: Effect Unit + , onSuccess :: ContractInfo.ContractUpdated -> Effect Unit + , connectedWallet :: WalletInfo Wallet.Api + , transactionsEndpoint :: TransactionsEndpoint + , marloweContext :: Machine.MarloweContext + , contractInfo :: ContractInfo + } + mkComponent :: MkComponentM (Props -> JSX) mkComponent = do runtime <- asks _.runtime @@ -826,7 +826,7 @@ mkComponent = do notifyFormComponent <- mkNotifyFormComponent advanceFormComponent <- mkAdvanceFormComponent - liftEffect $ component "ApplyInputs" \{ connectedWallet, onSuccess, onDismiss, marloweContext, transactionsEndpoint } -> React.do + liftEffect $ component "ApplyInputs" \{ connectedWallet, onSuccess, onDismiss, marloweContext, contractInfo, transactionsEndpoint } -> React.do walletRef <- React.useRef connectedWallet previewFlow /\ setPreviewFlow <- React.useState' $ DetailedFlow { showPrevStep: true } let @@ -843,12 +843,8 @@ mkComponent = do let setNextFlow = case previewFlow of DetailedFlow { showPrevStep: true } -> do - traceM "NEXT STEP" - traceM previewFlow setPreviewFlow $ DetailedFlow { showPrevStep: false } DetailedFlow { showPrevStep: false } -> do - traceM "NEXT STEP" - traceM previewFlow setPreviewFlow $ DetailedFlow { showPrevStep: true } SimplifiedFlow -> pure unit @@ -892,7 +888,7 @@ mkComponent = do , extraClassNames: "me-3" } ] - , DOM.div { className: "col-6 text-end" } $ + , DOM.div { className: "col-6 text-end" } $ do [ DOM.button { className: "btn btn-primary me-2" , disabled: not $ Machine.canDeposit allInputsChoices @@ -949,11 +945,24 @@ mkComponent = do , content: wrappedContentWithFooter body footer } - Machine.PickingInput { inputChoices } -> do + Machine.PickingInput { errors: Nothing, inputChoices, environment } -> do let applyPickInputSucceeded input = do setNextFlow - machine.applyAction <<< Machine.PickInputSucceeded $ input + let + V1.Environment { timeInterval } = environment + transactionInput = V1.TransactionInput + { inputs: foldr List.Cons List.Nil input + , interval: timeInterval + } + { initialContract, state, contract } = marloweContext + case V1.computeTransaction transactionInput state contract of + V1.TransactionOutput t -> do + let + newMarloweContext = { initialContract, state: t.txOutState, contract: t.txOutContract } + machine.applyAction <<< Machine.PickInputSucceeded $ { input, newMarloweContext } + V1.Error err -> do + machine.applyAction <<< Machine.PickInputFailed $ show err case inputChoices of DepositInputs depositInputs -> depositFormComponent { depositInputs, connectedWallet, marloweContext, onDismiss, onSuccess: applyPickInputSucceeded <<< Just } @@ -961,9 +970,11 @@ mkComponent = do choiceFormComponent { choiceInputs, connectedWallet, marloweContext, onDismiss, onSuccess: applyPickInputSucceeded <<< Just } SpecificNotifyInput notifyInput -> notifyFormComponent { notifyInput, connectedWallet, marloweContext, onDismiss, onSuccess: applyPickInputSucceeded <<< Just $ V1.NormalInput V1.INotify } - AdvanceContract cont -> + AdvanceContract _ -> advanceFormComponent { marloweContext, onDismiss, onSuccess: applyPickInputSucceeded Nothing } - Machine.CreatingTx { allInputsChoices, errors } -> case previewFlow of + Machine.PickingInput { errors: Just error } -> do + DOOM.text error + Machine.CreatingTx { errors } -> case previewFlow of DetailedFlow _ -> do creatingTxDetails Nothing onDismiss "createTx placeholder" $ case errors of Just err -> Just $ err @@ -996,155 +1007,36 @@ mkComponent = do , description: DOOM.text "We are submitting the initial transaction." , content: DOOM.text "Auto submitting tx... (progress bar?)" } - Machine.InputApplied {} -> case previewFlow of + Machine.InputApplied ia -> case previewFlow of DetailedFlow { showPrevStep: true } -> do submittingTransaction onDismiss "Final request placeholder" (Just "201") - _ -> - inputApplied onDismiss - --- let --- body = DOM.div { className: "row" } --- [ DOM.div { className: "col-12" } $ yamlSyntaxHighlighter contract { sortKeys: mkFn2 sortMarloweKeys } --- ] - --- footer = DOM.div { className: "row" } --- [ DOM.div { className: "col-3 text-center" } $ --- DOM.button --- { className: "btn btn-primary" --- , disabled: isNothing possibleDeposits || isJust possibleNextTimeoutAdvance --- , onClick: handler_ $ case possibleDeposits of --- Just deposits -> setStep (Creating $ PerformingDeposit deposits) --- Nothing -> pure unit --- } --- [ R.text "Deposit" ] --- , DOM.div { className: "col-3 text-center" } $ --- DOM.button --- { className: "btn btn-primary" --- , disabled: isNothing possibleChoiceInputs || isJust possibleNextTimeoutAdvance --- , onClick: handler_ $ case possibleChoiceInputs of --- Just choiceInputs -> setStep (Creating $ PerformingChoice choiceInputs) --- Nothing -> pure unit --- } --- [ R.text "Choice" ] --- , DOM.div { className: "col-3 text-center" } $ --- DOM.button --- { className: "btn btn-primary" --- , disabled: isNothing possibleNotifyInputs || isJust possibleNextTimeoutAdvance --- , onClick: handler_ $ case possibleNotifyInputs of --- Just notifyInputs -> setStep (Creating $ PerformingNotify notifyInputs) --- Nothing -> pure unit --- } --- [ R.text "Notify" ] --- , DOM.div { className: "col-3 text-center" } $ --- DOM.button --- { className: "btn btn-primary" --- , disabled: isNothing possibleNextTimeoutAdvance --- , onClick: handler_ $ case possibleNextTimeoutAdvance of --- Just cont -> setStep (Creating $ PerformingAdvance cont) --- Nothing -> pure unit --- } --- [ R.text "Advance" ] --- ] - --- if inModal then modal --- { title: R.text "Select input type" --- , onDismiss --- , body --- , footer --- , size: Modal.ExtraLarge --- } --- else --- body - --- step /\ setStep <- useState' (Creating SelectingInputType) --- let - --- possibleDeposits = do --- let --- dps = nextDeposit environment state contract --- NonEmpty.fromArray $ dps - --- possibleChoiceInputs = do --- let --- cis = nextChoice environment state contract --- NonEmpty.fromArray $ cis - --- possibleNotifyInputs = do --- let --- cis = nextNotify environment state contract --- NonEmpty.fromArray $ cis - --- possibleNextTimeoutAdvance = nextTimeoutAdvance environment contract - --- pure $ case step of --- Creating SelectingInputType -> do --- let --- body = DOM.div { className: "row" } --- [ DOM.div { className: "col-12" } $ yamlSyntaxHighlighter contract { sortKeys: mkFn2 sortMarloweKeys } --- ] - --- footer = DOM.div { className: "row" } --- [ DOM.div { className: "col-3 text-center" } $ --- DOM.button --- { className: "btn btn-primary" --- , disabled: isNothing possibleDeposits || isJust possibleNextTimeoutAdvance --- , onClick: handler_ $ case possibleDeposits of --- Just deposits -> setStep (Creating $ PerformingDeposit deposits) --- Nothing -> pure unit --- } --- [ R.text "Deposit" ] --- , DOM.div { className: "col-3 text-center" } $ --- DOM.button --- { className: "btn btn-primary" --- , disabled: isNothing possibleChoiceInputs || isJust possibleNextTimeoutAdvance --- , onClick: handler_ $ case possibleChoiceInputs of --- Just choiceInputs -> setStep (Creating $ PerformingChoice choiceInputs) --- Nothing -> pure unit --- } --- [ R.text "Choice" ] --- , DOM.div { className: "col-3 text-center" } $ --- DOM.button --- { className: "btn btn-primary" --- , disabled: isNothing possibleNotifyInputs || isJust possibleNextTimeoutAdvance --- , onClick: handler_ $ case possibleNotifyInputs of --- Just notifyInputs -> setStep (Creating $ PerformingNotify notifyInputs) --- Nothing -> pure unit --- } --- [ R.text "Notify" ] --- , DOM.div { className: "col-3 text-center" } $ --- DOM.button --- { className: "btn btn-primary" --- , disabled: isNothing possibleNextTimeoutAdvance --- , onClick: handler_ $ case possibleNextTimeoutAdvance of --- Just cont -> setStep (Creating $ PerformingAdvance cont) --- Nothing -> pure unit --- } --- [ R.text "Advance" ] --- ] - --- if inModal then modal --- { title: R.text "Select input type" --- , onDismiss --- , body --- , footer --- , size: Modal.ExtraLarge --- } --- else --- body --- Creating (PerformingDeposit deposits) -> do --- depositFormComponent { deposits, connectedWallet, timeInterval, transactionsEndpoint, onDismiss, onSuccess } --- Creating (PerformingNotify notifyInputs) -> do --- notifyFormComponent { notifyInputs, connectedWallet, timeInterval, transactionsEndpoint, onDismiss, onSuccess } --- Creating (PerformingChoice choiceInputs) -> do --- choiceFormComponent { choiceInputs, connectedWallet, timeInterval, transactionsEndpoint, onDismiss, onSuccess } --- Creating (PerformingAdvance cont) -> do --- advanceFormComponent { contract: cont, connectedWallet, timeInterval, transactionsEndpoint, onDismiss, onSuccess } --- _ -> DOM.div { className: "row" } [ R.text "TEST" ] + _ -> do + let + { submittedAt + , input: possibleInput + , environment + , newMarloweContext: { state, contract } + } = ia + + V1.Environment { timeInterval } = environment + transactionInput = V1.TransactionInput + { inputs: foldr List.Cons List.Nil possibleInput + , interval: timeInterval + } + contractUpdated = ContractInfo.ContractUpdated + { contractInfo + , transactionInput + , outputContract: contract + , outputState: state + , submittedAt + } + onSuccess' :: Effect Unit + onSuccess' = onSuccess contractUpdated + inputApplied onSuccess' address :: String address = "addr_test1qz4y0hs2kwmlpvwc6xtyq6m27xcd3rx5v95vf89q24a57ux5hr7g3tkp68p0g099tpuf3kyd5g80wwtyhr8klrcgmhasu26qcn" --- data TimeInterval = TimeInterval Instant Instant defaultTimeInterval :: Effect V1.TimeInterval defaultTimeInterval = do nowInstant <- now diff --git a/src/Component/ApplyInputs/Machine.purs b/src/Component/ApplyInputs/Machine.purs index b13044ed..25e25867 100644 --- a/src/Component/ApplyInputs/Machine.purs +++ b/src/Component/ApplyInputs/Machine.purs @@ -14,14 +14,15 @@ import Control.Monad.Error.Class (catchError) import Data.Array as Array import Data.Array.NonEmpty (NonEmptyArray) import Data.Array.NonEmpty as NonEmpty -import Data.DateTime.Instant (instant, toDateTime, unInstant) +import Data.DateTime.Instant (Instant, toDateTime) import Data.Either (Either(..), isLeft) import Data.Foldable (foldMap) import Data.Int as Int import Data.Maybe (Maybe(..), fromMaybe, isJust) -import Data.Time.Duration (Milliseconds(..), fromDuration) -import Data.Time.Duration as Time.Duration +import Data.Time.Duration (Milliseconds(..)) import Data.Variant (Variant) +import Debug (traceM) +import Effect (Effect) import Effect.Aff (Aff) import Effect.Class (liftEffect) import Effect.Now (now) @@ -29,7 +30,6 @@ import JS.Unsafe.Stringify (unsafeStringify) import Language.Marlowe.Core.V1.Semantics.Types as V1 import Marlowe.Runtime.Web.Client (ClientError, post', put') import Marlowe.Runtime.Web.Types (PostContractsError, PostTransactionsRequest(..), PostTransactionsResponse(..), PutTransactionRequest(..), ResourceWithLinks, Runtime(Runtime), ServerURL, TextEnvelope(TextEnvelope), TransactionEndpoint, TransactionsEndpoint, toTextEnvelope) -import Partial.Unsafe (unsafeCrashWith) import Wallet as Wallet import WalletContext (WalletContext(..), walletContext) @@ -83,14 +83,18 @@ data State | ChoosingInputType { autoRun :: AutoRun , errors :: Maybe String + , marloweContext :: MarloweContext , allInputsChoices :: AllInputsChoices + , environment :: V1.Environment , requiredWalletContext :: RequiredWalletContext , transactionsEndpoint :: TransactionsEndpoint } | PickingInput { autoRun :: AutoRun , errors :: Maybe String + , marloweContext :: MarloweContext , allInputsChoices :: AllInputsChoices + , environment :: V1.Environment , inputChoices :: InputChoices , requiredWalletContext :: RequiredWalletContext , transactionsEndpoint :: TransactionsEndpoint @@ -99,35 +103,44 @@ data State { autoRun :: AutoRun , errors :: Maybe String , allInputsChoices :: AllInputsChoices + , environment :: V1.Environment , transactionsEndpoint :: TransactionsEndpoint , inputChoices :: InputChoices , input :: Maybe V1.Input + , newMarloweContext :: MarloweContext , requiredWalletContext :: RequiredWalletContext } | SigningTx { autoRun :: AutoRun , errors :: Maybe String , allInputsChoices :: AllInputsChoices + , environment :: V1.Environment , inputChoices :: InputChoices , input :: Maybe V1.Input + , newMarloweContext :: MarloweContext , createTxResponse :: ResourceWithLinks PostTransactionsResponse (transaction :: TransactionEndpoint) } | SubmittingTx { autoRun :: AutoRun , errors :: Maybe String , allInputsChoices :: AllInputsChoices + , environment :: V1.Environment , inputChoices :: InputChoices , input :: Maybe V1.Input + , newMarloweContext :: MarloweContext , createTxResponse :: ResourceWithLinks PostTransactionsResponse (transaction :: TransactionEndpoint) , txWitnessSet :: CborHex TransactionWitnessSetObject } | InputApplied { autoRun :: AutoRun , allInputsChoices :: AllInputsChoices + , environment :: V1.Environment , inputChoices :: InputChoices , input :: Maybe V1.Input + , newMarloweContext :: MarloweContext , txWitnessSet :: CborHex TransactionWitnessSetObject , createTxResponse :: ResourceWithLinks PostTransactionsResponse (transaction :: TransactionEndpoint) + , submittedAt :: Instant } autoRunFromState :: State -> Maybe AutoRun @@ -151,13 +164,14 @@ data Action | FetchRequiredWalletContextSucceeded { allInputsChoices :: AllInputsChoices , requiredWalletContext :: RequiredWalletContext + , environment :: V1.Environment } | ChooseInputType | ChooseInputTypeFailed String | ChooseInputTypeSucceeded InputChoices | PickInput | PickInputFailed String - | PickInputSucceeded (Maybe V1.Input) + | PickInputSucceeded { input :: Maybe V1.Input, newMarloweContext :: MarloweContext } | CreateTx | CreateTxFailed String | CreateTxSucceeded (ResourceWithLinks PostTransactionsResponse (transaction :: TransactionEndpoint)) @@ -166,7 +180,7 @@ data Action | SignTxSucceeded (CborHex TransactionWitnessSetObject) | SubmitTx | SubmitTxFailed String - | SubmitTxSucceeded + | SubmitTxSucceeded Instant step :: State -> Action -> State step state action = do @@ -183,12 +197,14 @@ step state action = do FetchRequiredWalletContext { autoRun, marloweContext, transactionsEndpoint } -> FetchingRequiredWalletContext $ { autoRun, marloweContext, transactionsEndpoint, errors: Nothing } _ -> state - FetchingRequiredWalletContext r@{ autoRun, transactionsEndpoint } -> case action of + FetchingRequiredWalletContext r@{ autoRun, transactionsEndpoint, marloweContext } -> case action of FetchRequiredWalletContextFailed error -> FetchingRequiredWalletContext $ r { errors = Just error } - FetchRequiredWalletContextSucceeded { requiredWalletContext, allInputsChoices } -> ChoosingInputType + FetchRequiredWalletContextSucceeded { requiredWalletContext, allInputsChoices, environment } -> ChoosingInputType { autoRun , errors: Nothing , allInputsChoices + , environment + , marloweContext , requiredWalletContext , transactionsEndpoint } @@ -196,13 +212,15 @@ step state action = do ChoosingInputType r@{ errors: Just _ } -> case action of ChooseInputType -> ChoosingInputType r { errors = Nothing } _ -> state - ChoosingInputType r@{ allInputsChoices, autoRun, requiredWalletContext, transactionsEndpoint } -> case action of + ChoosingInputType r@{ allInputsChoices, environment, marloweContext, autoRun, requiredWalletContext, transactionsEndpoint } -> case action of ChooseInputTypeFailed error -> ChoosingInputType $ r { errors = Just error } ChooseInputTypeSucceeded inputChoices -> PickingInput { autoRun , errors: Nothing , allInputsChoices + , environment , inputChoices + , marloweContext , requiredWalletContext , transactionsEndpoint } @@ -210,12 +228,14 @@ step state action = do PickingInput r@{ errors: Just _ } -> case action of PickInput -> PickingInput $ r { errors = Nothing } _ -> state - PickingInput r@{ allInputsChoices, autoRun, inputChoices, requiredWalletContext, transactionsEndpoint } -> case action of + PickingInput r@{ allInputsChoices, environment, autoRun, inputChoices, requiredWalletContext, transactionsEndpoint } -> case action of PickInputFailed error -> PickingInput $ r { errors = Just error } - PickInputSucceeded input -> CreatingTx + PickInputSucceeded { input, newMarloweContext } -> CreatingTx { autoRun , errors: Nothing , allInputsChoices + , newMarloweContext + , environment , inputChoices , input , requiredWalletContext @@ -225,26 +245,26 @@ step state action = do CreatingTx r@{ errors: Just _ } -> case action of CreateTx -> CreatingTx $ r { errors = Nothing } _ -> state - CreatingTx r@{ allInputsChoices, autoRun, inputChoices, input } -> case action of + CreatingTx r@{ allInputsChoices, newMarloweContext, environment, autoRun, inputChoices, input } -> case action of CreateTxFailed error -> CreatingTx $ r { errors = Just error } CreateTxSucceeded createTxResponse -> SigningTx - { autoRun, errors: Nothing, allInputsChoices, inputChoices, input, createTxResponse } + { autoRun, errors: Nothing, allInputsChoices, newMarloweContext, environment, inputChoices, input, createTxResponse } _ -> state SigningTx r@{ errors: Just _ } -> case action of SignTx -> SigningTx $ r { errors = Nothing } _ -> state - SigningTx r@{ allInputsChoices, autoRun, inputChoices, input, createTxResponse } -> case action of + SigningTx r@{ allInputsChoices, environment, newMarloweContext, autoRun, inputChoices, input, createTxResponse } -> case action of SignTxFailed error -> SigningTx $ r { errors = Just error } SignTxSucceeded txWitnessSet -> SubmittingTx - { autoRun, errors: Nothing, allInputsChoices, inputChoices, input, createTxResponse, txWitnessSet } + { autoRun, errors: Nothing, allInputsChoices, newMarloweContext, environment, inputChoices, input, createTxResponse, txWitnessSet } _ -> state SubmittingTx r@{ errors: Just _ } -> case action of SubmitTx -> SubmittingTx $ r { errors = Nothing } _ -> state - SubmittingTx r@{ allInputsChoices, autoRun, inputChoices, input, createTxResponse, txWitnessSet } -> case action of + SubmittingTx r@{ allInputsChoices, environment, newMarloweContext, autoRun, inputChoices, input, createTxResponse, txWitnessSet } -> case action of SubmitTxFailed error -> SubmittingTx $ r { errors = Just error } - SubmitTxSucceeded -> - InputApplied { allInputsChoices, autoRun, inputChoices, input, txWitnessSet, createTxResponse } + SubmitTxSucceeded submittedAt -> + InputApplied { allInputsChoices, environment, newMarloweContext, autoRun, inputChoices, input, txWitnessSet, createTxResponse, submittedAt } _ -> state InputApplied _ -> state @@ -278,6 +298,7 @@ data WalletRequest data RuntimeRequest = CreateTxRequest { allInputsChoices :: AllInputsChoices + , environment :: V1.Environment , inputChoices :: InputChoices , input :: Maybe V1.Input , requiredWalletContext :: RequiredWalletContext @@ -306,9 +327,10 @@ nextRequest env state = do Just $ WalletRequest $ FetchWalletContextRequest { marloweContext, cardanoMultiplatformLib, walletInfo } FetchingRequiredWalletContext { marloweContext, errors: Nothing } -> Just $ WalletRequest $ FetchWalletContextRequest { marloweContext, cardanoMultiplatformLib, walletInfo } - CreatingTx { errors: Nothing, inputChoices, input, allInputsChoices, requiredWalletContext, transactionsEndpoint } -> do + CreatingTx { errors: Nothing, inputChoices, input, allInputsChoices, environment, requiredWalletContext, transactionsEndpoint } -> do Just $ RuntimeRequest $ CreateTxRequest { allInputsChoices + , environment , inputChoices , input , requiredWalletContext @@ -323,6 +345,18 @@ nextRequest env state = do Just $ RuntimeRequest $ SubmitTxRequest { txWitnessSet, createTxResponse, serverURL } _ -> Nothing + +-- This is pretty arbitrary choice - we should keep track which inputs are relevant +-- during the further steps. +mkEnvironment :: Effect V1.Environment +mkEnvironment = do + invalidBefore <- millisecondsFromNow (Milliseconds (Int.toNumber $ (-10) * 60 * 1000)) + invalidHereafter <- millisecondsFromNow (Milliseconds (Int.toNumber $ 5 * 60 * 1000)) + let + timeInterval = V1.TimeInterval invalidBefore invalidHereafter + environment = V1.Environment { timeInterval } + pure environment + -- We want to rewrite driver logic here based on the request type requestToAffAction :: Request -> Aff Action requestToAffAction = case _ of @@ -335,14 +369,15 @@ requestToAffAction = case _ of Left err -> pure $ FetchRequiredWalletContextFailed $ show err Right Nothing -> pure $ FetchRequiredWalletContextFailed "Wallet does not have a change address" Right (Just (WalletContext { changeAddress, usedAddresses })) -> liftEffect $ do - invalidBefore <- millisecondsFromNow (Milliseconds (Int.toNumber $ (-10) * 60 * 1000)) - invalidHereafter <- millisecondsFromNow (Milliseconds (Int.toNumber $ 5 * 60 * 1000)) + -- TODO: investingate if this is good strategy. We should probably migrate to something similiar to the + -- next endpoint implementation. + environment <- mkEnvironment let { contract, state } = marloweContext - timeInterval = V1.TimeInterval invalidBefore invalidHereafter - environment = V1.Environment { timeInterval } allInputsChoices = case nextTimeoutAdvance environment state contract of - Just advanceContinuation -> Left advanceContinuation + Just advanceContinuation -> do + traceM "We computed 'advance' reduction" + Left advanceContinuation Nothing -> do let deposits = NonEmpty.fromArray $ nextDeposit environment state contract @@ -353,6 +388,7 @@ requestToAffAction = case _ of pure $ FetchRequiredWalletContextSucceeded { requiredWalletContext: { changeAddress, usedAddresses } , allInputsChoices + , environment } SignTxRequest { walletInfo, tx } -> do let @@ -361,15 +397,17 @@ requestToAffAction = case _ of Left err -> pure $ SignTxFailed $ unsafeStringify err Right txWitnessSet -> pure $ SignTxSucceeded txWitnessSet RuntimeRequest runtimeRequest -> case runtimeRequest of - CreateTxRequest { input, requiredWalletContext, serverURL, transactionsEndpoint } -> do + CreateTxRequest { input, environment, requiredWalletContext, serverURL, transactionsEndpoint } -> do let inputs = foldMap Array.singleton input - create inputs requiredWalletContext serverURL transactionsEndpoint >>= case _ of + create inputs environment requiredWalletContext serverURL transactionsEndpoint >>= case _ of Right res -> pure $ CreateTxSucceeded res Left err -> pure $ CreateTxFailed $ show err SubmitTxRequest { txWitnessSet, createTxResponse, serverURL } -> do submit txWitnessSet serverURL createTxResponse.links.transaction >>= case _ of - Right _ -> pure $ SubmitTxSucceeded + Right _ -> do + n <- liftEffect $ now + pure $ SubmitTxSucceeded n Left err -> pure $ SubmitTxFailed $ show err driver :: Env -> State -> Maybe (Aff Action) @@ -380,29 +418,21 @@ driver env state = do -- Lower level helpers create :: Array V1.Input + -> V1.Environment -> WalletAddresses -> ServerURL -> TransactionsEndpoint -> Aff (Either ClientError' { resource :: PostTransactionsResponse, links :: { transaction :: TransactionEndpoint } }) -create inputs walletAddresses serverUrl transactionsEndpoint = do - nowInstant <- liftEffect $ now - let - nowPosixMilliseconds = unInstant nowInstant - oneHour = fromDuration $ Time.Duration.Hours 1.0 - oneMinuteAgo = fromDuration $ Time.Duration.Minutes (-1.0) - inOneHourInstant = case instant (nowPosixMilliseconds <> oneHour) of - Just instant' -> instant' - Nothing -> unsafeCrashWith "Component.ApplyInputs.Machine: Failed to subtract one minute to the current time" - beforeOneHourInstant = case instant (nowPosixMilliseconds <> oneMinuteAgo) of - Just instant' -> instant' - Nothing -> unsafeCrashWith "Component.ApplyInputs.Machine: Failed to add one hour to the current time" +create inputs environment walletAddresses serverUrl transactionsEndpoint = do let + V1.Environment { timeInterval } = environment + V1.TimeInterval invalidBefore invalidHereafter = timeInterval { changeAddress, usedAddresses } = walletAddresses req = PostTransactionsRequest { metadata: mempty - , invalidBefore: toDateTime beforeOneHourInstant - , invalidHereafter: toDateTime inOneHourInstant + , invalidBefore: toDateTime invalidBefore + , invalidHereafter: toDateTime invalidHereafter -- , version :: MarloweVersion , inputs , tags: mempty -- TODO: use instead of metadata diff --git a/src/Component/ContractList.purs b/src/Component/ContractList.purs index 4f9ccc75..4a59a307 100644 --- a/src/Component/ContractList.purs +++ b/src/Component/ContractList.purs @@ -108,9 +108,15 @@ useInput initialValue = React.do type SubmissionError = String +newtype NotSyncedYetInserts = NotSyncedYetInserts + { add :: ContractInfo.ContractCreated -> Effect Unit + , update :: ContractInfo.ContractUpdated -> Effect Unit + } + type Props = { possibleContracts :: Maybe (Array SomeContractInfo) -- `Maybe` indicates if the contracts where fetched already , contractMapInitialized :: Boolean + , notSyncedYetInserts :: NotSyncedYetInserts , connectedWallet :: Maybe (WalletInfo Wallet.Api) , possibleInitialModalAction :: Maybe ModalAction } @@ -142,7 +148,7 @@ data ModalAction , initialState :: V1.State , transactionEndpoints :: Array Runtime.TransactionEndpoint } - | ApplyInputs TransactionsEndpoint ApplyInputs.Machine.MarloweContext + | ApplyInputs ContractInfo TransactionsEndpoint ApplyInputs.Machine.MarloweContext | Withdrawal WithdrawalsEndpoint (NonEmptyArray.NonEmptyArray String) TxOutRef | ContractTemplate ContractTemplate @@ -188,7 +194,6 @@ mkContractList = do MessageHub msgHubProps <- asks _.msgHub Runtime runtime <- asks _.runtime walletInfoCtx <- asks _.walletInfoCtx - slotting <- asks _.slotting createContractComponent <- CreateContract.mkComponent applyInputsComponent <- ApplyInputs.mkComponent @@ -201,7 +206,9 @@ mkContractList = do invalidBefore <- liftEffect $ millisecondsFromNow (Duration.Milliseconds (Int.toNumber $ (-10) * 60 * 1000)) invalidHereafter <- liftEffect $ millisecondsFromNow (Duration.Milliseconds (Int.toNumber $ 5 * 60 * 1000)) - liftEffect $ component "ContractList" \{ connectedWallet, possibleInitialModalAction, possibleContracts, contractMapInitialized } -> React.do + liftEffect $ component "ContractList" \props@{ connectedWallet, possibleInitialModalAction, possibleContracts, contractMapInitialized } -> React.do + let + NotSyncedYetInserts notSyncedYetInserts = props.notSyncedYetInserts possibleWalletContext <- useContext walletInfoCtx <#> map (un WalletContext <<< snd) @@ -221,8 +228,8 @@ mkContractList = do possibleContracts' = do contracts <- possibleContracts let - -- FIXME: Quick and dirty hack to display just submited contracts as first - someFutureBlockNumber = Runtime.BlockNumber 129058430 + -- FIXME: Quick and dirty hack to display just submited contracts as first - `Nothing ` is lower than `Just` + -- someFutureBlockNumber = Runtime.BlockNumber 129058430 sortedContracts = case ordering.orderBy of OrderByCreationDate -> Array.sortBy (compare `on` ContractInfo.createdAt) contracts OrderByLastUpdateDate -> Array.sortBy (compare `on` ContractInfo.updatedAt) contracts @@ -244,39 +251,30 @@ mkContractList = do contains pattern (txOutRefToString contractId) || or (map (contains pattern) tagList) filtered <|> possibleContracts' - -- pure $ if ordering.orderAsc - -- then sortedContracts - -- else Array.reverse sortedContracts - - -- isLoadingContracts :: Boolean - -- isLoadingContracts = case possibleContracts'' of - -- Nothing -> true - -- Just [] -> true - -- Just contracts -> any (\(ContractInfo { marloweInfo }) -> isNothing marloweInfo) contracts - pure $ case possibleModalAction, connectedWallet of Just (NewContract possibleInitialContract), Just cw -> createContractComponent { connectedWallet: cw , onDismiss: resetModalAction - , onSuccess: \newContractDetails -> do - -- addToNotSyncedYet (newContractDetails.contractId) (ContractCreated newContractDetails) - + , onSuccess: \contractCreated -> do msgHubProps.add $ Success $ DOOM.text $ fold [ "Successfully created and submitted the contract. Contract transaction awaits to be included in the blockchain." , "Contract status should change to 'Confirmed' at that point." ] resetModalAction + notSyncedYetInserts.add contractCreated , possibleInitialContract } - Just (ApplyInputs transactionsEndpoint marloweContext), Just cw -> do + Just (ApplyInputs contractInfo transactionsEndpoint marloweContext), Just cw -> do let - onSuccess = \_ -> do + onSuccess = \contractUpdated -> do msgHubProps.add $ Success $ DOOM.text $ fold [ "Successfully applied the inputs. Input application transaction awaits to be included in the blockchain." ] + notSyncedYetInserts.update contractUpdated resetModalAction applyInputsComponent { transactionsEndpoint + , contractInfo , marloweContext , connectedWallet: cw , onSuccess @@ -376,143 +374,113 @@ mkContractList = do else buttons ] - , case possibleContracts'' of - Nothing -> DOOM.text "Initiating contract searching..." - Just [] | not contractMapInitialized -> DOOM.text "No contracts found yet for your wallet." - Just contracts -> DOM.div { className: "row" } $ DOM.div { className: "col-12 mt-3" } do - let - tdCentered :: forall jsx. ToJSX jsx => jsx -> JSX - tdCentered = DOM.td { className: "text-center" } - tdDateTime Nothing = tdCentered $ ([] :: Array JSX) - tdDateTime (Just dateTime) = tdCentered $ Array.singleton $ DOM.small {} do - let - jsDate = JSDate.fromDateTime dateTime - [ DOOM.text $ JSDate.toLocaleDateString jsDate - , DOOM.br {} - , DOOM.text $ JSDate.toLocaleTimeString jsDate - ] - tdInstant possibleInstant = do - let - possibleDateTime = Instant.toDateTime <$> possibleInstant - tdDateTime possibleDateTime - - tdContractId contractId possibleMarloweInfo transactionEndpoints = do - let - conractIdStr = txOutRefToString contractId - - copyToClipboard :: Effect Unit - copyToClipboard = window >>= navigator >>= clipboard >>= \c -> do - launchAff_ (Promise.toAffE $ Clipboard.writeText conractIdStr c) - - tdCentered $ DOM.span { className: "d-flex" } - [ DOM.a - do - let - onClick = case possibleMarloweInfo of - Just (MarloweInfo { state, currentContract, initialContract, initialState }) -> do - setModalAction $ ContractDetails - { contract: currentContract - , state - , initialState: initialState - , initialContract: initialContract - , transactionEndpoints - } - _ -> pure unit - { className: "cursor-pointer text-decoration-none text-reset text-decoration-underline-hover truncate-text w-16rem d-inline-block" - , onClick: handler_ onClick - -- , disabled - } - [ text conractIdStr ] - , DOM.a - { href: "#" - , onClick: handler_ copyToClipboard - , className: "cursor-pointer text-decoration-none text-decoration-underline-hover text-reset" - } - $ Icons.toJSX - $ unsafeIcon "clipboard-plus ms-1 d-inline-block" + , do + let + spinner = + DOM.div + { className: "col-12 position-relative d-flex justify-content-center align-items-center blur-bg z-index-sticky" } + $ loadingSpinnerLogo {} + case possibleContracts'', contractMapInitialized of + Nothing, _ -> spinner + Just [], false -> spinner + Just [], true -> DOOM.text "No contracts found yet for your wallet." + Just contracts, _ -> DOM.div { className: "row" } $ DOM.div { className: "col-12 mt-3" } do + let + tdCentered :: forall jsx. ToJSX jsx => jsx -> JSX + tdCentered = DOM.td { className: "text-center" } + tdDateTime Nothing = tdCentered $ ([] :: Array JSX) + tdDateTime (Just dateTime) = tdCentered $ Array.singleton $ DOM.small {} do + let + jsDate = JSDate.fromDateTime dateTime + [ DOOM.text $ JSDate.toLocaleDateString jsDate + , DOOM.br {} + , DOOM.text $ JSDate.toLocaleTimeString jsDate ] - - [ table { striped: Table.striped.boolean true, hover: true } - [ DOM.thead {} do - let - orderingTh = Table.orderingHeader ordering updateOrdering - th label = DOM.th { className: "text-center text-muted" } [ label ] - [ DOM.tr {} - [ DOM.th { className: "text-center" } $ DOOM.img { src: "/images/calendar_month.svg" } - , DOM.th { className: "text-center" } $ DOOM.img { src: "/images/event_available.svg" } - , DOM.th { className: "text-center" } $ DOOM.img { src: "/images/fingerprint.svg" } - , DOM.th { className: "text-center" } $ DOOM.img { src: "/images/sell.svg" } - , DOM.th { className: "text-center" } $ DOOM.img { src: "/images/frame_65.svg" } - ] - , DOM.tr {} - [ do - let - label = DOOM.fragment [ DOOM.text "Created" ] --, DOOM.br {}, DOOM.text "(Block number)"] - orderingTh label OrderByCreationDate - , do - let - label = DOOM.fragment [ DOOM.text "Updated" ] --, DOOM.br {}, DOOM.text "(Block number)"] - orderingTh label OrderByLastUpdateDate - , DOM.th { className: "text-center w-16rem" } $ DOOM.text "Contract Id" - , th $ DOOM.text "Tags" - , th $ DOOM.text "Actions" - ] + tdInstant possibleInstant = do + let + possibleDateTime = Instant.toDateTime <$> possibleInstant + tdDateTime possibleDateTime + + tdContractId contractId possibleMarloweInfo transactionEndpoints = do + let + conractIdStr = txOutRefToString contractId + + copyToClipboard :: Effect Unit + copyToClipboard = window >>= navigator >>= clipboard >>= \c -> do + launchAff_ (Promise.toAffE $ Clipboard.writeText conractIdStr c) + + tdCentered $ DOM.span { className: "d-flex" } + [ DOM.a + do + let + onClick = case possibleMarloweInfo of + Just (MarloweInfo { state, currentContract, initialContract, initialState }) -> do + setModalAction $ ContractDetails + { contract: currentContract + , state + , initialState: initialState + , initialContract: initialContract + , transactionEndpoints + } + _ -> pure unit + { className: "cursor-pointer text-decoration-none text-reset text-decoration-underline-hover truncate-text w-16rem d-inline-block" + , onClick: handler_ onClick + -- , disabled + } + [ text conractIdStr ] + , DOM.a + { href: "#" + , onClick: handler_ copyToClipboard + , className: "cursor-pointer text-decoration-none text-decoration-underline-hover text-reset" + } + $ Icons.toJSX + $ unsafeIcon "clipboard-plus ms-1 d-inline-block" ] - , DOM.tbody {} $ contracts <#> \someContract -> do - let - createdAt = ContractInfo.createdAt someContract - updatedAt = ContractInfo.updatedAt someContract - tags = runLiteTags $ ContractInfo.someContractTags someContract - contractId = ContractInfo.someContractContractId someContract - - DOM.tr { className: "align-middle" } $ case someContract of - (SyncedConractInfo ci@(ContractInfo { _runtime, endpoints, marloweInfo })) -> do - let - ContractHeader { contractId } = _runtime.contractHeader - [ tdInstant createdAt - , tdInstant $ updatedAt <|> createdAt - , do - let - transactionEndpoints = _runtime.transactions <#> \(_ /\ transactionEndpoint) -> transactionEndpoint - tdContractId contractId marloweInfo transactionEndpoints - , tdCentered [ DOOM.text $ intercalate ", " tags ] --- , tdCentered --- [ do --- case endpoints.transactions, marloweInfo of --- Just transactionsEndpoint, Just (MarloweInfo { initialContract, state: Just state, currentContract: Just contract }) -> do --- let --- <<<<<<< HEAD --- onClick = case marloweInfo of --- Just (MarloweInfo { state, currentContract, initialContract, initialState }) -> do --- let --- transactionEndpoints = _runtime.transactions <#> \(_ /\ transactionEndpoint) -> transactionEndpoint --- setModalAction $ ContractDetails --- { contract: currentContract --- , state --- , initialState: initialState --- , initialContract: initialContract --- , transactionEndpoints --- } --- _ -> pure unit --- { className: "cursor-pointer text-decoration-none text-reset text-decoration-underline-hover truncate-text w-16rem d-inline-block" --- , onClick: handler_ onClick --- -- , disabled --- } --- [ text conractIdStr ] --- , DOM.a --- { href: "#" --- , onClick: handler_ copyToClipboard --- , className: "cursor-pointer text-decoration-none text-decoration-underline-hover text-reset" --- } --- $ Icons.toJSX --- $ unsafeIcon "clipboard-plus ms-1 d-inline-block" --- ] --- , tdCentered --- [ do --- let --- tags = runLiteTags contractTags --- DOOM.text $ intercalate ", " tags --- ] + + [ table { striped: Table.striped.boolean true, hover: true } + [ DOM.thead {} do + let + orderingTh = Table.orderingHeader ordering updateOrdering + th label = DOM.th { className: "text-center text-muted" } [ label ] + [ DOM.tr {} + [ DOM.th { className: "text-center" } $ DOOM.img { src: "/images/calendar_month.svg" } + , DOM.th { className: "text-center" } $ DOOM.img { src: "/images/event_available.svg" } + , DOM.th { className: "text-center" } $ DOOM.img { src: "/images/fingerprint.svg" } + , DOM.th { className: "text-center" } $ DOOM.img { src: "/images/sell.svg" } + , DOM.th { className: "text-center" } $ DOOM.img { src: "/images/frame_65.svg" } + ] + , DOM.tr {} + [ do + let + label = DOOM.fragment [ DOOM.text "Created" ] --, DOOM.br {}, DOOM.text "(Block number)"] + orderingTh label OrderByCreationDate + , do + let + label = DOOM.fragment [ DOOM.text "Updated" ] --, DOOM.br {}, DOOM.text "(Block number)"] + orderingTh label OrderByLastUpdateDate + , DOM.th { className: "text-center w-16rem" } $ DOOM.text "Contract Id" + , th $ DOOM.text "Tags" + , th $ DOOM.text "Actions" + ] + ] + , DOM.tbody {} $ contracts <#> \someContract -> do + let + createdAt = ContractInfo.createdAt someContract + updatedAt = ContractInfo.updatedAt someContract + tags = runLiteTags $ ContractInfo.someContractTags someContract + contractId = ContractInfo.someContractContractId someContract + + DOM.tr { className: "align-middle" } $ case someContract of + (SyncedConractInfo ci@(ContractInfo { _runtime, endpoints, marloweInfo })) -> do + let + ContractHeader { contractId } = _runtime.contractHeader + [ tdInstant createdAt + , tdInstant $ updatedAt <|> createdAt + , do + let + transactionEndpoints = _runtime.transactions <#> \(_ /\ transactionEndpoint) -> transactionEndpoint + tdContractId contractId marloweInfo transactionEndpoints + , tdCentered [ DOOM.text $ intercalate ", " tags ] , tdCentered [ do case endpoints.transactions, marloweInfo, possibleWalletContext of @@ -533,7 +501,7 @@ mkContractList = do , tooltipPlacement: Just placement.left , disabled: not $ Array.any identity $ map (\role -> canInput (V1.Role role) environment state contract) (catMaybes roles) - , onClick: setModalAction $ ApplyInputs transactionsEndpoint marloweContext + , onClick: setModalAction $ ApplyInputs ci transactionsEndpoint marloweContext } Just transactionsEndpoint, Just (MarloweInfo { initialContract, state: Just state, currentContract: Just contract }), @@ -550,7 +518,7 @@ mkContractList = do , tooltipPlacement: Just placement.left , disabled: not $ Array.any identity $ map (\addr -> canInput (V1.Address $ bech32ToString addr) environment state contract) usedAddresses - , onClick: setModalAction $ ApplyInputs transactionsEndpoint marloweContext + , onClick: setModalAction $ ApplyInputs ci transactionsEndpoint marloweContext } _, Just (MarloweInfo { state: Nothing, currentContract: Nothing }), _ -> DOOM.text "Complete" _, _, _ -> mempty @@ -585,35 +553,28 @@ mkContractList = do _ -> mempty _, _ -> mempty ] - ] - NotSyncedCreatedContract {} -> do - [ tdInstant createdAt - , tdInstant $ updatedAt <|> createdAt - , tdContractId contractId Nothing [] - , tdCentered [ DOOM.text $ intercalate ", " tags ] - , tdCentered ([] :: Array JSX) -- FIXME: Withdrawals should be still possible - ] - NotSyncedUpdatedContract { contractInfo }-> do - [ tdInstant createdAt - , tdInstant $ updatedAt <|> createdAt - , do - let - ContractInfo { _runtime } = contractInfo - transactionEndpoints = _runtime.transactions <#> \(_ /\ transactionEndpoint) -> transactionEndpoint - tdContractId contractId Nothing transactionEndpoints - , tdCentered [ DOOM.text $ intercalate ", " tags ] - , tdCentered ([] :: Array JSX) -- FIXME: Withdrawals should be still possible - ] - - ] - ] - _ -> - DOM.div - -- { className: "col-12 position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center blur-bg z-index-sticky" - { className: "col-12 position-relative d-flex justify-content-center align-items-center blur-bg z-index-sticky" - } - $ loadingSpinnerLogo - {} + ] + NotSyncedCreatedContract {} -> do + [ tdInstant createdAt + , tdInstant $ updatedAt <|> createdAt + , tdContractId contractId Nothing [] + , tdCentered [ DOOM.text $ intercalate ", " tags ] + , tdCentered ([ DOOM.text "Placeholder - CREATED" ] :: Array JSX) -- FIXME: Withdrawals should be still possible + ] + NotSyncedUpdatedContract { contractInfo }-> do + [ tdInstant createdAt + , tdInstant $ updatedAt <|> createdAt + , do + let + ContractInfo { _runtime } = contractInfo + transactionEndpoints = _runtime.transactions <#> \(_ /\ transactionEndpoint) -> transactionEndpoint + tdContractId contractId Nothing transactionEndpoints + , tdCentered [ DOOM.text $ intercalate ", " tags ] + , tdCentered ([ DOOM.text "Placeholder - UPDATED" ] :: Array JSX) -- FIXME: Withdrawals should be still possible + ] + + ] + ] ] _, _ -> mempty diff --git a/src/Component/Types/ContractInfo.purs b/src/Component/Types/ContractInfo.purs index 47ff3de2..b2a642c5 100644 --- a/src/Component/Types/ContractInfo.purs +++ b/src/Component/Types/ContractInfo.purs @@ -68,6 +68,7 @@ newtype ContractInfo = ContractInfo } } +derive instance Eq ContractInfo derive instance Newtype ContractInfo _ fetchAppliedInputs :: ServerURL -> Array Runtime.TransactionEndpoint -> Aff (V (Array (Runtime.ClientError String)) (Array ((Maybe V1.InputContent) /\ V1.TimeInterval))) @@ -103,13 +104,12 @@ type ContractCreatedDetails = newtype ContractCreated = ContractCreated ContractCreatedDetails type ContractUpdatedDetails = - { contractInfo :: ContractInfo - , ouptutContract :: V1.Contract - , outputState :: V1.State - , submittedAt :: Instant - , transactionInput :: V1.TransactionInput - , txId :: Runtime.TxId - } + { contractInfo :: ContractInfo + , transactionInput :: V1.TransactionInput + , outputContract :: V1.Contract + , outputState :: V1.State + , submittedAt :: Instant + } newtype ContractUpdated = ContractUpdated ContractUpdatedDetails @@ -139,6 +139,15 @@ data SomeContractInfo | NotSyncedCreatedContract ContractCreatedDetails | NotSyncedUpdatedContract ContractUpdatedDetails +someContractInfoFromContractInfo :: ContractInfo -> SomeContractInfo +someContractInfoFromContractInfo = SyncedConractInfo + +someContractInfoFromContractCreated :: ContractCreated -> SomeContractInfo +someContractInfoFromContractCreated (ContractCreated details) = NotSyncedCreatedContract details + +someContractInfoFromContractUpdated :: ContractUpdated -> SomeContractInfo +someContractInfoFromContractUpdated (ContractUpdated details) = NotSyncedUpdatedContract details + createdAt :: SomeContractInfo -> Maybe Instant createdAt (SyncedConractInfo (ContractInfo { createdAt: c })) = c createdAt (NotSyncedCreatedContract { submittedAt: c }) = Just c diff --git a/src/Component/Types/ContractInfoMap.purs b/src/Component/Types/ContractInfoMap.purs new file mode 100644 index 00000000..6673414f --- /dev/null +++ b/src/Component/Types/ContractInfoMap.purs @@ -0,0 +1,204 @@ +module Component.Types.ContractInfoMap + ( ContractInfoMap + , insertContractCreated + , insertContractUpdated + , updateSynced + , getContractsMap + , uninitialized + ) where + +import Prelude + +import Component.Types (ContractInfo(..)) +import Component.Types.ContractInfo (ContractCreated, ContractUpdated(..), MarloweInfo(..), NotSyncedYet(..), SomeContractInfo(..), addContractCreated, addContractUpdated, emptyNotSyncedYet, someContractInfoFromContractCreated, someContractInfoFromContractUpdated) +import Component.Types.ContractInfo as ContractInfo +import Contrib.Cardano (Slotting, slotToTimestamp) +import Data.Array as Array +import Data.DateTime.Instant (Instant) +import Data.Map (Map) +import Data.Map as Map +import Data.Maybe (Maybe(..), fromMaybe) +import Data.Set as Set +import Data.These (These, maybeThese, theseLeft, theseRight) +import Data.Tuple.Nested ((/\)) +import Language.Marlowe.Core.V1.Semantics (emptyState) as V1 +import Marlowe.Runtime.Web.Streaming (ContractWithTransactionsMap) +import Marlowe.Runtime.Web.Types (ContractHeader(..), PolicyId(..), TxHeader(..), TxOutRef(..)) +import Marlowe.Runtime.Web.Types as Runtime + +-- * The `contractsMap` contains correct final view of the contracts and is a public piece of the API +-- the `contractsSources` piece is private. +-- * The `contractsMap` creation depends on the union ordering so we have to keep only +-- the smaller `NotSyncedYet` `Map`s up to date in `contractsSources`. +-- * The `ContractWithTransactionsMap` contains all the contracts which are synced meaning +-- it could contain duplicates for some contracts from `NotSyncedYet` `Map`s. +data ContractInfoMap + = UninitializedContractInfoMap + { slotting :: Slotting + } + | ContractInfoMap + { slotting :: Slotting + , contractsSources :: These NotSyncedYet ContractWithTransactionsMap + , contractsMap :: Map Runtime.ContractId SomeContractInfo + } + +uninitialized :: Slotting -> ContractInfoMap +uninitialized slotting = UninitializedContractInfoMap { slotting } + +-- This is private constructor which doesn't check for consistency +-- all the modification should be proxied through exposed methods. +-- +-- TODO: We can optimize this by expecting `Map ContractId ContractInfo` instead +-- of `ContractWithTransactionsMap` so we don't reconstruct the map every time. +mkContractInfoMap :: Slotting -> Maybe ContractWithTransactionsMap -> Maybe NotSyncedYet -> ContractInfoMap +mkContractInfoMap slotting possiblySynced possiblyNotSyncedYet = fromMaybe (UninitializedContractInfoMap { slotting }) do + contractsSources <- maybeThese possiblyNotSyncedYet possiblySynced + let + ns = fromMaybe Map.empty do + NotSyncedYet { created, updated } <- possiblyNotSyncedYet + pure $ map someContractInfoFromContractCreated created + `Map.union` map someContractInfoFromContractUpdated updated + s = fromMaybe Map.empty do + synced <- possiblySynced + pure $ Map.catMaybes $ synced <#> \contractWithTransactions -> do + let + { contract: + { resource: contractHeader@(Runtime.ContractHeader { roleTokenMintingPolicyId, tags }) + , links: endpoints + } + , contractState + , transactions + } = contractWithTransactions + marloweInfo = do + Runtime.ContractState contractState' <- contractState + pure $ MarloweInfo + { initialContract: contractState'.initialContract + , currencySymbol: case roleTokenMintingPolicyId of + PolicyId "" -> Nothing + PolicyId policyId -> Just $ policyId + , state: contractState'.state + , currentContract: contractState'.currentContract + , initialState: V1.emptyState -- FIXME: No initial state on the API LEVEL? + , unclaimedPayouts: contractState'.unclaimedPayouts + } + Runtime.ContractHeader { contractId, block } = contractHeader + blockSlotTimestamp (Runtime.BlockHeader { slotNo }) = slotToTimestamp slotting slotNo + + createdAt :: Maybe Instant + createdAt = blockSlotTimestamp <$> block + + updatedAt :: Maybe Instant + updatedAt = do + Runtime.TxHeader tx /\ _ <- Array.head transactions + blockSlotTimestamp <$> tx.block + + pure $ SyncedConractInfo $ ContractInfo $ + { contractId + , createdAt + , updatedAt + , endpoints + , marloweInfo + , tags + , _runtime: { contractHeader, transactions } + } + pure $ ContractInfoMap { contractsMap: ns `Map.union` s, contractsSources, slotting } + +contractInfoMapSlotting :: ContractInfoMap -> Slotting +contractInfoMapSlotting = case _ of + UninitializedContractInfoMap { slotting } -> slotting + ContractInfoMap { slotting } -> slotting + +getContractsMap :: ContractInfoMap -> Maybe (Map Runtime.ContractId SomeContractInfo) +getContractsMap (UninitializedContractInfoMap _) = Nothing +getContractsMap (ContractInfoMap { contractsMap }) = Just contractsMap + +-- We want to check if the contract in the synced map is exactly the same as the contract +-- which are updating - the safest way to do equality check is to use transaction ids. +-- +-- If the contract was changed or is missing we have to ignore the update information - it is no longer relevant. +-- If it is we want to remove the contract from the synced map and add it to the not synced yet map. +-- +-- This check is rather a coner case checking (rarely happens on the app level - too short timespan) +-- but still valid from the structure consistency perspective. +isContractUpdateStillRelevant :: ContractUpdated -> Maybe ContractWithTransactionsMap -> Boolean +isContractUpdateStillRelevant contractUpdated possibleContractsMap = fromMaybe false do + let + ContractUpdated { contractInfo } = contractUpdated + ContractInfo { contractId, _runtime: runtime } = contractInfo + + contractsMap <- possibleContractsMap + contractWithTransactions <- Map.lookup contractId contractsMap + let + runtime' = do + let + { contract: { resource: contractHeader }, transactions } = contractWithTransactions + { contractHeader, transactions } + allTxIds { contractHeader: ContractHeader { contractId: TxOutRef { txId } }, transactions } = + txId Array.: (transactions <#> \(TxHeader { transactionId } /\ _) -> transactionId) + txIds = allTxIds runtime + txIds' = allTxIds runtime' + pure $ txIds == txIds' + +updateSynced :: Maybe ContractWithTransactionsMap -> ContractInfoMap -> ContractInfoMap +updateSynced possiblySynced contractInfoMap = do + let + possiblyNotSyncedYet = case contractInfoMap of + UninitializedContractInfoMap _ -> Nothing + ContractInfoMap { contractsSources } -> theseLeft contractsSources + slotting = contractInfoMapSlotting contractInfoMap + + syncedKeys = fromMaybe mempty do + synced <- possiblySynced + pure $ Map.keys synced + + possiblyNotSyncedYet' = do + NotSyncedYet { created, updated } <- possiblyNotSyncedYet + let + updated' = Map.filter (_ `isContractUpdateStillRelevant` possiblySynced) updated + + pure $ NotSyncedYet $ + { created: Map.filter (\(ContractInfo.ContractCreated { contractId }) -> contractId `not Set.member` syncedKeys) created + , updated: updated' + } + mkContractInfoMap slotting possiblySynced possiblyNotSyncedYet' + +insertContractCreated :: ContractCreated -> ContractInfoMap -> ContractInfoMap +insertContractCreated contractCreated@(ContractInfo.ContractCreated { contractId }) contractInfoMap = do + let + possiblySynced = case contractInfoMap of + UninitializedContractInfoMap _ -> Nothing + ContractInfoMap { contractsSources } -> theseRight contractsSources + + isSyncedAlready = case possiblySynced of + Nothing -> false + Just synced -> contractId `Map.member` synced + + add = + if isSyncedAlready then identity + else addContractCreated contractCreated + + notSyncedYet = add $ case contractInfoMap of + UninitializedContractInfoMap _ -> emptyNotSyncedYet + ContractInfoMap { contractsSources } -> + fromMaybe emptyNotSyncedYet $ theseLeft contractsSources + slotting = contractInfoMapSlotting contractInfoMap + mkContractInfoMap slotting possiblySynced $ Just notSyncedYet + +insertContractUpdated :: ContractUpdated -> ContractInfoMap -> ContractInfoMap +insertContractUpdated contractUpdated contractInfoMap = do + let + possiblySynced = case contractInfoMap of + UninitializedContractInfoMap _ -> Nothing + ContractInfoMap { contractsSources } -> theseRight contractsSources + + if isContractUpdateStillRelevant contractUpdated possiblySynced + then do + let + notSyncedYet = addContractUpdated contractUpdated $ case contractInfoMap of + UninitializedContractInfoMap _ -> emptyNotSyncedYet + ContractInfoMap { contractsSources } -> + fromMaybe emptyNotSyncedYet $ theseLeft contractsSources + slotting = contractInfoMapSlotting contractInfoMap + mkContractInfoMap slotting possiblySynced $ Just notSyncedYet + else + contractInfoMap diff --git a/src/Contrib/React/Basic/Hooks.purs b/src/Contrib/React/Basic/Hooks.purs index 24788e26..96251bb6 100644 --- a/src/Contrib/React/Basic/Hooks.purs +++ b/src/Contrib/React/Basic/Hooks.purs @@ -187,3 +187,19 @@ useVersionedState' a = React.do let setState = updateState <<< const pure $ currState /\ setState + +newtype UseVersionedStateWithRef a hooks = UseVersionedStateWithRef (UseStateRef Int a (UseVersionedState a hooks)) + +derive instance Newtype (UseVersionedStateWithRef a hooks) _ + +useVersionedStateWithRef + :: forall a + . a + -> Hook + (UseVersionedStateWithRef a) + ({ state :: a, version :: Int } /\ Ref a /\ ((a -> a) -> Effect Unit)) +useVersionedStateWithRef a = React.coerceHook React.do + currState /\ updateState <- useVersionedState a + stateRef <- useStateRef currState.version currState.state + pure $ currState /\ stateRef /\ updateState +