From d4d4f14aa94fa4c63b6f6cee948675ff9529f90f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Thu, 23 Jan 2025 09:01:50 +0100 Subject: [PATCH 01/18] Use address instead of account --- frontend/src/app/[username]/index.tsx | 6 +++--- frontend/src/app/components/NavDrawer.tsx | 4 ++-- frontend/src/app/components/WSTTable.tsx | 4 ++-- frontend/src/app/mint-authority/page.tsx | 22 +++++++++++----------- frontend/src/app/store/types.ts | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/[username]/index.tsx b/frontend/src/app/[username]/index.tsx index c9852bc..07c612c 100644 --- a/frontend/src/app/[username]/index.tsx +++ b/frontend/src/app/[username]/index.tsx @@ -41,7 +41,7 @@ export default function Profile() { if (getUserAccountDetails()?.status === 'Frozen') { changeAlertInfo({ severity: 'error', - message: 'Cannot send WST with frozen account.', + message: 'Cannot send WST with frozen address.', open: true, link: '' }); @@ -51,7 +51,7 @@ export default function Profile() { changeAlertInfo({severity: 'info', message: 'Transaction processing', open: true, link: ''}); const accountInfo = getUserAccountDetails(); if (!accountInfo) { - console.error("No valid send account found! Cannot send."); + console.error("No valid send address found! Cannot send."); return; } lucid.selectWallet.fromSeed(accountInfo.mnemonic); @@ -125,7 +125,7 @@ export default function Profile() {
- Account Balance + Address Balance {getUserAccountDetails()?.balance} WST {getUserAccountDetails()?.address.slice(0,15)} diff --git a/frontend/src/app/components/NavDrawer.tsx b/frontend/src/app/components/NavDrawer.tsx index eb1129e..2215027 100644 --- a/frontend/src/app/components/NavDrawer.tsx +++ b/frontend/src/app/components/NavDrawer.tsx @@ -21,7 +21,7 @@ const drawerWidth = 200; const iconMapping = { 'Mint Actions': , - 'Accounts': , + 'Addresses': , 'Wallet': }; @@ -30,7 +30,7 @@ export default function NavDrawer() { // Define list items based on the current user const listItems: MenuTab[] = currentUser === 'Mint Authority' ? - ['Mint Actions', 'Accounts'] : + ['Mint Actions', 'Addresses'] : ['Wallet']; const handleListItemClick = (item: MenuTab) => { diff --git a/frontend/src/app/components/WSTTable.tsx b/frontend/src/app/components/WSTTable.tsx index 018fc28..45b7a44 100644 --- a/frontend/src/app/components/WSTTable.tsx +++ b/frontend/src/app/components/WSTTable.tsx @@ -51,8 +51,8 @@ export default function WSTTable() { Address - Account Status - Account Balance + Address Status + Address Balance diff --git a/frontend/src/app/mint-authority/page.tsx b/frontend/src/app/mint-authority/page.tsx index b08b0fd..7c87b07 100644 --- a/frontend/src/app/mint-authority/page.tsx +++ b/frontend/src/app/mint-authority/page.tsx @@ -33,10 +33,10 @@ export default function Home() { const [sendTokensAmount, setSendTokens] = useState(0); 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 [freezeAccountNumber, setFreezeAccountNumber] = useState('address to freeze'); + const [unfreezeAccountNumber, setUnfreezeAccountNumber] = useState('address to unfreeze'); const [freezeReason, setFreezeReason] = useState('Enter reason here'); - const [seizeAccountNumber, setSeizeAccountNumber] = useState('account to seize'); + const [seizeAccountNumber, setSeizeAccountNumber] = useState('address to seize'); const [seizeReason, setSeizeReason] = useState('Enter reason here'); useEffect(() => { @@ -177,7 +177,7 @@ export default function Home() { }; const onFreeze = async () => { - console.log('freeze an account'); + console.log('freeze an address'); lucid.selectWallet.fromSeed(mintAccount.mnemonic); changeAlertInfo({severity: 'info', message: 'Freeze request processing', open: true, link: ''}); const requestData = { @@ -198,7 +198,7 @@ export default function Home() { console.log('Freeze response:', response.data); const tx = await lucid.fromTx(response.data.cborHex); const txId = await signAndSentTx(lucid, tx); - changeAlertInfo({severity: 'success', message: 'Account successfully frozen', open: true, link: `https://preview.cardanoscan.io/transaction/${txId.inputs[0].transaction_id}`}); + changeAlertInfo({severity: 'success', message: 'Address successfully frozen', open: true, link: `https://preview.cardanoscan.io/transaction/${txId.inputs[0].transaction_id}`}); const frozenWalletKey = (Object.keys(accounts) as (keyof Accounts)[]).find( (key) => accounts[key].address === freezeAccountNumber ); @@ -243,7 +243,7 @@ export default function Home() { console.log('Unfreeze response:', response.data); const tx = await lucid.fromTx(response.data.cborHex); const txId = await signAndSentTx(lucid, tx); - changeAlertInfo({severity: 'success', message: 'Account successfully unfrozen', open: true, link: `https://preview.cardanoscan.io/transaction/${txId.inputs[0].transaction_id}`}); + changeAlertInfo({severity: 'success', message: 'Address successfully unfrozen', open: true, link: `https://preview.cardanoscan.io/transaction/${txId.inputs[0].transaction_id}`}); const unfrozenWalletKey = (Object.keys(accounts) as (keyof Accounts)[]).find( (key) => accounts[key].address === freezeAccountNumber ); @@ -330,7 +330,7 @@ export default function Home() { setFreezeAccountNumber(e.target.value)} - label="Account Number" + label="Address" fullWidth={true} /> setUnfreezeAccountNumber(e.target.value)} - label="Account Number" + label="Address" fullWidth={true} /> @@ -357,7 +357,7 @@ const seizeContent = setSeizeAccountNumber(e.target.value)} -label="Account Number" +label="Address" fullWidth={true} />
; - case 'Accounts': + case 'Addresses': return <> - User Accounts + Addresses ; diff --git a/frontend/src/app/store/types.ts b/frontend/src/app/store/types.ts index 19454b8..eed7fbe 100644 --- a/frontend/src/app/store/types.ts +++ b/frontend/src/app/store/types.ts @@ -1,5 +1,5 @@ export type UserName = 'Mint Authority' | 'User A' | 'User B' | 'Connected Wallet'; -export type MenuTab = 'Mint Actions' | 'Accounts' | 'Wallet'; +export type MenuTab = 'Mint Actions' | 'Addresses' | 'Wallet'; export type AccountInfo = { address: string, mnemonic: string, From 744e9931d0fae384593230b5bb1e03dd3f6360e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Thu, 23 Jan 2025 09:43:47 +0100 Subject: [PATCH 02/18] Rename accounts. Copy address from WST table --- frontend/src/app/[username]/index.tsx | 4 ++-- frontend/src/app/[username]/page.tsx | 4 ++-- frontend/src/app/clientLayout.tsx | 10 +++++----- frontend/src/app/components/ProfileSwitcher.tsx | 6 +++--- frontend/src/app/components/WSTTable.tsx | 6 ++++++ frontend/src/app/mint-authority/page.tsx | 10 +++++----- frontend/src/app/store/store.tsx | 8 ++++---- frontend/src/app/store/types.ts | 8 ++++---- 8 files changed, 31 insertions(+), 25 deletions(-) diff --git a/frontend/src/app/[username]/index.tsx b/frontend/src/app/[username]/index.tsx index 07c612c..767d80e 100644 --- a/frontend/src/app/[username]/index.tsx +++ b/frontend/src/app/[username]/index.tsx @@ -27,8 +27,8 @@ export default function Profile() { const getUserAccountDetails = () => { switch (currentUser) { - case "User A": return accounts.userA; - case "User B": return accounts.userB; + case "Alice": return accounts.alice; + case "Bob": return accounts.bob; case "Connected Wallet": return accounts.walletUser; }; }; diff --git a/frontend/src/app/[username]/page.tsx b/frontend/src/app/[username]/page.tsx index 018517f..5e533e8 100644 --- a/frontend/src/app/[username]/page.tsx +++ b/frontend/src/app/[username]/page.tsx @@ -2,8 +2,8 @@ import Profile from '.'; export async function generateStaticParams() { return [ - { username: 'user-a' }, - { username: 'user-b' }, + { username: 'alice' }, + { username: 'bob' }, { username: 'connected-wallet' } // connected wallet ] } diff --git a/frontend/src/app/clientLayout.tsx b/frontend/src/app/clientLayout.tsx index 4a0f482..de97958 100644 --- a/frontend/src/app/clientLayout.tsx +++ b/frontend/src/app/clientLayout.tsx @@ -22,13 +22,13 @@ export default function ClientLayout({ children }: { children: React.ReactNode } try { // retrieve wallet info const mintAuthorityWallet = await getWalletFromSeed(mintAccount.mnemonic); - const walletA = await getWalletFromSeed(accounts.userA.mnemonic); - const walletB = await getWalletFromSeed(accounts.userB.mnemonic); + const walletA = await getWalletFromSeed(accounts.alice.mnemonic); + const walletB = await getWalletFromSeed(accounts.bob.mnemonic); // Update Zustand store with the initialized wallet information changeMintAccountDetails({ ...mintAccount, address: mintAuthorityWallet.address}); - changeWalletAccountDetails('userA', { ...accounts.userA, address: walletA.address},); - changeWalletAccountDetails('userB', { ...accounts.userB, address: walletB.address}); + changeWalletAccountDetails('alice', { ...accounts.alice, address: walletA.address},); + changeWalletAccountDetails('bob', { ...accounts.bob, address: walletB.address}); const initialLucid = await makeLucid(); setLucidInstance(initialLucid); @@ -41,7 +41,7 @@ export default function ClientLayout({ children }: { children: React.ReactNode } fetchUserWallets(); },[]); - if(accounts.userB.address === '') { + if(accounts.bob.address === '') { return
; diff --git a/frontend/src/app/components/ProfileSwitcher.tsx b/frontend/src/app/components/ProfileSwitcher.tsx index c1823b9..fef3c59 100644 --- a/frontend/src/app/components/ProfileSwitcher.tsx +++ b/frontend/src/app/components/ProfileSwitcher.tsx @@ -86,9 +86,9 @@ export default function ProfileSwitcher() { onClose={handleClose} > handleSelect('Mint Authority')}>Mint Authority - handleSelect('User A')}>User A - handleSelect('User B')}>User B - handleWalletConnect('Connected Wallet')}>Lace + handleSelect('Alice')}>Alice + handleSelect('Bob')}>Bob + {/* handleWalletConnect('Connected Wallet')}>Lace */} ); diff --git a/frontend/src/app/components/WSTTable.tsx b/frontend/src/app/components/WSTTable.tsx index 45b7a44..375406e 100644 --- a/frontend/src/app/components/WSTTable.tsx +++ b/frontend/src/app/components/WSTTable.tsx @@ -12,10 +12,12 @@ import TableCell from "@mui/material/TableCell"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import Paper from "@mui/material/Paper"; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; //Local Imports import useStore from '../store/store'; import { useEffect } from "react"; +import IconButton from './WSTIconButton'; const progLogicBase : LucidCredential = { type: "Script", @@ -43,6 +45,9 @@ export default function WSTTable() { getAccounts(); }, []); + const copyToClipboard = (str: string) => { + navigator.clipboard.writeText(str); + } return ( @@ -61,6 +66,7 @@ export default function WSTTable() { {`${acct?.address.slice(0,15)}...${acct?.address.slice(104,108)}`} + copyToClipboard(acct.address)} icon={}/> {acct.status} diff --git a/frontend/src/app/mint-authority/page.tsx b/frontend/src/app/mint-authority/page.tsx index 7c87b07..04d598d 100644 --- a/frontend/src/app/mint-authority/page.tsx +++ b/frontend/src/app/mint-authority/page.tsx @@ -49,13 +49,13 @@ export default function Home() { const fetchUserDetails = async () => { const mintBalance = await getWalletBalance(mintAccount.address); - const userABalance = await getWalletBalance(accounts.userA.address); - const userBBalance = await getWalletBalance(accounts.userB.address); + const userABalance = await getWalletBalance(accounts.alice.address); + const userBBalance = await getWalletBalance(accounts.bob.address); // Update Zustand store with the initialized wallet information await changeMintAccountDetails({ ...mintAccount, balance: mintBalance}); - await changeWalletAccountDetails('userA', { ...accounts.userA, balance: userABalance}); - await changeWalletAccountDetails('userB', { ...accounts.userB, balance: userBBalance}); + await changeWalletAccountDetails('alice', { ...accounts.alice, balance: userABalance}); + await changeWalletAccountDetails('bob', { ...accounts.bob, balance: userBBalance}); }; const fetchBlacklistStatus = async () => { @@ -400,7 +400,7 @@ maxRows={3} return <> - Mint Balance + Mint Authority Balance {mintAccount.balance} WST UserID: {mintAccount.address.slice(0,15)} diff --git a/frontend/src/app/store/store.tsx b/frontend/src/app/store/store.tsx index 2da39f2..6d045ab 100644 --- a/frontend/src/app/store/store.tsx +++ b/frontend/src/app/store/store.tsx @@ -32,13 +32,13 @@ const useStore = create((set) => ({ balance: 0, }, accounts: { - userA: { + alice: { address: '', mnemonic: 'during dolphin crop lend pizza guilt hen earn easy direct inhale deputy detect season army inject exhaust apple hard front bubble emotion short portion', balance: 0, status: 'Active', }, - userB: { + bob: { address: '', mnemonic: 'silver legal flame powder fence kiss stable margin refuse hold unknown valid wolf kangaroo zero able waste jewel find salad sadness exhibit hello tape', balance: 0, @@ -82,8 +82,8 @@ const useStore = create((set) => ({ case 'Mint Authority': firstAccessibleTab = 'Mint Actions'; break; - case 'User A': - case 'User B': + case 'Alice': + case 'Bob': firstAccessibleTab = 'Wallet'; break; case 'Connected Wallet': diff --git a/frontend/src/app/store/types.ts b/frontend/src/app/store/types.ts index eed7fbe..b8e736c 100644 --- a/frontend/src/app/store/types.ts +++ b/frontend/src/app/store/types.ts @@ -1,4 +1,4 @@ -export type UserName = 'Mint Authority' | 'User A' | 'User B' | 'Connected Wallet'; +export type UserName = 'Mint Authority' | 'Alice' | 'Bob' | 'Connected Wallet'; export type MenuTab = 'Mint Actions' | 'Addresses' | 'Wallet'; export type AccountInfo = { address: string, @@ -6,10 +6,10 @@ export type AccountInfo = { balance: number, status?: 'Active' | 'Frozen', }; -export type AccountKey = 'userA' | 'userB' | 'walletUser'; +export type AccountKey = 'alice' | 'bob' | 'walletUser'; export type Accounts = { - userA: AccountInfo; - userB: AccountInfo; + alice: AccountInfo; + bob: AccountInfo; walletUser: AccountInfo; }; export type Severity = 'success' | 'error' | 'info' | 'warning'; From 97cf4feaf206503b71f455bc79dff9550ea4f4a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Thu, 23 Jan 2025 10:27:22 +0100 Subject: [PATCH 03/18] More fixes --- frontend/src/app/mint-authority/page.tsx | 1 + src/lib/Wst/Offchain/BuildTx/TransferLogic.hs | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/mint-authority/page.tsx b/frontend/src/app/mint-authority/page.tsx index 04d598d..3d3aa57 100644 --- a/frontend/src/app/mint-authority/page.tsx +++ b/frontend/src/app/mint-authority/page.tsx @@ -229,6 +229,7 @@ export default function Home() { const requestData = { issuer: mintAccount.address, blacklist_address: unfreezeAccountNumber, + reason: "(unfreeze)" }; try { const response = await axios.post( diff --git a/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs b/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs index e1ddde3..6181a92 100644 --- a/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs +++ b/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs @@ -117,7 +117,7 @@ instance ToSchema BlacklistReason where -} addBlacklistReason :: (C.IsShelleyBasedEra era, MonadBuildTx era m) => BlacklistReason -> m () addBlacklistReason (BlacklistReason reason) = - addBtx (set (L.txMetadata . L._TxMetadata . at 0) (Just (C.TxMetaMap [(C.TxMetaText "blacklist.reason", C.metaTextChunks reason)]))) + addBtx (set (L.txMetadata . L._TxMetadata . at 1) (Just (C.TxMetaMap [(C.TxMetaText "reason", C.metaTextChunks reason)]))) 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, MonadError (AppError era) m) => BlacklistReason -> C.PaymentCredential -> [UTxODat era BlacklistNode]-> m () insertBlacklistNode reason cred blacklistNodes = Utils.inBabbage @era $ do @@ -272,8 +272,7 @@ instance ToSchema SeizeReason where -} addSeizeReason :: (C.IsShelleyBasedEra era, MonadBuildTx era m) => SeizeReason -> m () addSeizeReason (SeizeReason reason) = - addBtx (set (L.txMetadata . L._TxMetadata . at 1) (Just (C.TxMetaMap [(C.TxMetaText "seize.reason", C.metaTextChunks reason)]))) - + addBtx (set (L.txMetadata . L._TxMetadata . at 1) (Just (C.TxMetaMap [(C.TxMetaText "reason", C.metaTextChunks reason)]))) 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) => SeizeReason -> UTxODat era ProgrammableLogicGlobalParams -> UTxODat era a -> C.PaymentCredential -> [UTxODat era DirectorySetNode] -> m () seizeSmartTokens reason paramsTxIn seizingTxo destinationCred directoryList = Utils.inBabbage @era $ do From d1e5cd97bc097ebc3b9d0f7045a77da4f31e7418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Thu, 23 Jan 2025 10:37:37 +0100 Subject: [PATCH 04/18] Enable connected wallet again --- frontend/src/app/components/ProfileSwitcher.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/ProfileSwitcher.tsx b/frontend/src/app/components/ProfileSwitcher.tsx index fef3c59..85ceeb1 100644 --- a/frontend/src/app/components/ProfileSwitcher.tsx +++ b/frontend/src/app/components/ProfileSwitcher.tsx @@ -88,7 +88,7 @@ export default function ProfileSwitcher() { handleSelect('Mint Authority')}>Mint Authority handleSelect('Alice')}>Alice handleSelect('Bob')}>Bob - {/* handleWalletConnect('Connected Wallet')}>Lace */} + handleWalletConnect('Connected Wallet')}>Lace ); From b674d446ca9171c3d206c7485a1c3db560077c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Thu, 23 Jan 2025 11:30:42 +0100 Subject: [PATCH 05/18] Fix link to transaction --- frontend/src/app/mint-authority/page.tsx | 11 ++++++----- frontend/src/app/utils/walletUtils.ts | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/mint-authority/page.tsx b/frontend/src/app/mint-authority/page.tsx index 3d3aa57..fba0b29 100644 --- a/frontend/src/app/mint-authority/page.tsx +++ b/frontend/src/app/mint-authority/page.tsx @@ -126,7 +126,7 @@ export default function Home() { const txId = await cmlClonedSignedTx.submit(); await lucid.awaitTx(txId); - changeAlertInfo({severity: 'success', message: 'Successful new WST mint. View the transaction here:', open: true, link: `https://preview.cardanoscan.io/transaction/${txIDObject.inputs[0].transaction_id}`}); + changeAlertInfo({severity: 'success', message: 'Successful new WST mint. View the transaction here:', open: true, link: `https://preview.cexplorer.io/tx/${txIDObject.inputs[0].transaction_id}`}); await fetchUserDetails(); @@ -169,7 +169,7 @@ export default function Home() { balance: newAccountBalance, }); } - changeAlertInfo({severity: 'success', message: 'Transaction sent successfully!', open: true, link: `https://preview.cardanoscan.io/transaction/${txId.inputs[0].transaction_id}`}); + changeAlertInfo({severity: 'success', message: 'Transaction sent successfully!', open: true, link: `https://preview.cexplorer.io/tx/${txId}`}); await fetchUserDetails(); } catch (error) { console.error('Send failed:', error); @@ -198,7 +198,8 @@ export default function Home() { console.log('Freeze response:', response.data); const tx = await lucid.fromTx(response.data.cborHex); const txId = await signAndSentTx(lucid, tx); - changeAlertInfo({severity: 'success', message: 'Address successfully frozen', open: true, link: `https://preview.cardanoscan.io/transaction/${txId.inputs[0].transaction_id}`}); + console.log(txId); + changeAlertInfo({severity: 'success', message: 'Address successfully frozen', open: true, link: `https://preview.cexplorer.io/tx/${txId}`}); const frozenWalletKey = (Object.keys(accounts) as (keyof Accounts)[]).find( (key) => accounts[key].address === freezeAccountNumber ); @@ -244,7 +245,7 @@ export default function Home() { console.log('Unfreeze response:', response.data); const tx = await lucid.fromTx(response.data.cborHex); const txId = await signAndSentTx(lucid, tx); - changeAlertInfo({severity: 'success', message: 'Address successfully unfrozen', open: true, link: `https://preview.cardanoscan.io/transaction/${txId.inputs[0].transaction_id}`}); + changeAlertInfo({severity: 'success', message: 'Address successfully unfrozen', open: true, link: `https://preview.cexplorer.io/tx/${txId}`}); const unfrozenWalletKey = (Object.keys(accounts) as (keyof Accounts)[]).find( (key) => accounts[key].address === freezeAccountNumber ); @@ -300,7 +301,7 @@ export default function Home() { balance: newAccountBalance, }); } - changeAlertInfo({severity: 'success', message: 'Funds successfully seized', open: true, link: `https://preview.cardanoscan.io/transaction/${txId.inputs[0].transaction_id}`}); + changeAlertInfo({severity: 'success', message: 'Funds successfully seized', open: true, link: `https://preview.cexplorer.io/tx/${txId}`}); await fetchUserDetails(); } catch (error) { console.error('Seize failed:', error); diff --git a/frontend/src/app/utils/walletUtils.ts b/frontend/src/app/utils/walletUtils.ts index 4992efc..c924337 100644 --- a/frontend/src/app/utils/walletUtils.ts +++ b/frontend/src/app/utils/walletUtils.ts @@ -82,7 +82,7 @@ export async function getBlacklist(){ } } -export async function signAndSentTx(lucid: LucidEvolution, tx: TxSignBuilder) { +export async function signAndSentTx(lucid: LucidEvolution, tx: TxSignBuilder): Promise { const txBuilder = await makeTxSignBuilder(lucid.wallet(), tx.toTransaction()).complete(); const cmlTx = txBuilder.toTransaction(); const witnessSet = txBuilder.toTransaction().witness_set(); @@ -99,7 +99,7 @@ export async function signAndSentTx(lucid: LucidEvolution, tx: TxSignBuilder) { const txId = await cmlClonedSignedTx.submit(); await lucid.awaitTx(txId); console.log(cmlClonedSignedTx); - return txIDObject + return txId } export type WalletType = "Lace" | "Eternl" | "Nami" | "Yoroi"; From 5d13429ac141fa48c2fc36c4c1b540a63df8379a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Thu, 23 Jan 2025 12:52:10 +0100 Subject: [PATCH 06/18] Add screenshot --- README.md | 5 ++++- image.png | Bin 0 -> 49960 bytes 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 image.png diff --git a/README.md b/README.md index 0e08e1d..3b7c807 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Regulated stablecoin POC -This is a proof-of-concept for a regulated stablecoin. It is NOT a finished product. +This is a proof-of-concept for a regulated token with freeze and seize capabilities. + +![Screenshot of the UI showing the minting authority.](image.png) # Overview @@ -20,6 +22,7 @@ This repository contains * A user interface that implements the use cases using browser-based wallets. Based on next.js and lucid. * An OCI container image with the on-chain code, the off-chain code and the UI + With the container image it is possible to run the complete system locally with just a single command. There is no need to install the build toolchain or to operate a cardano node or related infrastructure. The image can even be used to interact with existing deployments of the POC. diff --git a/image.png b/image.png new file mode 100644 index 0000000000000000000000000000000000000000..6ce5e9e33c5a133a1638daeb524341b0ce9410d8 GIT binary patch literal 49960 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 M(BgLEZx4R|UtCdkF#rGn literal 0 HcmV?d00001 From cb26911f489541cd0962e9155944f72dd53484ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Thu, 23 Jan 2025 14:29:18 +0100 Subject: [PATCH 07/18] Fix link to transactio in wallet --- frontend/src/app/[username]/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/[username]/index.tsx b/frontend/src/app/[username]/index.tsx index 767d80e..fc8065d 100644 --- a/frontend/src/app/[username]/index.tsx +++ b/frontend/src/app/[username]/index.tsx @@ -77,7 +77,7 @@ export default function Profile() { const txId = await signAndSentTx(lucid, tx); await updateAccountBalance(sendRecipientAddress); await updateAccountBalance(accountInfo.address); - changeAlertInfo({severity: 'success', message: 'Transaction sent successfully!', open: true, link: `https://preview.cardanoscan.io/transaction/${txId.inputs[0].transaction_id}`}); + changeAlertInfo({severity: 'success', message: 'Transaction sent successfully!', open: true, link: `https://preview.cexplorer.io/tx/${txId}`}); } catch (error) { console.error('Send failed:', error); } From 3ed3621c4406b7e0d5ee7bd0dcaa57577bde050d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Thu, 23 Jan 2025 14:40:09 +0100 Subject: [PATCH 08/18] Linter warning --- frontend/src/app/utils/walletUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/utils/walletUtils.ts b/frontend/src/app/utils/walletUtils.ts index c924337..f58c5a8 100644 --- a/frontend/src/app/utils/walletUtils.ts +++ b/frontend/src/app/utils/walletUtils.ts @@ -90,7 +90,6 @@ export async function signAndSentTx(lucid: LucidEvolution, tx: TxSignBuilder): P // console.log('Calculated Script Data Hash:', expectedScriptDataHash?.to_hex()); const cmlTxBodyClone = CML.TransactionBody.from_cbor_hex(cmlTx!.body().to_cbor_hex()); const txIDinAlert = await cmlTxBodyClone.to_json(); - const txIDObject = JSON.parse(txIDinAlert); // console.log('Preclone script hash:', cmlTxBodyClone.script_data_hash()?.to_hex()); cmlTxBodyClone.set_script_data_hash(expectedScriptDataHash!); // console.log('Postclone script hash:', cmlTxBodyClone.script_data_hash()?.to_hex()); From 3c2cb8a4694a362d73cfd18325399cc9a5927afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Thu, 23 Jan 2025 15:35:00 +0100 Subject: [PATCH 09/18] Fix mint endpoint --- frontend/src/app/mint-authority/page.tsx | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/mint-authority/page.tsx b/frontend/src/app/mint-authority/page.tsx index fba0b29..463de52 100644 --- a/frontend/src/app/mint-authority/page.tsx +++ b/frontend/src/app/mint-authority/page.tsx @@ -106,28 +106,9 @@ export default function Home() { ); console.log('Mint response:', response.data); const tx = await lucid.fromTx(response.data.cborHex); - // await signAndSentTx(lucid, tx); - const txBuilder = await makeTxSignBuilder(lucid.wallet(), tx.toTransaction()).complete(); - const cmlTx = txBuilder.toTransaction() - // console.log("TxBody: " + cmlTx.body().to_json()); - const witnessSet = txBuilder.toTransaction().witness_set(); - const expectedScriptDataHash : CML.ScriptDataHash | undefined = CML.calc_script_data_hash(witnessSet.redeemers()!, CML.PlutusDataList.new(), lucid.config().costModels!, witnessSet.languages()); - // console.log('Calculated Script Data Hash:', expectedScriptDataHash?.to_hex()); - const cmlTxBodyClone = CML.TransactionBody.from_cbor_hex(cmlTx!.body().to_cbor_hex()); - // console.log("TxBody: " + cmlTxBodyClone.to_json()); - const txIDinAlert = await cmlTxBodyClone.to_json(); - const txIDObject = JSON.parse(txIDinAlert); - // console.log('Preclone script hash:', cmlTxBodyClone.script_data_hash()?.to_hex()); - cmlTxBodyClone.set_script_data_hash(expectedScriptDataHash!); - // console.log('Postclone script hash:', cmlTxBodyClone.script_data_hash()?.to_hex()); - const cmlClonedTx = CML.Transaction.new(cmlTxBodyClone, cmlTx!.witness_set(), true, cmlTx!.auxiliary_data()); - const cmlClonedSignedTx = await makeTxSignBuilder(lucid.wallet(), cmlClonedTx).sign.withWallet().complete(); - - const txId = await cmlClonedSignedTx.submit(); - await lucid.awaitTx(txId); - - changeAlertInfo({severity: 'success', message: 'Successful new WST mint. View the transaction here:', open: true, link: `https://preview.cexplorer.io/tx/${txIDObject.inputs[0].transaction_id}`}); + const txId = await signAndSentTx(lucid, tx); + changeAlertInfo({severity: 'success', message: 'Successful new WST mint. View the transaction here:', open: true, link: `https://preview.cexplorer.io/tx/${txId}`}); await fetchUserDetails(); } catch (error) { From 3ca46760f6b10b5cb47db5ccf40b58e46a1d607d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Sat, 25 Jan 2025 09:34:22 +0100 Subject: [PATCH 10/18] Add override to submit failing tx to network --- src/lib/Wst/Offchain/BuildTx/Failing.hs | 97 +++++++++++++++++++ src/lib/Wst/Offchain/BuildTx/TransferLogic.hs | 24 ++--- src/lib/Wst/Offchain/Endpoints/Deployment.hs | 12 ++- src/lib/Wst/Server.hs | 7 +- src/lib/Wst/Server/Types.hs | 1 + src/test/unit/Wst/Test/UnitTest.hs | 16 +-- src/wst-poc.cabal | 4 + 7 files changed, 134 insertions(+), 27 deletions(-) create mode 100644 src/lib/Wst/Offchain/BuildTx/Failing.hs diff --git a/src/lib/Wst/Offchain/BuildTx/Failing.hs b/src/lib/Wst/Offchain/BuildTx/Failing.hs new file mode 100644 index 0000000..19680c7 --- /dev/null +++ b/src/lib/Wst/Offchain/BuildTx/Failing.hs @@ -0,0 +1,97 @@ +{-# LANGUAGE ConstraintKinds #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE UndecidableInstances #-} +{-| Tools for deliberately building a transaction +with "scriptValidity" flag set to "invalid". +-} +module Wst.Offchain.BuildTx.Failing( + IsEra, + BlacklistedTransferPolicy(..), + balanceTxEnvFailing +) where + +import Cardano.Api.Experimental (IsEra, obtainCommonConstraints, useEra) +import Cardano.Api.Experimental qualified as C +import Cardano.Api.Shelley qualified as C +import Cardano.Ledger.Api qualified as L +import Control.Lens (Iso', _3, _Just, at, iso, set, (&), (.~)) +import Control.Monad.Except (MonadError, throwError) +import Control.Monad.Reader (MonadReader, ReaderT, ask, asks, runReaderT) +import Control.Monad.Trans.Class (MonadTrans (..)) +import Convex.BuildTx (BuildTxT) +import Convex.BuildTx qualified as BuildTx +import Convex.CardanoApi.Lenses qualified as L +import Convex.Class (MonadBlockchain (utxoByTxIn), queryProtocolParameters) +import Convex.CoinSelection qualified as CoinSelection +import Convex.PlutusLedger.V1 (transCredential) +import Convex.Scripts (toHashableScriptData) +import Convex.Utils (mapError) +import Convex.Utxos (BalanceChanges) +import Convex.Utxos qualified as Utxos +import Convex.Wallet.Operator (returnOutputFor) +import Data.Bifunctor (Bifunctor (..)) +import Data.Map (Map) +import Wst.AppError (AppError (..)) +import Wst.Offchain.BuildTx.TransferLogic (FindProofResult (..), + blacklistInitialNode) +import Wst.Offchain.Env (HasOperatorEnv (..), OperatorEnv (..)) +import Wst.Offchain.Query (UTxODat (..)) + +{-| What to do if a transfer cannot proceed because of blacklisting +-} +data BlacklistedTransferPolicy + = SubmitFailingTx -- ^ Deliberately submit a transaction with "scriptValidity = False". This will result in the collateral input being spent! + | DontSubmitFailingTx -- ^ Don't submit a transaction + deriving stock (Eq, Show) + +{-| Balance a transaction using the operator's funds and return output +-} +balanceTxEnvFailing :: forall era env m. (IsEra era, MonadBlockchain era m, MonadReader env m, HasOperatorEnv era env, MonadError (AppError era) m, C.IsBabbageBasedEra era) => BlacklistedTransferPolicy -> BuildTxT era m (FindProofResult era) -> m (C.BalancedTxBody era, BalanceChanges) +balanceTxEnvFailing policy btx = do + OperatorEnv{bteOperatorUtxos, bteOperator} <- asks operatorEnv + params <- queryProtocolParameters + (r, txBuilder) <- BuildTx.runBuildTxT $ btx <* BuildTx.setMinAdaDepositAll params + -- TODO: change returnOutputFor to consider the stake address reference + -- (needs to be done in sc-tools) + let credential = C.PaymentCredentialByKey $ fst bteOperator + output <- returnOutputFor credential + (balBody, balChanges) <- case r of + CredentialNotBlacklisted{} -> + mapError BalancingError (CoinSelection.balanceTx mempty output (Utxos.fromApiUtxo bteOperatorUtxos) txBuilder CoinSelection.TrailingChange) + CredentialBlacklisted UTxODat{uIn} + | policy == SubmitFailingTx -> + fmap (first setScriptsInvalid) + $ runBacklistResetT uIn + $ mapError BalancingError (CoinSelection.balanceTx mempty output (Utxos.fromApiUtxo bteOperatorUtxos) txBuilder CoinSelection.TrailingChange) + | otherwise -> + throwError (TransferBlacklistedCredential (transCredential credential)) + NoBlacklistNodes -> throwError BlacklistNodeNotFound + pure (balBody, balChanges) + +newtype BlacklistResetT m a = BlacklistResetT (ReaderT C.TxIn m a) + deriving newtype (Functor, Applicative, Monad, MonadError e, MonadTrans) + +instance (C.IsBabbageBasedEra era, MonadBlockchain era m) => MonadBlockchain era (BlacklistResetT m) where + utxoByTxIn txis = BlacklistResetT $ do + txi <- ask + let newDat = C.TxOutDatumInline C.babbageBasedEra (toHashableScriptData blacklistInitialNode) + fmap (set (_UTxO . at txi . _Just . L._TxOut . _3) newDat) $ utxoByTxIn txis + +runBacklistResetT :: C.TxIn -> BlacklistResetT m a -> m a +runBacklistResetT txi (BlacklistResetT action) = runReaderT action txi + +_UTxO :: Iso' (C.UTxO era) (Map C.TxIn (C.TxOut C.CtxUTxO era)) +_UTxO = iso t f where + t (C.UTxO k) = k + f = C.UTxO + +setScriptsInvalid :: + forall era. + ( IsEra era + ) + => C.BalancedTxBody era + -> C.BalancedTxBody era +setScriptsInvalid (C.BalancedTxBody a (C.UnsignedTx b) c d) = obtainCommonConstraints (useEra @era) $ + let b' = C.UnsignedTx (b & L.isValidTxL @(C.LedgerEra era) .~ L.IsValid False) + in C.BalancedTxBody a b' c d diff --git a/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs b/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs index 6181a92..3aafe53 100644 --- a/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs +++ b/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs @@ -5,6 +5,7 @@ module Wst.Offchain.BuildTx.TransferLogic ( transferSmartTokens, + FindProofResult(..), issueSmartTokens, SeizeReason(..), seizeSmartTokens, @@ -14,6 +15,7 @@ module Wst.Offchain.BuildTx.TransferLogic removeBlacklistNode, paySmartTokensToDestination, registerTransferScripts, + blacklistInitialNode ) where @@ -54,7 +56,7 @@ import SmartTokens.Contracts.ExampleTransferLogic (BlacklistProof (..)) import SmartTokens.Types.ProtocolParams import SmartTokens.Types.PTokenDirectory (BlacklistNode (..), DirectorySetNode (..)) -import Wst.AppError (AppError (BlacklistNodeNotFound, DuplicateBlacklistNode, TransferBlacklistedCredential)) +import Wst.AppError (AppError (BlacklistNodeNotFound, DuplicateBlacklistNode)) import Wst.Offchain.BuildTx.ProgrammableLogic (issueProgrammableToken, seizeProgrammableToken, transferProgrammableToken) @@ -222,7 +224,7 @@ issueSmartTokens paramsTxOut (an, q) directoryList destinationCred = Utils.inBab paySmartTokensToDestination (an, q) issuedPolicyId destinationCred pure $ C.AssetId issuedPolicyId an -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, Env.HasOperatorEnv era env, MonadError (AppError era) m) => UTxODat era ProgrammableLogicGlobalParams -> [UTxODat era BlacklistNode] -> [UTxODat era DirectorySetNode] -> [UTxODat era a] -> (C.AssetId, C.Quantity) -> C.PaymentCredential -> m () +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, Env.HasOperatorEnv era env, MonadError (AppError era) m) => UTxODat era ProgrammableLogicGlobalParams -> [UTxODat era BlacklistNode] -> [UTxODat era DirectorySetNode] -> [UTxODat era a] -> (C.AssetId, C.Quantity) -> C.PaymentCredential -> m (FindProofResult era) transferSmartTokens paramsTxIn blacklistNodes directoryList spendingUserOutputs (assetId, q) destinationCred = Utils.inBabbage @era $ do nid <- queryNetworkId userCred <- Env.operatorPaymentCredential @@ -238,7 +240,7 @@ transferSmartTokens paramsTxIn blacklistNodes directoryList spendingUserOutputs C.AdaAssetId -> error "Ada is not programmable" transferProgrammableToken paramsTxIn txins (transPolicyId programmablePolicyId) directoryList -- Invoking the programmableBase and global scripts - addTransferWitness blacklistNodes -- Proof of non-membership of the blacklist + result <- addTransferWitness blacklistNodes -- Proof of non-membership of the blacklist -- Send outputs to destinationCred destStakeCred <- either (error . ("Could not unTrans credential: " <>) . show) pure $ unTransStakeCredential $ transCredential destinationCred @@ -255,6 +257,7 @@ transferSmartTokens paramsTxIn blacklistNodes directoryList spendingUserOutputs returnAddr = C.makeShelleyAddressInEra C.shelleyBasedEra nid progLogicBaseCred (C.StakeAddressByValue srcStakeCred) returnOutput = C.TxOut returnAddr returnVal C.TxOutDatumNone C.ReferenceScriptNone prependTxOut returnOutput -- Add the seized output to the transaction + pure result {-| Reason for adding an address to the blacklist -} @@ -338,6 +341,7 @@ tryFindProof :: [UTxODat era BlacklistNode] -> Credential -> UTxODat era Blackli tryFindProof blacklistNodes cred = case findProof blacklistNodes cred of CredentialNotBlacklisted r -> r + CredentialBlacklisted r -> r _ -> error $ "tryFindProof failed for " <> show cred {-| Find the blacklist node that covers the credential. @@ -352,18 +356,10 @@ findProof blacklistNodes cred = then CredentialBlacklisted node else CredentialNotBlacklisted node -{-| Check that the credential is not blacklisted. Throw an error if the - credential is blacklisted. --} -checkNotBlacklisted :: forall era m. MonadError (AppError era) m => [UTxODat era BlacklistNode] -> Credential -> m () -checkNotBlacklisted nodes cred = case findProof nodes cred of - CredentialNotBlacklisted{} -> pure () - _ -> throwError (TransferBlacklistedCredential cred) - {-| Add a proof that the user is allowed to transfer programmable tokens. Uses the user from 'HasOperatorEnv env'. Fails if the user is blacklisted. -} -addTransferWitness :: forall env era m. (MonadError (AppError era) 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) => [UTxODat era BlacklistNode] -> m () +addTransferWitness :: forall env era m. (MonadError (AppError era) 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) => [UTxODat era BlacklistNode] -> m (FindProofResult era) addTransferWitness blacklistNodes = Utils.inBabbage @era $ do opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) -- In this case 'operator' is the user nid <- queryNetworkId @@ -390,7 +386,7 @@ addTransferWitness blacklistNodes = Utils.inBabbage @era $ do -- This means we're traversing the list of blacklist nodes an additional time. -- But here is the only place where we can use MonadError. So we have to do it -- here to allow the client code to handle the error properly. - checkNotBlacklisted blacklistNodes (transCredential $ C.PaymentCredentialByKey opPkh) + let proofResult = findProof blacklistNodes (transCredential $ C.PaymentCredentialByKey opPkh) addRequiredSignature opPkh addReferencesWithTxBody witnessReferences @@ -398,12 +394,12 @@ addTransferWitness blacklistNodes = Utils.inBabbage @era $ do (C.makeStakeAddress nid transferStakeCred) (C.Quantity 0) $ C.ScriptWitness C.ScriptWitnessForStakeAddr . transferStakeWitness + pure proofResult addReferencesWithTxBody :: (MonadBuildTx era m, C.IsBabbageBasedEra era) => (C.TxBodyContent C.BuildTx era -> [C.TxIn]) -> m () addReferencesWithTxBody f = addTxBuilder (TxBuilder $ \body -> over (L.txInsReference . L._TxInsReferenceIso) (nub . (f body <>))) - addSeizeWitness :: forall env era 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 () addSeizeWitness = Utils.inBabbage @era $ do opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) diff --git a/src/lib/Wst/Offchain/Endpoints/Deployment.hs b/src/lib/Wst/Offchain/Endpoints/Deployment.hs index 4defc89..5ed13c4 100644 --- a/src/lib/Wst/Offchain/Endpoints/Deployment.hs +++ b/src/lib/Wst/Offchain/Endpoints/Deployment.hs @@ -30,6 +30,8 @@ import SmartTokens.Types.PTokenDirectory (DirectorySetNode (..)) import Wst.AppError (AppError (NoTokensToSeize)) import Wst.Offchain.BuildTx.DirectorySet (InsertNodeArgs (inaNewKey)) import Wst.Offchain.BuildTx.DirectorySet qualified as BuildTx +import Wst.Offchain.BuildTx.Failing (BlacklistedTransferPolicy, IsEra, + balanceTxEnvFailing) import Wst.Offchain.BuildTx.ProgrammableLogic qualified as BuildTx import Wst.Offchain.BuildTx.ProtocolParams qualified as BuildTx import Wst.Offchain.BuildTx.TransferLogic (BlacklistReason) @@ -39,7 +41,6 @@ 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 transaction and the 'TxIn' that was selected for the one-shot NFTs. -} @@ -176,17 +177,20 @@ transferSmartTokensTx :: forall era env m. , C.IsBabbageBasedEra era , C.HasScriptLanguageInEra C.PlutusScriptV3 era , MonadUtxoQuery m + , IsEra era ) - => C.AssetId -- ^ AssetId to transfer + => BlacklistedTransferPolicy + -> C.AssetId -- ^ AssetId to transfer -> Quantity -- ^ Amount of tokens to be minted -> C.PaymentCredential -- ^ Destination credential -> m (C.Tx era) -transferSmartTokensTx assetId quantity destCred = do +transferSmartTokensTx policy assetId quantity destCred = do directory <- Query.registryNodes @era blacklist <- Query.blacklistNodes @era userOutputsAtProgrammable <- Env.operatorPaymentCredential >>= Query.userProgrammableOutputs paramsTxIn <- Query.globalParamsNode @era - (tx, _) <- Env.balanceTxEnv_ $ do + (tx, _) <- balanceTxEnvFailing policy $ do + -- TODO: use a different balancing mechanism if we expect the scripts to fail BuildTx.transferSmartTokens paramsTxIn blacklist directory userOutputsAtProgrammable (assetId, quantity) destCred pure (Convex.CoinSelection.signBalancedTxBody [] tx) diff --git a/src/lib/Wst/Server.hs b/src/lib/Wst/Server.hs index f03b5a3..ebc78f0 100644 --- a/src/lib/Wst/Server.hs +++ b/src/lib/Wst/Server.hs @@ -33,6 +33,7 @@ import SmartTokens.Types.PTokenDirectory (blnKey) import System.Environment qualified import Wst.App (WstApp, runWstAppServant) import Wst.AppError (AppError (..)) +import Wst.Offchain.BuildTx.Failing (BlacklistedTransferPolicy (..), IsEra) import Wst.Offchain.Endpoints.Deployment qualified as Endpoints import Wst.Offchain.Env qualified as Env import Wst.Offchain.Query (UTxODat (uDatum)) @@ -215,15 +216,17 @@ transferProgrammableTokenEndpoint :: forall era env m. , C.IsBabbageBasedEra era , C.HasScriptLanguageInEra C.PlutusScriptV3 era , MonadUtxoQuery m + , IsEra era ) => TransferProgrammableTokenArgs -> m (TextEnvelopeJSON (C.Tx era)) -transferProgrammableTokenEndpoint TransferProgrammableTokenArgs{ttaSender, ttaRecipient, ttaAssetName, ttaQuantity, ttaIssuer} = do +transferProgrammableTokenEndpoint TransferProgrammableTokenArgs{ttaSender, ttaRecipient, ttaAssetName, ttaQuantity, ttaIssuer, ttaSubmitFailingTx} = do operatorEnv <- Env.loadOperatorEnvFromAddress ttaSender dirEnv <- asks Env.directoryEnv logic <- Env.transferLogicForDirectory (paymentKeyHashFromAddress ttaIssuer) assetId <- Env.programmableTokenAssetId dirEnv <$> Env.transferLogicForDirectory (paymentKeyHashFromAddress ttaIssuer) <*> pure ttaAssetName + let policy = maybe DontSubmitFailingTx (\k -> if k then SubmitFailingTx else DontSubmitFailingTx) ttaSubmitFailingTx Env.withEnv $ Env.withOperator operatorEnv $ Env.withDirectory dirEnv $ Env.withTransfer logic $ do - TextEnvelopeJSON <$> Endpoints.transferSmartTokensTx assetId ttaQuantity (paymentCredentialFromAddress ttaRecipient) + TextEnvelopeJSON <$> Endpoints.transferSmartTokensTx policy assetId ttaQuantity (paymentCredentialFromAddress ttaRecipient) addToBlacklistEndpoint :: forall era env m. ( MonadReader env m diff --git a/src/lib/Wst/Server/Types.hs b/src/lib/Wst/Server/Types.hs index 28fdaac..4291bb9 100644 --- a/src/lib/Wst/Server/Types.hs +++ b/src/lib/Wst/Server/Types.hs @@ -136,6 +136,7 @@ data TransferProgrammableTokenArgs = , ttaIssuer :: C.Address C.ShelleyAddr , ttaAssetName :: AssetName , ttaQuantity :: Quantity + , ttaSubmitFailingTx :: Maybe Bool } deriving stock (Eq, Show, Generic) diff --git a/src/test/unit/Wst/Test/UnitTest.hs b/src/test/unit/Wst/Test/UnitTest.hs index 391adeb..8de53e0 100644 --- a/src/test/unit/Wst/Test/UnitTest.hs +++ b/src/test/unit/Wst/Test/UnitTest.hs @@ -31,6 +31,7 @@ import SmartTokens.Core.Scripts (ScriptTarget (Debug, Production)) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (Assertion, testCase) import Wst.Offchain.BuildTx.DirectorySet (InsertNodeArgs (..)) +import Wst.Offchain.BuildTx.Failing (BlacklistedTransferPolicy (..)) import Wst.Offchain.BuildTx.Utils (addConwayStakeCredentialCertificate) import Wst.Offchain.Endpoints.Deployment qualified as Endpoints import Wst.Offchain.Env (DirectoryScriptRoot) @@ -56,7 +57,8 @@ scriptTargetTests target = , testCase "smart token transfer" (mockchainSucceedsWithTarget target $ deployDirectorySet >>= transferSmartTokens) , testCase "blacklist credential" (mockchainSucceedsWithTarget target $ void $ deployDirectorySet >>= blacklistCredential) , testCase "unblacklist credential" (mockchainSucceedsWithTarget target $ void $ deployDirectorySet >>= unblacklistCredential) - , testCase "blacklisted transfer" (mockchainFails blacklistTransfer assertBlacklistedAddressException) + , testCase "blacklisted transfer" (mockchainFails (blacklistTransfer DontSubmitFailingTx) assertBlacklistedAddressException) + , testCase "blacklisted transfer (failing tx)" (mockchainSucceedsWithTarget target (blacklistTransfer SubmitFailingTx)) , testCase "seize user output" (mockchainSucceedsWithTarget target $ deployDirectorySet >>= seizeUserOutput) , testCase "deploy all" (mockchainSucceedsWithTarget target deployAll) ] @@ -152,7 +154,7 @@ transferSmartTokens scriptRoot = failOnError $ Env.withEnv $ do asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator $ do opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) - Endpoints.transferSmartTokensTx aid 80 (C.PaymentCredentialByKey userPkh) + Endpoints.transferSmartTokensTx DontSubmitFailingTx aid 80 (C.PaymentCredentialByKey userPkh) >>= void . sendTx . signTxOperator admin Query.programmableLogicOutputs @C.ConwayEra @@ -208,8 +210,8 @@ unblacklistCredential scriptRoot = failOnError $ Env.withEnv $ do pure paymentCred -blacklistTransfer :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m) => m () -blacklistTransfer = failOnError $ Env.withEnv $ do +blacklistTransfer :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m) => BlacklistedTransferPolicy -> m () +blacklistTransfer policy = failOnError $ Env.withEnv $ do scriptRoot <- runReaderT deployDirectorySet Production userPkh <- asWallet Wallet.w2 $ asks (fst . Env.bteOperator . Env.operatorEnv) let userPaymentCred = C.PaymentCredentialByKey userPkh @@ -221,7 +223,7 @@ blacklistTransfer = failOnError $ Env.withEnv $ do opPkh <- asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator $ do opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) - Endpoints.transferSmartTokensTx aid 50 (C.PaymentCredentialByKey userPkh) + Endpoints.transferSmartTokensTx policy aid 50 (C.PaymentCredentialByKey userPkh) >>= void . sendTx . signTxOperator admin pure opPkh @@ -230,7 +232,7 @@ blacklistTransfer = failOnError $ Env.withEnv $ do asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator $ Endpoints.insertBlacklistNodeTx "" userPaymentCred >>= void . sendTx . signTxOperator admin - asWallet Wallet.w2 $ Env.withDirectoryFor scriptRoot $ Env.withTransfer transferLogic $ Endpoints.transferSmartTokensTx aid 30 (C.PaymentCredentialByKey opPkh) + asWallet Wallet.w2 $ Env.withDirectoryFor scriptRoot $ Env.withTransfer transferLogic $ Endpoints.transferSmartTokensTx policy aid 30 (C.PaymentCredentialByKey opPkh) >>= void . sendTx . signTxOperator (user Wallet.w2) seizeUserOutput :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m) => DirectoryScriptRoot -> m () @@ -244,7 +246,7 @@ seizeUserOutput scriptRoot = failOnError $ Env.withEnv $ do >>= void . sendTx . signTxOperator admin asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator $ do - Endpoints.transferSmartTokensTx aid 50 (C.PaymentCredentialByKey userPkh) + Endpoints.transferSmartTokensTx DontSubmitFailingTx aid 50 (C.PaymentCredentialByKey userPkh) >>= void . sendTx . signTxOperator admin Query.programmableLogicOutputs @C.ConwayEra >>= void . expectN 2 "programmable logic outputs" diff --git a/src/wst-poc.cabal b/src/wst-poc.cabal index 2a0c8a8..6738528 100644 --- a/src/wst-poc.cabal +++ b/src/wst-poc.cabal @@ -76,6 +76,7 @@ library Wst.Client Wst.JSON.Utils Wst.Offchain.BuildTx.DirectorySet + Wst.Offchain.BuildTx.Failing Wst.Offchain.BuildTx.LinkedList Wst.Offchain.BuildTx.ProgrammableLogic Wst.Offchain.BuildTx.ProtocolParams @@ -100,6 +101,8 @@ library , blockfrost-client , blockfrost-client-core , cardano-api + , cardano-ledger-api + , cardano-ledger-binary , cardano-ledger-shelley , containers , convex-base @@ -123,6 +126,7 @@ library , servant-client-core , servant-server , text + , transformers , wai-cors , warp From 41a1d10eda1cd7f34b273a6b6a40279f856a5cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Sat, 25 Jan 2025 09:38:05 +0100 Subject: [PATCH 11/18] Fix UI build --- frontend/src/app/[username]/index.tsx | 1 + frontend/src/app/mint-authority/page.tsx | 2 +- frontend/src/app/utils/walletUtils.ts | 1 - generated/openapi/schema.json | 3 +++ src/wst-poc.cabal | 1 - 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/[username]/index.tsx b/frontend/src/app/[username]/index.tsx index fc8065d..763901a 100644 --- a/frontend/src/app/[username]/index.tsx +++ b/frontend/src/app/[username]/index.tsx @@ -61,6 +61,7 @@ export default function Profile() { quantity: sendTokenAmount, recipient: sendRecipientAddress, sender: accountInfo.address, + submit_failing_tx: false }; try { const response = await axios.post( diff --git a/frontend/src/app/mint-authority/page.tsx b/frontend/src/app/mint-authority/page.tsx index 463de52..7d0d7e5 100644 --- a/frontend/src/app/mint-authority/page.tsx +++ b/frontend/src/app/mint-authority/page.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; //Lucid imports -import { CML, makeTxSignBuilder, paymentCredentialOf } from '@lucid-evolution/lucid'; +import { paymentCredentialOf } from '@lucid-evolution/lucid'; import type { Credential as LucidCredential } from "@lucid-evolution/core-types"; //Mui imports diff --git a/frontend/src/app/utils/walletUtils.ts b/frontend/src/app/utils/walletUtils.ts index f58c5a8..1b272d4 100644 --- a/frontend/src/app/utils/walletUtils.ts +++ b/frontend/src/app/utils/walletUtils.ts @@ -89,7 +89,6 @@ export async function signAndSentTx(lucid: LucidEvolution, tx: TxSignBuilder): P const expectedScriptDataHash : CML.ScriptDataHash | undefined = CML.calc_script_data_hash(witnessSet.redeemers()!, CML.PlutusDataList.new(), lucid.config().costModels!, witnessSet.languages()); // console.log('Calculated Script Data Hash:', expectedScriptDataHash?.to_hex()); const cmlTxBodyClone = CML.TransactionBody.from_cbor_hex(cmlTx!.body().to_cbor_hex()); - const txIDinAlert = await cmlTxBodyClone.to_json(); // console.log('Preclone script hash:', cmlTxBodyClone.script_data_hash()?.to_hex()); cmlTxBodyClone.set_script_data_hash(expectedScriptDataHash!); // console.log('Postclone script hash:', cmlTxBodyClone.script_data_hash()?.to_hex()); diff --git a/generated/openapi/schema.json b/generated/openapi/schema.json index 7dee249..568fa41 100644 --- a/generated/openapi/schema.json +++ b/generated/openapi/schema.json @@ -156,6 +156,9 @@ }, "sender": { "$ref": "#/components/schemas/Address" + }, + "submit_failing_tx": { + "type": "boolean" } }, "required": [ diff --git a/src/wst-poc.cabal b/src/wst-poc.cabal index 6738528..f34e266 100644 --- a/src/wst-poc.cabal +++ b/src/wst-poc.cabal @@ -102,7 +102,6 @@ library , blockfrost-client-core , cardano-api , cardano-ledger-api - , cardano-ledger-binary , cardano-ledger-shelley , containers , convex-base From 936cdc589ea8e6a52954c5cff1697683d0c2f18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Sat, 25 Jan 2025 11:50:55 +0100 Subject: [PATCH 12/18] Start updating balance --- frontend/src/app/store/store.tsx | 4 ++++ frontend/src/app/store/types.ts | 1 + frontend/src/app/utils/walletUtils.ts | 8 +++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/store/store.tsx b/frontend/src/app/store/store.tsx index 6d045ab..f3a42d6 100644 --- a/frontend/src/app/store/store.tsx +++ b/frontend/src/app/store/store.tsx @@ -30,24 +30,28 @@ const useStore = create((set) => ({ address: 'addr_test1qq986m3uel86pl674mkzneqtycyg7csrdgdxj6uf7v7kd857kquweuh5kmrj28zs8czrwkl692jm67vna2rf7xtafhpqk3hecm', mnemonic: 'problem alert infant glance toss gospel tonight sheriff match else hover upset chicken desert anxiety cliff moment song large seed purpose chalk loan onion', balance: 0, + adaBalance: 0, }, accounts: { alice: { address: '', mnemonic: 'during dolphin crop lend pizza guilt hen earn easy direct inhale deputy detect season army inject exhaust apple hard front bubble emotion short portion', balance: 0, + adaBalance: 0, status: 'Active', }, bob: { address: '', mnemonic: 'silver legal flame powder fence kiss stable margin refuse hold unknown valid wolf kangaroo zero able waste jewel find salad sadness exhibit hello tape', balance: 0, + adaBalance: 0, status: 'Active', }, walletUser: { address: '', mnemonic: '', balance: 0, + adaBalance: 0, status: 'Active', }, }, diff --git a/frontend/src/app/store/types.ts b/frontend/src/app/store/types.ts index b8e736c..4fc519b 100644 --- a/frontend/src/app/store/types.ts +++ b/frontend/src/app/store/types.ts @@ -4,6 +4,7 @@ export type AccountInfo = { address: string, mnemonic: string, balance: number, + adaBalance: number, status?: 'Active' | 'Frozen', }; export type AccountKey = 'alice' | 'bob' | 'walletUser'; diff --git a/frontend/src/app/utils/walletUtils.ts b/frontend/src/app/utils/walletUtils.ts index 1b272d4..b9ed213 100644 --- a/frontend/src/app/utils/walletUtils.ts +++ b/frontend/src/app/utils/walletUtils.ts @@ -39,7 +39,9 @@ export async function getWalletFromSeed(mnemonic: string) { } } -export async function getWalletBalance(address: string){ +export type WalletBalance = { wst: number, ada: number } + +export async function getWalletBalance(address: string): Promise { try { const response = await axios.get( `/api/v1/query/user-funds/${address}`, @@ -52,9 +54,13 @@ export async function getWalletBalance(address: string){ const balance = "b34a184f1f2871aa4d33544caecefef5242025f45c3fa5213d7662a9"; const stableTokenUnit = "575354"; let stableBalance = 0; + let adaBalance = 0; + console.log(response.data); if (response?.data && response.data[balance] && response.data[balance][stableTokenUnit]) { stableBalance = response.data[balance][stableTokenUnit]; + } + // console.log('Get wallet balance:', response.data); return stableBalance; } catch (error) { From ec1dcd7041196ed694a55d69c24425a30337aaaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Sat, 25 Jan 2025 12:00:22 +0100 Subject: [PATCH 13/18] Show ada balance --- frontend/src/app/[username]/index.tsx | 3 ++- frontend/src/app/mint-authority/page.tsx | 3 ++- frontend/src/app/store/store.tsx | 12 ++++-------- frontend/src/app/store/types.ts | 6 +++--- frontend/src/app/utils/walletUtils.ts | 13 +++++-------- 5 files changed, 16 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/[username]/index.tsx b/frontend/src/app/[username]/index.tsx index 763901a..4b9f162 100644 --- a/frontend/src/app/[username]/index.tsx +++ b/frontend/src/app/[username]/index.tsx @@ -127,7 +127,8 @@ export default function Profile() { Address Balance - {getUserAccountDetails()?.balance} WST + {getUserAccountDetails()?.balance.wst} WST + {getUserAccountDetails()?.balance.ada} Ada {getUserAccountDetails()?.address.slice(0,15)} diff --git a/frontend/src/app/mint-authority/page.tsx b/frontend/src/app/mint-authority/page.tsx index 7d0d7e5..7f4df5e 100644 --- a/frontend/src/app/mint-authority/page.tsx +++ b/frontend/src/app/mint-authority/page.tsx @@ -384,7 +384,8 @@ maxRows={3} Mint Authority Balance - {mintAccount.balance} WST + {mintAccount.balance.wst} WST + {mintAccount.balance.ada} Ada UserID: {mintAccount.address.slice(0,15)} diff --git a/frontend/src/app/store/store.tsx b/frontend/src/app/store/store.tsx index f3a42d6..a8c1431 100644 --- a/frontend/src/app/store/store.tsx +++ b/frontend/src/app/store/store.tsx @@ -29,29 +29,25 @@ const useStore = create((set) => ({ name: 'Mint Authority', address: 'addr_test1qq986m3uel86pl674mkzneqtycyg7csrdgdxj6uf7v7kd857kquweuh5kmrj28zs8czrwkl692jm67vna2rf7xtafhpqk3hecm', mnemonic: 'problem alert infant glance toss gospel tonight sheriff match else hover upset chicken desert anxiety cliff moment song large seed purpose chalk loan onion', - balance: 0, - adaBalance: 0, + balance: {ada: 0, wst: 0}, }, accounts: { alice: { address: '', mnemonic: 'during dolphin crop lend pizza guilt hen earn easy direct inhale deputy detect season army inject exhaust apple hard front bubble emotion short portion', - balance: 0, - adaBalance: 0, + balance: {ada: 0, wst: 0}, status: 'Active', }, bob: { address: '', mnemonic: 'silver legal flame powder fence kiss stable margin refuse hold unknown valid wolf kangaroo zero able waste jewel find salad sadness exhibit hello tape', - balance: 0, - adaBalance: 0, + balance: {ada: 0, wst: 0}, status: 'Active', }, walletUser: { address: '', mnemonic: '', - balance: 0, - adaBalance: 0, + balance: {ada: 0, wst: 0}, status: 'Active', }, }, diff --git a/frontend/src/app/store/types.ts b/frontend/src/app/store/types.ts index 4fc519b..d2b2ed3 100644 --- a/frontend/src/app/store/types.ts +++ b/frontend/src/app/store/types.ts @@ -3,8 +3,7 @@ export type MenuTab = 'Mint Actions' | 'Addresses' | 'Wallet'; export type AccountInfo = { address: string, mnemonic: string, - balance: number, - adaBalance: number, + balance: WalletBalance, status?: 'Active' | 'Frozen', }; export type AccountKey = 'alice' | 'bob' | 'walletUser'; @@ -19,4 +18,5 @@ export type AlertInfo = { severity: Severity, message: string, link?: string, -}; \ No newline at end of file +}; +export type WalletBalance = { wst: number, ada: number } diff --git a/frontend/src/app/utils/walletUtils.ts b/frontend/src/app/utils/walletUtils.ts index b9ed213..77f5a99 100644 --- a/frontend/src/app/utils/walletUtils.ts +++ b/frontend/src/app/utils/walletUtils.ts @@ -4,6 +4,7 @@ import axios from 'axios'; //Lucis imports import { Address, Assets, Blockfrost, CML, credentialToAddress, Lucid, LucidEvolution, makeTxSignBuilder, paymentCredentialOf, toUnit, TxSignBuilder, Unit, valueToAssets, walletFromSeed } from "@lucid-evolution/lucid"; import type { Credential as LucidCredential } from "@lucid-evolution/core-types"; +import { WalletBalance } from '../store/types'; async function loadKey() { const response = await axios.get("/blockfrost-key", @@ -39,9 +40,7 @@ export async function getWalletFromSeed(mnemonic: string) { } } -export type WalletBalance = { wst: number, ada: number } - -export async function getWalletBalance(address: string): Promise { +export async function getWalletBalance(address: string): Promise { try { const response = await axios.get( `/api/v1/query/user-funds/${address}`, @@ -55,17 +54,15 @@ export async function getWalletBalance(address: string): Promise { const stableTokenUnit = "575354"; let stableBalance = 0; let adaBalance = 0; - console.log(response.data); if (response?.data && response.data[balance] && response.data[balance][stableTokenUnit]) { stableBalance = response.data[balance][stableTokenUnit]; - + adaBalance = response.data["lovelace"] / 1000000; } - // console.log('Get wallet balance:', response.data); - return stableBalance; + return {wst: stableBalance, ada: adaBalance }; } catch (error) { console.error('Failed to get balance', error); - return 0; + return { wst: 0, ada: 0}; } } From da54536989fcd0da4765f62ca158577492663c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Sat, 25 Jan 2025 12:11:27 +0100 Subject: [PATCH 14/18] Fix table; add override to transfer --- frontend/src/app/[username]/index.tsx | 13 ++++++++++--- frontend/src/app/components/WSTTable.tsx | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/[username]/index.tsx b/frontend/src/app/[username]/index.tsx index 4b9f162..05ec8c1 100644 --- a/frontend/src/app/[username]/index.tsx +++ b/frontend/src/app/[username]/index.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react'; import axios from 'axios'; //Mui imports -import { Box, Typography } from '@mui/material'; +import { Box, Checkbox, FormControlLabel, Typography } from '@mui/material'; //Local components import useStore from '../store/store'; @@ -19,6 +19,7 @@ import CopyTextField from '../components/CopyTextField'; export default function Profile() { const { lucid, currentUser, mintAccount, changeAlertInfo, changeWalletAccountDetails } = useStore(); const accounts = useStore((state) => state.accounts); + const [overrideTx, setOverrideTx] = useState(false); useEffect(() => { useStore.getState(); @@ -38,7 +39,7 @@ export default function Profile() { const [sendRecipientAddress, setsendRecipientAddress] = useState('address'); const onSend = async () => { - if (getUserAccountDetails()?.status === 'Frozen') { + if (getUserAccountDetails()?.status === 'Frozen' && !overrideTx) { changeAlertInfo({ severity: 'error', message: 'Cannot send WST with frozen address.', @@ -61,7 +62,7 @@ export default function Profile() { quantity: sendTokenAmount, recipient: sendRecipientAddress, sender: accountInfo.address, - submit_failing_tx: false + submit_failing_tx: overrideTx }; try { const response = await axios.post( @@ -112,6 +113,12 @@ export default function Profile() { label="Recipient’s Address" fullWidth={true} /> + setOverrideTx(x.target.checked)} />} + label="Force send failing transaction" + sx={{ mb: 2 }} + /> + ; const receiveContent = diff --git a/frontend/src/app/components/WSTTable.tsx b/frontend/src/app/components/WSTTable.tsx index 375406e..d496630 100644 --- a/frontend/src/app/components/WSTTable.tsx +++ b/frontend/src/app/components/WSTTable.tsx @@ -72,7 +72,7 @@ export default function WSTTable() { {acct.status} - {`${acct?.balance} WST`} + {`${acct?.balance.wst} WST`} )) From b6ba7eff05aaa9dcae856ab076ac303ff0148804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Sat, 25 Jan 2025 12:11:51 +0100 Subject: [PATCH 15/18] Override field is mandatory --- src/lib/Wst/Server.hs | 2 +- src/lib/Wst/Server/Types.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/Wst/Server.hs b/src/lib/Wst/Server.hs index ebc78f0..9c268a4 100644 --- a/src/lib/Wst/Server.hs +++ b/src/lib/Wst/Server.hs @@ -224,7 +224,7 @@ transferProgrammableTokenEndpoint TransferProgrammableTokenArgs{ttaSender, ttaRe dirEnv <- asks Env.directoryEnv logic <- Env.transferLogicForDirectory (paymentKeyHashFromAddress ttaIssuer) assetId <- Env.programmableTokenAssetId dirEnv <$> Env.transferLogicForDirectory (paymentKeyHashFromAddress ttaIssuer) <*> pure ttaAssetName - let policy = maybe DontSubmitFailingTx (\k -> if k then SubmitFailingTx else DontSubmitFailingTx) ttaSubmitFailingTx + let policy = if ttaSubmitFailingTx then SubmitFailingTx else DontSubmitFailingTx Env.withEnv $ Env.withOperator operatorEnv $ Env.withDirectory dirEnv $ Env.withTransfer logic $ do TextEnvelopeJSON <$> Endpoints.transferSmartTokensTx policy assetId ttaQuantity (paymentCredentialFromAddress ttaRecipient) diff --git a/src/lib/Wst/Server/Types.hs b/src/lib/Wst/Server/Types.hs index 4291bb9..315e489 100644 --- a/src/lib/Wst/Server/Types.hs +++ b/src/lib/Wst/Server/Types.hs @@ -136,7 +136,7 @@ data TransferProgrammableTokenArgs = , ttaIssuer :: C.Address C.ShelleyAddr , ttaAssetName :: AssetName , ttaQuantity :: Quantity - , ttaSubmitFailingTx :: Maybe Bool + , ttaSubmitFailingTx :: Bool } deriving stock (Eq, Show, Generic) From 954382953bbf84a6314583c95b7109dfa3bcd712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Sun, 26 Jan 2025 11:57:09 +0100 Subject: [PATCH 16/18] More progress on submitting invalid transaction --- cabal.project | 5 +- frontend/src/app/[username]/index.tsx | 5 +- frontend/src/app/utils/walletUtils.ts | 36 ++++++++--- src/lib/Wst/Offchain/BuildTx/Failing.hs | 66 ++++++-------------- src/lib/Wst/Offchain/Endpoints/Deployment.hs | 4 +- src/lib/Wst/Server.hs | 25 ++++++-- src/test/unit/Wst/Test/UnitTest.hs | 22 +++++-- src/wst-poc.cabal | 2 - 8 files changed, 89 insertions(+), 76 deletions(-) diff --git a/cabal.project b/cabal.project index 82879c2..b06ef13 100644 --- a/cabal.project +++ b/cabal.project @@ -41,9 +41,8 @@ source-repository-package source-repository-package type: git - -- location: https://github.com/j-mueller/sc-tools - location: https://github.com/amirmrad/sc-tools - tag: 6c63efe07015e87719d77fa3fabfe07f959c7227 + location: https://github.com/j-mueller/sc-tools + tag: a3662e093f40082dd6fa525bb0640a10caa1bd70 subdir: src/devnet src/blockfrost diff --git a/frontend/src/app/[username]/index.tsx b/frontend/src/app/[username]/index.tsx index 05ec8c1..6dca4a1 100644 --- a/frontend/src/app/[username]/index.tsx +++ b/frontend/src/app/[username]/index.tsx @@ -74,9 +74,10 @@ export default function Profile() { }, } ); - console.log('Send response:', response.data); + const isValid = !(getUserAccountDetails()?.status === 'Frozen'); + console.log('Send response:', response.data, isValid); const tx = await lucid.fromTx(response.data.cborHex); - const txId = await signAndSentTx(lucid, tx); + const txId = await signAndSentTx(lucid, tx, isValid); await updateAccountBalance(sendRecipientAddress); await updateAccountBalance(accountInfo.address); changeAlertInfo({severity: 'success', message: 'Transaction sent successfully!', open: true, link: `https://preview.cexplorer.io/tx/${txId}`}); diff --git a/frontend/src/app/utils/walletUtils.ts b/frontend/src/app/utils/walletUtils.ts index 77f5a99..3d746e3 100644 --- a/frontend/src/app/utils/walletUtils.ts +++ b/frontend/src/app/utils/walletUtils.ts @@ -1,5 +1,5 @@ //Axios imports -import axios from 'axios'; +import axios, { AxiosResponse } from 'axios'; //Lucis imports import { Address, Assets, Blockfrost, CML, credentialToAddress, Lucid, LucidEvolution, makeTxSignBuilder, paymentCredentialOf, toUnit, TxSignBuilder, Unit, valueToAssets, walletFromSeed } from "@lucid-evolution/lucid"; @@ -85,22 +85,42 @@ export async function getBlacklist(){ } } -export async function signAndSentTx(lucid: LucidEvolution, tx: TxSignBuilder): Promise { +export async function submitTx(tx: string): Promise> { + return axios.post( + '/api/v1/tx/submit', + { + description: "", + type: "Tx ConwayEra", + cborHex: tx + }, + { + headers: { + 'Content-Type': 'application/json;charset=utf-8', + }, + } + ); + } + +export async function signAndSentTx(lucid: LucidEvolution, tx: TxSignBuilder, isValid: boolean = true): Promise { + console.log("tx.toTransaction().isValid()", tx.toTransaction().is_valid()); const txBuilder = await makeTxSignBuilder(lucid.wallet(), tx.toTransaction()).complete(); + console.log("txBuilder.toTransaction().isValid()", txBuilder.toTransaction().is_valid()) const cmlTx = txBuilder.toTransaction(); const witnessSet = txBuilder.toTransaction().witness_set(); const expectedScriptDataHash : CML.ScriptDataHash | undefined = CML.calc_script_data_hash(witnessSet.redeemers()!, CML.PlutusDataList.new(), lucid.config().costModels!, witnessSet.languages()); - // console.log('Calculated Script Data Hash:', expectedScriptDataHash?.to_hex()); const cmlTxBodyClone = CML.TransactionBody.from_cbor_hex(cmlTx!.body().to_cbor_hex()); // console.log('Preclone script hash:', cmlTxBodyClone.script_data_hash()?.to_hex()); cmlTxBodyClone.set_script_data_hash(expectedScriptDataHash!); // console.log('Postclone script hash:', cmlTxBodyClone.script_data_hash()?.to_hex()); - const cmlClonedTx = CML.Transaction.new(cmlTxBodyClone, cmlTx!.witness_set(), true, cmlTx!.auxiliary_data()); + const cmlClonedTx = CML.Transaction.new(cmlTxBodyClone, cmlTx!.witness_set(), isValid, cmlTx!.auxiliary_data()); const cmlClonedSignedTx = await makeTxSignBuilder(lucid.wallet(), cmlClonedTx).sign.withWallet().complete(); - const txId = await cmlClonedSignedTx.submit(); - await lucid.awaitTx(txId); - console.log(cmlClonedSignedTx); - return txId + // const txId = await cmlClonedSignedTx.submit(); + const txId = await submitTx(cmlClonedSignedTx.toCBOR()); + const i = txId.data; + console.log(txId); + await lucid.awaitTx(txId.data); + // console.log(cmlClonedSignedTx); + return i } export type WalletType = "Lace" | "Eternl" | "Nami" | "Yoroi"; diff --git a/src/lib/Wst/Offchain/BuildTx/Failing.hs b/src/lib/Wst/Offchain/BuildTx/Failing.hs index 19680c7..4688e85 100644 --- a/src/lib/Wst/Offchain/BuildTx/Failing.hs +++ b/src/lib/Wst/Offchain/BuildTx/Failing.hs @@ -11,30 +11,25 @@ module Wst.Offchain.BuildTx.Failing( balanceTxEnvFailing ) where -import Cardano.Api.Experimental (IsEra, obtainCommonConstraints, useEra) -import Cardano.Api.Experimental qualified as C +import Cardano.Api.Experimental (IsEra) import Cardano.Api.Shelley qualified as C -import Cardano.Ledger.Api qualified as L -import Control.Lens (Iso', _3, _Just, at, iso, set, (&), (.~)) +import Control.Lens (set) import Control.Monad.Except (MonadError, throwError) -import Control.Monad.Reader (MonadReader, ReaderT, ask, asks, runReaderT) -import Control.Monad.Trans.Class (MonadTrans (..)) +import Control.Monad.Reader (MonadReader, asks) import Convex.BuildTx (BuildTxT) import Convex.BuildTx qualified as BuildTx import Convex.CardanoApi.Lenses qualified as L -import Convex.Class (MonadBlockchain (utxoByTxIn), queryProtocolParameters) +import Convex.Class (MonadBlockchain, queryProtocolParameters) import Convex.CoinSelection qualified as CoinSelection import Convex.PlutusLedger.V1 (transCredential) -import Convex.Scripts (toHashableScriptData) import Convex.Utils (mapError) import Convex.Utxos (BalanceChanges) import Convex.Utxos qualified as Utxos import Convex.Wallet.Operator (returnOutputFor) -import Data.Bifunctor (Bifunctor (..)) -import Data.Map (Map) +import Data.Aeson (FromJSON, ToJSON) +import GHC.Generics (Generic) import Wst.AppError (AppError (..)) -import Wst.Offchain.BuildTx.TransferLogic (FindProofResult (..), - blacklistInitialNode) +import Wst.Offchain.BuildTx.TransferLogic (FindProofResult (..)) import Wst.Offchain.Env (HasOperatorEnv (..), OperatorEnv (..)) import Wst.Offchain.Query (UTxODat (..)) @@ -43,11 +38,12 @@ import Wst.Offchain.Query (UTxODat (..)) data BlacklistedTransferPolicy = SubmitFailingTx -- ^ Deliberately submit a transaction with "scriptValidity = False". This will result in the collateral input being spent! | DontSubmitFailingTx -- ^ Don't submit a transaction - deriving stock (Eq, Show) + deriving stock (Eq, Show, Generic) + deriving anyclass (ToJSON, FromJSON) {-| Balance a transaction using the operator's funds and return output -} -balanceTxEnvFailing :: forall era env m. (IsEra era, MonadBlockchain era m, MonadReader env m, HasOperatorEnv era env, MonadError (AppError era) m, C.IsBabbageBasedEra era) => BlacklistedTransferPolicy -> BuildTxT era m (FindProofResult era) -> m (C.BalancedTxBody era, BalanceChanges) +balanceTxEnvFailing :: forall era env m. (MonadBlockchain era m, MonadReader env m, HasOperatorEnv era env, MonadError (AppError era) m, C.IsBabbageBasedEra era) => BlacklistedTransferPolicy -> BuildTxT era m (FindProofResult era) -> m (C.BalancedTxBody era, BalanceChanges) balanceTxEnvFailing policy btx = do OperatorEnv{bteOperatorUtxos, bteOperator} <- asks operatorEnv params <- queryProtocolParameters @@ -57,41 +53,15 @@ balanceTxEnvFailing policy btx = do let credential = C.PaymentCredentialByKey $ fst bteOperator output <- returnOutputFor credential (balBody, balChanges) <- case r of - CredentialNotBlacklisted{} -> + CredentialNotBlacklisted{} -> do mapError BalancingError (CoinSelection.balanceTx mempty output (Utxos.fromApiUtxo bteOperatorUtxos) txBuilder CoinSelection.TrailingChange) - CredentialBlacklisted UTxODat{uIn} - | policy == SubmitFailingTx -> - fmap (first setScriptsInvalid) - $ runBacklistResetT uIn - $ mapError BalancingError (CoinSelection.balanceTx mempty output (Utxos.fromApiUtxo bteOperatorUtxos) txBuilder CoinSelection.TrailingChange) - | otherwise -> + CredentialBlacklisted UTxODat{} + | policy == SubmitFailingTx -> do + -- deliberately set the script validity flag to false + -- this means we will be losing the collateral! + let builder' = txBuilder <> BuildTx.liftTxBodyEndo (set L.txScriptValidity (C.TxScriptValidity C.alonzoBasedEra C.ScriptInvalid)) + mapError BalancingError (CoinSelection.balanceTx mempty output (Utxos.fromApiUtxo bteOperatorUtxos) builder' CoinSelection.TrailingChange) + | otherwise -> do throwError (TransferBlacklistedCredential (transCredential credential)) NoBlacklistNodes -> throwError BlacklistNodeNotFound pure (balBody, balChanges) - -newtype BlacklistResetT m a = BlacklistResetT (ReaderT C.TxIn m a) - deriving newtype (Functor, Applicative, Monad, MonadError e, MonadTrans) - -instance (C.IsBabbageBasedEra era, MonadBlockchain era m) => MonadBlockchain era (BlacklistResetT m) where - utxoByTxIn txis = BlacklistResetT $ do - txi <- ask - let newDat = C.TxOutDatumInline C.babbageBasedEra (toHashableScriptData blacklistInitialNode) - fmap (set (_UTxO . at txi . _Just . L._TxOut . _3) newDat) $ utxoByTxIn txis - -runBacklistResetT :: C.TxIn -> BlacklistResetT m a -> m a -runBacklistResetT txi (BlacklistResetT action) = runReaderT action txi - -_UTxO :: Iso' (C.UTxO era) (Map C.TxIn (C.TxOut C.CtxUTxO era)) -_UTxO = iso t f where - t (C.UTxO k) = k - f = C.UTxO - -setScriptsInvalid :: - forall era. - ( IsEra era - ) - => C.BalancedTxBody era - -> C.BalancedTxBody era -setScriptsInvalid (C.BalancedTxBody a (C.UnsignedTx b) c d) = obtainCommonConstraints (useEra @era) $ - let b' = C.UnsignedTx (b & L.isValidTxL @(C.LedgerEra era) .~ L.IsValid False) - in C.BalancedTxBody a b' c d diff --git a/src/lib/Wst/Offchain/Endpoints/Deployment.hs b/src/lib/Wst/Offchain/Endpoints/Deployment.hs index 5ed13c4..84e639e 100644 --- a/src/lib/Wst/Offchain/Endpoints/Deployment.hs +++ b/src/lib/Wst/Offchain/Endpoints/Deployment.hs @@ -30,7 +30,7 @@ import SmartTokens.Types.PTokenDirectory (DirectorySetNode (..)) import Wst.AppError (AppError (NoTokensToSeize)) import Wst.Offchain.BuildTx.DirectorySet (InsertNodeArgs (inaNewKey)) import Wst.Offchain.BuildTx.DirectorySet qualified as BuildTx -import Wst.Offchain.BuildTx.Failing (BlacklistedTransferPolicy, IsEra, +import Wst.Offchain.BuildTx.Failing (BlacklistedTransferPolicy, balanceTxEnvFailing) import Wst.Offchain.BuildTx.ProgrammableLogic qualified as BuildTx import Wst.Offchain.BuildTx.ProtocolParams qualified as BuildTx @@ -177,7 +177,6 @@ transferSmartTokensTx :: forall era env m. , C.IsBabbageBasedEra era , C.HasScriptLanguageInEra C.PlutusScriptV3 era , MonadUtxoQuery m - , IsEra era ) => BlacklistedTransferPolicy -> C.AssetId -- ^ AssetId to transfer @@ -190,7 +189,6 @@ transferSmartTokensTx policy assetId quantity destCred = do userOutputsAtProgrammable <- Env.operatorPaymentCredential >>= Query.userProgrammableOutputs paramsTxIn <- Query.globalParamsNode @era (tx, _) <- balanceTxEnvFailing policy $ do - -- TODO: use a different balancing mechanism if we expect the scripts to fail BuildTx.transferSmartTokens paramsTxIn blacklist directory userOutputsAtProgrammable (assetId, quantity) destCred pure (Convex.CoinSelection.signBalancedTxBody [] tx) diff --git a/src/lib/Wst/Server.hs b/src/lib/Wst/Server.hs index 9c268a4..2624a75 100644 --- a/src/lib/Wst/Server.hs +++ b/src/lib/Wst/Server.hs @@ -12,6 +12,8 @@ module Wst.Server( defaultServerArgs ) where +import Blammo.Logging.Simple (HasLogger, Message ((:#)), MonadLogger, logInfo, + (.=)) import Blockfrost.Client.Types qualified as Blockfrost import Cardano.Api.Shelley qualified as C import Control.Lens qualified as L @@ -20,6 +22,7 @@ import Control.Monad.IO.Class (MonadIO (..)) import Control.Monad.Reader (MonadReader, asks) import Convex.CardanoApi.Lenses qualified as L import Convex.Class (MonadBlockchain (sendTx), MonadUtxoQuery) +import Data.Aeson.Types (KeyValue) import Data.Data (Proxy (..)) import Data.List (nub) import Network.Wai.Handler.Warp qualified as Warp @@ -33,7 +36,7 @@ import SmartTokens.Types.PTokenDirectory (blnKey) import System.Environment qualified import Wst.App (WstApp, runWstAppServant) import Wst.AppError (AppError (..)) -import Wst.Offchain.BuildTx.Failing (BlacklistedTransferPolicy (..), IsEra) +import Wst.Offchain.BuildTx.Failing (BlacklistedTransferPolicy (..)) import Wst.Offchain.Endpoints.Deployment qualified as Endpoints import Wst.Offchain.Env qualified as Env import Wst.Offchain.Query (UTxODat (uDatum)) @@ -77,7 +80,7 @@ defaultServerArgs = , saStaticFiles = Nothing } -runServer :: (Env.HasRuntimeEnv env, Env.HasDirectoryEnv env) => env -> ServerArgs -> IO () +runServer :: (Env.HasRuntimeEnv env, Env.HasDirectoryEnv env, HasLogger env) => env -> ServerArgs -> IO () runServer env ServerArgs{saPort, saStaticFiles} = do let bf = Blockfrost.projectId $ Env.envBlockfrost $ Env.runtimeEnv env app = cors (const $ Just simpleCorsResourcePolicy) @@ -87,7 +90,7 @@ runServer env ServerArgs{saPort, saStaticFiles} = do port = saPort Warp.run port app -server :: forall env. (Env.HasRuntimeEnv env, Env.HasDirectoryEnv env) => env -> Server APIInEra +server :: forall env. (Env.HasRuntimeEnv env, Env.HasDirectoryEnv env, HasLogger env) => env -> Server APIInEra server env = hoistServer (Proxy @APIInEra) (runWstAppServant env) $ healthcheck :<|> queryApi @env @@ -104,7 +107,7 @@ queryApi = :<|> queryAllFunds @C.ConwayEra @env (Proxy @C.ConwayEra) :<|> computeUserAddress (Proxy @C.ConwayEra) -txApi :: forall env. (Env.HasDirectoryEnv env) => ServerT (BuildTxAPI C.ConwayEra) (WstApp env C.ConwayEra) +txApi :: forall env. (Env.HasDirectoryEnv env, HasLogger env) => ServerT (BuildTxAPI C.ConwayEra) (WstApp env C.ConwayEra) txApi = (issueProgrammableTokenEndpoint @C.ConwayEra @env :<|> transferProgrammableTokenEndpoint @C.ConwayEra @env @@ -216,7 +219,7 @@ transferProgrammableTokenEndpoint :: forall era env m. , C.IsBabbageBasedEra era , C.HasScriptLanguageInEra C.PlutusScriptV3 era , MonadUtxoQuery m - , IsEra era + , MonadLogger m ) => TransferProgrammableTokenArgs -> m (TextEnvelopeJSON (C.Tx era)) transferProgrammableTokenEndpoint TransferProgrammableTokenArgs{ttaSender, ttaRecipient, ttaAssetName, ttaQuantity, ttaIssuer, ttaSubmitFailingTx} = do @@ -225,6 +228,7 @@ transferProgrammableTokenEndpoint TransferProgrammableTokenArgs{ttaSender, ttaRe logic <- Env.transferLogicForDirectory (paymentKeyHashFromAddress ttaIssuer) assetId <- Env.programmableTokenAssetId dirEnv <$> Env.transferLogicForDirectory (paymentKeyHashFromAddress ttaIssuer) <*> pure ttaAssetName let policy = if ttaSubmitFailingTx then SubmitFailingTx else DontSubmitFailingTx + logInfo $ "Transfer programmable tokens" :# [logPolicy policy, logSender ttaSender, logRecipient ttaRecipient] Env.withEnv $ Env.withOperator operatorEnv $ Env.withDirectory dirEnv $ Env.withTransfer logic $ do TextEnvelopeJSON <$> Endpoints.transferSmartTokensTx policy assetId ttaQuantity (paymentCredentialFromAddress ttaRecipient) @@ -296,3 +300,14 @@ submitTxEndpoint :: forall era m. => TextEnvelopeJSON (C.Tx era) -> m C.TxId submitTxEndpoint (TextEnvelopeJSON tx) = do either (throwError . SubmitError) pure =<< sendTx tx + +-- structured Logging + +logPolicy :: (KeyValue e kv) => BlacklistedTransferPolicy -> kv +logPolicy p = "policy" .= p + +logSender :: (KeyValue e kv) => C.Address C.ShelleyAddr -> kv +logSender p = "sender" .= p + +logRecipient :: (KeyValue e kv) => C.Address C.ShelleyAddr -> kv +logRecipient p = "recipient" .= p diff --git a/src/test/unit/Wst/Test/UnitTest.hs b/src/test/unit/Wst/Test/UnitTest.hs index 8de53e0..c052f1d 100644 --- a/src/test/unit/Wst/Test/UnitTest.hs +++ b/src/test/unit/Wst/Test/UnitTest.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} module Wst.Test.UnitTest( tests @@ -9,10 +10,11 @@ import Cardano.Ledger.Api qualified as Ledger import Cardano.Ledger.Plutus.ExUnits (ExUnits (..)) import Control.Lens ((%~), (&), (^.)) import Control.Monad (void) +import Control.Monad.IO.Class (MonadIO (..)) import Control.Monad.Reader (MonadReader (ask), ReaderT (runReaderT), asks) import Convex.BuildTx qualified as BuildTx import Convex.Class (MonadBlockchain (queryProtocolParameters, sendTx), - MonadMockchain, MonadUtxoQuery) + MonadMockchain, MonadUtxoQuery, ValidationError, getTxById) import Convex.CoinSelection (ChangeOutputPosition (TrailingChange)) import Convex.MockChain (MockchainT) import Convex.MockChain.CoinSelection (tryBalanceAndSubmit) @@ -29,7 +31,7 @@ import Data.String (IsString (..)) import GHC.Exception (SomeException, throw) import SmartTokens.Core.Scripts (ScriptTarget (Debug, Production)) import Test.Tasty (TestTree, testGroup) -import Test.Tasty.HUnit (Assertion, testCase) +import Test.Tasty.HUnit (Assertion, assertEqual, testCase) import Wst.Offchain.BuildTx.DirectorySet (InsertNodeArgs (..)) import Wst.Offchain.BuildTx.Failing (BlacklistedTransferPolicy (..)) import Wst.Offchain.BuildTx.Utils (addConwayStakeCredentialCertificate) @@ -58,7 +60,7 @@ scriptTargetTests target = , testCase "blacklist credential" (mockchainSucceedsWithTarget target $ void $ deployDirectorySet >>= blacklistCredential) , testCase "unblacklist credential" (mockchainSucceedsWithTarget target $ void $ deployDirectorySet >>= unblacklistCredential) , testCase "blacklisted transfer" (mockchainFails (blacklistTransfer DontSubmitFailingTx) assertBlacklistedAddressException) - , testCase "blacklisted transfer (failing tx)" (mockchainSucceedsWithTarget target (blacklistTransfer SubmitFailingTx)) + , testCase "blacklisted transfer (failing tx)" (mockchainSucceedsWithTarget target (blacklistTransfer SubmitFailingTx >>= assertFailingTx)) , testCase "seize user output" (mockchainSucceedsWithTarget target $ deployDirectorySet >>= seizeUserOutput) , testCase "deploy all" (mockchainSucceedsWithTarget target deployAll) ] @@ -210,7 +212,7 @@ unblacklistCredential scriptRoot = failOnError $ Env.withEnv $ do pure paymentCred -blacklistTransfer :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m) => BlacklistedTransferPolicy -> m () +blacklistTransfer :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m) => BlacklistedTransferPolicy -> m (Either (ValidationError C.ConwayEra) C.TxId) blacklistTransfer policy = failOnError $ Env.withEnv $ do scriptRoot <- runReaderT deployDirectorySet Production userPkh <- asWallet Wallet.w2 $ asks (fst . Env.bteOperator . Env.operatorEnv) @@ -233,7 +235,7 @@ blacklistTransfer policy = failOnError $ Env.withEnv $ do >>= void . sendTx . signTxOperator admin asWallet Wallet.w2 $ Env.withDirectoryFor scriptRoot $ Env.withTransfer transferLogic $ Endpoints.transferSmartTokensTx policy aid 30 (C.PaymentCredentialByKey opPkh) - >>= void . sendTx . signTxOperator (user Wallet.w2) + >>= sendTx . signTxOperator (user Wallet.w2) seizeUserOutput :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m) => DirectoryScriptRoot -> m () seizeUserOutput scriptRoot = failOnError $ Env.withEnv $ do @@ -352,3 +354,13 @@ nodeParamsFor = \case mockchainSucceedsWithTarget :: ScriptTarget -> ReaderT ScriptTarget (MockchainT C.ConwayEra IO) a -> Assertion mockchainSucceedsWithTarget target = mockchainSucceedsWith (nodeParamsFor target) . flip runReaderT target + +{-| Assert that the transaction exists on the mockchain and that its script validity flag +is set to 'C.ScriptInvalid' +-} +assertFailingTx :: (MonadMockchain era m, C.IsAlonzoBasedEra era, MonadFail m, MonadIO m) => Either (ValidationError era) C.TxId -> m () +assertFailingTx = \case + Left err -> fail $ "Expected TxId, got: " <> show err + Right txId -> do + C.TxBody C.TxBodyContent{C.txScriptValidity} <- getTxById txId >>= maybe (fail $ "Tx not found: " <> show txId) (pure . C.getTxBody) + liftIO (assertEqual "Tx validity" (C.TxScriptValidity C.alonzoBasedEra C.ScriptInvalid) txScriptValidity) diff --git a/src/wst-poc.cabal b/src/wst-poc.cabal index f34e266..1165909 100644 --- a/src/wst-poc.cabal +++ b/src/wst-poc.cabal @@ -101,7 +101,6 @@ library , blockfrost-client , blockfrost-client-core , cardano-api - , cardano-ledger-api , cardano-ledger-shelley , containers , convex-base @@ -125,7 +124,6 @@ library , servant-client-core , servant-server , text - , transformers , wai-cors , warp From 68a24eb01da18e285848217b2db3a9faf7001d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Mon, 27 Jan 2025 12:13:10 +0100 Subject: [PATCH 17/18] Finish 'force submit' failing transaction --- frontend/src/app/[username]/index.tsx | 7 +++---- frontend/src/app/utils/walletUtils.ts | 16 ++++++++-------- nix/project.nix | 2 +- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/[username]/index.tsx b/frontend/src/app/[username]/index.tsx index 6dca4a1..4f74aec 100644 --- a/frontend/src/app/[username]/index.tsx +++ b/frontend/src/app/[username]/index.tsx @@ -74,10 +74,9 @@ export default function Profile() { }, } ); - const isValid = !(getUserAccountDetails()?.status === 'Frozen'); - console.log('Send response:', response.data, isValid); + console.log('Send response:', response.data); const tx = await lucid.fromTx(response.data.cborHex); - const txId = await signAndSentTx(lucid, tx, isValid); + const txId = await signAndSentTx(lucid, tx); await updateAccountBalance(sendRecipientAddress); await updateAccountBalance(accountInfo.address); changeAlertInfo({severity: 'success', message: 'Transaction sent successfully!', open: true, link: `https://preview.cexplorer.io/tx/${txId}`}); @@ -116,7 +115,7 @@ export default function Profile() { /> setOverrideTx(x.target.checked)} />} - label="Force send failing transaction" + label="⚠️ Force send failing transaction" sx={{ mb: 2 }} /> diff --git a/frontend/src/app/utils/walletUtils.ts b/frontend/src/app/utils/walletUtils.ts index 3d746e3..8db795f 100644 --- a/frontend/src/app/utils/walletUtils.ts +++ b/frontend/src/app/utils/walletUtils.ts @@ -101,25 +101,25 @@ export async function submitTx(tx: string): Promise> { ); } -export async function signAndSentTx(lucid: LucidEvolution, tx: TxSignBuilder, isValid: boolean = true): Promise { - console.log("tx.toTransaction().isValid()", tx.toTransaction().is_valid()); +export async function signAndSentTx(lucid: LucidEvolution, tx: TxSignBuilder): Promise { + const isValid = tx.toTransaction().is_valid(); const txBuilder = await makeTxSignBuilder(lucid.wallet(), tx.toTransaction()).complete(); - console.log("txBuilder.toTransaction().isValid()", txBuilder.toTransaction().is_valid()) const cmlTx = txBuilder.toTransaction(); const witnessSet = txBuilder.toTransaction().witness_set(); const expectedScriptDataHash : CML.ScriptDataHash | undefined = CML.calc_script_data_hash(witnessSet.redeemers()!, CML.PlutusDataList.new(), lucid.config().costModels!, witnessSet.languages()); const cmlTxBodyClone = CML.TransactionBody.from_cbor_hex(cmlTx!.body().to_cbor_hex()); - // console.log('Preclone script hash:', cmlTxBodyClone.script_data_hash()?.to_hex()); cmlTxBodyClone.set_script_data_hash(expectedScriptDataHash!); - // console.log('Postclone script hash:', cmlTxBodyClone.script_data_hash()?.to_hex()); + const cmlClonedTx = CML.Transaction.new(cmlTxBodyClone, cmlTx!.witness_set(), isValid, cmlTx!.auxiliary_data()); const cmlClonedSignedTx = await makeTxSignBuilder(lucid.wallet(), cmlClonedTx).sign.withWallet().complete(); - // const txId = await cmlClonedSignedTx.submit(); - const txId = await submitTx(cmlClonedSignedTx.toCBOR()); + + // We need to reconstruct the transaction from CBOR again, using CML, because the 'cmlClonedSignedTx.toTransaction().is_valid' always + // returns true (overriding what we specified when we created cmlClonedTx) + const clonedTx2 = CML.Transaction.new(CML.TransactionBody.from_cbor_hex(cmlClonedSignedTx.toTransaction().body().to_cbor_hex()), cmlClonedSignedTx!.toTransaction().witness_set(), isValid, cmlClonedSignedTx.toTransaction()!.auxiliary_data()); + const txId = await submitTx(clonedTx2.to_cbor_hex()); const i = txId.data; console.log(txId); await lucid.awaitTx(txId.data); - // console.log(cmlClonedSignedTx); return i } diff --git a/nix/project.nix b/nix/project.nix index b724edc..c7fa0f8 100644 --- a/nix/project.nix +++ b/nix/project.nix @@ -3,7 +3,7 @@ let sha256map = { # "https://github.com/j-mueller/sc-tools"."dbff9d50478fbce9ee5c718f0536f4183685edd9" = "sha256-b47wr0xuUZohxPnL3Zi6iAYhkY0K7NFHpsv8TXr9LHM="; - "https://github.com/amirmrad/sc-tools"."6c63efe07015e87719d77fa3fabfe07f959c7227" = "sha256-f1qpkjL0YgK2/k8M1BgFYT7bcE14sm0qucbqRjtCbU8="; + "https://github.com/j-mueller/sc-tools"."a3662e093f40082dd6fa525bb0640a10caa1bd70" = "sha256-4GfNKmbSf1fbBEGmQFFZoSahVssBVFfCqU3tjfR1uYs="; "https://github.com/colll78/plutarch-plutus"."b2379767c7f1c70acf28206bf922f128adc02f28" = "sha256-mhuW2CHxnc6FDWuMcjW/51PKuPOdYc4yxz+W5RmlQew="; "https://github.com/input-output-hk/catalyst-onchain-libs"."650a3435f8efbd4bf36e58768fac266ba5beede4" = "sha256-NUh+l97+eO27Ppd8Bx0yMl0E5EV+p7+7GuFun1B8gRc="; }; From 2f300dddc456de4f9f07f9f4da16d62b0ff7dd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jann=20M=C3=BCller?= Date: Mon, 27 Jan 2025 12:20:11 +0100 Subject: [PATCH 18/18] Update schema --- generated/openapi/schema.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/generated/openapi/schema.json b/generated/openapi/schema.json index 568fa41..7545d68 100644 --- a/generated/openapi/schema.json +++ b/generated/openapi/schema.json @@ -166,7 +166,8 @@ "recipient", "issuer", "asset_name", - "quantity" + "quantity", + "submit_failing_tx" ], "type": "object" },