Skip to content

Commit df304ad

Browse files
committed
chore: add unit tests for multi-asset seize
1 parent f43a6a6 commit df304ad

File tree

8 files changed

+182
-49
lines changed

8 files changed

+182
-49
lines changed

src/examples/regulated-stablecoin/lib/Wst/Offchain/BuildTx/ProgrammableLogic.hs

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,22 @@ module Wst.Offchain.BuildTx.ProgrammableLogic
1111
where
1212

1313
import Cardano.Api qualified as C
14+
import Cardano.Api.Internal.Tx.Body qualified as C
1415
import Cardano.Api.Shelley qualified as C
1516
import Control.Lens ((^.))
17+
import Control.Lens qualified as L
18+
import Control.Monad (forM_)
1619
import Control.Monad.Reader (MonadReader, asks)
17-
import Convex.BuildTx (MonadBuildTx, addReference, addWithdrawalWithTxBody,
18-
buildScriptWitness, findIndexReference,
19-
findIndexSpending, prependTxOut, spendPlutusInlineDatum)
20+
import Convex.BuildTx (MonadBuildTx, addOutput, addReference,
21+
addWithdrawalWithTxBody, buildScriptWitness,
22+
findIndexReference, findIndexSpending,
23+
spendPlutusInlineDatum)
2024
import Convex.CardanoApi.Lenses as L
2125
import Convex.Class (MonadBlockchain (queryNetworkId))
2226
import Convex.PlutusLedger.V1 (transPolicyId)
2327
import Convex.Utils qualified as Utils
2428
import Data.Foldable (find)
25-
import Data.List (partition)
29+
import Data.List (findIndex, partition)
2630
import Data.Maybe (fromJust)
2731
import GHC.Exts (IsList (..))
2832
import PlutusLedgerApi.V3 (CurrencySymbol (..))
@@ -44,65 +48,84 @@ import Wst.Offchain.Query (UTxODat (..))
4448
NOTE: Seems the issuer is only able to seize 1 UTxO at a time.
4549
In the future we should allow multiple UTxOs in 1 Tx.
4650
-}
47-
seizeProgrammableToken :: forall a env era m. (MonadReader env m, Env.HasDirectoryEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m) => UTxODat era ProgrammableLogicGlobalParams -> UTxODat era a -> C.PolicyId -> [UTxODat era DirectorySetNode] -> m ()
48-
seizeProgrammableToken UTxODat{uIn = paramsTxIn} UTxODat{uIn = seizingTxIn, uOut = seizingTxOut} seizingTokenPolicyId directoryList = Utils.inBabbage @era $ do
51+
seizeProgrammableToken ::
52+
forall a env era m.
53+
( MonadReader env m,
54+
Env.HasDirectoryEnv env,
55+
C.IsBabbageBasedEra era,
56+
MonadBlockchain era m,
57+
C.HasScriptLanguageInEra C.PlutusScriptV3 era,
58+
MonadBuildTx era m
59+
) =>
60+
UTxODat era ProgrammableLogicGlobalParams ->
61+
[UTxODat era a] ->
62+
C.PolicyId ->
63+
[UTxODat era DirectorySetNode] ->
64+
m ()
65+
seizeProgrammableToken UTxODat{uIn = paramsTxIn} seizingUTxOs seizingTokenPolicyId directoryList = Utils.inBabbage @era $ do
4966
nid <- queryNetworkId
5067
globalStakeScript <- asks (Env.dsProgrammableLogicGlobalScript . Env.directoryEnv)
5168
baseSpendingScript <- asks (Env.dsProgrammableLogicBaseScript . Env.directoryEnv)
5269

5370
let globalStakeCred = C.StakeCredentialByScript $ C.hashScript $ C.PlutusScript C.PlutusScriptV3 globalStakeScript
71+
programmableLogicBaseCredential = C.PaymentCredentialByScript $ C.hashScript $ C.PlutusScript C.PlutusScriptV3 baseSpendingScript
5472

5573
-- Finds the directory node entry that references the programmable token symbol
5674
dirNodeRef <-
5775
maybe (error "Cannot seize non-programmable token. Entry does not exist in directoryList") (pure . uIn) $
5876
find (isNodeWithProgrammableSymbol (transPolicyId seizingTokenPolicyId)) directoryList
5977

6078
-- destStakeCred <- either (error . ("Could not unTrans credential: " <>) . show) pure $ unTransStakeCredential $ transCredential seizeDestinationCred
61-
let
62-
-- issuerDestinationAddress = C.makeShelleyAddressInEra C.shelleyBasedEra nid progLogicBaseCred (C.StakeAddressByValue destStakeCred)
63-
64-
(seizedAddr, remainingValue) = case seizingTxOut of
65-
(C.TxOut a v _ _) ->
66-
let (_seized, other) =
67-
partition
68-
( \case
69-
(C.AdaAssetId, _q) -> False
70-
(C.AssetId a_ _, _q) -> a_ == seizingTokenPolicyId
71-
)
72-
$ toList $ C.txOutValueToValue v
73-
in (a, fromList other)
74-
75-
remainingTxOutValue = C.TxOutValueShelleyBased C.shelleyBasedEra $ C.toLedgerValue @era C.maryBasedEra remainingValue
7679

77-
seizedOutput = C.TxOut seizedAddr remainingTxOutValue C.TxOutDatumNone C.ReferenceScriptNone
80+
forM_ seizingUTxOs $ \UTxODat{uIn = seizingTxIn, uOut = seizingTxOut} -> do
81+
spendPlutusInlineDatum seizingTxIn baseSpendingScript ()
82+
let (seizedAddr, remainingValue, seizedDatum, referenceScript) = case seizingTxOut of
83+
(C.TxOut a v dat refScript) ->
84+
let (_seized, other) =
85+
partition
86+
( \case
87+
(C.AdaAssetId, _q) -> False
88+
(C.AssetId a_ _, _q) -> a_ == seizingTokenPolicyId
89+
)
90+
$ toList $ C.txOutValueToValue v
91+
in (a, fromList other, dat, refScript)
92+
remainingTxOutValue = C.TxOutValueShelleyBased C.shelleyBasedEra $ C.toLedgerValue @era C.maryBasedEra remainingValue
93+
seizedOutput = C.TxOut seizedAddr remainingTxOutValue seizedDatum referenceScript
94+
addOutput (C.fromCtxUTxOTxOut seizedOutput)
7895

96+
let
7997
-- Finds the index of the directory node reference in the transaction ref
8098
-- inputs
8199
directoryNodeReferenceIndex txBody =
82100
fromIntegral @Int @Integer $ findIndexReference dirNodeRef txBody
83101

84102
-- Finds the index of the issuer input in the transaction body
85103
seizingInputIndex txBody =
86-
fromIntegral @Int @Integer $ findIndexSpending seizingTxIn txBody
87-
88-
-- Finds the index of the issuer seized output in the transaction body
89-
seizingOutputIndex txBody =
90-
fromIntegral @Int @Integer $ fst $ fromJust (find ((== seizedOutput) . snd ) $ zip [0 ..] $ txBody ^. L.txOuts)
104+
map (\UTxODat{uIn = seizingTxIn} -> fromIntegral @Int @Integer $ findIndexSpending seizingTxIn txBody) seizingUTxOs
105+
106+
-- Finds the index of the first output to the programmable logic base credential
107+
firstSeizeContinuationOutputIndex txBody =
108+
fromIntegral @Int @Integer $
109+
fromJust $
110+
findIndex
111+
( maybe False ((== programmableLogicBaseCredential) . C.fromShelleyPaymentCredential)
112+
. L.preview (L._TxOut . L._1 . L._AddressInEra . L._Address . L._2)
113+
)
114+
(txBody ^. L.txOuts)
91115

92116
-- The seizing redeemer for the global script
93117
programmableLogicGlobalRedeemer txBody =
94118
SeizeAct
95-
{ plgrSeizeInputIdx = seizingInputIndex txBody,
96-
plgrSeizeOutputIdx = seizingOutputIndex txBody,
97-
plgrDirectoryNodeIdx = directoryNodeReferenceIndex txBody
119+
{ plgrDirectoryNodeIdx = directoryNodeReferenceIndex txBody,
120+
plgrInputIdxs = seizingInputIndex txBody,
121+
plgrOutputsStartIdx = firstSeizeContinuationOutputIndex txBody,
122+
plgrLengthInputIdxs = fromIntegral @Int @Integer $ length seizingUTxOs
98123
}
99124

100125
programmableGlobalWitness txBody = buildScriptWitness globalStakeScript C.NoScriptDatumForStake (programmableLogicGlobalRedeemer txBody)
101126

102-
prependTxOut seizedOutput
103127
addReference paramsTxIn -- Protocol Params TxIn
104128
addReference dirNodeRef -- Directory Node TxIn
105-
spendPlutusInlineDatum seizingTxIn baseSpendingScript () -- Redeemer is ignored in programmableLogicBase
106129
addWithdrawalWithTxBody -- Add the global script witness to the transaction
107130
(C.makeStakeAddress nid globalStakeCred)
108131
(C.Quantity 0)

src/examples/regulated-stablecoin/lib/Wst/Offchain/BuildTx/TransferLogic.hs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module Wst.Offchain.BuildTx.TransferLogic
99
issueSmartTokens,
1010
SeizeReason(..),
1111
seizeSmartTokens,
12+
multiSeizeSmartTokens,
1213
initBlacklist,
1314
BlacklistReason(..),
1415
insertBlacklistNode,
@@ -20,15 +21,17 @@ where
2021
import Cardano.Api qualified as C
2122
import Cardano.Api.Shelley qualified as C
2223
import Control.Lens (at, over, set, (&), (?~), (^.))
24+
import Control.Lens qualified as L
2325
import Control.Monad (when)
2426
import Control.Monad.Error.Lens (throwing_)
2527
import Control.Monad.Except (MonadError)
2628
import Control.Monad.Reader (MonadReader, asks)
2729
import Convex.BuildTx (MonadBuildTx (addTxBuilder), TxBuilder (TxBuilder),
28-
addBtx, addRequiredSignature, addScriptWithdrawal,
29-
addWithdrawalWithTxBody, buildScriptWitness,
30-
findIndexReference, mintPlutus, payToAddress,
31-
prependTxOut, spendPlutusInlineDatum)
30+
addBtx, addOutput, addRequiredSignature,
31+
addScriptWithdrawal, addWithdrawalWithTxBody,
32+
buildScriptWitness, findIndexReference, mintPlutus,
33+
payToAddress, payToAddressTxOut, prependTxOut,
34+
spendPlutusInlineDatum)
3235
import Convex.CardanoApi.Lenses qualified as L
3336
import Convex.Class (MonadBlockchain (queryNetworkId))
3437
import Convex.PlutusLedger.V1 (transCredential, transPolicyId,
@@ -282,7 +285,7 @@ seizeSmartTokens reason paramsTxIn seizingTxo destinationCred directoryList = Ut
282285
(toList $ C.txOutValueToValue v)
283286

284287
(progTokenPolId, an, q) <- maybe (error "No programmable token found in seizing transaction") pure maybeProgAsset
285-
seizeProgrammableToken paramsTxIn seizingTxo progTokenPolId directoryList
288+
seizeProgrammableToken paramsTxIn [seizingTxo] progTokenPolId directoryList
286289
addSeizeWitness
287290

288291
progLogicBaseCred <- asks (Env.programmableLogicBaseCredential . Env.directoryEnv)
@@ -295,7 +298,36 @@ seizeSmartTokens reason paramsTxIn seizingTxo destinationCred directoryList = Ut
295298

296299
addSeizeReason reason
297300
-- Send seized funds to destinationCred
298-
payToAddress destinationAddress seizedVal
301+
addOutput $ payToAddressTxOut destinationAddress seizedVal
302+
303+
-- This function should probably accept the programmable token policy which we want to seize as a parameter.
304+
-- As of now, it just assumes that the first non-ada token in the seizing inputs is the token we want to seize.
305+
multiSeizeSmartTokens :: forall env era a m. (MonadReader env m, Env.HasOperatorEnv era env, Env.HasTransferLogicEnv env, Env.HasDirectoryEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m) => SeizeReason -> UTxODat era ProgrammableLogicGlobalParams -> C.PolicyId -> [UTxODat era a] -> C.PaymentCredential -> [UTxODat era DirectorySetNode] -> m ()
306+
multiSeizeSmartTokens reason paramsTxIn toSeizePolicyId seizingTxos destinationCred directoryList = Utils.inBabbage @era $ do
307+
nid <- queryNetworkId
308+
309+
let seizedValue = C.policyAssetsToValue toSeizePolicyId $
310+
foldl (\acc (uOut -> utxoDat) ->
311+
let filteredAssets = foldMap ( \(pid, assets) ->
312+
if pid == toSeizePolicyId then assets else mempty
313+
)
314+
(toList $ C.valueToPolicyAssets (L.view (L._TxOut . L._2 . L._TxOutValue) utxoDat))
315+
in acc <> filteredAssets
316+
)
317+
(mempty :: C.PolicyAssets)
318+
seizingTxos
319+
320+
seizeProgrammableToken paramsTxIn seizingTxos toSeizePolicyId directoryList
321+
addSeizeWitness
322+
323+
progLogicBaseCred <- asks (Env.programmableLogicBaseCredential . Env.directoryEnv)
324+
destStakeCred <- either (error . ("Could not unTrans credential: " <>) . show) pure $ unTransStakeCredential $ transCredential destinationCred
325+
let
326+
destinationAddress = C.makeShelleyAddressInEra C.shelleyBasedEra nid progLogicBaseCred (C.StakeAddressByValue destStakeCred)
327+
328+
addSeizeReason reason
329+
-- Send seized funds to destinationCred
330+
addOutput $ payToAddressTxOut destinationAddress seizedValue
299331

300332
addIssueWitness :: forall era env m. (MonadReader env m, Env.HasOperatorEnv era env, Env.HasTransferLogicEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m) => m ()
301333
addIssueWitness = Utils.inBabbage @era $ do

src/examples/regulated-stablecoin/lib/Wst/Offchain/Endpoints/Deployment.hs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
{-# LANGUAGE NamedFieldPuns #-}
21
{-| Deploy the directory and global params
32
-}
43
module Wst.Offchain.Endpoints.Deployment(
@@ -9,6 +8,7 @@ module Wst.Offchain.Endpoints.Deployment(
98
insertBlacklistNodeTx,
109
removeBlacklistNodeTx,
1110
seizeCredentialAssetsTx,
11+
seizeMultiCredentialAssetsTx
1212
) where
1313

1414
import Cardano.Api (Quantity)
@@ -25,7 +25,9 @@ import Data.Function (on)
2525
import GHC.IsList (IsList (..))
2626
import ProgrammableTokens.OffChain.BuildTx qualified as BuildTx
2727
import ProgrammableTokens.OffChain.Env.Operator qualified as Env
28+
import ProgrammableTokens.OffChain.Env.TransferLogic (programmableTokenPolicyId)
2829
import ProgrammableTokens.OffChain.Error (AsProgrammableTokensError (..))
30+
import ProgrammableTokens.OffChain.Query (utxoHasPolicyId)
2931
import ProgrammableTokens.OffChain.Query qualified as Query
3032
import SmartTokens.Core.Scripts (ScriptTarget (..))
3133
import Wst.AppError (AsRegulatedStablecoinError (..))
@@ -175,3 +177,36 @@ seizeCredentialAssetsTx reason sanctionedCred = do
175177
(tx, _) <- Env.balanceTxEnv_ $ do
176178
BuildTx.seizeSmartTokens reason paramsTxIn seizeTxo (C.PaymentCredentialByKey opPkh) directory
177179
pure (Convex.CoinSelection.signBalancedTxBody [] tx)
180+
181+
seizeMultiCredentialAssetsTx :: forall era env err m.
182+
( MonadReader env m
183+
, Env.HasOperatorEnv era env
184+
, Env.HasTransferLogicEnv env
185+
, Env.HasDirectoryEnv env
186+
, MonadBlockchain era m
187+
, MonadError err m
188+
, C.IsBabbageBasedEra era
189+
, C.HasScriptLanguageInEra C.PlutusScriptV3 era
190+
, MonadUtxoQuery m
191+
, AsProgrammableTokensError err
192+
, AsBalancingError err era
193+
, AsCoinSelectionError err
194+
, AsRegulatedStablecoinError err
195+
)
196+
=> BuildTx.SeizeReason
197+
-> Int
198+
-> C.PaymentCredential -- ^ Source/User credential
199+
-> m (C.Tx era)
200+
seizeMultiCredentialAssetsTx reason numUTxOsToSeize sanctionedCred = do
201+
toSeizePolicyId <- asks programmableTokenPolicyId
202+
opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv @era)
203+
directory <- Query.registryNodes @era
204+
205+
utxosToSeize <- take numUTxOsToSeize . filter (utxoHasPolicyId toSeizePolicyId) <$> Query.userProgrammableOutputs sanctionedCred
206+
207+
when (null utxosToSeize) $
208+
throwing_ _NoTokensToSeize
209+
paramsTxIn <- Query.globalParamsNode @era
210+
(tx, _) <- Env.balanceTxEnv_ $ do
211+
BuildTx.multiSeizeSmartTokens reason paramsTxIn toSeizePolicyId utxosToSeize (C.PaymentCredentialByKey opPkh) directory
212+
pure (Convex.CoinSelection.signBalancedTxBody [] tx)

src/examples/regulated-stablecoin/test/unit/Wst/Test/UnitTest.hs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module Wst.Test.UnitTest(
55

66
import Cardano.Api qualified as C
77
import Cardano.Api.Shelley qualified as C
8-
import Control.Monad (void)
8+
import Control.Monad (void, when)
99
import Control.Monad.Except (MonadError)
1010
import Control.Monad.Reader (MonadReader (ask), ReaderT (runReaderT), asks)
1111
import Convex.BuildTx qualified as BuildTx
@@ -22,6 +22,7 @@ import Data.List (isPrefixOf)
2222
import Data.String (IsString (..))
2323
import GHC.Exception (SomeException, throw)
2424
import ProgrammableTokens.OffChain.Endpoints qualified as Endpoints
25+
import ProgrammableTokens.OffChain.Env (programmableTokenPolicyId)
2526
import ProgrammableTokens.OffChain.Env.Operator qualified as Env
2627
import ProgrammableTokens.OffChain.Query qualified as Query
2728
import ProgrammableTokens.Test (deployDirectorySet)
@@ -55,6 +56,7 @@ scriptTargetTests target =
5556
, testCase "blacklisted transfer" (mockchainFails (blacklistTransfer DontSubmitFailingTx) assertBlacklistedAddressException)
5657
, testCase "blacklisted transfer (failing tx)" (Test.mockchainSucceedsWithTarget @(AppError C.ConwayEra) target (blacklistTransfer SubmitFailingTx >>= Test.assertFailingTx))
5758
, testCase "seize user output" (Test.mockchainSucceedsWithTarget @(AppError C.ConwayEra) target $ deployDirectorySet admin >>= seizeUserOutput)
59+
, testCase "seize multi user outputs" (Test.mockchainSucceedsWithTarget @(AppError C.ConwayEra) target $ deployDirectorySet admin >>= seizeMultiUserOutputs)
5860
, testCase "deploy all" (Test.mockchainSucceedsWithTarget @(AppError C.ConwayEra) target deployAll)
5961
]
6062
]
@@ -224,8 +226,48 @@ seizeUserOutput scriptRoot = Env.withEnv $ do
224226
Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh)
225227
>>= void . Test.expectN 1 "user programmable outputs"
226228
Query.userProgrammableOutputs (C.PaymentCredentialByKey opPkh)
229+
>>= void . Test.expectN 2 "operator programmable outputs"
230+
231+
seizeMultiUserOutputs :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m, MonadError (AppError C.ConwayEra) m) => DirectoryScriptRoot -> m ()
232+
seizeMultiUserOutputs scriptRoot = Env.withEnv $ do
233+
userPkh <- asWallet @C.ConwayEra Wallet.w2 $ asks (fst . Env.bteOperator . Env.operatorEnv @C.ConwayEra)
234+
let userPaymentCred = C.PaymentCredentialByKey userPkh
235+
236+
aid <- issueTransferLogicProgrammableToken scriptRoot
237+
238+
asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator @C.ConwayEra $ Endpoints.deployBlacklistTx
239+
>>= void . sendTx . signTxOperator admin
240+
241+
asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator @C.ConwayEra $ do
242+
Endpoints.transferSmartTokensTx DontSubmitFailingTx aid 50 (C.PaymentCredentialByKey userPkh)
243+
>>= void . sendTx . signTxOperator admin
244+
Query.programmableLogicOutputs @C.ConwayEra
245+
>>= void . Test.expectN 2 "programmable logic outputs"
246+
Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh)
247+
>>= void . Test.expectN 1 "user programmable outputs"
248+
249+
asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator @C.ConwayEra $ do
250+
Endpoints.transferSmartTokensTx DontSubmitFailingTx aid 50 (C.PaymentCredentialByKey userPkh)
251+
>>= void . sendTx . signTxOperator admin
252+
Query.programmableLogicOutputs @C.ConwayEra
253+
>>= void . Test.expectN 3 "programmable logic outputs"
254+
Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh)
227255
>>= void . Test.expectN 2 "user programmable outputs"
228256

257+
asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator @C.ConwayEra $ do
258+
opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv @C.ConwayEra)
259+
toSeizePolicyId <- asks programmableTokenPolicyId
260+
Endpoints.seizeMultiCredentialAssetsTx mempty 2 userPaymentCred
261+
>>= void . sendTx . signTxOperator admin
262+
Query.programmableLogicOutputs @C.ConwayEra
263+
>>= void . Test.expectN 4 "programmable logic outputs"
264+
userOutputs <- Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh)
265+
Test.expectN 2 "user programmable outputs" userOutputs
266+
mapM_ (\utxo -> when (Query.utxoHasPolicyId toSeizePolicyId utxo) $ fail "User should not have any UTxOs with the programmable token policy ID") userOutputs
267+
268+
Query.userProgrammableOutputs (C.PaymentCredentialByKey opPkh)
269+
>>= void . Test.expectN 2 "operator programmable outputs"
270+
229271
-- TODO: registration to be moved to the endpoints
230272
registerTransferScripts :: (MonadFail m, MonadError (AppError C.ConwayEra) m, MonadReader env m, Env.HasTransferLogicEnv env, MonadMockchain C.ConwayEra m) => C.Hash C.PaymentKey -> m C.TxId
231273
registerTransferScripts pkh = do

src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Query.hs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ module ProgrammableTokens.OffChain.Query(
1313
issuanceCborHexUTxO,
1414
globalParamsNode,
1515
programmableLogicOutputs,
16-
selectProgammableOutputsFor
16+
selectProgammableOutputsFor,
1717

18+
utxoHasPolicyId,
19+
hasPolicyId,
20+
extractValue,
1821
) where
1922

2023
import Cardano.Api qualified as C

0 commit comments

Comments
 (0)