Creating Endpoints

Creating Endpoints

Now that we are confident with our smart contract, it's time that we make it accessible to end user.

The approach here would be

  1. Front-end asks to construct transaction body for the concerned operation.
  2. It then receives this transaction body, which is complete besides missing for signature for spending inputs belonging to browser wallet. It calls wallet api's signTx method upon this body to get this signature (key witness).
  3. Frontend now passes this unsigned transaction body along with the witness it received to our backend endpoint which will add this witness to the transaction body, making it complete and would then submit it.

We'll use Servant (opens in a new tab) to create our endpoints and one may understand it by following their easy to understand tutorial here (opens in a new tab).

💡

Do note that we can also sign the transactions in server using the signTx function defined in TxBody.hs (opens in a new tab)

Providing Data Provider

Defining Provider Configuration

As noted earlier, building transaction bodies require gathering suitable information from the blockchain. For this purpose, we'll require a provider. Atlas is unopinionated and allows user to plug in provider of their choice, including a locally hosted one.

Currently Atlas supports the following providers (& it would be highly appreciated if community enriches this by contributing to Atlas (opens in a new tab)):

To provide information about the provider, we will create a config.json file whose contents could be as follows:

💡

We have given a sample config.json file here (opens in a new tab).

{
  "coreProvider": { "maestroToken": "<Your-API-Key>" },
  "networkId": "testnet-preprod",
  "logging": [{ "type": { "tag": "stderr" }, "severity": "Debug", "verbosity": "V2" }],
  "utxoCacheEnable": false
}

Here is the explaination for each of the JSON keys above:

  • coreProvider: This field is the differentiating factor between different providers. Above we have given how it would look like for locally ran node & Maestro. Note that local node option still requires Maestro key for lookupDatum query.
  • networkId: Specifies your network and must be one of mainnet, testnet-preprod, testnet-preview, testnet (for legacy testnet) & privnet (for local private network).
  • logging: It's a list of scribes (opens in a new tab) to register. Its parameters (like severity, verbosity) and its general usage can be understood by going over their official haddock documentation here (opens in a new tab). Katip is also explained in this (opens in a new tab) book on web development in Haskell.
  • utxoCacheEnable: Enabling this boolean will enable cache (using Data.Cache (opens in a new tab)) whereby queries related to fetching UTxOs won't generate call to provider if the entry exists in cache (& has not yet expired).

Parsing Given Configuration

The file server-main.hs (opens in a new tab) fires up our server. It reads & parses the configuration file and using it makes our endpoints (which we will define shortly) available. Here is its entire code.

-- | Getting path for our core configuration.
parseArgs :: IO FilePath
parseArgs = do
  args <- getArgs
  case args of
    coreCfg: _ -> return coreCfg
    _invalidArgument -> fail "Error: wrong arguments, needed a path to the CoreConfig JSON configuration file\n"
 
main :: IO ()
main = do
  putStrLn "Writing Swagger file ..."
  BL8.writeFile "swagger-api.json" (encodePretty apiSwagger)
 
  putStrLn "parsing Config ..."
  coreCfgPath <- parseArgs
  coreCfg <- coreConfigIO coreCfgPath  -- Parsing our core configuration.
 
  putStrLn "Loading Providers ..."
  withCfgProviders coreCfg "api-server"  $ \providers -> do
    let port = 8081
        ctx = Ctx coreCfg providers
    putStrLn $ "Starting server at \n " <> "http://localhost:" <> show port
    run port $ app ctx
 
 
app :: Ctx -> Application
app ctx = cors (const $ Just simpleCorsResourcePolicy { corsRequestHeaders = [HttpTypes.hContentType] }) $ serve appApi $ hoistServer appApi (Handler . ExceptT . try)  $ apiServer ctx

Focussing on the highlighted lines, you can see that it first reads the path to the configuration file (you would for instance run this file like so cabal run betref-server -- config.json) in line coreCfgPath <- parseArgs, then it parses this file coreCfg <- coreConfigIO coreCfgPath.

We then see the use of an interesting function withCfgProviders. It's type is withCfgProviders :: GYCoreConfig -> GYLogNamespace -> (GYProviders -> IO a) -> IO a, thus, this function first takes our parsed configuration file, then a namespace, finally followed by a continuation GYProviders -> IO a. Idea here is that this function will setup a GYProviders from the parsed configuration file and send it to this continuation to obtain its result.

Defining Endpoints

Shared Context

📃

Entire code for it is available here (opens in a new tab)

Our endpoints would need an information for our provider, thus we have created the type for it, called Ctx. It's usage is made clear by function defined next, runQuery which in essence correspond to ctxRunC we saw in section on Integration Tests. Reasoning for runTxI & runTxF follows similarly.

Note about our handling of collateral: Browser wallets usually have the option to set for collateral, in such a case wallets would create an UTxO specifically to be used as collateral and such an UTxO will be reserved, i.e., wallet won't be spending it. CIP 40 (opens in a new tab) changed the properties related to collateral and therefore we can safely take even that UTxO as collateral which has large amounts of ada and it could also contain multiple assets. Therefore if there is no collateral set by browser wallet, framework is capable of choosing suitable UTxO as collateral (and also sets for return collateral & total collateral fields appropriately) and in that case it is also free to spend it, if required by transaction builder. But if however there is a 5-ada collateral set by wallet, then framework would use it as collateral and would also reserve it, i.e., it won't pick to spend it unless explicitly mentioned by transaction skeleton. Also note that, we'll use browser wallet's getCollateral() method to get for collateral. This method usually returns a list of ada-only UTxOs in wallet within a specific range (like in case of Nami, it is those with ada less than or equal to 50). We would send first element of this list (if exists) to backend and framework would check if the value contained in this UTxO is exactly 5 ada or not (like Nami's getCollateral method returns only a singleton list if collateral is set in wallet), if not, framework would ignore this (i.e., would not reserve for it) and would itself pick suitable UTxO as collateral. If however you want this to be reserved (& of course used as collateral) regardless of it's value, see the comment in call to runGYTxMonadNodeF in runTxF function.

-- | Our Context.
data Ctx = Ctx
  { ctxCoreCfg   :: !GYCoreConfig
  , ctxProviders :: !GYProviders
  }
 
-- | To run for simple queries, the one which don't requiring building for transaction skeleton.
runQuery :: Ctx -> GYTxQueryMonadNode a -> IO a
runQuery ctx q = do
  let nid       = cfgNetworkId $ ctxCoreCfg ctx
      providers = ctxProviders ctx
  runGYTxQueryMonadNode nid providers q
 
-- | Wraps our skeleton under `Identity` and calls `runTxF`.
runTxI :: Ctx
       -> [GYAddress]           -- ^ User's used addresses.
       -> GYAddress             -- ^ User's change address.
       -> Maybe GYTxOutRefCbor  -- ^ Browser wallet's reserved collateral (if set).
       -> GYTxMonadNode (GYTxSkeleton v)
       -> IO GYTxBody
runTxI = coerce (runTxF @Identity)
 
-- | Tries to build for given skeletons wrapped under traversable structure.
runTxF :: Traversable t
       => Ctx
       -> [GYAddress]           -- ^ User's used addresses.
       -> GYAddress             -- ^ User's change address.
       -> Maybe GYTxOutRefCbor  -- ^ Browser wallet's reserved collateral (if set).
       -> GYTxMonadNode (t (GYTxSkeleton v))
       -> IO (t GYTxBody)
runTxF ctx addrs addr collateral skeleton  = do
  let nid       = cfgNetworkId $ ctxCoreCfg ctx
      providers = ctxProviders ctx
  runGYTxMonadNodeF GYRandomImproveMultiAsset nid providers addrs addr
    (collateral >>=
      (\c -> Just (getTxOutRefHex c,
                   True  -- Make this as `False` to not do 5-ada-only check for value in this given UTxO to be used as collateral.
                  )
      )
    ) skeleton

Submit Endpoint

📃

Entire code for it is available here (opens in a new tab)

We'll soon see endpoints which will return for unsigned transaction to the browser but assuming that we already have a unsigned transaction CBOR & the missing signature, let's see how we can define an endpoint which will add this missing key witness to the transaction body and would then submit it using our provider.

Input to this endpoint is a type AddWitAndSubmitParams encapsulating our unsigned transaction body & missing key witness.

Then we have our function handleAddWitAndSubmitTx which adds the witness to the transaction making it complete and then it submits it. The response generated here is of type SubmitTxResponse and you can modify the same to include other fields if required.

-- | Return type of API when submitting a transaction.
data SubmitTxResponse = SubmitTxResponse
                          { submitTxFee :: !Integer
                          , submitTxId  :: !GYTxId
                          } deriving (Show, Generic, ToJSON, Swagger.ToSchema)
 
-- | Input parameters to add for reference script.
data AddWitAndSubmitParams = AddWitAndSubmitParams
  { awasTxUnsigned :: !GYTx
  , awasTxWit      :: !GYTxWitness
  } deriving (Generic, FromJSON, Swagger.ToSchema)
 
-- | Construct `SubmitTxResponse` return type from the given signed transaction body.
txBodySubmitTxResponse :: GYTxBody -> SubmitTxResponse
txBodySubmitTxResponse txBody = SubmitTxResponse
                                  { submitTxFee = txBodyFee txBody
                                  , submitTxId  = txBodyTxId txBody
                                  }
 
-- | Type for our Servant API.
type TxAPI =
      "add-wit-and-submit"
    :> ReqBody '[JSON] AddWitAndSubmitParams
    :> Post '[JSON] SubmitTxResponse
 
-- | Serving our API.
handleTx :: Ctx -> ServerT TxAPI IO
handleTx = handleAddWitAndSubmitTx
 
-- | Handle for adding key witness to the unsigned transaction & then submit it.
handleAddWitAndSubmitTx :: Ctx -> AddWitAndSubmitParams -> IO SubmitTxResponse
handleAddWitAndSubmitTx ctx AddWitAndSubmitParams{..} = do
  let txBody = getTxBody awasTxUnsigned
  void $ gySubmitTx (ctxProviders ctx) $ makeSignedTransaction awasTxWit txBody
  return $ txBodySubmitTxResponse txBody

Transaction Building Endpoints

📃

Entire code for it is available here (opens in a new tab)

At this point, it should be easy to follow the code here. We first define the input type for our endpoint, we also derive its FromJSON instance so that we can parse it from JSON that our front-end will send for it and we also derive its Swagger.ToSchema instance so as to document our endpoint. Then our endpoint calls the relevant operation which we defined before to get transactoin skeleton, using which we obtain the transaction body with the help of functions such as runTxI and return the result (wrapped in our UnsignedTxResponse type).

💡

You can see that all of our endpoints here ask for a list of used addresses, this makes them compatible with wallets that are not in single address mode (by default) such as Eternl (opens in a new tab).

-- | Input wrapper around corresponding Plutus type.
data BetRefParams = BetRefParams
  { brpOracleAddress :: !GYAddress
  , brpBetUntil      :: !GYTime
  , brpBetReveal     :: !GYTime
  , brpBetStep       :: !GYValue
  } deriving (Show, Generic, FromJSON, Swagger.ToSchema)
 
-- | Convert the above `BetRefParams` with corresponding representation defined in our Plutus validator script.
betParamsToScript :: BetRefParams -> Script.BetRefParams
betParamsToScript brp = Script.BetRefParams
  { Script.brpOraclePkh = pubKeyHashToPlutus $ fromJust $ addressToPubKeyHash $ brpOracleAddress brp
  , Script.brpBetUntil = timeToPlutus $  brpBetUntil brp
  , Script.brpBetReveal = timeToPlutus $ brpBetReveal brp
  , Script.brpBetStep = valueToPlutus $  brpBetStep brp
  }
 
-- | Input parameters for place bet operation.
data PlaceBetRefParams = PlaceBetRefParams
  { pbrUsedAddrs  :: ![GYAddress]
  , pbrChangeAddr :: !GYAddress
  , pbrCollateral :: !(Maybe GYTxOutRefCbor)
  , pbrBetParams  :: !BetRefParams
  , pbrBetGuess   :: !Integer
  , pbrBetAmt     :: !GYValue
  , pbrRefScript  :: !GYTxOutRef
  , pbrPrevBetRef :: !(Maybe GYTxOutRef)
  } deriving (Show, Generic, FromJSON, Swagger.ToSchema)
 
-- | Input parameters for take bets operation.
data TakeBetRefParams = TakeBetRefParams
  { tbrUsedAddrs         :: ![GYAddress]
  , tbrChangeAddr        :: !GYAddress
  , tbrCollateral        :: !(Maybe GYTxOutRefCbor)
  , tbrBetParams         :: !BetRefParams
  , tbrRefScript         :: !GYTxOutRef
  , tbrPrevBetRef        :: !GYTxOutRef
  , tbrOracleRefInputRef :: !GYTxOutRef
  } deriving (Show, Generic, FromJSON, Swagger.ToSchema)
 
-- | Input parameters to add for reference script.
data AddRefScriptParams = AddRefScriptParams
  { arsUsedAddrs  :: ![GYAddress]
  , arsChangeAddr :: !GYAddress
  , arsCollateral :: !(Maybe GYTxOutRefCbor)
  , arsPutAddress :: !GYAddress
  , arsBetParams  :: !BetRefParams
  } deriving (Show, Generic, FromJSON, Swagger.ToSchema)
 
-- | Input parameters to add for reference input.
data AddRefInputParams = AddRefInputParams
  { ariUsedAddrs  :: ![GYAddress]
  , ariChangeAddr :: !GYAddress
  , ariCollateral :: !(Maybe GYTxOutRefCbor)
  , ariPutAddress :: !GYAddress
  , ariBetAnswer  :: !Integer
  } deriving (Show, Generic, FromJSON, Swagger.ToSchema)
 
-- | Return type for our API endpoints defined here.
data UnsignedTxResponse = UnsignedTxResponse
  { urspTxBodyHex :: !T.Text              -- ^ Unsigned transaction cbor.
  , urspTxFee     :: !(Maybe Integer)     -- ^ Tx fees.
  , urspUtxoRef   :: !(Maybe GYTxOutRef)  -- ^ Some operations might need to show for relevant UTxO generated.
  } deriving (Show, Generic, FromJSON, ToJSON, Swagger.ToSchema)
 
-- | Construct `UnsignedTxResponse` return type for our endpoint given the transaction body & relevant index for UTxO (if such exists).
unSignedTxWithFee :: GYTxBody -> Maybe GYTxOutRef -> UnsignedTxResponse
unSignedTxWithFee txBody mUtxoRef = UnsignedTxResponse
  { urspTxBodyHex  = T.pack $ txToHex $ unsignedTx txBody
  , urspTxFee      = Just $ txBodyFee txBody
  , urspUtxoRef    = mUtxoRef
  }
 
-- | Type for our Servant API.
type BetRefApi =
       "place"
    :> ReqBody '[JSON] PlaceBetRefParams
    :> Post    '[JSON] UnsignedTxResponse
  :<|> "take"
    :> ReqBody '[JSON] TakeBetRefParams
    :> Post    '[JSON] UnsignedTxResponse
  :<|> "add-ref-script"
    :> ReqBody  '[JSON] AddRefScriptParams
    :> Post     '[JSON] UnsignedTxResponse
  :<|> "add-ref-input"
    :> ReqBody  '[JSON] AddRefInputParams
    :> Post     '[JSON] UnsignedTxResponse
 
-- | Serving our API.
handleBetRefApi :: Ctx -> ServerT BetRefApi IO
handleBetRefApi ctx =   handlePlaceBet ctx
                   :<|> handleTakeBet ctx
                   :<|> handleAddRefScript ctx
                   :<|> handleOracleRefInput ctx
 
-- | Handle for place bet operation.
handlePlaceBet :: Ctx -> PlaceBetRefParams -> IO UnsignedTxResponse
handlePlaceBet ctx PlaceBetRefParams{..} = do
  let brp = betParamsToScript pbrBetParams
  validatorAddress <- runQuery ctx (betRefAddress brp)
  txBody <- runTxI ctx pbrUsedAddrs pbrChangeAddr pbrCollateral
              $ placeBet pbrRefScript (betParamsToScript pbrBetParams) (Script.OracleAnswerDatum pbrBetGuess) pbrBetAmt (head pbrUsedAddrs) pbrPrevBetRef
  placeUtxoRef <- case find (\utxo -> utxoAddress utxo == validatorAddress) $ utxosToList $ txBodyUTxOs txBody of
        Nothing -> fail "Shouldn't happen: No reference for placed bet in body"
        Just utxo -> pure $ utxoRef utxo
  pure $ unSignedTxWithFee txBody $ Just placeUtxoRef
 
-- | Handle for take bets operation.
handleTakeBet :: Ctx -> TakeBetRefParams -> IO UnsignedTxResponse
handleTakeBet ctx TakeBetRefParams{..} = do
  txBody <- runTxI ctx tbrUsedAddrs tbrChangeAddr tbrCollateral
              $ takeBets tbrRefScript (betParamsToScript tbrBetParams) tbrPrevBetRef (head tbrUsedAddrs) tbrOracleRefInputRef
  pure $ unSignedTxWithFee txBody Nothing
 
-- | Handle for adding reference script.
handleAddRefScript :: Ctx -> AddRefScriptParams  -> IO UnsignedTxResponse
handleAddRefScript  ctx AddRefScriptParams{..} = do
  let validator = betRefValidator' (betParamsToScript arsBetParams)
  txBody <- runTxI ctx arsUsedAddrs arsChangeAddr arsCollateral
              $ pure $ addRefScript' arsPutAddress validator
  let refs   = Limbo.findRefScriptsInBody txBody
  outRef <- case Map.lookup (Some (validatorToScript validator)) refs of
                 Nothing  -> fail "Shouldn't happen: No reference for added Script in body"
                 Just ref -> return ref
  pure $ unSignedTxWithFee txBody $ Just outRef
 
-- | Handle for adding reference input.
handleOracleRefInput :: Ctx -> AddRefInputParams  -> IO UnsignedTxResponse
handleOracleRefInput  ctx AddRefInputParams{..} = do
  let ourDatumPlutus = Script.OracleAnswerDatum ariBetAnswer
      ourDatumGY = datumFromPlutusData ourDatumPlutus
  txBody <- runTxI ctx ariUsedAddrs ariChangeAddr ariCollateral
              $ pure $ addRefInput' ariPutAddress ourDatumPlutus
  let utxos = utxosToList $ txBodyUTxOs txBody
      ourDatumHash = hashDatum ourDatumGY
      mRefInputUtxo = find (\utxo ->
        case utxoOutDatum utxo of
          GYOutDatumHash dh  -> ourDatumHash == dh
          GYOutDatumInline d -> ourDatumGY == d
          GYOutDatumNone     -> False
        ) utxos
  case mRefInputUtxo of
    Nothing               -> fail "Shouldn't happen: Couldn't find the desired UTxO in Tx outputs"
    Just GYUTxO {utxoRef} -> pure $ unSignedTxWithFee txBody $ Just utxoRef

Wrap-Up

Our both the endpoints file (transaction submition & transaction building) our wrapped up in our Api.hs (opens in a new tab) following the usual servant boilerplate.

-- | Type for our Servant API.
type Api =
        "tx"  :> TxAPI
  :<|>  "betref" :>  BetRefApi
 
appApi :: Proxy Api
appApi = Proxy
 
apiSwagger  :: Swagger
apiSwagger  = toSwagger appApi
 
apiServer :: Ctx -> ServerT Api IO
apiServer ctx =
       handleTx ctx
  :<|> handleBetRefApi ctx

Now coming back to our server-main.hs (opens in a new tab) file, we can now understand the highlighted code sections which relates to obtaining the Swagger file (generated from apiSwagger function above) and running up our servant server.

💡

We follow simpleCorsResourcePolicy (also allowing Content-Type request header) so that calls by our front-end (which runs on different origin) don't get blocked.

-- | Getting path for our core configuration.
parseArgs :: IO FilePath
parseArgs = do
  args <- getArgs
  case args of
    coreCfg: _ -> return coreCfg
    _invalidArgument -> fail "Error: wrong arguments, needed a path to the CoreConfig JSON configuration file\n"
 
main :: IO ()
main = do
  putStrLn "Writing Swagger file ..."
  BL8.writeFile "swagger-api.json" (encodePretty apiSwagger)
 
  putStrLn "parsing Config ..."
  coreCfgPath <- parseArgs
  coreCfg <- coreConfigIO coreCfgPath  -- Parsing our core configuration.
 
  putStrLn "Loading Providers ..."
  withCfgProviders coreCfg "api-server"  $ \providers -> do
    let port = 8081
        ctx = Ctx coreCfg providers
    putStrLn $ "Starting server at \n " <> "http://localhost:" <> show port
    run port $ app ctx
 
 
app :: Ctx -> Application
app ctx = cors (const $ Just simpleCorsResourcePolicy { corsRequestHeaders = [HttpTypes.hContentType] }) $ serve appApi $ hoistServer appApi (Handler . ExceptT . try)  $ apiServer ctx

Next we'll see how to call these endpoints in our front-end!