Skip to main content

Tạo Endpoint

Bây giờ chúng ta đã tự tin với hợp đồng thông minh của mình, đã đến lúc chúng ta cung cấp cho người dùng cuối khả năng truy cập hợp đồng này.

Cách tiếp cận ở đây là giao diện người dùng trước tiên sẽ hỏi chúng ta về nội dung giao dịch chưa được ký bằng cách cung cấp cho chúng ta các tham số liên quan cần thiết để xây dựng nó. Sau đó, giao diện người dùng sẽ yêu cầu phần thân giao dịch này được đăng nhập trong trình duyệt để lấy phần thân giao dịch đã ký mà sau đó nó sẽ gửi thông qua Endpoint của ví trình duyệt hoặc thông qua việc gọi Endpoint gửi mà chúng ta sẽ xác định ở đây.

Chúng ta sẽ sử dụng Servant để tạo các Endpoint của chúng ta và người ta có thể hiểu nó bằng cách làm theo hướng dẫn dễ hiểu của họ tại đây.

note

Xin lưu ý rằng chúng ta cũng có thể ký các giao dịch trong máy chủ bằng chức signTxnăng được xác định trongTxBody.hs

Cung cấp nhà cung cấp dữ liệu

Xác định cấu hình nhà cung cấp

Như đã lưu ý trước đó, việc xây dựng các cơ quan giao dịch yêu cầu thu thập thông tin phù hợp từ chuỗi khối. Vì mục đích này, chúng ta sẽ yêu cầu một nhà cung cấp. Atlas không có ý kiến ​​và cho phép người dùng kết nối nhà cung cấp mà họ chọn, bao gồm cả nhà cung cấp được lưu trữ cục bộ.

Hiện tại Atlas hỗ trợ các nhà cung cấp sau (& sẽ được đánh giá cao nếu cộng đồng làm phong phú điều này bằng cách đóng góp cho Atlas) :

  • Maestro.
  • Node được chạy cục bộ.
  • Cardano DB Sync (nhưng chỉ dành cho thử nghiệm mạng riêng).

Để cung cấp thông tin về nhà cung cấp, chúng ta sẽ tạo một config.jsontệp có nội dung như sau:

note

Chúng ta đã đưa ra một config.jsontập tin mẫu ở đây.

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

Dưới đây là phần giải thích cho từng khóa JSON ở trên:

  • coreProvider: Trường này là yếu tố phân biệt giữa các nhà cung cấp khác nhau. Ở trên, chúng ta đã đưa ra giao diện của Node & Maestro được chạy cục bộ. Lưu ý rằng tùy chọn Node cục bộ vẫn yêu cầu khóa Maestro cho lookupDatumtruy vấn.
  • networkId: Chỉ định mạng của bạn và phải là một trong mainnettestnet-preprodtestnet-previewtestnet(đối với mạng thử nghiệm kế thừa) & privnet(đối với mạng riêng cục bộ).
  • logging: Đó là danh sách những người ghi chép đăng ký. Các thông số của nó (như severityverbosity) và cách sử dụng chung của nó có thể được hiểu bằng cách xem qua tài liệu về cá tuyết chấm đen chính thức của họ tại đây. Katip cũng được giải thích trong này cuốn sách về phát triển web trong Haskell.
  • utxoCacheEnable: Kích hoạt boolean này sẽ kích hoạt bộ đệm (sử dụngData.Cache) theo đó các truy vấn liên quan đến việc tìm nạp UTxO sẽ không tạo lệnh gọi tới nhà cung cấp nếu mục nhập tồn tại trong bộ đệm (& chưa hết hạn).

Phân tích cấu hình đã cho

Tập tinserver-main.hs kích hoạt máy chủ của chúng ta. Nó đọc và phân tích cú pháp tệp cấu hình và sử dụng nó để làm cho các Endpoint của chúng ta (mà chúng ta sẽ xác định ngay sau đây) khả dụng. Đây là toàn bộ mã của nó.

-- | 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

Tập trung vào các dòng được đánh dấu, bạn có thể thấy rằng trước tiên nó đọc đường dẫn đến tệp cấu hình (chẳng hạn như bạn sẽ chạy tệp này như vậy cabal run betref-server -- config.json) theo dòng , sau đó nó phân tích cú pháp tệp này .coreCfgPath <- parseArgs``coreCfg <- coreConfigIO coreCfgPath

Sau đó, chúng ta thấy việc sử dụng một chức năng thú vị withCfgProviders. Đó là loại là , do đó, chức năng này đầu tiên lấy tệp cấu hình được phân tích cú pháp của chúng ta, sau đó là một không gian tên, cuối cùng theo sau là phần tiếp theo . Ý tưởng ở đây là chức năng này sẽ thiết lập một từ tệp cấu hình được phân tích cú pháp và gửi nó đến phần tiếp theo này để lấy kết quả của nó.withCfgProviders::GYCoreConfig->GYLogNamespace-> (GYProviders->IO a)  ->IO a``GYProviders -> IO a``GYProviders

Xác định Endpoint

Bối cảnh được chia sẻ

note

Toàn bộ mã cho nó có sẵn ở đây

Các Endpoint của chúng ta sẽ cần một thông tin cho nhà cung cấp của chúng ta, do đó chúng ta đã tạo loại cho nó, được gọi là Ctx. Việc sử dụng nó được làm rõ bằng chức năng được xác định tiếp theo, runQueryvề bản chất tương ứng với ctxRunCchúng ta đã thấy trong phần Kiểm tra tích hợp  . Lập luận cho runTxIrunTxFrunTxWithStrategyFtheo sau tương tự.

-- | 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
-> GYTxOutRefCbor -- ^ user's collateral
-> 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
-> GYTxOutRefCbor -- ^ user's collateral
-> GYTxMonadNode (t (GYTxSkeleton v))
-> IO (t GYTxBody)
runTxF = runTxWithStrategyF GYRandomImproveMultiAsset

-- | Create 'TxBody' from a 'GYTxSkeleton', with the specified coin selection strategy.
runTxWithStrategyF :: Traversable t
=> GYCoinSelectionStrategy
-> Ctx
-> [GYAddress]
-> GYAddress
-> GYTxOutRefCbor
-> GYTxMonadNode (t (GYTxSkeleton v))
-> IO (t GYTxBody)
runTxWithStrategyF cstrat ctx addrs addr collateral skeleton = do
let nid = cfgNetworkId $ ctxCoreCfg ctx
providers = ctxProviders ctx
runGYTxMonadNodeF cstrat nid providers addrs addr (getTxOutRefHex collateral) skeleton

Gửi Endpoint

note

Toàn bộ mã cho nó có sẵn ở đây

Chúng ta sẽ sớm thấy các Endpoint sẽ trả về giao dịch chưa ký cho trình duyệt nhưng giả sử rằng chúng ta đã có CBOR giao dịch đã ký, hãy xem cách chúng ta có thể xác định Endpoint để gửi nó bằng nhà cung cấp của mình.

Tập trung vào handleSubmitTxchức năng, chúng ta thấy rằng trước tiên chúng ta phân tích cú pháp văn bản CBOR thu được trong GYTxloại của chúng ta, sau đó chúng ta chỉ cần gọi thao tác gửi cho nhà cung cấp của chúng ta. Sau đó, chúng ta gửi lại phản hồi cho giao diện người dùng của chúng ta biết về phí giao dịch (mặc dù nó đã biết khi thực hiện thao tác ký, nhưng các trường khác cũng có thể được thêm vào đây tùy thuộc vào trường hợp sử dụng) và id giao dịch.

note
-- | Return type of API when submitting a transaction.
data SubmitTxResponse = SubmitTxResponse
{ submitTxFee :: !Integer
, submitTxId :: !GYTxId
} deriving (Show, Generic, ToJSON, 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 =
"submit" :> ReqBody '[JSON] GYTx :> Post '[JSON] SubmitTxResponse

-- | Serving our API.
handleTx :: Ctx -> ServerT TxAPI IO
handleTx = handleSubmitTx

-- | Handle for submit operation.
handleSubmitTx :: Ctx -> GYTx -> IO SubmitTxResponse
handleSubmitTx ctx tx = do
void (gySubmitTx (ctxProviders ctx) tx)
return $ txBodySubmitTxResponse (getTxBody tx)

Endpoint xây dựng giao dịch

note

Toàn bộ mã cho nó có sẵn ở đây

Tại thời điểm này, thật dễ dàng để làm theo mã ở đây. Trước tiên, chúng ta xác định loại đầu vào cho Endpoint của mình, chúng ta cũng lấy được FromJSONphiên bản của nó để chúng ta có thể phân tích cú pháp từ JSON mà giao diện người dùng của chúng ta sẽ gửi cho nó và chúng ta cũng lấy được Swagger.ToSchemaphiên bản của nó để ghi lại Endpoint của chúng ta. Sau đó, Endpoint của chúng ta gọi thao tác có liên quan mà chúng ta đã xác định trước đó để lấy khung giao dịch, sử dụng thao tác này để lấy phần thân giao dịch với sự trợ giúp của các hàm chẳng hạn như runTxIvà trả về kết quả (được bao bọc trong UnsignedTxResponseloại của chúng ta).

note

Bạn có thể thấy rằng tất cả các Endpoint của chúng ta ở đây đều yêu cầu danh sách các địa chỉ đã sử dụng, điều này làm cho chúng tương thích với các ví không ở chế độ địa chỉ đơn (theo mặc định) , chẳng hạn như Eternl.

-- | 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 :: !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 :: !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 :: !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 :: !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.
, urspUtxoRefIdx :: !(Maybe Word) -- ^ Some operations might need to show for relevant UTxO generated, this index will let UI know of it. Note that Transaction ID would change after getting signed.
} 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 Word -> UnsignedTxResponse
unSignedTxWithFee txBody mUtxoRefIdx = UnsignedTxResponse
{ urspTxBodyHex = T.pack $ txToHex $ unsignedTx txBody
, urspTxFee = Just $ txBodyFee txBody
, urspUtxoRefIdx = mUtxoRefIdx
}

-- | 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 $ snd $ txOutRefToTuple 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 $ snd $ txOutRefToTuple 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 $ snd $ txOutRefToTuple utxoRef)

Đóng gói lại

Cả hai tệp Endpoint của chúng ta (gửi giao dịch & xây dựng giao dịch) được gói gọn trongApi.hs theo khuôn mẫu đầy tớ thông thường.

-- | 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

Bây giờ trở lại với tệp server-main.hs, giờ đây chúng ta có thể hiểu các phần mã được đánh dấu liên quan đến việc lấy tệp Swagger (được tạo từ apiSwaggerchức năng ở trên) và chạy máy chủ phục vụ của chúng ta.

note

Chúng ta tuân theo simpleCorsResourcePolicy(cũng cho phép Content-Typetiêu đề yêu cầu) để các cuộc gọi từ giao diện người dùng của chúng ta (chạy trên nguồn gốc khác) không bị chặn.

-- | 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

Tiếp theo, chúng ta sẽ xem cách gọi các Endpoint này trong giao diện người dùng của chúng ta!


Picture