Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions frontend/src/app/mint-authority/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default function Home() {
const [mintRecipientAddress, setMintRecipientAddress] = useState('mint recipient address');
const [sendRecipientAddress, setsendRecipientAddress] = useState('send recipient address');
const [freezeAccountNumber, setFreezeAccountNumber] = useState('account to freeze');
const [unfreezeAccountNumber, setUnfreezeAccountNumber] = useState('account to unfreeze');
const [freezeReason, setFreezeReason] = useState('Enter reason here');
const [seizeAccountNumber, setSeizeAccountNumber] = useState('account to seize');
const [seizeReason, setSeizeReason] = useState('Enter reason here');
Expand Down Expand Up @@ -216,6 +217,51 @@ export default function Home() {
}
};

const onUnfreeze = async () => {
console.log('unfreeze an account');
lucid.selectWallet.fromSeed(mintAccount.mnemonic);
changeAlertInfo({severity: 'info', message: 'Unfreeze request processing', open: true,});
const requestData = {
issuer: mintAccount.address,
blacklist_address: unfreezeAccountNumber,
};
try {
const response = await axios.post(
'/api/v1/tx/programmable-token/unblacklist',
requestData,
{
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
}
);
console.log('Unfreeze response:', response.data);
const tx = await lucid.fromTx(response.data.cborHex);
await signAndSentTx(lucid, tx);
changeAlertInfo({severity: 'success', message: 'Account successfully unfrozen', open: true,});
const unfrozenWalletKey = (Object.keys(accounts) as (keyof Accounts)[]).find(
(key) => accounts[key].address === freezeAccountNumber
);
if (unfrozenWalletKey) {
changeWalletAccountDetails(unfrozenWalletKey, {
...accounts[unfrozenWalletKey],
status: 'Active',
});
}
} catch (error: any) {
if (error.response.data.includes('BlacklistNodeNotFound')) {
changeAlertInfo({
severity: 'error',
message: 'This account is not frozen.',
open: true,
});
return;
} else {
console.error('Unfreeze failed:', error);
}
}
};

const onSeize = async () => {
console.log('seize account funds');
lucid.selectWallet.fromSeed(mintAccount.mnemonic);
Expand Down Expand Up @@ -292,6 +338,15 @@ export default function Home() {
/>
</Box>

const unFreezeContent = <Box>
<WSTTextField
value={unfreezeAccountNumber}
onChange={(e) => setUnfreezeAccountNumber(e.target.value)}
label="Account Number"
fullWidth={true}
/>
</Box>

const seizeContent = <Box>
<WSTTextField
value={seizeAccountNumber}
Expand Down Expand Up @@ -358,6 +413,12 @@ maxRows={3}
buttonLabel: 'Freeze',
onAction: onFreeze
},
{
label: 'Unfreeze',
content: unFreezeContent,
buttonLabel: 'Unfreeze',
onAction: onUnfreeze
},
{
label: 'Seize',
content: seizeContent,
Expand Down
61 changes: 45 additions & 16 deletions generated/openapi/schema.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,6 @@
{
"components": {
"schemas": {
"AddToBlacklistArgs": {
"properties": {
"blacklist_address": {
"$ref": "#/components/schemas/Address"
},
"issuer": {
"$ref": "#/components/schemas/Address"
}
},
"required": [
"issuer",
"blacklist_address"
],
"type": "object"
},
"AddVKeyWitnessArgs_ConwayEra": {
"properties": {
"w_tx": {
Expand All @@ -39,6 +24,21 @@
"Asset name": {
"type": "string"
},
"BlacklistNodeArgs": {
"properties": {
"blacklist_address": {
"$ref": "#/components/schemas/Address"
},
"issuer": {
"$ref": "#/components/schemas/Address"
}
},
"required": [
"issuer",
"blacklist_address"
],
"type": "object"
},
"Hash PaymentKey": {
"description": "Hash of a payment key",
"example": "f6ac5676b58d8ce280c1f09af4a2e82dd58c1aa2fb075aa005afa1da",
Expand Down Expand Up @@ -417,7 +417,7 @@
"content": {
"application/json;charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/AddToBlacklistArgs"
"$ref": "#/components/schemas/BlacklistNodeArgs"
}
}
}
Expand Down Expand Up @@ -526,6 +526,35 @@
}
}
},
"/api/v1/tx/programmable-token/unblacklist": {
"post": {
"description": "Remove a credential from the blacklist",
"requestBody": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/BlacklistNodeArgs"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/TextEnvelopeJSON"
}
}
},
"description": ""
},
"400": {
"description": "Invalid `body`"
}
}
}
},
"/api/v1/tx/submit": {
"post": {
"description": "Submit a transaction to the blockchain",
Expand Down
1 change: 1 addition & 0 deletions src/lib/Wst/AppError.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ data AppError era =
| BlockfrostErr BlockfrostError
| NoTokensToSeize -- ^ No tokens to seize
| DuplicateBlacklistNode -- ^ Attempting to add a duplicate blacklist node
| BlacklistNodeNotFound -- ^ Attempting to remove a blacklist node that does not exist
| TransferBlacklistedCredential Credential -- ^ Attempting to transfer funds from a blacklisted address
| SubmitError (ValidationError era)
deriving stock (Show)
13 changes: 9 additions & 4 deletions src/lib/Wst/Client.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Wst.Client (
postIssueProgrammableTokenTx,
postTransferProgrammableTokenTx,
postAddToBlacklistTx,
postRemoveFromBlacklistTx,
postSeizeFundsTx
) where

Expand All @@ -22,7 +23,7 @@ import Servant.Client (ClientEnv, client, runClientM)
import Servant.Client.Core (ClientError)
import SmartTokens.Types.ProtocolParams (ProgrammableLogicGlobalParams)
import Wst.Offchain.Query (UTxODat)
import Wst.Server.Types (API, APIInEra, AddToBlacklistArgs,
import Wst.Server.Types (API, APIInEra, BlacklistNodeArgs,
IssueProgrammableTokenArgs (..), SeizeAssetsArgs,
TextEnvelopeJSON, TransferProgrammableTokenArgs (..))

Expand All @@ -46,13 +47,17 @@ postTransferProgrammableTokenTx env args = do
let _ :<|> _ :<|> ((_ :<|> transferProgrammableTokenTx :<|> _) :<|> _) = client (Proxy @(API era))
runClientM (transferProgrammableTokenTx args) env

postAddToBlacklistTx :: forall era. C.IsShelleyBasedEra era => ClientEnv -> AddToBlacklistArgs -> IO (Either ClientError (TextEnvelopeJSON (C.Tx era)))
postAddToBlacklistTx :: forall era. C.IsShelleyBasedEra era => ClientEnv -> BlacklistNodeArgs -> IO (Either ClientError (TextEnvelopeJSON (C.Tx era)))
postAddToBlacklistTx env args = do
let _ :<|> _ :<|> ((_ :<|> _ :<|> addToBlacklistTx :<|> _) :<|> _) = client (Proxy @(API era))
runClientM (addToBlacklistTx args) env

postRemoveFromBlacklistTx :: forall era. C.IsShelleyBasedEra era => ClientEnv -> BlacklistNodeArgs -> IO (Either ClientError (TextEnvelopeJSON (C.Tx era)))
postRemoveFromBlacklistTx env args = do
let _ :<|> _ :<|> ((_ :<|> _ :<|> _ :<|> removeFromBlacklistTx :<|> _) :<|> _) = client (Proxy @(API era))
runClientM (removeFromBlacklistTx args) env

postSeizeFundsTx :: forall era. C.IsShelleyBasedEra era => ClientEnv -> SeizeAssetsArgs -> IO (Either ClientError (TextEnvelopeJSON (C.Tx era)))
postSeizeFundsTx env args = do
let _ :<|> _ :<|> ((_ :<|> _ :<|> _ :<|> seizeFunds) :<|> _) = client (Proxy @(API era))
let _ :<|> _ :<|> ((_ :<|> _ :<|> _ :<|> _ :<|> seizeFunds) :<|> _) = client (Proxy @(API era))
runClientM (seizeFunds args) env

49 changes: 42 additions & 7 deletions src/lib/Wst/Offchain/BuildTx/TransferLogic.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module Wst.Offchain.BuildTx.TransferLogic
seizeSmartTokens,
initBlacklist,
insertBlacklistNode,
spendBlacklistOutput,
removeBlacklistNode,
paySmartTokensToDestination,
registerTransferScripts,
)
Expand All @@ -36,7 +36,7 @@ import Convex.Utxos (UtxoSet (UtxoSet))
import Convex.Wallet (selectMixedInputsCovering)
import Data.Foldable (maximumBy)
import Data.Function (on)
import Data.List (nub, sort)
import Data.List (find, nub, sort)
import Data.Monoid (Last (..))
import GHC.Exts (IsList (..))
import PlutusLedgerApi.Data.V3 (Credential (..), PubKeyHash (PubKeyHash),
Expand All @@ -46,7 +46,7 @@ import SmartTokens.Contracts.ExampleTransferLogic (BlacklistProof (..))
import SmartTokens.Types.ProtocolParams
import SmartTokens.Types.PTokenDirectory (BlacklistNode (..),
DirectorySetNode (..))
import Wst.AppError (AppError (DuplicateBlacklistNode, TransferBlacklistedCredential))
import Wst.AppError (AppError (BlacklistNodeNotFound, DuplicateBlacklistNode, TransferBlacklistedCredential))
import Wst.Offchain.BuildTx.ProgrammableLogic (issueProgrammableToken,
seizeProgrammableToken,
transferProgrammableToken)
Expand Down Expand Up @@ -135,13 +135,48 @@ insertBlacklistNode cred blacklistNodes = Utils.inBabbage @era $ do
opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv)
addRequiredSignature opPkh

spendBlacklistOutput :: 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.TxIn -> m ()
spendBlacklistOutput txin = Utils.inBabbage @era $ do
spendingScript <- asks (Env.tleBlacklistSpendingScript . Env.transferLogicEnv)
spendPlutusInlineDatum txin spendingScript ()
removeBlacklistNode :: 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, MonadError (AppError era) m) => C.PaymentCredential -> [UTxODat era BlacklistNode]-> m ()
removeBlacklistNode cred blacklistNodes = Utils.inBabbage @era $ do
opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv)
blacklistSpendingScript <- asks (Env.tleBlacklistSpendingScript . Env.transferLogicEnv)
blacklistMintingScript <- asks (Env.tleBlacklistMintingScript . Env.transferLogicEnv)
blacklistPolicyId <- asks (Env.blacklistNodePolicyId . Env.transferLogicEnv)

-- find node to remove
UTxODat{uIn = delNodeRef, uOut = (C.TxOut _delAddr delOutVal _ _), uDatum = delNodeDatum}
<- maybe (throwError BlacklistNodeNotFound) pure $ find ((== unwrapCredential (transCredential cred)) . blnKey . uDatum) blacklistNodes


let expectedAssetName = C.AssetName $ case transCredential cred of
PubKeyCredential (PubKeyHash s) -> PlutusTx.fromBuiltin s
ScriptCredential (ScriptHash s) -> PlutusTx.fromBuiltin s

v = C.selectAsset (C.txOutValueToValue delOutVal) (C.AssetId blacklistPolicyId expectedAssetName)

when (v /= 1) $ error "Unexpected blacklist node token quantity. Head node should not be deleted"

-- find the node to update to point to the node after the node to remove
let UTxODat {uIn = prevNodeRef,uOut = (C.TxOut prevAddr prevVal _ _), uDatum = prevNode} =
maximumBy (compare `on` (blnKey . uDatum)) $
filter ((< unwrapCredential (transCredential cred)) . blnKey . uDatum) blacklistNodes

-- update the previous node to point to the node after the node to remove
updatedPrevNode = prevNode {blnNext=blnNext delNodeDatum}
updatedPrevNodeDatum = C.TxOutDatumInline C.babbageBasedEra $ C.toHashableScriptData updatedPrevNode
updatedPrevNodeOutput = C.TxOut prevAddr prevVal updatedPrevNodeDatum C.ReferenceScriptNone


-- spend the node to remove
spendPlutusInlineDatum delNodeRef blacklistSpendingScript ()
-- set previous node output
spendPlutusInlineDatum prevNodeRef blacklistSpendingScript ()
-- set previous node output
prependTxOut updatedPrevNodeOutput
-- burn the removed node blacklist token
mintPlutus blacklistMintingScript () expectedAssetName (-1)
addRequiredSignature opPkh


{-| Add a smart token output that locks the given value,
addressed to the payment credential
-}
Expand Down
26 changes: 8 additions & 18 deletions src/lib/Wst/Offchain/Endpoints/Deployment.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module Wst.Offchain.Endpoints.Deployment(
issueSmartTokensTx,
transferSmartTokensTx,
insertBlacklistNodeTx,
blacklistCredentialTx,
removeBlacklistNodeTx,
seizeCredentialAssetsTx,
) where

Expand Down Expand Up @@ -132,6 +132,13 @@ insertBlacklistNodeTx cred = do
(tx, _) <- Env.balanceTxEnv_ (BuildTx.insertBlacklistNode cred blacklist)
pure (Convex.CoinSelection.signBalancedTxBody [] tx)

removeBlacklistNodeTx :: 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)
removeBlacklistNodeTx cred = do
blacklist <- Query.blacklistNodes @era
(tx, _) <- Env.balanceTxEnv_ (BuildTx.removeBlacklistNode cred blacklist)
pure (Convex.CoinSelection.signBalancedTxBody [] tx)


{-| Build a transaction that issues a progammable token
-}
issueSmartTokensTx :: forall era env m.
Expand Down Expand Up @@ -182,23 +189,6 @@ transferSmartTokensTx assetId quantity destCred = do
BuildTx.transferSmartTokens paramsTxIn blacklist directory userOutputsAtProgrammable (assetId, quantity) destCred
pure (Convex.CoinSelection.signBalancedTxBody [] tx)

blacklistCredentialTx :: 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 -- ^ Source/User credential
-> m (C.Tx era)
blacklistCredentialTx sanctionedCred = do
blacklist <- Query.blacklistNodes @era
(tx, _) <- Env.balanceTxEnv_ $ do
BuildTx.insertBlacklistNode sanctionedCred blacklist
pure (Convex.CoinSelection.signBalancedTxBody [] tx)

seizeCredentialAssetsTx :: forall era env m.
( MonadReader env m
Expand Down
Loading
Loading