From 09bc7dd5792d21df70f3fe48032ad1f4ef6f92f6 Mon Sep 17 00:00:00 2001 From: Amir Rad Date: Mon, 23 Dec 2024 16:05:01 +0000 Subject: [PATCH] WIP issue and transfer smart tokens endpoints --- src/lib/Wst/Offchain/BuildTx/DirectorySet.hs | 3 +- .../Wst/Offchain/BuildTx/ProgrammableLogic.hs | 24 ++-- src/lib/Wst/Offchain/BuildTx/TransferLogic.hs | 126 +++++++++++++++--- src/lib/Wst/Offchain/Endpoints/Deployment.hs | 84 +++++++++++- src/lib/Wst/Offchain/Env.hs | 9 +- src/lib/Wst/Offchain/Query.hs | 24 +++- src/test/Wst/Test/UnitTest.hs | 100 +++++++++++++- 7 files changed, 326 insertions(+), 44 deletions(-) diff --git a/src/lib/Wst/Offchain/BuildTx/DirectorySet.hs b/src/lib/Wst/Offchain/BuildTx/DirectorySet.hs index db795c4..9911865 100644 --- a/src/lib/Wst/Offchain/BuildTx/DirectorySet.hs +++ b/src/lib/Wst/Offchain/BuildTx/DirectorySet.hs @@ -84,6 +84,7 @@ initDirectorySet = Utils.inBabbage @era $ do prependTxOut output + {-| Data for a new node to be inserted into the directory -} data InsertNodeArgs = @@ -99,8 +100,8 @@ insertDirectoryNode UTxODat{uIn=paramsRef} UTxODat{uIn, uOut=firstTxOut, uDatum= initialTxIn <- asks (Env.dsTxIn . Env.directoryEnv) paramsPolicyId <- asks (Env.protocolParamsPolicyId . Env.directoryEnv) directorySpendingScript <- asks (Env.dsDirectorySpendingScript . Env.directoryEnv) + directoryMintingScript <- asks (Env.dsDirectoryMintingScript . Env.directoryEnv) let - directoryMintingScript = directoryNodeMintingScript initialTxIn firstTxVal :: C.TxOutValue era firstTxVal = case firstTxOut of diff --git a/src/lib/Wst/Offchain/BuildTx/ProgrammableLogic.hs b/src/lib/Wst/Offchain/BuildTx/ProgrammableLogic.hs index 7243998..11abdbe 100644 --- a/src/lib/Wst/Offchain/BuildTx/ProgrammableLogic.hs +++ b/src/lib/Wst/Offchain/BuildTx/ProgrammableLogic.hs @@ -86,8 +86,8 @@ issueProgrammableToken paramsTxOut (an, q) IssueNewTokenArgs{intaMintingLogic, i progLogicScriptCredential <- either (const $ error "could not parse protocol params") pure $ unTransCredential progLogicCred directoryNodeSymbol <- either (const $ error "could not parse protocol params") pure $ unTransPolicyId directoryNodeCS - netId <- queryNetworkId - DirectoryEnv{dsProgrammableLogicBaseScript} <- asks Env.directoryEnv + -- + -- DirectoryEnv{dsProgrammableLogicBaseScript} <- asks Env.directoryEnv -- TODO: maybe move programmableLogicMintingScript to DirectoryEnv let mintingScript = programmableLogicMintingScript progLogicScriptCredential (C.StakeCredentialByScript $ C.hashScript $ C.PlutusScript C.plutusScriptVersion intaMintingLogic) directoryNodeSymbol @@ -98,17 +98,17 @@ issueProgrammableToken paramsTxOut (an, q) IssueNewTokenArgs{intaMintingLogic, i maximumBy (compare `on` (key . uDatum)) $ filter ((<= issuedSymbol) . key . uDatum) directoryList - receivingAddress = - C.makeShelleyAddressInEra - C.shelleyBasedEra - netId - (C.PaymentCredentialByScript $ C.hashScript $ C.PlutusScript C.plutusScriptVersion dsProgrammableLogicBaseScript) - C.NoStakeAddress -- FIXME: use owner credential + -- receivingAddress = + -- C.makeShelleyAddressInEra + -- C.shelleyBasedEra + -- netId + -- (C.PaymentCredentialByScript $ C.hashScript $ C.PlutusScript C.plutusScriptVersion dsProgrammableLogicBaseScript) + -- C.NoStakeAddress -- FIXME: use owner credential - receivingVal = C.TxOutValueShelleyBased C.shelleyBasedEra $ C.toLedgerValue @era C.maryBasedEra - $ fromList [(C.AssetId issuedPolicyId an, q)] + -- receivingVal = C.TxOutValueShelleyBased C.shelleyBasedEra $ C.toLedgerValue @era C.maryBasedEra + -- $ fromList [(C.AssetId issuedPolicyId an, q)] - dat = C.TxOutDatumInline C.babbageBasedEra $ toHashableScriptData () -- TODO: What should the datum be? + -- dat = C.TxOutDatumInline C.babbageBasedEra $ toHashableScriptData () -- TODO: What should the datum be? if key dirNodeData == issuedSymbol then @@ -125,7 +125,7 @@ issueProgrammableToken paramsTxOut (an, q) IssueNewTokenArgs{intaMintingLogic, i insertDirectoryNode paramsTxOut udat nodeArgs -- add programmable logic output - prependTxOut $ C.TxOut receivingAddress receivingVal dat C.ReferenceScriptNone + -- prependTxOut $ C.TxOut receivingAddress receivingVal dat C.ReferenceScriptNone pure issuedPolicyId diff --git a/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs b/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs index b422559..901e576 100644 --- a/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs +++ b/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs @@ -1,11 +1,14 @@ -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} module Wst.Offchain.BuildTx.TransferLogic - ( transferStablecoins, - issueStablecoins, - seizeStablecoins, + ( transferSmartTokens, + issueSmartTokens, + seizeSmartTokens, + initBlacklist, + insertBlacklistNode, ) where @@ -15,11 +18,13 @@ import Control.Lens (over) import Control.Monad.Reader (MonadReader, asks) import Convex.BuildTx (MonadBuildTx, addBtx, addReference, addRequiredSignature, addScriptWithdrawal, addWithdrawalWithTxBody, - buildScriptWitness, findIndexReference, payToAddress) + buildScriptWitness, findIndexReference, mintPlutus, + payToAddress, prependTxOut, spendPlutusInlineDatum) import Convex.CardanoApi.Lenses as L import Convex.Class (MonadBlockchain (queryNetworkId)) import Convex.PlutusLedger.V1 (transCredential, transPolicyId, unTransStakeCredential) +import Convex.Scripts qualified as C import Convex.Utils qualified as Utils import Convex.Utxos (UtxoSet (UtxoSet)) import Convex.Wallet (selectMixedInputsCovering) @@ -27,25 +32,114 @@ import Convex.Wallet.Operator (Operator (..), verificationKey) import Data.Foldable (maximumBy) import Data.Function (on) import Data.Monoid (Last (..)) +import Debug.Trace (trace) import GHC.Exts (IsList (..)) +import PlutusLedgerApi.Data.V3 (Credential (..), PubKeyHash (PubKeyHash), + ScriptHash (..)) +import PlutusLedgerApi.V3 qualified as PlutusTx +import SmartTokens.CodeLens (_printTerm) import SmartTokens.Contracts.ExampleTransferLogic (BlacklistProof (..)) import SmartTokens.Types.ProtocolParams import SmartTokens.Types.PTokenDirectory (BlacklistNode (..), DirectorySetNode (..)) -import Wst.Offchain.BuildTx.ProgrammableLogic (IssueNewTokenArgs, +import Wst.Offchain.BuildTx.ProgrammableLogic (IssueNewTokenArgs (..), issueProgrammableToken, seizeProgrammableToken, transferProgrammableToken) import Wst.Offchain.Env qualified as Env import Wst.Offchain.Query (UTxODat (..)) +import Wst.Offchain.Scripts (scriptPolicyIdV3) -issueStablecoins :: forall era env m. (MonadReader env m, Env.HasTransferLogicEnv env, Env.HasDirectoryEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m, Env.HasOperatorEnv era env) => UTxODat era ProgrammableLogicGlobalParams -> (C.AssetName, C.Quantity) -> IssueNewTokenArgs -> [UTxODat era DirectorySetNode] -> C.PaymentCredential -> m () -issueStablecoins paramsTxOut (an, q) inta directoryList destinationCred = Utils.inBabbage @era $ do +intaFromEnv :: forall env m. (MonadReader env m, Env.HasTransferLogicEnv env)=> m IssueNewTokenArgs +intaFromEnv = do + Env.TransferLogicEnv{Env.tleIssuerScript, Env.tleMintingScript, Env.tleTransferScript} <- asks Env.transferLogicEnv + pure $ IssueNewTokenArgs + { intaTransferLogic= tleTransferScript + , intaMintingLogic= tleMintingScript + , intaIssuerLogic= tleIssuerScript + } + + +blacklistInitialNode :: BlacklistNode +blacklistInitialNode = BlacklistNode {blnNext=PubKeyCredential "ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", blnKey= PubKeyCredential ""} + +initBlacklist :: 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 () +initBlacklist = Utils.inBabbage @era $ do + nid <- queryNetworkId + + -- create blacklist head node data + let blacklistInitialNodeDatum = C.TxOutDatumInline C.babbageBasedEra $ C.toHashableScriptData blacklistInitialNode + + -- mint blacklist policy token + mintingScript <- asks (Env.tleBlacklistMintingScript . Env.transferLogicEnv) + let assetName = C.AssetName "" + quantity = 1 + + mintPlutus mintingScript () assetName quantity + + -- send blacklist output to blacklist spending script + spendingScript <- asks (Env.tleBlacklistSpendingScript . Env.transferLogicEnv) + let policyId = scriptPolicyIdV3 mintingScript + spendingHash = C.hashScript $ C.PlutusScript C.PlutusScriptV3 spendingScript + addr = C.makeShelleyAddressInEra C.shelleyBasedEra nid (C.PaymentCredentialByScript spendingHash) C.NoStakeAddress + val = C.TxOutValueShelleyBased C.shelleyBasedEra $ C.toLedgerValue @era C.maryBasedEra $ fromList [(C.AssetId policyId assetName, quantity)] + txout = C.TxOut addr val blacklistInitialNodeDatum C.ReferenceScriptNone + + prependTxOut txout + + -- add operator signature + opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) + addRequiredSignature opPkh + +insertBlacklistNode :: forall era env m. (MonadReader env m, Env.HasOperatorEnv era env, Env.HasTransferLogicEnv env, C.IsBabbageBasedEra era, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m) => C.PaymentCredential -> [UTxODat era BlacklistNode]-> m () +insertBlacklistNode cred blacklistNodes = Utils.inBabbage @era $ do + -- mint new blacklist token + mintingScript <- asks (Env.tleBlacklistMintingScript . Env.transferLogicEnv) + let newAssetName = C.AssetName $ case transCredential cred of + PubKeyCredential (PubKeyHash s) -> PlutusTx.fromBuiltin s + ScriptCredential (ScriptHash s) -> PlutusTx.fromBuiltin s + quantity = 1 + mintPlutus mintingScript () newAssetName quantity + + let + -- find the node to insert on + UTxODat {uIn = prevNodeRef,uOut = (C.TxOut prevAddr prevVal _ _), uDatum = prevNode} = + maximumBy (compare `on` (blnKey . uDatum)) $ + filter ((<= transCredential cred) . blnKey . uDatum) blacklistNodes + + -- create new blacklist node data + newNode = BlacklistNode {blnNext=blnNext prevNode, blnKey= transCredential cred} + newNodeDatum = C.TxOutDatumInline C.babbageBasedEra $ C.toHashableScriptData newNode + newNodeVal = C.TxOutValueShelleyBased C.shelleyBasedEra $ C.toLedgerValue @era C.maryBasedEra $ fromList [(C.AssetId (scriptPolicyIdV3 mintingScript) newAssetName, quantity)] + newNodeOutput = C.TxOut prevAddr newNodeVal newNodeDatum C.ReferenceScriptNone + + -- update the previous node to point to the new node + newPrevNode = prevNode {blnNext=transCredential cred} + newPrevNodeDatum = C.TxOutDatumInline C.babbageBasedEra $ C.toHashableScriptData newPrevNode + newPrevNodeOutput = C.TxOut prevAddr prevVal newPrevNodeDatum C.ReferenceScriptNone + + -- spend previous node + spendingScript <- asks (Env.tleBlacklistSpendingScript . Env.transferLogicEnv) + spendPlutusInlineDatum prevNodeRef spendingScript () + -- set previous node output + prependTxOut newPrevNodeOutput + -- set new node output + prependTxOut newNodeOutput + + -- add operator signature + opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) + addRequiredSignature opPkh + +-- TODO +_removeBlacklistNode = undefined + +issueSmartTokens :: forall era env m. (MonadReader env m, Env.HasTransferLogicEnv env, Env.HasDirectoryEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m, Env.HasOperatorEnv era env) => UTxODat era ProgrammableLogicGlobalParams -> (C.AssetName, C.Quantity) -> [UTxODat era DirectorySetNode] -> C.PaymentCredential -> m () +issueSmartTokens paramsTxOut (an, q) directoryList destinationCred = Utils.inBabbage @era $ do nid <- queryNetworkId directoryEnv <- asks Env.directoryEnv let progLogicBaseCred = Env.programmableLogicBaseCredential directoryEnv - + inta <- intaFromEnv issuedPolicyId <- issueProgrammableToken paramsTxOut (an, q) inta directoryList -- TODO: check if there is a better way to achieve: C.PaymentCredential -> C.StakeCredential stakeCred <- either (error . ("Could not unTrans credential: " <>) . show) pure $ unTransStakeCredential $ transCredential destinationCred @@ -55,8 +149,8 @@ issueStablecoins paramsTxOut (an, q) inta directoryList destinationCred = Utils. addIssueWitness payToAddress addr value -transferStablecoins :: forall env era a m. (MonadReader env m, Env.HasTransferLogicEnv env, Env.HasDirectoryEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m) => C.PaymentCredential -> [UTxODat era BlacklistNode] -> [UTxODat era DirectorySetNode] -> [UTxODat era a] -> (C.AssetId, C.Quantity) -> C.PaymentCredential -> m () -transferStablecoins userCred blacklistNodes directoryList spendingUserOutputs (assetId, q) destinationCred = Utils.inBabbage @era $ do +transferSmartTokens :: forall env era a m. (MonadReader env m, Env.HasTransferLogicEnv env, Env.HasDirectoryEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m) => C.PaymentCredential -> [UTxODat era BlacklistNode] -> [UTxODat era DirectorySetNode] -> [UTxODat era a] -> (C.AssetId, C.Quantity) -> C.PaymentCredential -> m () +transferSmartTokens userCred blacklistNodes directoryList spendingUserOutputs (assetId, q) destinationCred = Utils.inBabbage @era $ do nid <- queryNetworkId progLogicBaseCred <- asks (Env.programmableLogicBaseCredential . Env.directoryEnv) @@ -85,10 +179,10 @@ transferStablecoins userCred blacklistNodes directoryList spendingUserOutputs (a fromList [(assetId, C.selectAsset totalVal assetId - q)] returnAddr = undefined returnOutput = C.TxOut returnAddr returnVal C.TxOutDatumNone C.ReferenceScriptNone - addBtx (over L.txOuts (returnOutput :)) -- Add the seized output to the transaction + prependTxOut returnOutput -- Add the seized output to the transaction -seizeStablecoins :: 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) => UTxODat era a -> UTxODat era a -> [UTxODat era DirectorySetNode] -> C.PaymentCredential -> m () -seizeStablecoins seizingTxo issuerTxo directoryList destinationCred = Utils.inBabbage @era $ do +seizeSmartTokens :: 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) => UTxODat era a -> UTxODat era a -> [UTxODat era DirectorySetNode] -> C.PaymentCredential -> m () +seizeSmartTokens seizingTxo issuerTxo directoryList destinationCred = Utils.inBabbage @era $ do -- Add issuer and programmableLogic witnesses let Last maybeProgAsset = case uOut seizingTxo of (C.TxOut _a v _d _r) -> diff --git a/src/lib/Wst/Offchain/Endpoints/Deployment.hs b/src/lib/Wst/Offchain/Endpoints/Deployment.hs index 7b7fd81..ee92c6d 100644 --- a/src/lib/Wst/Offchain/Endpoints/Deployment.hs +++ b/src/lib/Wst/Offchain/Endpoints/Deployment.hs @@ -2,23 +2,33 @@ -} module Wst.Offchain.Endpoints.Deployment( deployTx, + deployBlacklistTx, insertNodeTx, - issueProgrammableTokenTx + issueProgrammableTokenTx, + issueSmartTokensTx, + transferSmartTokensTx, + insertBlacklistNodeTx, ) where import Cardano.Api (Quantity) import Cardano.Api.Shelley qualified as C +import Control.Monad (when) import Control.Monad.Except (MonadError) import Control.Monad.Reader (MonadReader, asks) import Convex.BuildTx qualified as BuildTx import Convex.Class (MonadBlockchain, MonadUtxoQuery) import Convex.CoinSelection qualified +import Data.Foldable (maximumBy) +import Data.Function (on) +import SmartTokens.Types.PTokenDirectory (DirectorySetNode (..)) import Wst.AppError (AppError) -import Wst.Offchain.BuildTx.DirectorySet (InsertNodeArgs) +import Wst.Offchain.BuildTx.DirectorySet (InsertNodeArgs (inaNewKey)) import Wst.Offchain.BuildTx.DirectorySet qualified as BuildTx import Wst.Offchain.BuildTx.ProgrammableLogic qualified as BuildTx import Wst.Offchain.BuildTx.ProtocolParams qualified as BuildTx +import Wst.Offchain.BuildTx.TransferLogic qualified as BuildTx import Wst.Offchain.Env qualified as Env +import Wst.Offchain.Query (UTxODat (..)) import Wst.Offchain.Query qualified as Query {-| Build a transaction that deploys the directory and global params. Returns the @@ -38,8 +48,12 @@ deployTx = do insertNodeTx :: forall era env m. (MonadReader env m, Env.HasOperatorEnv era env, Env.HasDirectoryEnv env, MonadBlockchain era m, MonadError (AppError era) m, C.IsBabbageBasedEra era, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadUtxoQuery m) => InsertNodeArgs -> m (C.Tx era) insertNodeTx args = do -- 1. Find the head node + directoryList <- Query.registryNodes @era -- FIXME: Error handling. And how can we actually identify the head node if the query returns more than one? - headNode <- head <$> Query.registryNodes @era + let headNode@UTxODat{uDatum = dirNodeDat} = + maximumBy (compare `on` (key . uDatum)) $ + filter ((<= inaNewKey args) . key . uDatum) directoryList + when (key dirNodeDat == inaNewKey args) $ error "Node already exists" -- 2. Find the global parameter node paramsNode <- Query.globalParamsNode @era @@ -71,3 +85,67 @@ issueProgrammableTokenTx issueTokenArgs assetName quantity = do let hsh = C.hashScript (C.PlutusScript C.plutusScriptVersion $ BuildTx.intaMintingLogic issueTokenArgs) BuildTx.addScriptWithdrawal hsh 0 $ BuildTx.buildScriptWitness (BuildTx.intaMintingLogic issueTokenArgs) C.NoScriptDatumForStake () pure (Convex.CoinSelection.signBalancedTxBody [] tx) + +deployBlacklistTx :: (MonadReader env m, Env.HasOperatorEnv era env, MonadBlockchain era m, MonadError (AppError era) m, C.IsBabbageBasedEra era, C.HasScriptLanguageInEra C.PlutusScriptV3 era) => m (C.Tx era) +deployBlacklistTx = do + opEnv <- asks Env.operatorEnv + (tx, _) <- Env.withEnv $ Env.withOperator opEnv + $ Env.balanceTxEnv + $ BuildTx.initBlacklist + pure (Convex.CoinSelection.signBalancedTxBody [] tx) + +insertBlacklistNodeTx :: forall era env m. (MonadReader env m, Env.HasOperatorEnv era env, Env.HasTransferLogicEnv env, MonadBlockchain era m, MonadError (AppError era) m, C.IsBabbageBasedEra era, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadUtxoQuery m) => C.PaymentCredential -> m (C.Tx era) +insertBlacklistNodeTx cred = do + blacklist <- Query.blacklistNodes @era + (tx, _) <- Env.balanceTxEnv (BuildTx.insertBlacklistNode cred blacklist) + pure (Convex.CoinSelection.signBalancedTxBody [] tx) + +{-| Build a transaction that issues a progammable token +-} +issueSmartTokensTx :: forall era env m. + ( MonadReader env m + , Env.HasOperatorEnv era env + , Env.HasDirectoryEnv env + , Env.HasTransferLogicEnv env + , MonadBlockchain era m + , MonadError (AppError era) m + , C.IsBabbageBasedEra era + , C.HasScriptLanguageInEra C.PlutusScriptV3 era + , MonadUtxoQuery m + ) + => C.AssetName -- ^ Name of the asset + -> Quantity -- ^ Amount of tokens to be minted + -> C.PaymentCredential -- ^ Destination credential + -> m (C.Tx era) +issueSmartTokensTx assetName quantity destinationCred = do + directory <- Query.registryNodes @era + paramsNode <- Query.globalParamsNode @era + (tx, _) <- Env.balanceTxEnv $ do + BuildTx.issueSmartTokens paramsNode (assetName, quantity) directory destinationCred + pure (Convex.CoinSelection.signBalancedTxBody [] tx) + +{-| Build a transaction that issues a progammable token +-} +transferSmartTokensTx :: forall era env m. + ( MonadReader env m + , Env.HasOperatorEnv era env + , Env.HasDirectoryEnv env + , Env.HasTransferLogicEnv env + , MonadBlockchain era m + , MonadError (AppError era) m + , C.IsBabbageBasedEra era + , C.HasScriptLanguageInEra C.PlutusScriptV3 era + , MonadUtxoQuery m + ) + => C.PaymentCredential -- ^ Source/User credential + -> C.AssetId -- ^ Name of the asset + -> Quantity -- ^ Amount of tokens to be minted + -> C.PaymentCredential -- ^ Destination credential + -> m (C.Tx era) +transferSmartTokensTx srcCred assetName quantity destCred = do + directory <- Query.registryNodes @era + blacklist <- Query.blacklistNodes @era + userOutputsAtProgrammable <- undefined + (tx, _) <- Env.balanceTxEnv $ do + BuildTx.transferSmartTokens srcCred blacklist directory userOutputsAtProgrammable (assetName, quantity) destCred + pure (Convex.CoinSelection.signBalancedTxBody [] tx) diff --git a/src/lib/Wst/Offchain/Env.hs b/src/lib/Wst/Offchain/Env.hs index 4e598da..3704554 100644 --- a/src/lib/Wst/Offchain/Env.hs +++ b/src/lib/Wst/Offchain/Env.hs @@ -3,6 +3,7 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-} + {-| Transaction building environment -} module Wst.Offchain.Env( @@ -61,7 +62,7 @@ import Cardano.Api.Shelley qualified as C import Control.Lens (makeLensesFor) import Control.Lens qualified as L import Control.Monad.Except (MonadError, throwError) -import Control.Monad.Reader (MonadReader, ReaderT, asks, runReaderT) +import Control.Monad.Reader (MonadReader, ReaderT, ask, asks, runReaderT) import Convex.BuildTx (BuildTxT) import Convex.BuildTx qualified as BuildTx import Convex.Class (MonadBlockchain, MonadUtxoQuery (..), @@ -200,8 +201,7 @@ globalParams scripts = data TransferLogicEnv = TransferLogicEnv - { tleBlacklistPolicy :: C.PolicyId - , tleBlacklistMintingScript :: PlutusScript PlutusScriptV3 + { tleBlacklistMintingScript :: PlutusScript PlutusScriptV3 , tleBlacklistSpendingScript :: PlutusScript PlutusScriptV3 , tleMintingScript :: PlutusScript PlutusScriptV3 , tleTransferScript :: PlutusScript PlutusScriptV3 @@ -220,8 +220,7 @@ mkTransferLogicEnv cred = blacklistPolicy = scriptPolicyIdV3 blacklistMinting in TransferLogicEnv - { tleBlacklistPolicy = blacklistPolicy - , tleBlacklistMintingScript = blacklistMinting + { tleBlacklistMintingScript = blacklistMinting , tleBlacklistSpendingScript = blacklistSpendingScript cred , tleMintingScript = permissionedTransferScript cred , tleTransferScript = freezeAndSezieTransferScript blacklistPolicy diff --git a/src/lib/Wst/Offchain/Query.hs b/src/lib/Wst/Offchain/Query.hs index a5b244c..5194cc7 100644 --- a/src/lib/Wst/Offchain/Query.hs +++ b/src/lib/Wst/Offchain/Query.hs @@ -3,6 +3,7 @@ -} module Wst.Offchain.Query( -- * Queries + blacklistNodes, registryNodes, globalParamsNode, programmableLogicOutputs, @@ -24,13 +25,16 @@ import Convex.Utxos (UtxoSet, toApiUtxo) import Data.Aeson (FromJSON, ToJSON) import Data.Map qualified as Map import Data.Maybe (listToMaybe, mapMaybe) +import Debug.Trace (trace) import GHC.Generics (Generic) import PlutusTx qualified import SmartTokens.Types.ProtocolParams (ProgrammableLogicGlobalParams) -import SmartTokens.Types.PTokenDirectory (DirectorySetNode (..)) +import SmartTokens.Types.PTokenDirectory (BlacklistNode, DirectorySetNode (..)) import Wst.AppError (AppError (GlobalParamsNodeNotFound)) import Wst.Offchain.Env (DirectoryEnv (dsDirectorySpendingScript, dsProgrammableLogicBaseScript), - HasDirectoryEnv (directoryEnv)) + HasDirectoryEnv (directoryEnv), + HasTransferLogicEnv (transferLogicEnv), + TransferLogicEnv (tleBlacklistSpendingScript)) import Wst.Offchain.Scripts (protocolParamsSpendingScript) -- TODO: We should probably filter the UTxOs to check that they have the correct NFTs @@ -53,6 +57,13 @@ registryNodes = asks (C.PaymentCredentialByScript . C.hashScript . C.PlutusScript C.PlutusScriptV3 . dsDirectorySpendingScript . directoryEnv) >>= fmap (extractUTxO @era) . utxosByPaymentCredential +{-| Find all UTxOs that make up the blacklist +-} +blacklistNodes :: forall era env m. (MonadReader env m, HasTransferLogicEnv env, MonadUtxoQuery m, C.IsBabbageBasedEra era) => m [UTxODat era BlacklistNode] +blacklistNodes = + asks (C.PaymentCredentialByScript . C.hashScript . C.PlutusScript C.PlutusScriptV3 . tleBlacklistSpendingScript . transferLogicEnv) + >>= fmap (extractUTxO @era) . utxosByPaymentCredential + {-| Find the UTxO with the global params -} globalParamsNode :: forall era m. (MonadUtxoQuery m, C.IsBabbageBasedEra era, MonadError (AppError era) m) => m (UTxODat era ProgrammableLogicGlobalParams) @@ -66,7 +77,14 @@ globalParamsNode = do programmableLogicOutputs :: forall era env m. (MonadReader env m, HasDirectoryEnv env, MonadUtxoQuery m, C.IsBabbageBasedEra era) => m [UTxODat era ()] programmableLogicOutputs = do asks (C.PaymentCredentialByScript . C.hashScript . C.PlutusScript C.PlutusScriptV3 . dsProgrammableLogicBaseScript . directoryEnv) - >>= fmap (extractUTxO @era) . utxosByPaymentCredential + >>= fmap (extractUtxoNoDatum @era) . utxosByPaymentCredential + +fromOutputNoDatum :: forall era. (C.IsBabbageBasedEra era) => C.TxIn -> C.TxOut C.CtxUTxO era -> Maybe (UTxODat era ()) +fromOutputNoDatum uIn uOut@(L.preview (L._TxOut . L._3 . L._TxOutDatumInline) >=> fromHashableScriptData -> Just ()) = Just UTxODat{uIn, uOut, uDatum = ()} +fromOutputNoDatum uIn uOut = Just $ UTxODat{uIn, uOut, uDatum = ()} + +extractUtxoNoDatum :: forall era b. (C.IsBabbageBasedEra era) => UtxoSet C.CtxUTxO b -> [UTxODat era ()] +extractUtxoNoDatum = mapMaybe (uncurry fromOutputNoDatum) . Map.toList . C.unUTxO . toApiUtxo @era fromOutput :: forall era a. (PlutusTx.FromData a, C.IsBabbageBasedEra era) => C.TxIn -> C.TxOut C.CtxUTxO era -> Maybe (UTxODat era a) fromOutput uIn uOut@(L.preview (L._TxOut . L._3 . L._TxOutDatumInline) >=> fromHashableScriptData -> Just uDatum) = Just UTxODat{uIn, uOut, uDatum} diff --git a/src/test/Wst/Test/UnitTest.hs b/src/test/Wst/Test/UnitTest.hs index 8b52004..4f89edd 100644 --- a/src/test/Wst/Test/UnitTest.hs +++ b/src/test/Wst/Test/UnitTest.hs @@ -7,16 +7,24 @@ import Cardano.Api qualified as C import Cardano.Api.Shelley qualified as C import Cardano.Ledger.Core qualified as Ledger import Control.Lens ((^.)) -import Control.Monad (void) +import Control.Monad (void, (<=<)) +import Control.Monad.Reader (asks) +import Control.Monad.Reader.Class (MonadReader) import Convex.BuildTx qualified as BuildTx -import Convex.Class (MonadBlockchain (queryProtocolParameters, sendTx), - MonadMockchain, MonadUtxoQuery) +import Convex.Class (MonadBlockchain (queryNetworkId, queryProtocolParameters, sendTx), + MonadMockchain, MonadUtxoQuery, nextSlot, + utxosByPaymentCredential) import Convex.CoinSelection (ChangeOutputPosition (TrailingChange)) import Convex.MockChain.CoinSelection (tryBalanceAndSubmit) import Convex.MockChain.Utils (mockchainSucceeds) import Convex.Utils (failOnError) +import Convex.Wallet (paymentCredential) +import Convex.Wallet qualified as BuildTx import Convex.Wallet.MockWallet qualified as Wallet import Convex.Wallet.Operator (signTxOperator) +import Convex.Wallet.Operator qualified as Env +import Debug.Trace (trace) +import GHC.IsList (IsList (..)) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase) import Wst.Offchain.BuildTx.DirectorySet (InsertNodeArgs (..)) @@ -33,6 +41,7 @@ tests = testGroup "unit tests" , testCase "insert directory node" (mockchainSucceeds insertDirectoryNode) , testGroup "issue programmable tokens" [ testCase "always succeeds validator" (mockchainSucceeds issueAlwaysSucceedsValidator) + , testCase "transfer logic issuance" (mockchainSucceeds issueTransferLogicProgrammableToken) -- TODO: Add test for the seize/freeze validator ] ] @@ -40,6 +49,7 @@ tests = testGroup "unit tests" deployDirectorySet :: (MonadUtxoQuery m, MonadBlockchain C.ConwayEra m, MonadFail m) => m C.TxIn deployDirectorySet = failOnError $ Env.withEnv $ asAdmin @C.ConwayEra $ do (tx, txI) <- Endpoints.deployTx + let id = C.getTxId $ C.getTxBody tx void $ sendTx $ signTxOperator admin tx Env.withDirectoryFor txI $ do Query.registryNodes @C.ConwayEra @@ -75,6 +85,56 @@ issueAlwaysSucceedsValidator = failOnError $ Env.withEnv $ do >>= void . expectN 1 "programmable logic outputs" pure () +{-| Issue some tokens with the smart stabelcoin transfer logic validator +-} +issueTransferLogicProgrammableToken :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m) => m () +issueTransferLogicProgrammableToken = failOnError $ Env.withEnv $ do + + -- register transfer minting script + -- register transfer spending script + -- register issuer spending script + + txI <- deployDirectorySet + + asAdmin @C.ConwayEra $ Env.withDirectoryFor txI $ do + opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) + -- register programmable global stake script + void $ registerTransferScripts opPkh + + asAdmin @C.ConwayEra $ Env.withDirectoryFor txI $ do + opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) + + Endpoints.issueSmartTokensTx "dummy asset" 100 (C.PaymentCredentialByKey opPkh) + >>= void . sendTx . signTxOperator admin + Query.registryNodes @C.ConwayEra + >>= void . expectN 2 " registry outputs" + Query.programmableLogicOutputs @C.ConwayEra + >>= void . expectN 1 "programmable logic outputs" + pure () + +{-| Issue some tokens with the smart stabelcoin transfer logic validator +-} +transferTransferLogicProgrammableToken :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m) => m () +transferTransferLogicProgrammableToken = failOnError $ Env.withEnv $ do + + txI <- deployDirectorySet + + asAdmin @C.ConwayEra $ Env.withDirectoryFor txI $ do + opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) + -- register programmable global stake script + void $ registerTransferScripts opPkh + + asAdmin @C.ConwayEra $ Env.withDirectoryFor txI $ do + opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) + + Endpoints.issueSmartTokensTx "dummy asset" 100 (C.PaymentCredentialByKey opPkh) + >>= void . sendTx . signTxOperator admin + Query.registryNodes @C.ConwayEra + >>= void . expectN 2 " registry outputs" + Query.programmableLogicOutputs @C.ConwayEra + >>= void . expectN 1 "programmable logic outputs" + pure () + dummyNodeArgs :: InsertNodeArgs dummyNodeArgs = @@ -86,7 +146,7 @@ dummyNodeArgs = {-| Register the 'alwaysSucceedsScript' stake validator -} -registerAlwaysSucceedsStakingCert :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m) => m () +registerAlwaysSucceedsStakingCert :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m) => m () registerAlwaysSucceedsStakingCert = failOnError $ do pp <- fmap C.unLedgerProtocolParameters queryProtocolParameters let script = C.PlutusScript C.plutusScriptVersion Scripts.alwaysSucceedsScript @@ -97,6 +157,38 @@ registerAlwaysSucceedsStakingCert = failOnError $ do BuildTx.addConwayStakeCredentialRegistrationCertificate cred (pp ^. Ledger.ppKeyDepositL) void (tryBalanceAndSubmit mempty Wallet.w1 txBody TrailingChange []) +registerTransferScripts :: (MonadFail m, MonadReader env m, Env.HasTransferLogicEnv env, MonadMockchain C.ConwayEra m) => C.Hash C.PaymentKey -> m C.TxId +registerTransferScripts pkh = failOnError $ do + pp <- fmap C.unLedgerProtocolParameters queryProtocolParameters + mintingScript <- asks (Env.tleMintingScript . Env.transferLogicEnv) + spendingScript <- asks (Env.tleTransferScript . Env.transferLogicEnv) + -- issuerScript <- asks (Env.tleIssuerScript . Env.transferLogicEnv) + let + hshMinting = C.hashScript $ C.PlutusScript C.plutusScriptVersion mintingScript + credMinting = C.StakeCredentialByScript hshMinting + + hshSpending = C.hashScript $ C.PlutusScript C.plutusScriptVersion spendingScript + credSpending = C.StakeCredentialByScript hshSpending + + -- hshIssuer = C.hashScript $ C.PlutusScript C.plutusScriptVersion issuerScript + -- credIssuer = C.StakeCredentialByScript hshIssuer + + txBody <- BuildTx.execBuildTxT $ do + BuildTx.addStakeScriptWitness credMinting mintingScript () + BuildTx.addConwayStakeCredentialRegistrationCertificate credMinting (pp ^. Ledger.ppKeyDepositL) + + BuildTx.addStakeScriptWitness credSpending spendingScript () + BuildTx.addConwayStakeCredentialRegistrationCertificate credSpending (pp ^. Ledger.ppKeyDepositL) + + -- BuildTx.addStakeScriptWitness credIssuer issuerScript () + -- BuildTx.addConwayStakeCredentialRegistrationCertificate credIssuer (pp ^. Ledger.ppKeyDepositL) + + BuildTx.addRequiredSignature pkh + + x <- tryBalanceAndSubmit mempty Wallet.w1 txBody TrailingChange [] + pure $ C.getTxId $ C.getTxBody x + + expectSingleton :: MonadFail m => String -> [a] -> m a expectSingleton msg = \case [a] -> pure a