diff --git a/.envrc.local.example b/.envrc.local.example new file mode 100644 index 00000000..19cab961 --- /dev/null +++ b/.envrc.local.example @@ -0,0 +1,4 @@ +export CIP_143_BLOCKFROST_TOKEN=preprod... +export WST_STATIC_FILES=frontend/out +export WST_DEMO_ENV=preprod-network/preprod-env.json +export WST_POLICY_ISSUER_STORE=preprod-network/policy-issuers.sqlite \ No newline at end of file diff --git a/.github/workflows/ci-compiled-scripts.yaml b/.github/workflows/ci-compiled-scripts.yaml index 76d4440b..8baa7e26 100644 --- a/.github/workflows/ci-compiled-scripts.yaml +++ b/.github/workflows/ci-compiled-scripts.yaml @@ -48,8 +48,8 @@ jobs: # git diff --quiet implies --exit-code run: | cabal run export-smart-tokens ./generated/scripts/unapplied - cabal run export-smart-tokens ./generated/scripts/preview 08a8d0bb8717839931b0a594f7c28b0a3b7c78f6e9172e977e250eab7637d879.0 08a8d0bb8717839931b0a594f7c28b0a3b7c78f6e9172e977e250eab7637d879.0 '"addr_test1qq986m3uel86pl674mkzneqtycyg7csrdgdxj6uf7v7kd857kquweuh5kmrj28zs8czrwkl692jm67vna2rf7xtafhpqk3hecm"' - cabal run export-smart-tokens ./generated/scripts/mainnet b1977c1eb33590ca1311384ab68cd36209832213ad4483feb8a1b7cb64828946.0 b1977c1eb33590ca1311384ab68cd36209832213ad4483feb8a1b7cb64828946.0 '"addr_test1qq986m3uel86pl674mkzneqtycyg7csrdgdxj6uf7v7kd857kquweuh5kmrj28zs8czrwkl692jm67vna2rf7xtafhpqk3hecm"' + cabal run export-smart-tokens ./generated/scripts/preview 08a8d0bb8717839931b0a594f7c28b0a3b7c78f6e9172e977e250eab7637d879#0 08a8d0bb8717839931b0a594f7c28b0a3b7c78f6e9172e977e250eab7637d879#0 addr_test1qq986m3uel86pl674mkzneqtycyg7csrdgdxj6uf7v7kd857kquweuh5kmrj28zs8czrwkl692jm67vna2rf7xtafhpqk3hecm + cabal run export-smart-tokens ./generated/scripts/mainnet b1977c1eb33590ca1311384ab68cd36209832213ad4483feb8a1b7cb64828946#0 b1977c1eb33590ca1311384ab68cd36209832213ad4483feb8a1b7cb64828946#0 addr_test1qq986m3uel86pl674mkzneqtycyg7csrdgdxj6uf7v7kd857kquweuh5kmrj28zs8czrwkl692jm67vna2rf7xtafhpqk3hecm cabal run write-openapi-schema -- generated/openapi/schema.json nix develop --accept-flake-config --command bash -c "aiken build src/examples/aiken/aiken --out ./src/examples/aiken/haskell/data/aiken-scripts.json" git diff --quiet diff --git a/.gitignore b/.gitignore index ee5789a4..1ca5120f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ get-protocol-parameters.sh protocol-parameters-mainnet.json .pre-commit-config.yaml tags -/mainnet \ No newline at end of file +/mainnet +operator/* \ No newline at end of file diff --git a/cabal.project b/cabal.project index 616c4d1a..1d4c5e04 100644 --- a/cabal.project +++ b/cabal.project @@ -18,6 +18,7 @@ index-state: , hackage.haskell.org 2025-04-15T08:13:08Z , cardano-haskell-packages 2025-04-11T16:42:25Z + constraints: plutus-core == 1.40.0.0, plutus-ledger-api == 1.40.0.0 @@ -26,11 +27,6 @@ allow-newer: *:plutus-core, *:plutus-ledger-api, -allow-older: - -- NOTE: Currently, plutarch depends on plutus-core version 1.40, while the rest of the (cardano) world is at 1.37. - -- TODO: Delete when plutarch is moving to 1.37 - plutarch:plutus-core - with-compiler: ghc-9.6.6 packages: @@ -43,8 +39,8 @@ packages: source-repository-package type: git location: https://github.com/j-mueller/sc-tools - tag: 100452e6b64200cdffcb2582be07c47e1efebb6b - --sha256: sha256-65swdL2zk1mbqdjten6SIX/2v8tADOX4AhzyE0ocpwY= + tag: dd881e1ab29ba10e3e5b0d163dfde00016c70b8f + --sha256: sha256-/21xOErXZon3rlYUzSkMRRZkFjxi3CrmkeSpJ7MtGSo= subdir: src/devnet src/coin-selection @@ -74,4 +70,3 @@ source-repository-package --sha256: sha256-sdeDXUiL1MbEtJYbN4URwpQ8CbUKjxxGXUxjj1qqi3E= subdir: src/plutarch-onchain-lib - diff --git a/frontend/README.md b/frontend/README.md index ee9866e0..fabcd38d 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -4,6 +4,13 @@ This is a front-end web application for the Cardano Wyoming stable token poc tha ## Getting Started +Make sure to set the environmental variable: +``` +export NEXT_PUBLIC_BLOCKFROST_API_KEY=your_blockfrost_key_here +``` + +Before running `npm run dev` + ### Running the Application To get the application running, follow these steps: diff --git a/frontend/middleware.ts b/frontend/middleware.ts new file mode 100644 index 00000000..389aba93 --- /dev/null +++ b/frontend/middleware.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const allowedUserRoutes = new Set(['alice', 'bob', 'connected-wallet']); + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + if ( + pathname.startsWith('/_next') || + pathname.startsWith('/api') || + pathname.startsWith('/assets') || + pathname === '/favicon.ico' + ) { + return NextResponse.next(); + } + + if (pathname === '/' || pathname === '/mint-authority') { + return NextResponse.redirect(new URL('/connected-wallet', request.url)); + } + + const segments = pathname.split('/').filter(Boolean); + if (segments.length > 0 && allowedUserRoutes.has(segments[0])) { + return NextResponse.next(); + } + + return NextResponse.redirect(new URL('/connected-wallet', request.url)); +} + +export const config = { + matcher: ['/((?!_next/static|_next/image|favicon.ico|api).*)'], +}; diff --git a/frontend/next.config.js b/frontend/next.config.js index cdb80f46..128a5c4b 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -59,15 +59,15 @@ module.exports = (phase, {defaultConfig}) => { }, ]; }, - async redirects() { - return [ - { - source: '/', - destination: '/mint-authority', - permanent: true, // Use true for a 301 redirect, false for 302 - }, - ]; - }, + async redirects() { + return [ + { + source: '/', + destination: '/connected-wallet', + permanent: true, // Use true for a 301 redirect, false for 302 + }, + ]; + }, experimental: { esmExternals: true, // Ensure modern module support }, @@ -87,4 +87,4 @@ module.exports = (phase, {defaultConfig}) => { "@lucid-evolution/lucid" ] } -} \ No newline at end of file +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f0cac9a1..2cc541bf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "react": "^18", "react-dom": "^18", "regenerator-runtime": "^0.14.1", + "use-sync-external-store": "^1.6.0", "zustand": "^5.0.2" }, "devDependencies": { @@ -7309,6 +7310,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index d6e44a68..20d15caa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "react": "^18", "react-dom": "^18", "regenerator-runtime": "^0.14.1", + "use-sync-external-store": "^1.6.0", "zustand": "^5.0.2" }, "devDependencies": { @@ -42,4 +43,4 @@ "tsconfig-paths-webpack-plugin": "^4.2.0", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/frontend/src/app/[username]/index.tsx b/frontend/src/app/[username]/index.tsx index 4b25181d..092ec90b 100644 --- a/frontend/src/app/[username]/index.tsx +++ b/frontend/src/app/[username]/index.tsx @@ -1,26 +1,55 @@ 'use client'; //React imports -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState, useMemo } from 'react'; //Axios imports import axios from 'axios'; //Mui imports -import { Box, Checkbox, FormControlLabel, Typography } from '@mui/material'; +import { Box, Checkbox, CircularProgress, FormControl, FormControlLabel, InputLabel, MenuItem, Paper, Select, Typography } from '@mui/material'; +import type { SelectChangeEvent } from '@mui/material/Select'; //Local components import useStore from '../store/store'; import { Accounts } from '../store/types'; -import { getWalletBalance, signAndSentTx } from '../utils/walletUtils'; +import { getWalletBalance, signAndSentTx, getProgrammableTokenAddress, areStakeScriptsRegistered, getFreezePolicyId, fetchPolicyHolders, getUserTotalProgrammableValue, PolicyTokenBalance, getPolicyIssuer } from '../utils/walletUtils'; import WalletCard from '../components/Card'; import WSTTextField from '../components/WSTTextField'; import CopyTextField from '../components/CopyTextField'; +import WSTTable, { WSTTableRow } from '../components/WSTTable'; import DemoEnvironmentContext from '../context/demoEnvironmentContext'; export default function Profile() { - const { lucid, currentUser, mintAccount, changeAlertInfo, changeWalletAccountDetails } = useStore(); + const { lucid, currentUser, changeAlertInfo, changeWalletAccountDetails, selectedTab } = useStore(); const accounts = useStore((state) => state.accounts); const [overrideTx, setOverrideTx] = useState(false); + const [isRegistering, setIsRegistering] = useState(false); + const [isInitializingBlacklist, setIsInitializingBlacklist] = useState(false); + const [userAssetName, setUserAssetName] = useState('WST'); + const [userMintAmount, setUserMintAmount] = useState(0); + const [userMintRecipient, setUserMintRecipient] = useState(''); + const [userAddressCleared, setUserAddressCleared] = useState(false); + const [userFreezeAddress, setUserFreezeAddress] = useState(''); + const [userFreezeReason, setUserFreezeReason] = useState('Enter reason here'); + const [userUnfreezeAddress, setUserUnfreezeAddress] = useState(''); + const [userSeizeAddress, setUserSeizeAddress] = useState(''); + const [userSeizeReason, setUserSeizeReason] = useState('Enter reason here'); + const [policyOptions, setPolicyOptions] = useState>([]); + const [selectedPolicy, setSelectedPolicy] = useState(''); + const [policyRows, setPolicyRows] = useState([]); + const [policyLoading, setPolicyLoading] = useState(false); + const [policyError, setPolicyError] = useState(null); + const [programmableBalances, setProgrammableBalances] = useState([]); + const [programmableBalanceLoading, setProgrammableBalanceLoading] = useState(false); + const [programmableBalanceError, setProgrammableBalanceError] = useState(null); + const [programmableBalanceRefreshKey, setProgrammableBalanceRefreshKey] = useState(0); + const [selectedTokenHex, setSelectedTokenHex] = useState(''); + const [isSendingTokens, setIsSendingTokens] = useState(false); + const [isUserMinting, setIsUserMinting] = useState(false); + const [isUserFreezing, setIsUserFreezing] = useState(false); + const [isUserUnfreezing, setIsUserUnfreezing] = useState(false); + const [isUserSeizing, setIsUserSeizing] = useState(false); + const demoEnv = useContext(DemoEnvironmentContext); useEffect(() => { @@ -28,11 +57,36 @@ export default function Profile() { // console.log("accounts changed:", accounts); }, [accounts]); + useEffect(() => { + if (!lucid || !accounts.walletUser.regular_address || accounts.walletUser.hasRegisteredScripts === true) { + return; + } + let cancelled = false; + const verifyRegistration = async () => { + try { + const registered = await areStakeScriptsRegistered(demoEnv, lucid, accounts.walletUser.regular_address); + if (registered && !cancelled) { + changeWalletAccountDetails('walletUser', { + ...accounts.walletUser, + hasRegisteredScripts: true, + }); + } + } catch (error) { + console.warn('Failed to verify stake script registration status', error); + } + }; + verifyRegistration(); + return () => { + cancelled = true; + }; + }, [accounts.walletUser, accounts.walletUser.hasRegisteredScripts, accounts.walletUser.regular_address, changeWalletAccountDetails, demoEnv, lucid]); + const getUserAccountDetails = () => { switch (currentUser) { case "Alice": return accounts.alice; case "Bob": return accounts.bob; case "Connected Wallet": return accounts.walletUser; + default: return undefined; }; }; @@ -42,6 +96,7 @@ export default function Profile() { case "Bob": return demoEnv.user_b; case "Mint Authority": return demoEnv.mint_authority; case "Connected Wallet": return ""; //TODO: this seems to be broken + case "Not Connected": return ""; }; } @@ -49,8 +104,186 @@ export default function Profile() { // temp state for each text field const [sendTokenAmount, setMintTokens] = useState(0); const [sendRecipientAddress, setsendRecipientAddress] = useState('address'); + const accountDetails = getUserAccountDetails(); + const accountAdaBalance = accountDetails?.balance?.ada ?? 0; + const accountCollateralCount = accountDetails?.balance?.adaOnlyOutputs ?? 0; + const connectedWalletPreview = accounts.walletUser.regular_address ? accounts.walletUser.regular_address.slice(0, 15) : ''; + const tokenOptions = useMemo( + () => + programmableBalances.flatMap((group) => + group.tokens.map((token) => { + const quantityBigInt = (() => { + try { + return BigInt(token.quantity ?? '0'); + } catch { + return 0n; + } + })(); + return { + value: token.assetNameHex, + label: token.displayName, + policyId: group.policyId, + quantity: quantityBigInt, + quantityDisplay: token.quantity, + }; + }) + ), + [programmableBalances] + ); + + const selectedTokenOption = tokenOptions.find((option) => option.value === selectedTokenHex); + const selectedTokenBalance = selectedTokenOption?.quantity ?? 0n; + + const parsedSendAmount = Number.isFinite(sendTokenAmount) ? sendTokenAmount : 0; + const sendAmountBigInt = (() => { + if (parsedSendAmount <= 0) { + return 0n; + } + try { + return BigInt(Math.floor(parsedSendAmount)); + } catch { + return 0n; + } + })(); + const sendAmountValid = + Boolean(selectedTokenHex) && + parsedSendAmount > 0 && + sendAmountBigInt > 0n && + sendAmountBigInt <= selectedTokenBalance; + const showInsufficientBalance = + Boolean(selectedTokenHex) && parsedSendAmount > 0 && sendAmountBigInt > selectedTokenBalance; + + useEffect(() => { + if (tokenOptions.length === 0) { + setSelectedTokenHex(''); + return; + } + setSelectedTokenHex((prev) => + prev && tokenOptions.some((option) => option.value === prev) ? prev : tokenOptions[0].value + ); + }, [tokenOptions]); + + useEffect(() => { + if ( + currentUser !== 'Connected Wallet' || + !accounts.walletUser.hasRegisteredScripts || + !accounts.walletUser.regular_address + ) { + setPolicyOptions([]); + setSelectedPolicy(''); + setPolicyRows([]); + setPolicyError(null); + return; + } + let cancelled = false; + const loadPolicyIds = async () => { + try { + const freezePolicyId = await getFreezePolicyId(accounts.walletUser.regular_address); + if (!cancelled) { + const options = [{ id: freezePolicyId, label: 'Freeze / Seize policy' }]; + setPolicyOptions(options); + setSelectedPolicy(freezePolicyId); + setPolicyError(null); + } + } catch (error) { + console.warn('Failed to load freeze policy id', error); + if (!cancelled) { + setPolicyError('Unable to load policy identifiers.'); + } + } + }; + loadPolicyIds(); + return () => { + cancelled = true; + }; + }, [accounts.walletUser.hasRegisteredScripts, accounts.walletUser.regular_address, currentUser]); + + useEffect(() => { + if ( + currentUser !== 'Connected Wallet' || + !accounts.walletUser.hasRegisteredScripts || + !selectedPolicy || + !lucid + ) { + setPolicyRows([]); + setPolicyLoading(false); + return; + } + let cancelled = false; + const loadPolicyHoldersData = async () => { + setPolicyLoading(true); + setPolicyError(null); + try { + const holders = await fetchPolicyHolders(demoEnv, selectedPolicy); + const rows = await Promise.all( + holders.map(async (holder) => { + return { + regularAddress: '', + programmableAddress: holder.address, + status: 'Unknown', + assets: holder.assets, + } as WSTTableRow; + }) + ); + if (!cancelled) { + setPolicyRows(rows); + } + } catch (error) { + console.warn('Failed to load policy holders', error); + if (!cancelled) { + setPolicyError('Unable to load address holdings.'); + setPolicyRows([]); + } + } finally { + if (!cancelled) { + setPolicyLoading(false); + } + } + }; + loadPolicyHoldersData(); + return () => { + cancelled = true; + }; + }, [accounts.walletUser.hasRegisteredScripts, demoEnv, lucid, selectedPolicy, currentUser]); + + useEffect(() => { + if (currentUser !== 'Connected Wallet' || !accounts.walletUser.regular_address) { + setProgrammableBalances([]); + setProgrammableBalanceLoading(false); + setProgrammableBalanceError(null); + return; + } + let cancelled = false; + const loadProgrammableBalances = async () => { + setProgrammableBalanceLoading(true); + setProgrammableBalanceError(null); + try { + const balances = await getUserTotalProgrammableValue(accounts.walletUser.regular_address); + if (!cancelled) { + setProgrammableBalances(balances); + } + } catch (error) { + console.warn('Failed to load programmable token balances', error); + if (!cancelled) { + setProgrammableBalanceError('Unable to load programmable token balances.'); + setProgrammableBalances([]); + } + } finally { + if (!cancelled) { + setProgrammableBalanceLoading(false); + } + } + }; + loadProgrammableBalances(); + return () => { + cancelled = true; + }; + }, [accounts.walletUser.regular_address, currentUser, programmableBalanceRefreshKey]); const onSend = async () => { + if (isSendingTokens) { + return; + } if (getUserAccountDetails()?.status === 'Frozen' && !overrideTx) { changeAlertInfo({ severity: 'error', @@ -67,11 +300,64 @@ export default function Profile() { console.error("No valid send address found! Cannot send."); return; } - lucid.selectWallet.fromSeed(getUserMnemonic()); + if (!selectedTokenHex) { + changeAlertInfo({ + severity: 'error', + message: 'Select a programmable token to send.', + open: true, + link: '' + }); + return; + } + if (sendAmountBigInt === 0n) { + changeAlertInfo({ + severity: 'error', + message: 'Enter a token amount greater than zero.', + open: true, + link: '' + }); + return; + } + if (sendAmountBigInt > selectedTokenBalance) { + changeAlertInfo({ + severity: 'error', + message: 'Insufficient balance for this token.', + open: true, + link: '' + }); + return; + } + if (currentUser !== 'Connected Wallet') { + lucid.selectWallet.fromSeed(getUserMnemonic()); + } lucid.wallet().address().then(console.log); + const tokenPolicyId = selectedTokenOption?.policyId; + if (!tokenPolicyId) { + changeAlertInfo({ + severity: 'error', + message: 'Select a programmable token with a known policy before sending.', + open: true, + link: '' + }); + return; + } + let issuerForPolicy: string; + try { + issuerForPolicy = await getPolicyIssuer(tokenPolicyId); + } catch (error) { + console.error('Failed to resolve policy issuer', error); + changeAlertInfo({ + severity: 'error', + message: 'Unable to resolve the issuer for the selected token policy.', + open: true, + link: '' + }); + return; + } + setIsSendingTokens(true); const requestData = { - asset_name: Buffer.from('WST', 'utf8').toString('hex'), // Convert "WST" to hex - issuer: mintAccount.regular_address, + asset_name: selectedTokenHex, + issuer: issuerForPolicy, quantity: sendTokenAmount, recipient: sendRecipientAddress, sender: accountInfo.regular_address, @@ -92,9 +378,153 @@ export default function Profile() { const txId = await signAndSentTx(lucid, tx); await updateAccountBalance(sendRecipientAddress); await updateAccountBalance(accountInfo.regular_address); - changeAlertInfo({severity: 'success', message: 'Transaction sent successfully!', open: true, link: `${demoEnv.explorer_url}/${txId}`}); + setProgrammableBalanceRefreshKey((key) => key + 1); + changeAlertInfo({ + severity: 'success', + message: 'Transaction sent successfully!', + open: true, + link: `${demoEnv.explorer_url}/${txId}`, + actionText: 'View on Explorer' + }); } catch (error) { console.error('Send failed:', error); + } finally { + setIsSendingTokens(false); + } + }; + + const onRegisterAsset = async () => { + if (isRegistering) { + return; + } + if (currentUser !== 'Connected Wallet') { + changeAlertInfo({ + severity: 'error', + message: 'Switch to the Connected Wallet profile to register a programmable asset.', + open: true, + link: '' + }); + return; + } + const issuerAddress = accounts.walletUser.regular_address; + if (!issuerAddress) { + changeAlertInfo({ + severity: 'error', + message: 'Connect Lace to populate your wallet address before registering.', + open: true, + link: '' + }); + return; + } + const announceSuccess = async (txId?: string, msg = 'Programmable asset registered successfully!') => { + const latestAccounts = useStore.getState().accounts; + let programmableAddress = latestAccounts.walletUser.programmable_token_address; + let registrationStatus = latestAccounts.walletUser.hasRegisteredScripts; + try { + const [updatedAddress, isRegistered] = await Promise.all([ + getProgrammableTokenAddress(issuerAddress), + areStakeScriptsRegistered(demoEnv, lucid, issuerAddress), + ]); + programmableAddress = updatedAddress ?? programmableAddress; + registrationStatus = isRegistered; + } catch (err) { + console.warn('Failed to refresh programmable token metadata after registration', err); + } + changeWalletAccountDetails('walletUser', { + ...latestAccounts.walletUser, + programmable_token_address: programmableAddress ?? latestAccounts.walletUser.programmable_token_address, + hasRegisteredScripts: registrationStatus ?? latestAccounts.walletUser.hasRegisteredScripts, + }); + changeAlertInfo({ + severity: 'success', + message: msg, + open: true, + link: txId ? `${demoEnv.explorer_url}/${txId}` : '', + actionText: txId ? 'View on Explorer' : undefined + }); + }; + + try { + setIsRegistering(true); + changeAlertInfo({ severity: 'info', message: 'Preparing registration transaction…', open: true, link: '' }); + const response = await axios.post( + '/api/v1/tx/programmable-token/register-transfer-scripts', + { issuer: issuerAddress }, + { headers: { 'Content-Type': 'application/json;charset=utf-8' } } + ); + const tx = await lucid.fromTx(response.data.cborHex); + const txId = await signAndSentTx(lucid, tx); + await announceSuccess(txId); + } catch (error: any) { + const errPayload = error?.response?.data ?? ''; + const errAsString = typeof errPayload === 'string' ? errPayload : JSON.stringify(errPayload); + if (errAsString.includes('StakeKeyRegisteredDELEG')) { + await announceSuccess(undefined, 'Programmable asset scripts already registered.'); + console.warn('Scripts already registered, treating as success.'); + } else { + console.error('Register asset failed:', error); + changeAlertInfo({ + severity: 'error', + message: 'Registration failed. Please try again after checking the console output.', + open: true, + link: '' + }); + } + } finally { + setIsRegistering(false); + } + }; + + const onBlacklistInit = async () => { + if (isInitializingBlacklist) { + return; + } + if (currentUser !== 'Connected Wallet') { + changeAlertInfo({ + severity: 'error', + message: 'Switch to the Connected Wallet profile to initialise the blacklist.', + open: true, + link: '' + }); + return; + } + const issuerAddress = accounts.walletUser.regular_address; + if (!issuerAddress) { + changeAlertInfo({ + severity: 'error', + message: 'Connect Lace to populate your wallet address before initialising the blacklist.', + open: true, + link: '' + }); + return; + } + try { + setIsInitializingBlacklist(true); + changeAlertInfo({ severity: 'info', message: 'Preparing blacklist initialisation transaction…', open: true, link: '' }); + const response = await axios.post( + '/api/v1/tx/programmable-token/blacklist-init', + { issuer: issuerAddress }, + { headers: { 'Content-Type': 'application/json;charset=utf-8' } } + ); + const tx = await lucid.fromTx(response.data.cborHex); + const txId = await signAndSentTx(lucid, tx); + changeAlertInfo({ + severity: 'success', + message: 'Blacklist initialised successfully!', + open: true, + link: `${demoEnv.explorer_url}/${txId}`, + actionText: 'View on Explorer' + }); + } catch (error) { + console.error('Blacklist init failed:', error); + changeAlertInfo({ + severity: 'error', + message: 'Blacklist initialisation failed. Check the console for details.', + open: true, + link: '' + }); + } finally { + setIsInitializingBlacklist(false); } }; @@ -110,14 +540,270 @@ export default function Profile() { }); } }; + + const ensureWalletReady = (requireRegistration = true): string | null => { + const issuerAddress = accounts.walletUser.regular_address; + if (!issuerAddress) { + changeAlertInfo({ + severity: 'error', + message: 'Connect your Lace wallet to continue.', + open: true, + link: '' + }); + return null; + } + if (requireRegistration && !accounts.walletUser.hasRegisteredScripts) { + changeAlertInfo({ + severity: 'error', + message: 'Register your programmable asset before using mint actions.', + open: true, + link: '' + }); + return null; + } + return issuerAddress; + }; + + const handleUserAddressClearedChange = (event: { target: { checked: boolean | ((prevState: boolean) => boolean); }; }) => { + setUserAddressCleared(event.target.checked); + }; + + const onUserMint = async () => { + if (isUserMinting) { + return; + } + const issuerAddress = ensureWalletReady(); + if (!issuerAddress) return; + if (!userAddressCleared) { + changeAlertInfo({ + severity: 'error', + message: 'Confirm the recipient address is cleared before minting.', + open: true, + link: '' + }); + return; + } + changeAlertInfo({severity: 'info', message: 'Processing mint request…', open: true, link: ''}); + const assetLabel = userAssetName.trim() === '' ? 'TOKEN' : userAssetName; + const recipient = userMintRecipient.trim() !== '' ? userMintRecipient : accounts.walletUser.programmable_token_address || issuerAddress; + const requestData = { + asset_name: Buffer.from(assetLabel, 'utf8').toString('hex'), + issuer: issuerAddress, + quantity: userMintAmount, + recipient + }; + setIsUserMinting(true); + try { + const response = await axios.post( + '/api/v1/tx/programmable-token/issue', + requestData, + { + headers: {'Content-Type': 'application/json;charset=utf-8'} + } + ); + const tx = await lucid.fromTx(response.data.cborHex); + const txId = await signAndSentTx(lucid, tx); + changeAlertInfo({ + severity: 'success', + message: 'Mint successful!', + open: true, + link: `${demoEnv.explorer_url}/${txId}`, + actionText: 'View on Explorer' + }); + await updateAccountBalance(recipient); + await updateAccountBalance(issuerAddress); + setProgrammableBalanceRefreshKey((key) => key + 1); + } catch (error) { + console.error('Connected wallet mint failed:', error); + changeAlertInfo({ + severity: 'error', + message: 'Mint failed. See console for details.', + open: true, + link: '' + }); + } finally { + setIsUserMinting(false); + } + }; + + const onUserFreeze = async () => { + if (isUserFreezing) { + return; + } + const issuerAddress = ensureWalletReady(); + if (!issuerAddress || !userFreezeAddress) return; + changeAlertInfo({severity: 'info', message: 'Processing freeze request…', open: true, link: ''}); + const requestData = { + issuer: issuerAddress, + blacklist_address: userFreezeAddress, + reason: userFreezeReason + }; + setIsUserFreezing(true); + try { + const response = await axios.post( + '/api/v1/tx/programmable-token/blacklist', + requestData, + { headers: {'Content-Type': 'application/json;charset=utf-8'} } + ); + const tx = await lucid.fromTx(response.data.cborHex); + const txId = await signAndSentTx(lucid, tx); + changeAlertInfo({ + severity: 'success', + message: 'Address frozen.', + open: true, + link: `${demoEnv.explorer_url}/${txId}`, + actionText: 'View on Explorer' + }); + const frozenWalletKey = (Object.keys(accounts) as (keyof Accounts)[]).find( + (key) => accounts[key].regular_address === userFreezeAddress + ); + if (frozenWalletKey) { + changeWalletAccountDetails(frozenWalletKey, { + ...accounts[frozenWalletKey], + status: 'Frozen', + }); + } + } catch (error: any) { + if (error?.response?.data?.includes?.('DuplicateBlacklistNode')) { + changeAlertInfo({ + severity: 'error', + message: 'This address is already frozen.', + open: true, + link: '' + }); + } else { + console.error('Connected wallet freeze failed:', error); + } + } finally { + setIsUserFreezing(false); + } + }; + + const onUserUnfreeze = async () => { + if (isUserUnfreezing) { + return; + } + const issuerAddress = ensureWalletReady(); + if (!issuerAddress || !userUnfreezeAddress) return; + changeAlertInfo({severity: 'info', message: 'Processing unfreeze request…', open: true, link: ''}); + const requestData = { + issuer: issuerAddress, + blacklist_address: userUnfreezeAddress, + reason: '(unfreeze)' + }; + setIsUserUnfreezing(true); + try { + const response = await axios.post( + '/api/v1/tx/programmable-token/unblacklist', + requestData, + { headers: {'Content-Type': 'application/json;charset=utf-8'} } + ); + const tx = await lucid.fromTx(response.data.cborHex); + const txId = await signAndSentTx(lucid, tx); + changeAlertInfo({ + severity: 'success', + message: 'Address unfrozen.', + open: true, + link: `${demoEnv.explorer_url}/${txId}`, + actionText: 'View on Explorer' + }); + const unfrozenWalletKey = (Object.keys(accounts) as (keyof Accounts)[]).find( + (key) => accounts[key].regular_address === userUnfreezeAddress + ); + if (unfrozenWalletKey) { + changeWalletAccountDetails(unfrozenWalletKey, { + ...accounts[unfrozenWalletKey], + status: 'Active', + }); + } + } catch (error: any) { + if (error?.response?.data?.includes?.('BlacklistNodeNotFound')) { + changeAlertInfo({ + severity: 'error', + message: 'This address is not frozen.', + open: true, + link: '' + }); + } else { + console.error('Connected wallet unfreeze failed:', error); + } + } finally { + setIsUserUnfreezing(false); + } + }; + + const onUserSeize = async () => { + if (isUserSeizing) { + return; + } + const issuerAddress = ensureWalletReady(); + if (!issuerAddress || !userSeizeAddress) return; + changeAlertInfo({severity: 'info', message: 'Processing seizure request…', open: true, link: ''}); + const requestData = { + issuer: issuerAddress, + target: userSeizeAddress, + reason: userSeizeReason + }; + setIsUserSeizing(true); + try { + const response = await axios.post( + '/api/v1/tx/programmable-token/seize', + requestData, + { headers: {'Content-Type': 'application/json;charset=utf-8'} } + ); + const tx = await lucid.fromTx(response.data.cborHex); + const txId = await signAndSentTx(lucid, tx); + await updateAccountBalance(userSeizeAddress); + changeAlertInfo({ + severity: 'success', + message: 'Funds seized.', + open: true, + link: `${demoEnv.explorer_url}/${txId}`, + actionText: 'View on Explorer' + }); + } catch (error) { + console.error('Connected wallet seize failed:', error); + changeAlertInfo({ + severity: 'error', + message: 'Seizure failed. See console for details.', + open: true, + link: '' + }); + } finally { + setIsUserSeizing(false); + } + }; const sendContent = + + Token to Send + + + {selectedTokenOption && ( + + Balance: {selectedTokenOption.quantityDisplay} + + )} setMintTokens(Number(e.target.value))} label="Number of Tokens to Send" fullWidth={true} + error={showInsufficientBalance} + helperText={showInsufficientBalance ? 'Insufficient balance' : undefined} /> ; - return ( -
- + const userMintContent = ( + + setUserAssetName(e.target.value)} + label="Asset Symbol" + fullWidth={true} + /> + setUserMintAmount(Number(e.target.value))} + label="Number of Tokens to Mint" + fullWidth={true} + /> + setUserMintRecipient(e.target.value)} + label="Mint Recipient’s Address (optional)" + fullWidth={true} + /> + } + label="Recipient address has been cleared" + sx={{ mb: 2 }} + /> + + ); + + const userFreezeContent = ( + + setUserFreezeAddress(e.target.value)} + label="Address to Freeze" + fullWidth={true} + /> + setUserFreezeReason(e.target.value)} + label="Reason" + fullWidth={true} + /> + + ); + + const userUnfreezeContent = ( + + setUserUnfreezeAddress(e.target.value)} + label="Address to Unfreeze" + fullWidth={true} + /> + + ); + + const userSeizeContent = ( + + setUserSeizeAddress(e.target.value)} + label="Address to Seize" + fullWidth={true} + /> + setUserSeizeReason(e.target.value)} + label="Reason" + fullWidth={true} + /> + + ); + + const registerContent = ( + + + Register your programmable asset scripts using the connected Lace wallet. This registers the stake scripts that will allow you to mint your own programmable token. + + + + ); + + const blacklistInitContent = ( + + + Initialise the blacklist state for your programmable asset to enable future freeze and seize actions. + + + + ); + + const renderProgrammableBalanceCards = () => { + if (programmableBalanceLoading) { + return ( + + + + ); + } + if (programmableBalanceError) { + return ( + + {programmableBalanceError} + + ); + } + if (programmableBalances.length === 0) { + return ( + + No programmable tokens detected for this wallet yet. + + ); + } + return ( + + {programmableBalances.map((group) => ( + + + Policy ID + + + {group.policyId} + + {group.tokens.map((token, index) => ( + + + + {token.displayName} + + + {token.assetNameHex || '—'} + + + + {token.quantity} + + + ))} + + ))} + + ); + }; + + const renderWalletView = () => ( + <> + Address Balance - {getUserAccountDetails()?.balance.wst} WST - {getUserAccountDetails()?.balance.ada} Ada { (getUserAccountDetails()?.balance.adaOnlyOutputs === 0) && (({getUserAccountDetails()?.balance.adaOnlyOutputs} collateral UTxOs))} + {accountAdaBalance} Ada { (accountCollateralCount === 0) && (({accountCollateralCount} collateral UTxOs))} - {getUserAccountDetails()?.regular_address.slice(0,15)} + {accountDetails?.regular_address ? accountDetails.regular_address.slice(0,15) : ''} + +
+ +
+ {currentUser === 'Connected Wallet' && ( + + Programmable Token Balances + + Totals for every programmable token held by your connected wallet, grouped by policy ID. + + {renderProgrammableBalanceCards()} + + )} + + ); + + const renderMintActionsView = () => { + if (currentUser !== 'Connected Wallet') { + return ( + + Mint actions are only available when using the Connected Wallet profile. + + ); + } + + if (!accounts.walletUser.hasRegisteredScripts) { + return ( + + + Register your freeze and seize policy before accessing mint actions. + + + ); + } + + return ( + <> + + + Freeze and Seize Token Controls + Manage tokens issued by your connected Lace wallet. + + {connectedWalletPreview}
+ + ); + }; + + const renderAddressesView = () => { + if (currentUser !== 'Connected Wallet') { + return ( + + Switch to the Connected Wallet profile to view policy holdings. + + ); + } + if (!accounts.walletUser.hasRegisteredScripts) { + return ( + + + Register your programmable asset to view address holdings. + + + ); + } + return ( + <> + + + Addresses Holding Your Programmable Token + + Select a policy to inspect all holders and their balances. + + + + Policy + + + + {selectedPolicy && ( + + + + )} + {policyError && ( + + {policyError} + + )} + + + ); + }; + + const renderRegisterAssetView = () => ( + <> + + Register Freeze and Seize Programmable Asset + Use your connected Lace wallet to register the programmable token scripts. + +
+ +
+ + ); + + const renderInactiveState = () => ( + + Connect a Wallet to Continue + + Use the Connect Wallet button in the top-right corner to link Lace, Eternl, or Yoroi before accessing wallet features. + + + ); + + return ( +
+ {!selectedTab + ? renderInactiveState() + : selectedTab === 'Register Asset' + ? renderRegisterAssetView() + : selectedTab === 'Mint Actions' + ? renderMintActionsView() + : selectedTab === 'Addresses' + ? renderAddressesView() + : renderWalletView()}
); } diff --git a/frontend/src/app/clientLayout.tsx b/frontend/src/app/clientLayout.tsx index 1ee5153b..f981b2a4 100644 --- a/frontend/src/app/clientLayout.tsx +++ b/frontend/src/app/clientLayout.tsx @@ -1,94 +1,149 @@ "use client"; //React imports -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; //Mui imports import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter'; +import Box from '@mui/material/Box'; +import Skeleton from '@mui/material/Skeleton'; +import Typography from '@mui/material/Typography'; import NavDrawer from './components/NavDrawer'; -//Local file +//Local files import { ThemeModeProvider } from "./styles/themeContext"; import "./styles/globals.css"; -import { makeLucid, getWalletFromSeed } from "./utils/walletUtils"; +import { makeLucid } from "./utils/walletUtils"; import useStore from './store/store'; import WSTAppBar from "./components/WSTAppBar"; import AlertBar from './components/AlertBar'; -import { DemoEnvironment, previewEnv } from "./store/types"; -import axios from "axios"; +import { DemoEnvironment } from "./store/types"; import DemoEnvironmentContext from "./context/demoEnvironmentContext"; +import type { InitialWalletSnapshot } from "./lib/initialData"; +import WSTCommonButton from "./components/WSTCommonButton"; +import { shallow } from 'zustand/shallow'; -async function loadDemoEnvironment(): Promise { - const response = await axios.get("/api/v1/demo-environment", - { - headers: { - 'Content-Type': 'application/json;charset=utf-8', - }, - }); - return response?.data; -} +type ClientLayoutProps = { + children: React.ReactNode; + initialDemoEnvironment: DemoEnvironment; + initialWallets: InitialWalletSnapshot; +}; -async function getProgrammableTokenAddress(regular_address: string) { - const response = await axios.get(`/api/v1/query/address/${regular_address}`, - { - headers: { - 'Content-Type': 'application/json;charset=utf-8', - }, - }); - return response?.data; -} +type InitStatus = "loading" | "ready" | "error"; +const AppLoadingSkeleton = ({ + status, + errorMessage, + onRetry, +}: { + status: InitStatus; + errorMessage?: string | null; + onRetry?: () => void; +}) => ( + + + + + + {Array.from({ length: 4 }).map((_, index) => ( + + ))} + + + + {status === 'error' && ( + + + {errorMessage ?? 'Unable to prepare wallet state.'} + + {onRetry && } + + )} + +); + +export default function ClientLayout({ children, initialDemoEnvironment, initialWallets }: ClientLayoutProps) { + const { changeMintAccountDetails, changeWalletAccountDetails, setLucidInstance } = useStore( + (state) => ({ + changeMintAccountDetails: state.changeMintAccountDetails, + changeWalletAccountDetails: state.changeWalletAccountDetails, + setLucidInstance: state.setLucidInstance, + }), + shallow + ); + const alertInfo = useStore((state) => state.alertInfo); + const changeAlertInfo = useStore((state) => state.changeAlertInfo); + const [initStatus, setInitStatus] = useState("loading"); + const [initError, setInitError] = useState(null); + const [initAttempt, setInitAttempt] = useState(0); -export default function ClientLayout({ children }: { children: React.ReactNode }) { - const { mintAccount, accounts, changeMintAccountDetails, changeWalletAccountDetails, setLucidInstance } = useStore(); - const [demoEnv, setDemoEnv] = useState(previewEnv); + useEffect(() => { + let cancelled = false; + const initialise = async () => { + setInitStatus("loading"); + setInitError(null); + try { + const { mintAccount: currentMintAccount, accounts: currentAccounts } = useStore.getState(); + changeMintAccountDetails({ + ...currentMintAccount, + regular_address: initialWallets.mintAuthority.regularAddress, + programmable_token_address: initialWallets.mintAuthority.programmableTokenAddress, + }); + changeWalletAccountDetails("alice", { + ...currentAccounts.alice, + regular_address: initialWallets.alice.regularAddress, + programmable_token_address: initialWallets.alice.programmableTokenAddress, + }); + changeWalletAccountDetails("bob", { + ...currentAccounts.bob, + regular_address: initialWallets.bob.regularAddress, + programmable_token_address: initialWallets.bob.programmableTokenAddress, + }); - useEffect(() => { - const fetchUserWallets = async () => { - try { - const demoEnv = await loadDemoEnvironment(); - console.log("DemoEnvironment:", demoEnv); - setDemoEnv(demoEnv); + const lucid = await makeLucid(initialDemoEnvironment); + if (cancelled) { + return; + } + setLucidInstance(lucid); + setInitStatus("ready"); + } catch (error) { + console.error("Error initializing wallets:", error); + if (!cancelled) { + setInitError(error instanceof Error ? error.message : "Unknown error"); + setInitStatus("error"); + } + } + }; - // retrieve wallet info - const mintAuthorityWallet = await getWalletFromSeed(demoEnv, demoEnv.mint_authority); - const walletA = await getWalletFromSeed(demoEnv, demoEnv.user_a); - const walletB = await getWalletFromSeed(demoEnv, demoEnv.user_b); - const walletATokenAddr = await getProgrammableTokenAddress(walletA.address); - const walletBTokenAddr = await getProgrammableTokenAddress(walletB.address); - const mintAuthorityTokenAddr = await getProgrammableTokenAddress(mintAuthorityWallet.address); - - // Update Zustand store with the initialized wallet information - changeMintAccountDetails({ ...mintAccount, regular_address: mintAuthorityWallet.address, programmable_token_address: mintAuthorityTokenAddr}); - changeWalletAccountDetails('alice', { ...accounts.alice, regular_address: walletA.address, programmable_token_address: walletATokenAddr},); - changeWalletAccountDetails('bob', { ...accounts.bob, regular_address: walletB.address, programmable_token_address: walletBTokenAddr}); - - const initialLucid = await makeLucid(demoEnv); - setLucidInstance(initialLucid); - console.log('Wallets initialized'); - } catch (error) { - console.error('Error initializing wallets:', error); - } - }; - - fetchUserWallets(); - },[]); + initialise(); + return () => { + cancelled = true; + }; + }, [ + changeMintAccountDetails, + changeWalletAccountDetails, + initialDemoEnvironment, + initialWallets, + initAttempt, + setLucidInstance, + ]); - if(accounts.bob.regular_address === '') { - return
-
-
; - }; + const handleRetry = () => setInitAttempt((prev) => prev + 1); + const demoEnv = useMemo(() => initialDemoEnvironment, [initialDemoEnvironment]); return ( - -
- - -
{children}
- -
+ + {initStatus !== "ready" ? ( + + ) : ( +
+ + +
{children}
+ changeAlertInfo({ open: false })} /> +
+ )}
diff --git a/frontend/src/app/components/AlertBar.tsx b/frontend/src/app/components/AlertBar.tsx index e6cf7728..b43d5adf 100644 --- a/frontend/src/app/components/AlertBar.tsx +++ b/frontend/src/app/components/AlertBar.tsx @@ -1,43 +1,75 @@ -//React imports -import * as React from 'react'; - -//Mui imports -import Snackbar from '@mui/material/Snackbar'; -import Alert from '@mui/material/Alert'; - -//Local components -import useStore from '../store/store'; - -export default function AlertBar() { -const { alertInfo, changeAlertInfo } = useStore(); - -const handleClose = () => { - changeAlertInfo({ ...alertInfo, open: false }); -}; - - return ( - - - {alertInfo.message} - {alertInfo.link && ( - <> - {" "} - - {alertInfo.link} - - {" "} - - )} - - - ); -} +//React imports +import * as React from 'react'; + +//Mui imports +import Snackbar from '@mui/material/Snackbar'; +import Alert from '@mui/material/Alert'; +import Button from '@mui/material/Button'; + +//Local imports +import type { AlertInfo } from '../store/types'; + +type QueuedAlert = AlertInfo & { toastId: number }; + +type AlertBarProps = { + alertInfo: AlertInfo; + onClose: () => void; +}; + +export default function AlertBar({ alertInfo, onClose }: AlertBarProps) { + const [queue, setQueue] = React.useState([]); + const lastSeenId = React.useRef(null); + + React.useEffect(() => { + if (alertInfo.open && alertInfo.id !== lastSeenId.current) { + setQueue((prev) => [...prev, { ...alertInfo, toastId: alertInfo.id }]); + lastSeenId.current = alertInfo.id; + onClose(); + } + }, [alertInfo, onClose]); + + const dismissToast = (toastId: number) => { + setQueue((prev) => prev.filter((toast) => toast.toastId !== toastId)); + }; + + if (queue.length === 0) { + return null; + } + + return ( + <> + {queue.map((toast, index) => ( + dismissToast(toast.toastId)} + sx={{ + bottom: `${16 + index * 88}px !important`, + }} + > + dismissToast(toast.toastId)} + action={ + toast.link ? ( + + ) : undefined + } + > + {toast.message} + + + ))} + + ); +} diff --git a/frontend/src/app/components/Card.tsx b/frontend/src/app/components/Card.tsx index f2253905..c2116f63 100644 --- a/frontend/src/app/components/Card.tsx +++ b/frontend/src/app/components/Card.tsx @@ -1,6 +1,6 @@ 'use client' //React imports -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; //MUI imports import {Box} from '@mui/material'; @@ -9,47 +9,65 @@ import {Box} from '@mui/material'; import ContentTabs from './ContentTabs'; import WSTCommonButton from './WSTCommonButton'; -interface TabContent { - label: string - content: React.ReactNode - buttonLabel?: string - onAction?: () => void -} - -interface WalletCardProps { - tabs: TabContent[] -} - -export default function WalletCard({ tabs }: WalletCardProps) { - const [tabValue, setTabValue] = useState(0); - - const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { - setTabValue(newValue); - }; - - const { content, buttonLabel, onAction } = tabs[tabValue] +interface TabContent { + label: string; + content: React.ReactNode; + buttonLabel?: string; + onAction?: () => void; + buttonDisabled?: boolean; + buttonLoading?: boolean; +} + +interface WalletCardProps { + tabs: TabContent[] +} + +export default function WalletCard({ tabs }: WalletCardProps) { + const [tabValue, setTabValue] = useState(0); + + useEffect(() => { + setTabValue((prev) => { + if (tabs.length === 0) { + return 0; + } + return Math.min(prev, tabs.length - 1); + }); + }, [tabs]); + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + + if (tabs.length === 0) { + return null; + } + + const safeTabIndex = Math.min(tabValue, tabs.length - 1); + const { content, buttonLabel, onAction, buttonDisabled, buttonLoading } = tabs[safeTabIndex] return (
tab.label)} - value={tabValue} - onChange={handleTabChange} - /> + tabLabels={tabs.map((tab) => tab.label)} + value={safeTabIndex} + onChange={handleTabChange} + /> {content} {buttonLabel && ( - + )}
diff --git a/frontend/src/app/components/CopyTextField.tsx b/frontend/src/app/components/CopyTextField.tsx index 3d8b5391..dc6c02e2 100644 --- a/frontend/src/app/components/CopyTextField.tsx +++ b/frontend/src/app/components/CopyTextField.tsx @@ -10,13 +10,14 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'; //local components import IconButton from './WSTIconButton'; -interface PHATextFieldProps { - value: TextFieldProps['defaultValue']; - fullWidth?: TextFieldProps['fullWidth']; - label: TextFieldProps['label']; -} - -export default function CopyTextField({value, fullWidth, label}: PHATextFieldProps) { +interface PHATextFieldProps { + value: TextFieldProps['defaultValue']; + fullWidth?: TextFieldProps['fullWidth']; + label: TextFieldProps['label']; + sx?: TextFieldProps['sx']; +} + +export default function CopyTextField({value, fullWidth, label, sx}: PHATextFieldProps) { const inputRef = React.useRef(null); const copyToClipboard = () => { @@ -27,11 +28,12 @@ export default function CopyTextField({value, fullWidth, label}: PHATextFieldPro } return ( - , - 'Addresses': , - 'Wallet': -}; +const iconMapping = { + 'Mint Actions': , + 'Addresses': , + 'Wallet': , + 'Register Asset': +}; -export default function NavDrawer() { - const { currentUser, selectTab, selectedTab } = useStore(); +export default function NavDrawer() { + const { currentUser, selectTab, selectedTab, accounts } = useStore(); // Define list items based on the current user - const listItems: MenuTab[] = currentUser === 'Mint Authority' ? - ['Mint Actions', 'Addresses'] : - ['Wallet']; + const connectedWalletTabs: MenuTab[] = ['Wallet', 'Register Asset']; + if (accounts.walletUser.hasRegisteredScripts) { + connectedWalletTabs.push('Mint Actions', 'Addresses'); + } + + const listItems: MenuTab[] = + currentUser === 'Mint Authority' + ? ['Mint Actions', 'Addresses'] + : currentUser === 'Connected Wallet' + ? connectedWalletTabs + : currentUser === 'Not Connected' + ? [] + : ['Wallet']; const handleListItemClick = (item: MenuTab) => { selectTab(item); diff --git a/frontend/src/app/components/ProfileSwitcher.tsx b/frontend/src/app/components/ProfileSwitcher.tsx index 614a9967..c14829f9 100644 --- a/frontend/src/app/components/ProfileSwitcher.tsx +++ b/frontend/src/app/components/ProfileSwitcher.tsx @@ -1,97 +1,120 @@ - -'use client' -//React Imports -import * as React from 'react'; - -//Next.js Imports -import { useRouter } from 'next/navigation'; - -//MUI Imports -import Chip from '@mui/material/Chip'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; - -//Local Imports -import useStore from '../store/store'; -import { UserName } from '../store/types'; -import { selectLucidWallet, getWalletBalance } from '../utils/walletUtils'; -import DemoEnvironmentContext from '../context/demoEnvironmentContext'; - -export default function ProfileSwitcher() { - const [anchorEl, setAnchorEl] = React.useState(null); - const { currentUser, accounts, changeWalletAccountDetails } = useStore(); - const lucid = useStore(state => state.lucid); - const changeUserAccount = useStore(state => state.changeUserAccount); - const router = useRouter(); - const demoContext = React.useContext(DemoEnvironmentContext); - - React.useEffect(() => { - // Check the current path and redirect if the currentUser doesn't match - const expectedPath = - currentUser === 'Mint Authority' - ? '/mint-authority' - : `/${currentUser.toLowerCase().replace(/\s+/g, '-')}`; - const currentPath = window.location.pathname; - - if (currentPath !== expectedPath) { - router.push(expectedPath); - } - }, [currentUser, router]); - - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget as HTMLElement); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - const handleSelect = (user: UserName) => { - changeUserAccount(user); - - // Determine the URL - const newUrl = - user === 'Mint Authority' - ? '/mint-authority' - : `/${user.toLowerCase().replace(/\s+/g, '-')}`; - - router.push(newUrl); - handleClose(); - }; - - - const handleWalletConnect = async (user: UserName) => { - await selectLucidWallet(lucid, "Lace"); - const userAddress = await lucid.wallet().address(); - const userBalance = await getWalletBalance(demoContext, userAddress); - changeWalletAccountDetails('walletUser', { - ...accounts.walletUser, - regular_address: userAddress, - balance: userBalance, - }); - handleSelect(user); - }; - - return ( - <> - } - onDelete={handleClick} - /> - - handleSelect('Mint Authority')}>Mint Authority - handleSelect('Alice')}>Alice - handleSelect('Bob')}>Bob - handleWalletConnect('Connected Wallet')}>Lace - - - ); -} + +'use client' +//React Imports +import * as React from 'react'; + +//Next.js Imports +import { usePathname, useRouter } from 'next/navigation'; + +//MUI Imports +import Button from '@mui/material/Button'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; + +//Local Imports +import useStore from '../store/store'; +import { selectLucidWallet, getWalletBalance, getProgrammableTokenAddress, areStakeScriptsRegistered, WalletType } from '../utils/walletUtils'; +import DemoEnvironmentContext from '../context/demoEnvironmentContext'; +import type { UserName } from '../store/types'; + +export default function ProfileSwitcher() { + const [anchorEl, setAnchorEl] = React.useState(null); + const currentUser = useStore((state) => state.currentUser); + const accounts = useStore((state) => state.accounts); + const changeWalletAccountDetails = useStore((state) => state.changeWalletAccountDetails); + const changeAlertInfo = useStore((state) => state.changeAlertInfo); + const changeUserAccount = useStore((state) => state.changeUserAccount); + const lucid = useStore((state) => state.lucid); + const hasHydrated = useStore((state) => state.hasHydrated); + const router = useRouter(); + const pathname = usePathname(); + const demoContext = React.useContext(DemoEnvironmentContext); + + const getRouteForUser = React.useCallback((user: UserName) => { + if (user === 'Not Connected') { + return '/connected-wallet'; + } + if (user === 'Mint Authority') { + return '/connected-wallet'; + } + return `/${user.toLowerCase().replace(/\s+/g, '-')}`; + }, []); + + React.useEffect(() => { + if (!hasHydrated) { + return; + } + const expectedPath = getRouteForUser(currentUser); + if (pathname !== expectedPath) { + router.replace(expectedPath); + } + }, [currentUser, router, pathname, getRouteForUser, hasHydrated]); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget as HTMLElement); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleWalletConnect = async (walletType: WalletType) => { + try { + const cardanoApi = typeof window !== 'undefined' ? (window as any)?.cardano : undefined; + if (!lucid || !cardanoApi || !cardanoApi[walletType.toLowerCase()]) { + throw new Error(`${walletType} wallet is not available in this browser.`); + } + await selectLucidWallet(lucid, walletType); + const userAddress = await lucid.wallet().address(); + const [userBalance, programmableAddress, hasRegisteredScripts] = await Promise.all([ + getWalletBalance(demoContext, userAddress), + getProgrammableTokenAddress(userAddress), + areStakeScriptsRegistered(demoContext, lucid, userAddress), + ]); + changeWalletAccountDetails('walletUser', { + ...accounts.walletUser, + regular_address: userAddress, + programmable_token_address: programmableAddress, + balance: userBalance, + hasRegisteredScripts, + }); + changeUserAccount('Connected Wallet'); + router.push('/connected-wallet'); + } catch (error) { + console.error(`Failed to connect ${walletType}`, error); + changeAlertInfo({ + severity: 'error', + message: `Unable to connect to ${walletType}. Please ensure the wallet extension is installed and unlocked.`, + open: true, + link: '', + }); + } finally { + handleClose(); + } + }; + + const buttonLabel = currentUser === 'Connected Wallet' ? 'Connected Wallet' : 'Connect Wallet'; + + return ( + <> + + + handleWalletConnect('Lace')}>Lace + handleWalletConnect('Eternl')}>Eternl + handleWalletConnect('Yoroi')}>Yoroi + + + ); +} diff --git a/frontend/src/app/components/WSTAppBar.tsx b/frontend/src/app/components/WSTAppBar.tsx index 4f4f6e43..2ee44efc 100644 --- a/frontend/src/app/components/WSTAppBar.tsx +++ b/frontend/src/app/components/WSTAppBar.tsx @@ -21,7 +21,7 @@ export default function WSTAppBar() { height={39} alt="Logo for WST" /> - CIP-0143 Progammable Token Demo + CIP-0143 Programmable Token Demo diff --git a/frontend/src/app/components/WSTCommonButton.tsx b/frontend/src/app/components/WSTCommonButton.tsx index e9f8a8f5..73675349 100644 --- a/frontend/src/app/components/WSTCommonButton.tsx +++ b/frontend/src/app/components/WSTCommonButton.tsx @@ -1,18 +1,40 @@ -//Mui imports -import Button, { ButtonProps } from '@mui/material/Button'; - -interface PHAButtonProps { - disabled?: boolean; - size?: ButtonProps['size']; - fullWidth?: boolean; - startIcon?: React.ReactNode; - variant?: ButtonProps['variant']; - text?: string; - onClick?: React.MouseEventHandler; - } - - export default function WSTCommonButton({ disabled, size='medium', fullWidth, startIcon, variant='contained', text, onClick }: PHAButtonProps) { - return ( - - ); - } \ No newline at end of file +//Mui imports +import Button, { ButtonProps } from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; + +interface PHAButtonProps { + disabled?: boolean; + size?: ButtonProps['size']; + fullWidth?: boolean; + startIcon?: React.ReactNode; + variant?: ButtonProps['variant']; + text?: string; + onClick?: React.MouseEventHandler; + loading?: boolean; + } + +export default function WSTCommonButton({ + disabled, + size = 'medium', + fullWidth, + startIcon, + variant = 'contained', + text, + onClick, + loading = false, +}: PHAButtonProps) { + return ( + + ); +} diff --git a/frontend/src/app/components/WSTTable.tsx b/frontend/src/app/components/WSTTable.tsx index 86344c71..71a46c94 100644 --- a/frontend/src/app/components/WSTTable.tsx +++ b/frontend/src/app/components/WSTTable.tsx @@ -1,55 +1,50 @@ -'use client' -//Lucid imports -import type { Address, Credential as LucidCredential, Unit, UTxO } from "@lucid-evolution/core-types"; -import { toUnit } from "@lucid-evolution/lucid"; - -//Mui imports -import { Box } from "@mui/material"; -import TableContainer from "@mui/material/TableContainer"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -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 { useContext, useEffect } from "react"; -import IconButton from './WSTIconButton'; -import DemoEnvironmentContext from "../context/demoEnvironmentContext"; - - - -export default function WSTTable() { - const { lucid, accounts } = useStore(); - const accountArray = Object.values(accounts); - const demoEnv = useContext(DemoEnvironmentContext); - const stableCoin : Unit = toUnit(demoEnv.minting_policy, demoEnv.token_name); - - const progLogicBase : LucidCredential = { - type: "Script", - hash: demoEnv.prog_logic_base_hash - } - - const getAccounts = async () => { - const progUTxOs : UTxO[] = await lucid.utxosAtWithUnit(progLogicBase, stableCoin); - const addresses = new Set(); - const valueMap = new Map(); - progUTxOs.forEach(utxo => { - addresses.add(utxo.address) - valueMap.set(utxo.address, Number(utxo.assets[stableCoin])) - }); - } - - useEffect(() => { - getAccounts(); - }, []); - - const copyToClipboard = (str: string) => { - navigator.clipboard.writeText(str); - } +'use client' + +import { Box, CircularProgress, Typography } from "@mui/material"; +import TableContainer from "@mui/material/TableContainer"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +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'; + +import IconButton from './WSTIconButton'; + +export type WSTTableRow = { + regularAddress: string; + programmableAddress?: string; + status?: string; + balanceText?: string; + assets?: Array<{ unit: string; quantity: string; assetName?: string }>; +}; + +interface WSTTableProps { + rows: WSTTableRow[]; + loading?: boolean; + emptyMessage?: string; +} + +const formatAddress = (address?: string) => { + if (!address) return '—'; + if (address.length <= 20) return address; + return `${address.slice(0, 15)}...${address.slice(-4)}`; +}; + +const formatAssets = (assets?: Array<{ unit: string; quantity: string; assetName?: string }>) => { + if (!assets || assets.length === 0) { + return null; + } + return assets + .map(({ assetName, unit, quantity }) => `${assetName ?? unit}: ${quantity}`) + .join(', '); +}; + +export default function WSTTable({ rows, loading = false, emptyMessage = 'No addresses to display.' }: WSTTableProps) { + const copyToClipboard = (str: string) => { + navigator.clipboard.writeText(str); + }; return ( @@ -64,40 +59,50 @@ export default function WSTTable() { - { - accountArray.filter((acct) => acct.regular_address !== "").map((acct, i) => ( - - - {`${acct?.regular_address.slice(0,15)}...${acct?.regular_address.slice(104,108)}`} - copyToClipboard(acct.regular_address)} icon={}/> - - - {`${acct?.programmable_token_address.slice(0,15)}...${acct?.programmable_token_address.slice(104,108)}`} - copyToClipboard(acct.programmable_token_address)} icon={}/> - - - {acct.status} - - - {`${acct?.balance.wst} WST`} - - - )) - } - {/* {[...uniqueAddresses].map((address, index) => ( - - {`${address.slice(0,15)}...${address.slice(104,108)}`} - - Active - - - {`${balanceMap.get(address)} WST`} - - - ))} */} - - - + {loading ? ( + + + + + + ) : rows.length === 0 ? ( + + + + {emptyMessage} + + + + ) : ( + rows.map((row, i) => { + const balanceDisplay = formatAssets(row.assets) ?? row.balanceText ?? '—'; + return ( + + + {formatAddress(row.regularAddress)} + {row.regularAddress && ( + copyToClipboard(row.regularAddress)} icon={} /> + )} + + + {formatAddress(row.programmableAddress)} + {row.programmableAddress && ( + copyToClipboard(row.programmableAddress!)} icon={} /> + )} + + + {row.status ?? 'Active'} + + + {balanceDisplay} + + + ); + }) + )} + + + ); } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 0331dbe0..e336a318 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,32 +1,36 @@ //NextJS Imports -import type { Metadata } from "next"; - -//Local file -import "./styles/globals.css"; -import ClientLayout from "./clientLayout"; - -export const metadata: Metadata = { - title: "WST - Programmable token demonstration", - description: "Created by the djed team at IOG", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - - - - - - - {children} - - - - ); -} +import type { Metadata } from "next"; + +//Local files +import "./styles/globals.css"; +import ClientLayout from "./clientLayout"; +import { buildInitialWalletSnapshot, loadDemoEnvironment } from "./lib/demoEnvironment.server"; + +export const metadata: Metadata = { + title: "WST - Programmable token demonstration", + description: "Created by the djed team at IOG", +}; + +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const { environment } = await loadDemoEnvironment(); + const initialWallets = await buildInitialWalletSnapshot(environment); + + return ( + + + + + + + + + {children} + + + + ); +} diff --git a/frontend/src/app/lib/demoEnvironment.server.ts b/frontend/src/app/lib/demoEnvironment.server.ts new file mode 100644 index 00000000..c2b0da50 --- /dev/null +++ b/frontend/src/app/lib/demoEnvironment.server.ts @@ -0,0 +1,99 @@ +import "server-only"; + +import { walletFromSeed } from "@lucid-evolution/lucid"; + +import type { DerivedAccountSnapshot, InitialWalletSnapshot, LoadedDemoEnvironment } from "./initialData"; +import { DemoEnvironment, previewEnv } from "../store/types"; + +let cachedDemoEnvironment: DemoEnvironment | null = null; + +const resolveBaseUrl = () => { + const raw = + process.env.API_BASE_URL ?? + process.env.NEXT_PUBLIC_API_BASE_URL ?? + process.env.NEXT_PUBLIC_BACKEND_BASE_URL ?? + process.env.NEXT_PUBLIC_SITE_URL ?? + process.env.SITE_URL ?? + (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : undefined); + + if (raw && /^https?:\/\//i.test(raw)) { + return raw; + } + if (raw && !/^https?:\/\//i.test(raw)) { + return `https://${raw}`; + } + const port = process.env.PORT ?? '3000'; + return `http://localhost:${port}`; +}; + +const BASE_URL = resolveBaseUrl(); + +const buildUrl = (path: string) => { + try { + return new URL(path, BASE_URL).toString(); + } catch (error) { + console.warn(`Unable to resolve URL for ${path}, falling back to relative path.`, error); + return path; + } +}; + +async function fetchJson(path: string): Promise { + const response = await fetch(buildUrl(path), { + cache: "no-store", + }); + if (!response.ok) { + throw new Error(`Failed to fetch ${path}: ${response.status} ${response.statusText}`); + } + return response.json() as Promise; +} + +export async function loadDemoEnvironment(): Promise { + try { + const environment = await fetchJson("/api/v1/demo-environment"); + cachedDemoEnvironment = environment; + return { environment, wasFallback: false }; + } catch (error) { + console.warn("Falling back to cached demo environment", error); + if (cachedDemoEnvironment) { + return { environment: cachedDemoEnvironment, wasFallback: true }; + } + return { environment: previewEnv, wasFallback: true }; + } +} + +async function fetchProgrammableTokenAddress(address: string): Promise { + try { + return await fetchJson(`/api/v1/query/address/${address}`); + } catch (error) { + console.warn("Failed to fetch programmable token address", address, error); + return ""; + } +} + +async function deriveWalletSnapshot(env: DemoEnvironment, mnemonic: string): Promise { + const wallet = await walletFromSeed(mnemonic, { + password: "", + addressType: "Base", + accountIndex: 0, + network: env.network, + }); + const programmableTokenAddress = await fetchProgrammableTokenAddress(wallet.address); + return { + regularAddress: wallet.address, + programmableTokenAddress, + }; +} + +export async function buildInitialWalletSnapshot(env: DemoEnvironment): Promise { + const [mintAuthority, alice, bob] = await Promise.all([ + deriveWalletSnapshot(env, env.mint_authority), + deriveWalletSnapshot(env, env.user_a), + deriveWalletSnapshot(env, env.user_b), + ]); + + return { + mintAuthority, + alice, + bob, + }; +} diff --git a/frontend/src/app/lib/initialData.ts b/frontend/src/app/lib/initialData.ts new file mode 100644 index 00000000..1c7f5e68 --- /dev/null +++ b/frontend/src/app/lib/initialData.ts @@ -0,0 +1,17 @@ +import type { DemoEnvironment } from "../store/types"; + +export type DerivedAccountSnapshot = { + regularAddress: string; + programmableTokenAddress: string; +}; + +export type InitialWalletSnapshot = { + mintAuthority: DerivedAccountSnapshot; + alice: DerivedAccountSnapshot; + bob: DerivedAccountSnapshot; +}; + +export type LoadedDemoEnvironment = { + environment: DemoEnvironment; + wasFallback: boolean; +}; diff --git a/frontend/src/app/mint-authority/page.tsx b/frontend/src/app/mint-authority/page.tsx index f846c9dc..0c9ee244 100644 --- a/frontend/src/app/mint-authority/page.tsx +++ b/frontend/src/app/mint-authority/page.tsx @@ -1,453 +1,5 @@ -'use client'; -//React imports -import React, { useContext, useEffect, useState } from 'react'; - -//Axios imports -import axios from 'axios'; - -//Lucid imports -import { paymentCredentialOf } from '@lucid-evolution/lucid'; -import type { Credential as LucidCredential } from "@lucid-evolution/core-types"; - -//Mui imports -import { Box, Typography } from '@mui/material'; -import Checkbox from '@mui/material/Checkbox'; -import FormControlLabel from '@mui/material/FormControlLabel'; - -//Local components -import useStore from '../store/store'; -import { Accounts } from '../store/types'; -import WalletCard from '../components/Card'; -import WSTTextField from '../components/WSTTextField'; -import CopyTextField from '../components/CopyTextField'; -import WSTTable from '../components/WSTTable'; -import { getWalletBalance, signAndSentTx, getBlacklist } from '../utils/walletUtils'; -import DemoEnvironmentContext from '../context/demoEnvironmentContext'; - - - -export default function Home() { - const { lucid, mintAccount, accounts, selectedTab, changeAlertInfo, changeMintAccountDetails, changeWalletAccountDetails } = useStore(); - const [addressCleared, setAddressCleared] = useState(false); - // Temporary state for each text field - const [mintTokensAmount, setMintTokens] = useState(0); - const [sendTokensAmount, setSendTokens] = useState(0); - const [mintRecipientAddress, setMintRecipientAddress] = useState('mint recipient address'); - const [sendRecipientAddress, setsendRecipientAddress] = useState('send recipient address'); - 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('address to seize'); - const [seizeReason, setSeizeReason] = useState('Enter reason here'); - - const demoEnv = useContext(DemoEnvironmentContext); - - useEffect(() => { - const initialize = async () => { - await fetchUserDetails(); - await fetchBlacklistStatus(); - }; - initialize(); - }, [demoEnv]); - - const fetchUserDetails = async () => { - const mintBalance = await getWalletBalance(demoEnv, mintAccount.regular_address); - const userABalance = await getWalletBalance(demoEnv, accounts.alice.regular_address); - const userBBalance = await getWalletBalance(demoEnv, accounts.bob.regular_address); - - // Update Zustand store with the initialized wallet information - await changeMintAccountDetails({ ...mintAccount, balance: mintBalance}); - await changeWalletAccountDetails('alice', { ...accounts.alice, balance: userABalance}); - await changeWalletAccountDetails('bob', { ...accounts.bob, balance: userBBalance}); - }; - - const fetchBlacklistStatus = async () => { - const blacklist = await getBlacklist(demoEnv); - const { accounts, changeWalletAccountDetails } = useStore.getState(); - - Object.entries(accounts).map(async ([key, account]) => { - if (!account.regular_address || account.regular_address.trim() === "") { - // console.log(`${key} has no address yet, skipping`); - return; - } - const credential : LucidCredential = await paymentCredentialOf(account.regular_address); - if(blacklist.includes(credential.hash)) { - // console.log('a match was found', key as keyof typeof accounts); - changeWalletAccountDetails(key as keyof typeof accounts, { ...account, status: 'Frozen',}); - } - }); - }; - - const handleAddressClearedChange = (event: { target: { checked: boolean | ((prevState: boolean) => boolean); }; }) => { - setAddressCleared(event.target.checked); - }; - - const onMint = async () => { - if (addressCleared === false) { - // setAlertStatus(true); - console.error("Recipient Address not cleared."); - return; - } - changeAlertInfo({severity: 'info', message: 'Processing Mint Request', open: true, link: ''}); - lucid.selectWallet.fromSeed(demoEnv.mint_authority); - const requestData = { - asset_name: Buffer.from('WST', 'utf8').toString('hex'), // Convert "WST" to hex - issuer: mintAccount.regular_address, - quantity: mintTokensAmount, - recipient: mintRecipientAddress - }; - - try { - const response = await axios.post( - '/api/v1/tx/programmable-token/issue', - requestData, - { - headers: { - 'Content-Type': 'application/json;charset=utf-8', - }, - } - ); - console.log('Mint response:', response.data); - const tx = await lucid.fromTx(response.data.cborHex); - const txId = await signAndSentTx(lucid, tx); - - changeAlertInfo({severity: 'success', message: 'Successful new WST mint. View the transaction here:', open: true, link: `${demoEnv.explorer_url}/${txId}`}); - - await fetchUserDetails(); - } catch (error) { - console.error('Minting failed:', error); - } - }; - - const onSend = async () => { - lucid.selectWallet.fromSeed(demoEnv.mint_authority); - console.log('send tokens'); - changeAlertInfo({severity: 'info', message: 'Transaction processing', open: true, link: ''}); - const requestData = { - asset_name: Buffer.from('WST', 'utf8').toString('hex'), // Convert "WST" to hex - issuer: mintAccount.regular_address, - quantity: sendTokensAmount, - recipient: sendRecipientAddress, - sender: mintAccount.regular_address, - }; - try { - const response = await axios.post( - '/api/v1/tx/programmable-token/transfer', - requestData, - { - headers: { - 'Content-Type': 'application/json;charset=utf-8', - }, - } - ); - console.log('Send response:', response.data); - const tx = await lucid.fromTx(response.data.cborHex); - const txId = await signAndSentTx(lucid, tx); - const newAccountBalance = await getWalletBalance(demoEnv, sendRecipientAddress); - const recipientWalletKey = (Object.keys(accounts) as (keyof Accounts)[]).find( - (key) => accounts[key].regular_address === sendRecipientAddress - ); - if (recipientWalletKey) { - changeWalletAccountDetails(recipientWalletKey, { - ...accounts[recipientWalletKey], - balance: newAccountBalance, - }); - } - changeAlertInfo({severity: 'success', message: 'Transaction sent successfully!', open: true, link: `${demoEnv.explorer_url}/${txId}`}); - await fetchUserDetails(); - } catch (error) { - console.error('Send failed:', error); - } - }; - - const onFreeze = async () => { - console.log('freeze an address'); - lucid.selectWallet.fromSeed(demoEnv.mint_authority); - changeAlertInfo({severity: 'info', message: 'Freeze request processing', open: true, link: ''}); - const requestData = { - issuer: mintAccount.regular_address, - blacklist_address: freezeAccountNumber, - reason: freezeReason, - }; - try { - const response = await axios.post( - '/api/v1/tx/programmable-token/blacklist', - requestData, - { - headers: { - 'Content-Type': 'application/json;charset=utf-8', - }, - } - ); - console.log('Freeze response:', response.data); - const tx = await lucid.fromTx(response.data.cborHex); - const txId = await signAndSentTx(lucid, tx); - console.log(txId); - changeAlertInfo({severity: 'success', message: 'Address successfully frozen', open: true, link: `${demoEnv.explorer_url}/${txId}`}); - const frozenWalletKey = (Object.keys(accounts) as (keyof Accounts)[]).find( - (key) => accounts[key].regular_address === freezeAccountNumber - ); - if (frozenWalletKey) { - changeWalletAccountDetails(frozenWalletKey, { - ...accounts[frozenWalletKey], - status: 'Frozen', - }); - } - } catch (error: any) { - if (error.response.data.includes('DuplicateBlacklistNode')) { - changeAlertInfo({ - severity: 'error', - message: 'This account is already frozen.', - open: true, - }); - return; - } else { - console.error('Freeze failed:', error); - } - } - }; - - const onUnfreeze = async () => { - console.log('unfreeze an account'); - lucid.selectWallet.fromSeed(demoEnv.mint_authority); - changeAlertInfo({severity: 'info', message: 'Unfreeze request processing', open: true, link: ''}); - const requestData = { - issuer: mintAccount.regular_address, - blacklist_address: unfreezeAccountNumber, - reason: "(unfreeze)" - }; - 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); - const txId = await signAndSentTx(lucid, tx); - changeAlertInfo({severity: 'success', message: 'Address successfully unfrozen', open: true, link: `${demoEnv.explorer_url}/${txId}`}); - const unfrozenWalletKey = (Object.keys(accounts) as (keyof Accounts)[]).find( - (key) => accounts[key].regular_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(demoEnv.mint_authority); - changeAlertInfo({severity: 'info', message: 'WST seizure processing', open: true, link: ''}); - const requestData = { - issuer: mintAccount.regular_address, - target: seizeAccountNumber, - reason: seizeReason, - }; - try { - const response = await axios.post( - '/api/v1/tx/programmable-token/seize', - requestData, - { - headers: { - 'Content-Type': 'application/json;charset=utf-8', - }, - } - ); - console.log('Seize response:', response.data); - const tx = await lucid.fromTx(response.data.cborHex); - const txId = await signAndSentTx(lucid, tx); - const newAccountBalance = await getWalletBalance(demoEnv, seizeAccountNumber); - const seizeWalletKey = (Object.keys(accounts) as (keyof Accounts)[]).find( - (key) => accounts[key].regular_address === seizeAccountNumber - ); - if (seizeWalletKey) { - changeWalletAccountDetails(seizeWalletKey, { - ...accounts[seizeWalletKey], - balance: newAccountBalance, - }); - } - changeAlertInfo({severity: 'success', message: 'Funds successfully seized', open: true, link: `${demoEnv.explorer_url}/${txId}`}); - await fetchUserDetails(); - } catch (error) { - console.error('Seize failed:', error); - } - }; - - const mintContent = - setMintTokens(Number(e.target.value))} - label="Number of Tokens to Mint" - fullWidth={true} - /> - setMintRecipientAddress(e.target.value)} - label="Mint Recipient’s Address" - fullWidth={true} - /> - } - label="Address has been cleared" - sx={{ mb: 2 }} - /> - ; - - const freezeContent = - setFreezeAccountNumber(e.target.value)} - label="Address" - fullWidth={true} - /> - setFreezeReason(e.target.value)} - label="Reason" - fullWidth={true} - multiline={true} - minRows={2} - maxRows={3} - /> - - - const unFreezeContent = - setUnfreezeAccountNumber(e.target.value)} - label="Address" - fullWidth={true} - /> - - -const seizeContent = - setSeizeAccountNumber(e.target.value)} -label="Address" -fullWidth={true} -/> - setSeizeReason(e.target.value)} -label="Reason" -fullWidth={true} -multiline={true} -minRows={2} -maxRows={3} -/> - - - const sendContent = - setSendTokens(Number(e.target.value))} - label="Number of Tokens to Send" - fullWidth={true} - /> - setsendRecipientAddress(e.target.value)} - label="Recipient’s Address" - fullWidth={true} - /> - ; - - const receiveContent = - - ; - - const getContentComponent = () => { - switch (selectedTab) { - case 'Mint Actions': - return <> - - - Mint Authority Balance - {mintAccount.balance.wst} WST - {mintAccount.balance.ada} Ada { (mintAccount.balance.adaOnlyOutputs === 0) && (({mintAccount.balance.adaOnlyOutputs} collateral UTxOs))} - - UserID: {mintAccount.regular_address.slice(0,15)} - -
- - -
- ; - case 'Addresses': - return <> - - Addresses - - - ; - } - }; - - return ( -
- {getContentComponent()} -
- ); -} - - +import { redirect } from 'next/navigation'; + +export default function DeprecatedMintAuthorityPage() { + redirect('/connected-wallet'); +} diff --git a/frontend/src/app/store/store.tsx b/frontend/src/app/store/store.tsx index 2a1d035d..11b39bea 100644 --- a/frontend/src/app/store/store.tsx +++ b/frontend/src/app/store/store.tsx @@ -1,119 +1,220 @@ -//Zustand Imports -import { create } from "zustand"; - -//Local Imports -import { UserName, AccountInfo, Accounts, MenuTab, AccountKey, AlertInfo } from "./types"; -import { LucidEvolution } from "@lucid-evolution/lucid"; - -export type State = { - mintAccount: AccountInfo; - accounts: Accounts; - blacklistAddresses: string[]; - currentUser: UserName; - selectedTab: MenuTab; - alertInfo: AlertInfo; - lucid: LucidEvolution; -}; - -export type Actions = { - changeMintAccountDetails: (newAccountInfo: AccountInfo) => void; - changeWalletAccountDetails: (accountKey: AccountKey, newAccountInfo: AccountInfo) => void; - changeUserAccount: (newUser: UserName) => void; - selectTab: (tab: MenuTab) => void; - changeAlertInfo: (alertInfo: AlertInfo) => void; - setLucidInstance: (lucid: LucidEvolution) => void; -}; - -const useStore = create((set) => ({ - mintAccount: { - name: 'Mint Authority', - regular_address: '', - programmable_token_address: '', - balance: {ada: 0, wst: 0, adaOnlyOutputs: 0}, - }, - accounts: { - alice: { - regular_address: '', - programmable_token_address: '', - balance: {ada: 0, wst: 0, adaOnlyOutputs: 0}, - status: 'Active', - }, - bob: { - regular_address: '', - programmable_token_address: '', - balance: {ada: 0, wst: 0, adaOnlyOutputs: 0}, - status: 'Active', - }, - walletUser: { - regular_address: '', - programmable_token_address: '', - balance: {ada: 0, wst: 0, adaOnlyOutputs: 0}, - status: 'Active', - }, - }, - blacklistAddresses: [], - currentUser: 'Mint Authority', - selectedTab: 'Mint Actions', - alertInfo: { - open: false, - message: 'Transaction sent successfully!', - severity: 'success', - }, - lucid: {} as LucidEvolution, - - changeMintAccountDetails: (newAccountInfo: AccountInfo) => { - set(() => { - return { mintAccount: newAccountInfo }; - }); - }, - - changeWalletAccountDetails: (accountKey, newAccountInfo) => { - set((state) => ({ - accounts: { - ...state.accounts, - [accountKey]: newAccountInfo, - }, - })); - }, - - changeUserAccount: (newUser: UserName) => { - let firstAccessibleTab: MenuTab; - switch (newUser) { - case 'Mint Authority': - firstAccessibleTab = 'Mint Actions'; - break; - case 'Alice': - case 'Bob': - firstAccessibleTab = 'Wallet'; - break; - case 'Connected Wallet': - if (useStore.getState().accounts.walletUser.regular_address === useStore.getState().mintAccount.regular_address) - firstAccessibleTab = 'Mint Actions'; - else - firstAccessibleTab = 'Wallet'; - break - default: - firstAccessibleTab = 'Mint Actions'; - } - set({ currentUser: newUser, selectedTab: firstAccessibleTab }); - }, - - selectTab: (tab: MenuTab) => { - set({ selectedTab: tab }); - }, - - changeAlertInfo: (partial: Partial) => { - set((state) => ({ - alertInfo: { - ...state.alertInfo, - ...partial, - }, - })); - }, - - setLucidInstance: (lucid) => { - set({ lucid: lucid }); - } -})); - -export default useStore; +'use client'; + +// Zustand Imports +import { createWithEqualityFn } from 'zustand/traditional'; +import { persist } from 'zustand/middleware'; +import { shallow } from 'zustand/shallow'; +import type { StateCreator } from 'zustand'; + +// Local Imports +import { UserName, AccountInfo, Accounts, MenuTab, AccountKey, AlertInfo } from "./types"; +import { LucidEvolution } from "@lucid-evolution/lucid"; + +type AccountsSlice = { + mintAccount: AccountInfo; + accounts: Accounts; + blacklistAddresses: string[]; + changeMintAccountDetails: (newAccountInfo: AccountInfo) => void; + changeWalletAccountDetails: (accountKey: AccountKey, newAccountInfo: AccountInfo) => void; +}; + +type UiSlice = { + currentUser: UserName; + selectedTab: MenuTab | null; + alertInfo: AlertInfo; + hasHydrated: boolean; + changeUserAccount: (newUser: UserName) => void; + selectTab: (tab: MenuTab) => void; + changeAlertInfo: (partial: Partial) => void; + setHasHydrated: (value: boolean) => void; +}; + +type LucidSlice = { + lucid: LucidEvolution; + setLucidInstance: (lucid: LucidEvolution) => void; +}; + +export type StoreState = AccountsSlice & UiSlice & LucidSlice; + +const emptyAccount = (): AccountInfo => ({ + regular_address: '', + programmable_token_address: '', + balance: { ada: 0, wst: 0, adaOnlyOutputs: 0 }, + status: 'Active', + hasRegisteredScripts: false, +}); + +const initialAccounts = (): Accounts => ({ + alice: emptyAccount(), + bob: emptyAccount(), + walletUser: emptyAccount(), +}); + +const initialAlertInfo = (): AlertInfo => ({ + id: 0, + open: false, + message: 'Transaction sent successfully!', + severity: 'success', +}); + +const firstAccessibleTab = (user: UserName, walletUserAddress: string, mintAddress: string): MenuTab | null => { + switch (user) { + case 'Mint Authority': + return 'Mint Actions'; + case 'Alice': + case 'Bob': + return 'Wallet'; + case 'Connected Wallet': + return walletUserAddress !== '' && walletUserAddress === mintAddress ? 'Mint Actions' : 'Wallet'; + default: + return null; + } +}; + +const createAccountsSlice: StateCreator< + StoreState, + [], + [], + AccountsSlice +> = (set, _get, _api) => { + void _get; + void _api; + return { + mintAccount: { + name: 'Mint Authority', + regular_address: '', + programmable_token_address: '', + balance: { ada: 0, wst: 0, adaOnlyOutputs: 0 }, + }, + accounts: initialAccounts(), + blacklistAddresses: [], + + changeMintAccountDetails: (newAccountInfo) => { + set(() => ({ + mintAccount: newAccountInfo, + })); + }, + + changeWalletAccountDetails: (accountKey, newAccountInfo) => { + set((state) => ({ + accounts: { + ...state.accounts, + [accountKey]: newAccountInfo, + }, + })); + }, + }; +}; + +const createUiSlice: StateCreator< + StoreState, + [], + [], + UiSlice +> = (set, get, _api) => { + void _api; + return { + currentUser: 'Not Connected', + selectedTab: null, + alertInfo: initialAlertInfo(), + hasHydrated: false, + + changeUserAccount: (newUser) => { + const { accounts, mintAccount } = get(); + const tab = firstAccessibleTab(newUser, accounts.walletUser.regular_address, mintAccount.regular_address); + set({ currentUser: newUser, selectedTab: tab }); + }, + + selectTab: (tab) => { + set({ selectedTab: tab }); + }, + + changeAlertInfo: (partial) => { + set((state) => { + const next: AlertInfo = { + ...state.alertInfo, + ...partial, + }; + if (partial.open) { + next.id = Date.now(); + next.open = true; + } + if (partial.open === false) { + next.open = false; + } + return { alertInfo: next }; + }); + }, + + setHasHydrated: (value) => set({ hasHydrated: value }), + }; +}; + +const createLucidSlice: StateCreator< + StoreState, + [], + [], + LucidSlice +> = (set, _get, _api) => { + void _get; + void _api; + return { + lucid: {} as LucidEvolution, + setLucidInstance: (lucid) => set({ lucid }), + }; +}; + +const createStore = (set: any, get: any, api: any) => ({ + ...createAccountsSlice(set, get, api), + ...createUiSlice(set, get, api), + ...createLucidSlice(set, get, api), +}); + +const mergePersistedState = (persisted: any, currentState: StoreState): StoreState => { + if (!persisted) { + return currentState; + } + const persistedAccounts = persisted.accounts ?? {}; + return { + ...currentState, + ...persisted, + accounts: { + ...currentState.accounts, + ...persistedAccounts, + walletUser: { + ...currentState.accounts.walletUser, + ...persistedAccounts.walletUser, + }, + }, + }; +}; + +const useStoreBase = createWithEqualityFn()( + persist(createStore, { + name: 'wst-store', + version: 1, + partialize: (state) => ({ + currentUser: state.currentUser, + accounts: { + walletUser: state.accounts.walletUser, + }, + selectedTab: state.selectedTab, + }), + merge: (persistedState, currentState) => mergePersistedState(persistedState, currentState), + onRehydrateStorage: () => (state) => { + if (!state) { + return; + } + const walletConnected = Boolean(state.accounts.walletUser.regular_address); + if (!walletConnected) { + state.changeUserAccount('Not Connected'); + } + state.setHasHydrated(true); + }, + }), + shallow +); + +const useStore = useStoreBase; + +export default useStore; diff --git a/frontend/src/app/store/types.ts b/frontend/src/app/store/types.ts index 98637247..80667848 100644 --- a/frontend/src/app/store/types.ts +++ b/frontend/src/app/store/types.ts @@ -1,13 +1,14 @@ -export type UserName = 'Mint Authority' | 'Alice' | 'Bob' | 'Connected Wallet'; -export type MenuTab = 'Mint Actions' | 'Addresses' | 'Wallet'; +export type UserName = 'Mint Authority' | 'Alice' | 'Bob' | 'Connected Wallet' | 'Not Connected'; +export type MenuTab = 'Mint Actions' | 'Addresses' | 'Wallet' | 'Register Asset'; import { Network } from "@lucid-evolution/lucid"; -export type AccountInfo = { - regular_address: string, - programmable_token_address: string, - balance: WalletBalance, - status?: 'Active' | 'Frozen', -}; +export type AccountInfo = { + regular_address: string, + programmable_token_address: string, + balance: WalletBalance, + status?: 'Active' | 'Frozen', + hasRegisteredScripts?: boolean, +}; export type AccountKey = 'alice' | 'bob' | 'walletUser'; export type Accounts = { alice: AccountInfo; @@ -15,12 +16,14 @@ export type Accounts = { walletUser: AccountInfo; }; export type Severity = 'success' | 'error' | 'info' | 'warning'; -export type AlertInfo = { - open: boolean | undefined, - severity: Severity, - message: string, - link?: string, -}; +export type AlertInfo = { + id: number; + open: boolean; + severity: Severity; + message: string; + link?: string; + actionText?: string; +}; export type WalletBalance = { wst: number, ada: number, adaOnlyOutputs: number } // This should correspond to diff --git a/frontend/src/app/styles/globals.css b/frontend/src/app/styles/globals.css index 59e0b813..7b799893 100644 --- a/frontend/src/app/styles/globals.css +++ b/frontend/src/app/styles/globals.css @@ -33,17 +33,17 @@ a { gap: 32px; } -.cardWrapper { - background-color: white; - padding: 24px; - border-radius: 8px; - border: 1px solid #C9C6C6; - flex-basis: 50%; - flex-grow: 1; - height: 365px; - display: flex; - flex-direction: column; -} +.cardWrapper { + background-color: white; + padding: 24px; + border-radius: 8px; + border: 1px solid #C9C6C6; + flex-basis: 50%; + flex-grow: 1; + min-height: 365px; + display: flex; + flex-direction: column; +} .card { height: 100%; @@ -63,11 +63,11 @@ a { height: 100vh; } -.mainLoader { - border: .15em solid currentcolor; - border-radius: 50%; - animation: mainLoader 1.5s ease-out infinite; - display: inline-block; +.mainLoader { + border: .15em solid currentcolor; + border-radius: 50%; + animation: mainLoader 1.5s ease-out infinite; + display: inline-block; width: 1em; height: 1em; color: #3952CD; @@ -83,9 +83,36 @@ a { 50% { opacity: 1; } - 100% { - transform: scale(1); - opacity: 0; - } -} + 100% { + transform: scale(1); + opacity: 0; + } +} + +.statusScreen { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + width: 100%; + padding: 24px; +} + +.statusContent { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + text-align: center; + max-width: 360px; +} + +.statusContent .mainLoader { + margin-bottom: 8px; +} + +.statusError { + color: #B32A00; + font-size: 0.9rem; +} diff --git a/frontend/src/app/utils/walletUtils.ts b/frontend/src/app/utils/walletUtils.ts index 8a18c0b2..c37f4e1b 100644 --- a/frontend/src/app/utils/walletUtils.ts +++ b/frontend/src/app/utils/walletUtils.ts @@ -2,8 +2,8 @@ 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"; -import type { Credential as LucidCredential } from "@lucid-evolution/core-types"; +import { Address, Assets, Blockfrost, CML, credentialToAddress, credentialToRewardAddress, Lucid, LucidEvolution, makeTxSignBuilder, Network, paymentCredentialOf, scriptHashToCredential, toText, toUnit, TxSignBuilder, Unit, valueToAssets, walletFromSeed } from "@lucid-evolution/lucid"; +import type { Credential as LucidCredential, ScriptHash } from "@lucid-evolution/core-types"; import { WalletBalance, DemoEnvironment } from '../store/types'; export async function makeLucid(demoEnvironment: DemoEnvironment) { @@ -59,10 +59,10 @@ export async function getWalletBalance(demoEnv: DemoEnvironment, address: string } } -export async function getBlacklist(demoEnv: DemoEnvironment){ +export async function getBlacklist(address: string){ try { const response = await axios.get( - `/api/v1/query/blacklist/${demoEnv.transfer_logic_address}`, + `/api/v1/query/blacklist/${address}`, { headers: { 'Content-Type': 'application/json;charset=utf-8', @@ -70,7 +70,6 @@ export async function getBlacklist(demoEnv: DemoEnvironment){ } ); - // console.log('Get blacklist:', response); return response.data; } catch (error) { console.warn('Failed to get blacklist', error); @@ -78,6 +77,258 @@ export async function getBlacklist(demoEnv: DemoEnvironment){ } } +export async function getProgrammableTokenAddress(address: string): Promise { + const response = await axios.get(`/api/v1/query/address/${address}`, + { + headers: { + 'Content-Type': 'application/json;charset=utf-8', + }, + }); + return response?.data; +} + +export async function getFreezePolicyId(address: string): Promise { + const response = await axios.get(`/api/v1/query/freeze-policy-id/${address}`, + { + headers: { + 'Content-Type': 'application/json;charset=utf-8', + }, + }); + return response?.data; +} + +export async function getPolicyIssuer(policyId: string): Promise { + const response = await axios.get(`/api/v1/query/policy-issuer/${policyId}`, { + headers: { + 'Content-Type': 'application/json;charset=utf-8', + }, + }); + return response?.data; +} + +const getBlockfrostProjectId = (demoEnv: DemoEnvironment) => + process.env.NEXT_PUBLIC_BLOCKFROST_API_KEY ?? demoEnv.blockfrost_key; + +const trimTrailingSlash = (url: string) => url.replace(/\/+$/, ''); + +export async function getStakeScriptHashes(address: string): Promise { + try { + const response = await axios.get(`/api/v1/query/stake-scripts/${address}`, { + headers: { + 'Content-Type': 'application/json;charset=utf-8', + }, + }); + return response?.data ?? []; + } catch (error) { + console.warn('Failed to fetch stake script hashes', error); + return []; + } +} + +const buildAccountsEndpoint = (baseUrl: string, rewardAddress: string) => { + const trimmed = trimTrailingSlash(baseUrl); + return `${trimmed}/accounts/${rewardAddress}`; +}; + +const stakeScriptActive = async (demoEnv: DemoEnvironment, rewardAddress: string): Promise => { + try { + const response = await axios.get( + buildAccountsEndpoint(demoEnv.blockfrost_url, rewardAddress), + { + headers: { + project_id: getBlockfrostProjectId(demoEnv), + }, + } + ); + return response?.data?.active_epoch && response?.data?.active_epoch > 0; + } catch (error) { + console.warn(`Failed to check stake script status for ${rewardAddress}`, error); + return false; + } +}; + +export async function areStakeScriptsRegistered( + demoEnv: DemoEnvironment, + lucid: LucidEvolution, + issuerAddress: string +): Promise { + try { + if (!lucid || typeof lucid.config !== 'function') { + console.warn('Lucid is not initialised; cannot verify stake script registration.'); + return false; + } + const network = lucid.config().network ?? demoEnv.network; + const scriptHashes = await getStakeScriptHashes(issuerAddress); + if (scriptHashes.length === 0) { + return false; + } + const statuses = await Promise.all( + scriptHashes.map(async (hash) => { + const rewardAddress = deriveStakeAddressFromScriptHash(hash, lucid, network); + return stakeScriptActive(demoEnv, rewardAddress); + }) + ); + return statuses.every(Boolean); + } catch (error) { + console.warn('Failed to determine stake script registration status', error); + return false; + } +} + +export type PolicyHolder = { + address: string; + assets: Array<{ unit: string; quantity: string; assetName?: string }>; +}; + +export const decodeAssetName = (hexName?: string): string | undefined => { + if (!hexName) { + return undefined; + } + try { + return toText(hexName); + } catch (error) { + console.warn('Failed to decode asset name from hex', hexName, error); + return hexName; + } +}; + +type UserProgrammableValueResponse = Record>; + +const normaliseQuantity = (value: number | string | undefined): string => { + if (typeof value === 'number') { + return value.toString(); + } + if (typeof value === 'string' && value.trim().length > 0) { + return value; + } + return '0'; +}; + +const isAssetMap = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +export type PolicyTokenBalance = { + policyId: string; + tokens: Array<{ assetNameHex: string; displayName: string; quantity: string }>; +}; + +const parseProgrammableValue = (data: UserProgrammableValueResponse): PolicyTokenBalance[] => { + return Object.entries(data ?? {}) + .filter(([policyId, assets]) => policyId !== 'lovelace' && isAssetMap(assets)) + .map(([policyId, assets]) => { + const tokens = Object.entries(assets as Record).map(([assetHex, quantity]) => { + const displayName = + decodeAssetName(assetHex) ?? + (assetHex && assetHex.length > 0 ? assetHex : 'Unnamed asset'); + return { + assetNameHex: assetHex, + displayName, + quantity: normaliseQuantity(quantity), + }; + }); + return { policyId, tokens }; + }) + .filter((group) => group.tokens.length > 0); +}; + +export async function getUserTotalProgrammableValue(address: string): Promise { + if (!address) { + return []; + } + try { + const response = await axios.get( + `/api/v1/query/user-total-programmable-value/${address}`, + { + headers: { + 'Content-Type': 'application/json;charset=utf-8', + }, + } + ); + return parseProgrammableValue(response?.data ?? {}); + } catch (error) { + console.warn('Failed to fetch user programmable value', error); + throw error; + } +} + +const extractAssetNameHex = (assetUnit: string, policyId: string): string | undefined => { + if (!assetUnit?.startsWith(policyId)) { + return undefined; + } + return assetUnit.slice(policyId.length); +}; + +const fetchPolicyAssets = async (demoEnv: DemoEnvironment, policyId: string) => { + const projectId = getBlockfrostProjectId(demoEnv); + const baseUrl = trimTrailingSlash(demoEnv.blockfrost_url); + const assets: Array<{ asset: string; assetName?: string }> = []; + const pageSize = 100; + for (let page = 1; page <= 10; page++) { + try { + const { data } = await axios.get( + `${baseUrl}/assets/policy/${policyId}`, + { + params: { page, count: pageSize }, + headers: { project_id: projectId }, + } + ); + if (!Array.isArray(data) || data.length === 0) { + break; + } + assets.push( + ...data.map((entry: any) => ({ + asset: entry.asset, + assetName: + decodeAssetName(entry.asset_name) ?? + decodeAssetName(extractAssetNameHex(entry.asset, policyId)), + })) + ); + if (data.length < pageSize) { + break; + } + } catch (error) { + console.warn('Failed to fetch assets for policy', policyId, error); + break; + } + } + return assets; +}; + +export async function fetchPolicyHolders( + demoEnv: DemoEnvironment, + policyId: string +): Promise { + const projectId = getBlockfrostProjectId(demoEnv); + const baseUrl = trimTrailingSlash(demoEnv.blockfrost_url); + const assets = await fetchPolicyAssets(demoEnv, policyId); + const holders = new Map>(); + + await Promise.all( + assets.map(async ({ asset, assetName }) => { + try { + const { data } = await axios.get( + `${baseUrl}/assets/${asset}/addresses`, + { headers: { project_id: projectId } } + ); + if (Array.isArray(data)) { + data.forEach((entry: { address: string; quantity: string }) => { + const current = holders.get(entry.address) ?? []; + current.push({ unit: asset, quantity: entry.quantity, assetName }); + holders.set(entry.address, current); + }); + } + } catch (error) { + console.warn('Failed to fetch holders for asset', asset, error); + } + }) + ); + + return Array.from(holders.entries()).map(([address, assetsList]) => ({ + address, + assets: assetsList, + })); +} + export async function submitTx(tx: string): Promise> { return axios.post( '/api/v1/tx/submit', @@ -99,9 +350,16 @@ export async function signAndSentTx(lucid: LucidEvolution, tx: TxSignBuilder): P const txBuilder = await makeTxSignBuilder(lucid.wallet(), tx.toTransaction()).complete(); 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 redeemers = witnessSet.redeemers(); + const languages = witnessSet.languages(); + const expectedScriptDataHash : CML.ScriptDataHash | undefined = + redeemers && languages && languages.len() > 0 + ? CML.calc_script_data_hash(redeemers, CML.PlutusDataList.new(), lucid.config().costModels!, languages) + : undefined; const cmlTxBodyClone = CML.TransactionBody.from_cbor_hex(cmlTx!.body().to_cbor_hex()); - cmlTxBodyClone.set_script_data_hash(expectedScriptDataHash!); + if (expectedScriptDataHash) { + cmlTxBodyClone.set_script_data_hash(expectedScriptDataHash); + } const cmlClonedTx = CML.Transaction.new(cmlTxBodyClone, cmlTx!.witness_set(), isValid, cmlTx!.auxiliary_data()); const cmlClonedSignedTx = await makeTxSignBuilder(lucid.wallet(), cmlClonedTx).sign.withWallet().complete(); @@ -245,4 +503,11 @@ export async function deriveProgrammableAddress(demoEnv: DemoEnvironment, lucid: ); return userProgrammableTokenAddress; -} \ No newline at end of file +} + +export function deriveStakeAddressFromScriptHash(stakeScriptHash: ScriptHash, lucid: LucidEvolution, networkOverride?: Network): string{ + const network = networkOverride ?? lucid.config().network!; + // user's staking script credentials + const stakeCredential = scriptHashToCredential(stakeScriptHash); + return credentialToRewardAddress(network, stakeCredential); +} diff --git a/generated/openapi/schema.json b/generated/openapi/schema.json index ecf1d1f4..2ed26627 100644 --- a/generated/openapi/schema.json +++ b/generated/openapi/schema.json @@ -24,6 +24,17 @@ "Asset name": { "type": "string" }, + "BlacklistInitArgs": { + "properties": { + "issuer": { + "$ref": "#/components/schemas/Address" + } + }, + "required": [ + "issuer" + ], + "type": "object" + }, "BlacklistNodeArgs": { "properties": { "blacklist_address": { @@ -75,6 +86,39 @@ ], "type": "object" }, + "MultiSeizeAssetsArgs": { + "properties": { + "a_issuer": { + "$ref": "#/components/schemas/Address" + }, + "a_num_u_tx_os_to_seize": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "a_reason": { + "$ref": "#/components/schemas/SeizeReason" + }, + "a_target": { + "items": { + "$ref": "#/components/schemas/Address" + }, + "type": "array" + } + }, + "required": [ + "a_issuer", + "a_target", + "a_reason", + "a_num_u_tx_os_to_seize" + ], + "type": "object" + }, + "PolicyId": { + "description": "Policy ID", + "example": "01f4b788593d4f70de2a45c2e1e87088bfbdfa29577ae1b62aba60e095e3ab53", + "type": "string" + }, "ProgrammableLogicGlobalParams": { "description": "Global parameters of the programmable token directory", "properties": { @@ -99,6 +143,22 @@ "Quantity": { "type": "integer" }, + "RegisterTransferScriptsArgs": { + "properties": { + "issuer": { + "$ref": "#/components/schemas/Address" + } + }, + "required": [ + "issuer" + ], + "type": "object" + }, + "ScriptHash": { + "description": "Script hash", + "example": "01f4b788593d4f70de2a45c2e1e87088bfbdfa29577ae1b62aba60e095e3ab53", + "type": "string" + }, "SeizeAssetsArgs": { "properties": { "issuer": { @@ -370,6 +430,38 @@ } } }, + "/api/v1/query/freeze-policy-id/{address}": { + "get": { + "description": "The policy ID for the freeze and seize programmable token policy associated with this user", + "parameters": [ + { + "in": "path", + "name": "address", + "required": true, + "schema": { + "description": "bech32-serialised cardano address", + "example": "addr1q9d42egme33z960rr8vlnt69lpmythdpm7ydk2e6k5nj5ghay9rg60vw49kejfah76sqeh4yshlsntgg007y0wgjlfwju6eksr", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PolicyId" + } + } + }, + "description": "" + }, + "404": { + "description": "`address` not found" + } + } + } + }, "/api/v1/query/global-params": { "get": { "description": "The UTxO with the global parameters", @@ -387,6 +479,73 @@ } } }, + "/api/v1/query/policy-issuer/{policy_id}": { + "get": { + "description": "Issuer address associated with a freeze/seize policy id", + "parameters": [ + { + "in": "path", + "name": "policy_id", + "required": true, + "schema": { + "description": "hex-encoded policy identifier", + "example": "4cfd5e2b0c534b4e0cda0f5d84df7e0d3d3c6a74c0e5f3d823a58a38", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Address" + } + } + }, + "description": "" + }, + "404": { + "description": "`policy_id` not found" + } + } + } + }, + "/api/v1/query/stake-scripts/{address}": { + "get": { + "description": "The stake scripts for the programmable token", + "parameters": [ + { + "in": "path", + "name": "address", + "required": true, + "schema": { + "description": "bech32-serialised cardano address", + "example": "addr1q9d42egme33z960rr8vlnt69lpmythdpm7ydk2e6k5nj5ghay9rg60vw49kejfah76sqeh4yshlsntgg007y0wgjlfwju6eksr", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/ScriptHash" + }, + "type": "array" + } + } + }, + "description": "" + }, + "404": { + "description": "`address` not found" + } + } + } + }, "/api/v1/query/user-funds/{address}": { "get": { "description": "Total value locked in programmable token outputs addressed to the user", @@ -419,6 +578,38 @@ } } }, + "/api/v1/query/user-total-programmable-value/{address}": { + "get": { + "description": "Total value of all programmable tokens addressed to the user", + "parameters": [ + { + "in": "path", + "name": "address", + "required": true, + "schema": { + "description": "bech32-serialised cardano address", + "example": "addr1q9d42egme33z960rr8vlnt69lpmythdpm7ydk2e6k5nj5ghay9rg60vw49kejfah76sqeh4yshlsntgg007y0wgjlfwju6eksr", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Value" + } + } + }, + "description": "" + }, + "404": { + "description": "`address` not found" + } + } + } + }, "/api/v1/tx/add-vkey-witness": { "post": { "description": "Add a VKey witness to a transaction", @@ -477,6 +668,35 @@ } } }, + "/api/v1/tx/programmable-token/blacklist-init": { + "post": { + "description": "Initialize the blacklist", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/BlacklistInitArgs" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TextEnvelopeJSON" + } + } + }, + "description": "" + }, + "400": { + "description": "Invalid `body`" + } + } + } + }, "/api/v1/tx/programmable-token/issue": { "post": { "description": "Create some programmable tokens", @@ -506,6 +726,35 @@ } } }, + "/api/v1/tx/programmable-token/register-transfer-scripts": { + "post": { + "description": "Register the transfer scripts", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RegisterTransferScriptsArgs" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TextEnvelopeJSON" + } + } + }, + "description": "" + }, + "400": { + "description": "Invalid `body`" + } + } + } + }, "/api/v1/tx/programmable-token/seize": { "post": { "description": "Seize a user's funds", @@ -535,6 +784,35 @@ } } }, + "/api/v1/tx/programmable-token/seize-multi": { + "post": { + "description": "Seize multiple user's funds", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MultiSeizeAssetsArgs" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TextEnvelopeJSON" + } + } + }, + "description": "" + }, + "400": { + "description": "Invalid `body`" + } + } + } + }, "/api/v1/tx/programmable-token/transfer": { "post": { "description": "Transfer programmable tokens from one address to another", diff --git a/generated/scripts/mainnet/prefixIssuerCborHex.txt b/generated/scripts/mainnet/prefixIssuerCborHex.txt index f59b607e..252a264c 100644 --- a/generated/scripts/mainnet/prefixIssuerCborHex.txt +++ b/generated/scripts/mainnet/prefixIssuerCborHex.txt @@ -1 +1 @@ -5903c15903be01010033232323232323232323232323232322232323232323232325980099b874800000a26464646464646464b300133710900000245660026602a6602a6602a6602a66e1e60026eacc07cc08000a6eb80226eb801522298008015200094c00400a90005d6cdd5800a02e801a02c002191919192cc004cdc3a400000513232325980099b874800000a266ebcc098010c0980062941024181380118118009baa4c0122d87a9f581c922a3913b26bfd4d1eeb3c20c9d35fbe25852212d2b32b8ed292478eff0089919192cc004cdc3a400400513375e604c008604c00314a08120c09c008c08c004dd5260122d87a9f581c922a3913b26bfd4d1eeb3c20c9d35fbe25852212d2b32b8ed292478eff004084604800460400026ea8004c07cc084dd5180f8011980b19ba548008cc0600492f5c06602e03c6eacc07c030cdd780099ba548008cc0600492f5c0660320026eacc07cc080c080c08003229345901d45660026602a6602c66e952002330180124bd701980b80f1bab301f00c33019301f00f3756603e604060406040019149a2c80e901d180f007180f9baa301d001375860380166eb4c074008c064004c966002003168992cc00400626036005164060603600280b8cc0548966002003168acc004cdd7980d180d80080244dd5980e980d800c4c008c07000501920300013756603000c602e003164054603000460280026ea8c050c054010c050c050004c04cc04c004c048c048c04cdd51808801180880098089baa00123002375200244446601644b30010018802c56600266ebcc040c04400401a2600860266022003130023012001403c80700048a6002005001a5040284600c44b30010018a508acc004cdd79806000801c528c4c008c03400500a2012230052259800800c52f5c11330043003300b0013002300c0014020ae8088cc010896600200314a1159800980299b873009375460126014002900044c008c02c0062b30013375e601860140020091330062259800800c528c56600266e1cc02cdd518059806000a400115980099baf300e300c0010068a50898011806800a0148a5140288048c02c00626600c44b30010018a508acc004cdc398059baa300b300c001480022b30013375e601c601800200d1330082259800800c528c56600266e1cc034dd518069807000a400115980099baf3010300e0010088a50898011807800a0188a5140308058c03400626004601a0028052294100a2012300b0014020804100700094c004006942945004119180111980100100091801119801001000aab9f5734aae755d0aba25573c98010948 \ No newline at end of file +5903c15903be01010033232323232323232323232323232322232323232323232325980099b874800000a26464646464646464b300133710900000245660026602a6602a6602a6602a66e1e60026eacc07cc08000a6eb80226eb801522298008015200094c00400a90005d6cdd5800a02e801a02c002191919192cc004cdc3a400000513232325980099b874800000a266ebcc098010c0980062941024181380118118009baa4c0122d87a9f581c6a88e5f4051227ab54d6c637fd7d5d628d5452124e933da866559ef6ff0089919192cc004cdc3a400400513375e604c008604c00314a08120c09c008c08c004dd5260122d87a9f581c6a88e5f4051227ab54d6c637fd7d5d628d5452124e933da866559ef6ff004084604800460400026ea8004c07cc084dd5180f8011980b19ba548008cc0600492f5c06602e03c6eacc07c030cdd780099ba548008cc0600492f5c0660320026eacc07cc080c080c08003229345901d45660026602a6602c66e952002330180124bd701980b80f1bab301f00c33019301f00f3756603e604060406040019149a2c80e901d180f007180f9baa301d001375860380166eb4c074008c064004c966002003168992cc00400626036005164060603600280b8cc0548966002003168acc004cdd7980d180d80080244dd5980e980d800c4c008c07000501920300013756603000c602e003164054603000460280026ea8c050c054010c050c050004c04cc04c004c048c048c04cdd51808801180880098089baa00123002375200244446601644b30010018802c56600266ebcc040c04400401a2600860266022003130023012001403c80700048a6002005001a5040284600c44b30010018a508acc004cdd79806000801c528c4c008c03400500a2012230052259800800c52f5c11330043003300b0013002300c0014020ae8088cc010896600200314a1159800980299b873009375460126014002900044c008c02c0062b30013375e601860140020091330062259800800c528c56600266e1cc02cdd518059806000a400115980099baf300e300c0010068a50898011806800a0148a5140288048c02c00626600c44b30010018a508acc004cdc398059baa300b300c001480022b30013375e601c601800200d1330082259800800c528c56600266e1cc034dd518069807000a400115980099baf3010300e0010088a50898011807800a0188a5140308058c03400626004601a0028052294100a2012300b0014020804100700094c004006942945004119180111980100100091801119801001000aab9f5734aae755d0aba25573c98010948 \ No newline at end of file diff --git a/generated/scripts/mainnet/programmableLogicBaseSpending-6a88e5f4051227ab54d6c637fd7d5d628d5452124e933da866559ef6.json b/generated/scripts/mainnet/programmableLogicBaseSpending-6a88e5f4051227ab54d6c637fd7d5d628d5452124e933da866559ef6.json new file mode 100644 index 00000000..779bf6f8 --- /dev/null +++ b/generated/scripts/mainnet/programmableLogicBaseSpending-6a88e5f4051227ab54d6c637fd7d5d628d5452124e933da866559ef6.json @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV3", + "description": "Programmable Logic Base", + "cborHex": "58cb010100323232323223259800acc004cdd798021803000a6122d87a9f581c02e8cd69e393486840770b2ec08b6f3a62a6f82f70f2fb60ccfd59e9ff008a5189991919180111980100100091801119801001000912cc00400629422b3001300330090018a51898011804000a00c555cf88cdd79802800a6122d87a9f581c02e8cd69e393486840770b2ec08b6f3a62a6f82f70f2fb60ccfd59e9ff003005001400d149a2c8018dd5980298021802180218021802180218031baa300530063754002ae6955ceaba25742aae79" +} diff --git a/generated/scripts/mainnet/programmableLogicGlobalStake-02e8cd69e393486840770b2ec08b6f3a62a6f82f70f2fb60ccfd59e9.json b/generated/scripts/mainnet/programmableLogicGlobalStake-02e8cd69e393486840770b2ec08b6f3a62a6f82f70f2fb60ccfd59e9.json new file mode 100644 index 00000000..d584cc9c --- /dev/null +++ b/generated/scripts/mainnet/programmableLogicGlobalStake-02e8cd69e393486840770b2ec08b6f3a62a6f82f70f2fb60ccfd59e9.json @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV3", + "description": "Programmable Logic Global", + "cborHex": "590c47010100323232323232323232323232323232323232323232323232323232323232322323232323232323232325980099b874801000a2646464646464b30013370e900100144c8c8c8c8c8c8c96600266e1d20040028992cc004cc090cc090cc0926002606c0033758606c02d3330302225980099b89480f000a26600666e04009203c301e0018acc004cdc4a4050005133003337020049014180f800c56600266e25201400289980199b8100248050c0a40062666066444b30013370e00490004400626600666e040092002303d00140e400400281b1036206c375a606c00e6eb0c0d80512223232980091112cc00400e2b300133026330392259800800c52f5bded8c1132598009981a182118221baa30420010198cc005c04dd598211821800cc00cc10c009036452f5bded8c081f8c10cdd51820800a07800233036337600126e980052f5bded8c114a31640f1132323259800998181981a182118221baa30420020193322323253302f3303830460023046001133223232325980099b874800000a2646464b30013370e900000144cdd798270021827000c5282096304f002304b001375400913232325980099b874800800a2946294104b182780118258009baa0044120609800460900026ea8008c118c11c008c118c11c004c11cdd500118231baa002304200130420028cc004c10c01a608600b98009112cc00400a297adef6c608992cc00400a297adef6c608acc004cdc79bae001375c608a608c00513303d337600026ea0cdc01bad30483046003375a6090608c00466008608e006608e00515980099b90375c0026eb8c114c11800a26607a608c00666008608e00600513303d3046002330040033047002410c8219041182218228012080998049821182180118211821800c01103c200e8b207e30433754608200860846ea8c100c104c108dd50009802981f801a074805c00e97adef6c6040d4605200664453001222259800801c52f5bded8c113259800801c52f5bded8c115980099b8f375c0026eb8c100c10400e2b30013371e6eb80040222b30013375e6e98c10800cdd31821002466002444b300100289981f112cc004006297adef6c6089981e99bb030453046001375066e052000375a6090608c0026004608e0028208006264b300100289823001c56600266e44dd70009bae304430450028acc004cdd780098221822801456600266e1ccdc09bad30473045003375a608e608a004900044cc010c11800cc11800a26607866ec0004dd419b81375a608e608a0066eb4c11cc114008cc010c11800cc11800904244cc0f0cdd80009ba8375a608e608a00666008608c006004821226607866ec0c110c114008dd419b8148000dd698239822801198020019823001208441006086608800481fa6eacc10cc1040126eacc10cc10400d03b45903e456600266ebcc10cc104010c10cc10400e3300130420049821001c00900545903e207c8acc004cdd79821982080218219820801c400a2c81f103e2078303f304000340ed375600537560034bd6f7b630206c375c00633029303630383754606c606e606e606e0020186605c606c01c6eacc0d8014c8cdc39bd49800a4520000000000000000000000000000000000000000000000000000000000000000000803d28af41300122225980099b89480f000e3300133702006901e4cdc0001240793020001401115980099b89480a000e330013370200690144cdc0001240513021001401115980099b894805000e3300133702006900a4cdc000124029302b0014011198009112cc0040062005133003337000049001181f800a072a400100140d481c10382070800d2000803a062375a606c606e00f149a2c8198dd6181a800c59032181b00118190009baa303230330013032303337546062606460666ea8cc080034dd6981880219815112cc004006297ac08998149bad30320013002303300140b46eb0c0c0008c0c0004c0bc00626464b30013301e3370e605e6ea8c0c0c0c404520043301732303298009112cc004006200513300332325980099814181b181c1baa00100d8cc005c040126eacc0d8c0dc00902a44011033181a800981b1baa3034001303500140bd4bd6f7b630400502b1bac303000e0018a4d1640b46644653001222259800801440062646464b30013370e900000144c8c8c8c96600266e1d20040028992cc004cc0b0cc0b0cc0c4c0f8c100dd5181f181f981f80080a19baf303e001303d303e00a3303600f3756607c00b19800981f805cc0fc02a6606a607c01401280622c81d8dd6181e800c5903a181f001181d0009baa303a303b001303a303b37546072607460766ea8c020dd6981c800c4c8c8c8c96600266e1d20040028991919192cc004cc0bccc0bccdc800100199b9000300133039012375660820111980098210074c108036018807a2c81f0dd7182018208019bae303f002375c607a607c0146eb0c0f40062c81d0c0f8008c0e8004dd5181d181d800981d181d9baa3039303a303b375460106eb4c0e4005036181d001181b0009baa303600340c5002981980252f5bded8c08160c080034c0bc01cdd6181780119914c00488966002003100289980199192cc004cc09cc0d4c0dcdd500080644c8c8c96600266e1d200000289919192cc004cdc3a400000515980099817181d800806c66002e02015375660766078010817a2c81c22b30013302e303b0010118cc005c0402a6eacc0ecc0f002102f4590382070303c0023038001375460700031640d46072004606a0026ea8c0d4c0dcdd5181a981b181b9baa0018802206430340013035375460666068606a6ea8c0cc004c0d000502e52f5bded8c100140a86eb0c0b8c0bcc0bc02cdd618170072056302f002302b00137546056018664604a44b30010018a5eb822660486006605a0026004605c00281408c0acc0b4dd518150009bab302a0073029302a001375860500031640946052004604a0026ea8c094c098c098c09cdd51812981318139baa33230202259800800c5a2b30013003302800189814000c4c008c0a4005025204623301e00a3756604c604e60506ea8c098c09cc0a0dd50008009bac3024003302430243024302400130230013022001302237546040004604000260406ea8004dd4a451cb5f6e4c2c95c99112ecd8841b9f2bd8481d54ec600112f47a3866a190023002300c0012300b300b00122330032330042337126eb4c07c0066002009375c6038005375c60380028040dd5980f00080091809912cc00400629462a660086006603600226004603800280b08a6002005001801a02c14a04453001002a40012980080152000bad9bab00140150034010460046ea40048888cc044896600200310058acc004cdd7980c180c80080344c010c06cc06400626004603400280b1014000914c00400a0034a08088c02c8896600266e212014001899801980200119b81001480522b3001337109005000c4cc00cc054c054c054c054c054008cdc0800a4015133005002001404480888c048c048c048c048c048c048c048c048c048c048004c0248896600266e1c00520008980900144cc00cc04c008cdc0800a4004807888c8c8c96600266e1d200000289919192cc004cdc3a400000513375e602c008602c00314a08098c05c008c04c004dd500244c8c8c96600266e1d2002002899baf301600430160018a50404c602e00460260026ea8011010180a00118080009baa002230082259800800c528456600266ebcc04000400e2946260046022002806900b1114c0048a600200b002800a00e80140050041112cc00400a2003198009112cc00400a20031980098088014c04800a002801900c4c03800a601e00500148c02488896600200313300a00300289919192cc004cdd7801000c4cc034cdd80011ba63300b3756603000c6eacc06000e600200f005980b80220108acc004cdc81bae002375c00313300d0069800803c016008804226601a0073001007980b8024cc0340180150082026404c602800460260086026002807080488896600200510018cc0048896600200510018cc004c04000a6022005001400c805a601a005300e002800a460104444b300100189980480180144c8c8c96600266ebc00800626601866ec0008dd4198059bad3017006375a602e0073001007802cc058011008456600266e40dd70011bae0018998060034c00401e00b004402113300c0039800803cc0580126601800c00a8041012202430130023012004301200140344020ae8088cc00c8cdd7980500080180091801112cc00400629422a6600c6006601400226004601600280288c8c0088cc0080080048c0088cc00800800555cf919801000801ab9a14a2aae755d0aba25573c1" +} diff --git a/generated/scripts/mainnet/programmableTokenMinting-b0d8be8772baa2911da3e28550c3d11b28dd27d41d8869c66ba9dc53.json b/generated/scripts/mainnet/programmableTokenMinting-b0d8be8772baa2911da3e28550c3d11b28dd27d41d8869c66ba9dc53.json new file mode 100644 index 00000000..44de7a8c --- /dev/null +++ b/generated/scripts/mainnet/programmableTokenMinting-b0d8be8772baa2911da3e28550c3d11b28dd27d41d8869c66ba9dc53.json @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV3", + "description": "Programmable Token Minting", + "cborHex": "5903d301010033232323232323232323232323232322232323232323232325980099b874800000a26464646464646464b300133710900000245660026602a6602a6602a6602a66e1e60026eacc07cc08000a6eb80226eb801522298008015200094c00400a90005d6cdd5800a02e801a02c002191919192cc004cdc3a400000513232325980099b874800000a266ebcc098010c0980062941024181380118118009baa4c0122d87a9f581c6a88e5f4051227ab54d6c637fd7d5d628d5452124e933da866559ef6ff0089919192cc004cdc3a400400513375e604c008604c00314a08120c09c008c08c004dd5260122d87a9f581c6a88e5f4051227ab54d6c637fd7d5d628d5452124e933da866559ef6ff004084604800460400026ea8004c07cc084dd5180f8011980b19ba548008cc0600492f5c06602e03c6eacc07c030cdd780099ba548008cc0600492f5c0660320026eacc07cc080c080c08003229345901d45660026602a6602c66e952002330180124bd701980b80f1bab301f00c33019301f00f3756603e604060406040019149a2c80e901d180f007180f9baa301d001375860380166eb4c074008c064004c966002003168992cc00400626036005164060603600280b8cc0548966002003168acc004cdd7980d180d80080244dd5980e980d800c4c008c07000501920300013756603000c602e003164054603000460280026ea8c050c054010c050c050004c04cc04c004c048c048c04cdd51808801180880098089baa00123002375200244446601644b30010018802c56600266ebcc040c04400401a2600860266022003130023012001403c80700048a6002005001a5040284600c44b30010018a508acc004cdd79806000801c528c4c008c03400500a2012230052259800800c52f5c11330043003300b0013002300c0014020ae8088cc010896600200314a1159800980299b873009375460126014002900044c008c02c0062b30013375e601860140020091330062259800800c528c56600266e1cc02cdd518059806000a400115980099baf300e300c0010068a50898011806800a0148a5140288048c02c00626600c44b30010018a508acc004cdc398059baa300b300c001480022b30013375e601c601800200d1330082259800800c528c56600266e1cc034dd518069807000a400115980099baf3010300e0010088a50898011807800a0188a5140308058c03400626004601a0028052294100a2012300b0014020804100700094c004006942945004119180111980100100091801119801001000aab9f5734aae755d0aba25573c98011e581cb8e98f65c3f29808463ab331827b572beb7d9c866ef09505362550860001" +} diff --git a/generated/scripts/mainnet/transferLogicSpending-a67b4b754a79af7b2a447d4c67122e8f91cc7b8d6cdad1c462033f7d.json b/generated/scripts/mainnet/transferLogicSpending-a67b4b754a79af7b2a447d4c67122e8f91cc7b8d6cdad1c462033f7d.json new file mode 100644 index 00000000..a3901faf --- /dev/null +++ b/generated/scripts/mainnet/transferLogicSpending-a67b4b754a79af7b2a447d4c67122e8f91cc7b8d6cdad1c462033f7d.json @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV3", + "description": "Transfer Logic Spending", + "cborHex": "5902c70101003232323232323232322323232598009980319b87300a37546016601800490024c004dd618058014dd618059806000cca6002444b3001001880144cc00cc96600264646464b30013370e900000144c8c8c96600266e1d2000002899baf301700430170018a504054603000460280026ea930122d87a9f581c6a88e5f4051227ab54d6c637fd7d5d628d5452124e933da866559ef6ff0089919192cc004cdc3a400400513375e602e008602e00314a080a8c060008c050004dd5260122d87a9f581c6a88e5f4051227ab54d6c637fd7d5d628d5452124e933da866559ef6ff004048602a00460220026ea8004c040c048dd5000c4cd5d0180818091baa301030123754602060246ea8c040c044c048dd5000801c400d00e180798089baa300f301030113754601e0026020002806297ae0800a01037586016002911194c0048896600200314a31323232325980099b874801000a264646464b300133015330153372000400666e4000c004c8cc8c060896600200314a1153323301d00114a26006603c00226004603e00280d88cdd7980d80080b0009bab301a008899805980d805180d804c590181bae3019301a003375c60300046eb8c05c018dd6180b000c59014180b80118098009baa301330140013013301437546024602660286ea8c010dd69809180a1baa3012002403d00480120163300a2225980099b8848050006266006602460246024602460246024602460246024602400466e0400520148acc004cdc4240140031330033012301230123012301200233702002900544ccc0348896600266e1c00520008980a00144cc00cc054008cdc0800a4004809000800500f201e00245268b2012300c37546014004601400260146ea8004dd4a451ce24d28a1a5019b59bd930128d2c78af6715bbccf18bfb1ca09ef58a7002298008014006941005119180111980100100091801119801001000aab9f5734aae755d0aba25573c1" +} diff --git a/generated/scripts/preview/prefixIssuerCborHex.txt b/generated/scripts/preview/prefixIssuerCborHex.txt index 4221b71d..830bf759 100644 --- a/generated/scripts/preview/prefixIssuerCborHex.txt +++ b/generated/scripts/preview/prefixIssuerCborHex.txt @@ -1 +1 @@ -5903c15903be01010033232323232323232323232323232322232323232323232325980099b874800000a26464646464646464b300133710900000245660026602a6602a6602a6602a66e1e60026eacc07cc08000a6eb80226eb801522298008015200094c00400a90005d6cdd5800a02e801a02c002191919192cc004cdc3a400000513232325980099b874800000a266ebcc098010c0980062941024181380118118009baa4c0122d87a9f581c48339fc44241617a0232f8b6fcebd00c070f3edb24c3a98431bb5035ff0089919192cc004cdc3a400400513375e604c008604c00314a08120c09c008c08c004dd5260122d87a9f581c48339fc44241617a0232f8b6fcebd00c070f3edb24c3a98431bb5035ff004084604800460400026ea8004c07cc084dd5180f8011980b19ba548008cc0600492f5c06602e03c6eacc07c030cdd780099ba548008cc0600492f5c0660320026eacc07cc080c080c08003229345901d45660026602a6602c66e952002330180124bd701980b80f1bab301f00c33019301f00f3756603e604060406040019149a2c80e901d180f007180f9baa301d001375860380166eb4c074008c064004c966002003168992cc00400626036005164060603600280b8cc0548966002003168acc004cdd7980d180d80080244dd5980e980d800c4c008c07000501920300013756603000c602e003164054603000460280026ea8c050c054010c050c050004c04cc04c004c048c048c04cdd51808801180880098089baa00123002375200244446601644b30010018802c56600266ebcc040c04400401a2600860266022003130023012001403c80700048a6002005001a5040284600c44b30010018a508acc004cdd79806000801c528c4c008c03400500a2012230052259800800c52f5c11330043003300b0013002300c0014020ae8088cc010896600200314a1159800980299b873009375460126014002900044c008c02c0062b30013375e601860140020091330062259800800c528c56600266e1cc02cdd518059806000a400115980099baf300e300c0010068a50898011806800a0148a5140288048c02c00626600c44b30010018a508acc004cdc398059baa300b300c001480022b30013375e601c601800200d1330082259800800c528c56600266e1cc034dd518069807000a400115980099baf3010300e0010088a50898011807800a0188a5140308058c03400626004601a0028052294100a2012300b0014020804100700094c004006942945004119180111980100100091801119801001000aab9f5734aae755d0aba25573c98010948 \ No newline at end of file +5903c15903be01010033232323232323232323232323232322232323232323232325980099b874800000a26464646464646464b300133710900000245660026602a6602a6602a6602a66e1e60026eacc07cc08000a6eb80226eb801522298008015200094c00400a90005d6cdd5800a02e801a02c002191919192cc004cdc3a400000513232325980099b874800000a266ebcc098010c0980062941024181380118118009baa4c0122d87a9f581cbbc4afc85c16f76eadb475ad44fc864870bb016006eac4afeb89738bff0089919192cc004cdc3a400400513375e604c008604c00314a08120c09c008c08c004dd5260122d87a9f581cbbc4afc85c16f76eadb475ad44fc864870bb016006eac4afeb89738bff004084604800460400026ea8004c07cc084dd5180f8011980b19ba548008cc0600492f5c06602e03c6eacc07c030cdd780099ba548008cc0600492f5c0660320026eacc07cc080c080c08003229345901d45660026602a6602c66e952002330180124bd701980b80f1bab301f00c33019301f00f3756603e604060406040019149a2c80e901d180f007180f9baa301d001375860380166eb4c074008c064004c966002003168992cc00400626036005164060603600280b8cc0548966002003168acc004cdd7980d180d80080244dd5980e980d800c4c008c07000501920300013756603000c602e003164054603000460280026ea8c050c054010c050c050004c04cc04c004c048c048c04cdd51808801180880098089baa00123002375200244446601644b30010018802c56600266ebcc040c04400401a2600860266022003130023012001403c80700048a6002005001a5040284600c44b30010018a508acc004cdd79806000801c528c4c008c03400500a2012230052259800800c52f5c11330043003300b0013002300c0014020ae8088cc010896600200314a1159800980299b873009375460126014002900044c008c02c0062b30013375e601860140020091330062259800800c528c56600266e1cc02cdd518059806000a400115980099baf300e300c0010068a50898011806800a0148a5140288048c02c00626600c44b30010018a508acc004cdc398059baa300b300c001480022b30013375e601c601800200d1330082259800800c528c56600266e1cc034dd518069807000a400115980099baf3010300e0010088a50898011807800a0188a5140308058c03400626004601a0028052294100a2012300b0014020804100700094c004006942945004119180111980100100091801119801001000aab9f5734aae755d0aba25573c98010948 \ No newline at end of file diff --git a/generated/scripts/preview/programmableLogicBaseSpending-bbc4afc85c16f76eadb475ad44fc864870bb016006eac4afeb89738b.json b/generated/scripts/preview/programmableLogicBaseSpending-bbc4afc85c16f76eadb475ad44fc864870bb016006eac4afeb89738b.json new file mode 100644 index 00000000..9c10333d --- /dev/null +++ b/generated/scripts/preview/programmableLogicBaseSpending-bbc4afc85c16f76eadb475ad44fc864870bb016006eac4afeb89738b.json @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV3", + "description": "Programmable Logic Base", + "cborHex": "58cb010100323232323223259800acc004cdd798021803000a6122d87a9f581ca0bd609f3381a13ae1c3ada2fd1907245f8ad9305e4c467d78eabd96ff008a5189991919180111980100100091801119801001000912cc00400629422b3001300330090018a51898011804000a00c555cf88cdd79802800a6122d87a9f581ca0bd609f3381a13ae1c3ada2fd1907245f8ad9305e4c467d78eabd96ff003005001400d149a2c8018dd5980298021802180218021802180218031baa300530063754002ae6955ceaba25742aae79" +} diff --git a/generated/scripts/preview/programmableLogicGlobalStake-a0bd609f3381a13ae1c3ada2fd1907245f8ad9305e4c467d78eabd96.json b/generated/scripts/preview/programmableLogicGlobalStake-a0bd609f3381a13ae1c3ada2fd1907245f8ad9305e4c467d78eabd96.json new file mode 100644 index 00000000..909cffac --- /dev/null +++ b/generated/scripts/preview/programmableLogicGlobalStake-a0bd609f3381a13ae1c3ada2fd1907245f8ad9305e4c467d78eabd96.json @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV3", + "description": "Programmable Logic Global", + "cborHex": "590c47010100323232323232323232323232323232323232323232323232323232323232322323232323232323232325980099b874801000a2646464646464b30013370e900100144c8c8c8c8c8c8c96600266e1d20040028992cc004cc090cc090cc0926002606c0033758606c02d3330302225980099b89480f000a26600666e04009203c301e0018acc004cdc4a4050005133003337020049014180f800c56600266e25201400289980199b8100248050c0a40062666066444b30013370e00490004400626600666e040092002303d00140e400400281b1036206c375a606c00e6eb0c0d80512223232980091112cc00400e2b300133026330392259800800c52f5bded8c1132598009981a182118221baa30420010198cc005c04dd598211821800cc00cc10c009036452f5bded8c081f8c10cdd51820800a07800233036337600126e980052f5bded8c114a31640f1132323259800998181981a182118221baa30420020193322323253302f3303830460023046001133223232325980099b874800000a2646464b30013370e900000144cdd798270021827000c5282096304f002304b001375400913232325980099b874800800a2946294104b182780118258009baa0044120609800460900026ea8008c118c11c008c118c11c004c11cdd500118231baa002304200130420028cc004c10c01a608600b98009112cc00400a297adef6c608992cc00400a297adef6c608acc004cdc79bae001375c608a608c00513303d337600026ea0cdc01bad30483046003375a6090608c00466008608e006608e00515980099b90375c0026eb8c114c11800a26607a608c00666008608e00600513303d3046002330040033047002410c8219041182218228012080998049821182180118211821800c01103c200e8b207e30433754608200860846ea8c100c104c108dd50009802981f801a074805c00e97adef6c6040d4605200664453001222259800801c52f5bded8c113259800801c52f5bded8c115980099b8f375c0026eb8c100c10400e2b30013371e6eb80040222b30013375e6e98c10800cdd31821002466002444b300100289981f112cc004006297adef6c6089981e99bb030453046001375066e052000375a6090608c0026004608e0028208006264b300100289823001c56600266e44dd70009bae304430450028acc004cdd780098221822801456600266e1ccdc09bad30473045003375a608e608a004900044cc010c11800cc11800a26607866ec0004dd419b81375a608e608a0066eb4c11cc114008cc010c11800cc11800904244cc0f0cdd80009ba8375a608e608a00666008608c006004821226607866ec0c110c114008dd419b8148000dd698239822801198020019823001208441006086608800481fa6eacc10cc1040126eacc10cc10400d03b45903e456600266ebcc10cc104010c10cc10400e3300130420049821001c00900545903e207c8acc004cdd79821982080218219820801c400a2c81f103e2078303f304000340ed375600537560034bd6f7b630206c375c00633029303630383754606c606e606e606e0020186605c606c01c6eacc0d8014c8cdc39bd49800a4520000000000000000000000000000000000000000000000000000000000000000000803d28af41300122225980099b89480f000e3300133702006901e4cdc0001240793020001401115980099b89480a000e330013370200690144cdc0001240513021001401115980099b894805000e3300133702006900a4cdc000124029302b0014011198009112cc0040062005133003337000049001181f800a072a400100140d481c10382070800d2000803a062375a606c606e00f149a2c8198dd6181a800c59032181b00118190009baa303230330013032303337546062606460666ea8cc080034dd6981880219815112cc004006297ac08998149bad30320013002303300140b46eb0c0c0008c0c0004c0bc00626464b30013301e3370e605e6ea8c0c0c0c404520043301732303298009112cc004006200513300332325980099814181b181c1baa00100d8cc005c040126eacc0d8c0dc00902a44011033181a800981b1baa3034001303500140bd4bd6f7b630400502b1bac303000e0018a4d1640b46644653001222259800801440062646464b30013370e900000144c8c8c8c96600266e1d20040028992cc004cc0b0cc0b0cc0c4c0f8c100dd5181f181f981f80080a19baf303e001303d303e00a3303600f3756607c00b19800981f805cc0fc02a6606a607c01401280622c81d8dd6181e800c5903a181f001181d0009baa303a303b001303a303b37546072607460766ea8c020dd6981c800c4c8c8c8c96600266e1d20040028991919192cc004cc0bccc0bccdc800100199b9000300133039012375660820111980098210074c108036018807a2c81f0dd7182018208019bae303f002375c607a607c0146eb0c0f40062c81d0c0f8008c0e8004dd5181d181d800981d181d9baa3039303a303b375460106eb4c0e4005036181d001181b0009baa303600340c5002981980252f5bded8c08160c080034c0bc01cdd6181780119914c00488966002003100289980199192cc004cc09cc0d4c0dcdd500080644c8c8c96600266e1d200000289919192cc004cdc3a400000515980099817181d800806c66002e02015375660766078010817a2c81c22b30013302e303b0010118cc005c0402a6eacc0ecc0f002102f4590382070303c0023038001375460700031640d46072004606a0026ea8c0d4c0dcdd5181a981b181b9baa0018802206430340013035375460666068606a6ea8c0cc004c0d000502e52f5bded8c100140a86eb0c0b8c0bcc0bc02cdd618170072056302f002302b00137546056018664604a44b30010018a5eb822660486006605a0026004605c00281408c0acc0b4dd518150009bab302a0073029302a001375860500031640946052004604a0026ea8c094c098c098c09cdd51812981318139baa33230202259800800c5a2b30013003302800189814000c4c008c0a4005025204623301e00a3756604c604e60506ea8c098c09cc0a0dd50008009bac3024003302430243024302400130230013022001302237546040004604000260406ea8004dd4a451cd81446751b45947e4b99fe0c8df71c8b0962faf129e09d923618cb400023002300c0012300b300b00122330032330042337126eb4c07c0066002009375c6038005375c60380028040dd5980f00080091809912cc00400629462a660086006603600226004603800280b08a6002005001801a02c14a04453001002a40012980080152000bad9bab00140150034010460046ea40048888cc044896600200310058acc004cdd7980c180c80080344c010c06cc06400626004603400280b1014000914c00400a0034a08088c02c8896600266e212014001899801980200119b81001480522b3001337109005000c4cc00cc054c054c054c054c054008cdc0800a4015133005002001404480888c048c048c048c048c048c048c048c048c048c048004c0248896600266e1c00520008980900144cc00cc04c008cdc0800a4004807888c8c8c96600266e1d200000289919192cc004cdc3a400000513375e602c008602c00314a08098c05c008c04c004dd500244c8c8c96600266e1d2002002899baf301600430160018a50404c602e00460260026ea8011010180a00118080009baa002230082259800800c528456600266ebcc04000400e2946260046022002806900b1114c0048a600200b002800a00e80140050041112cc00400a2003198009112cc00400a20031980098088014c04800a002801900c4c03800a601e00500148c02488896600200313300a00300289919192cc004cdd7801000c4cc034cdd80011ba63300b3756603000c6eacc06000e600200f005980b80220108acc004cdc81bae002375c00313300d0069800803c016008804226601a0073001007980b8024cc0340180150082026404c602800460260086026002807080488896600200510018cc0048896600200510018cc004c04000a6022005001400c805a601a005300e002800a460104444b300100189980480180144c8c8c96600266ebc00800626601866ec0008dd4198059bad3017006375a602e0073001007802cc058011008456600266e40dd70011bae0018998060034c00401e00b004402113300c0039800803cc0580126601800c00a8041012202430130023012004301200140344020ae8088cc00c8cdd7980500080180091801112cc00400629422a6600c6006601400226004601600280288c8c0088cc0080080048c0088cc00800800555cf919801000801ab9a14a2aae755d0aba25573c1" +} diff --git a/generated/scripts/preview/programmableTokenMinting-29b5233b42bba0ca4cbbec2bdedfb0419451d4980afa0e12a70af88c.json b/generated/scripts/preview/programmableTokenMinting-29b5233b42bba0ca4cbbec2bdedfb0419451d4980afa0e12a70af88c.json new file mode 100644 index 00000000..28e1b498 --- /dev/null +++ b/generated/scripts/preview/programmableTokenMinting-29b5233b42bba0ca4cbbec2bdedfb0419451d4980afa0e12a70af88c.json @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV3", + "description": "Programmable Token Minting", + "cborHex": "5903d301010033232323232323232323232323232322232323232323232325980099b874800000a26464646464646464b300133710900000245660026602a6602a6602a6602a66e1e60026eacc07cc08000a6eb80226eb801522298008015200094c00400a90005d6cdd5800a02e801a02c002191919192cc004cdc3a400000513232325980099b874800000a266ebcc098010c0980062941024181380118118009baa4c0122d87a9f581cbbc4afc85c16f76eadb475ad44fc864870bb016006eac4afeb89738bff0089919192cc004cdc3a400400513375e604c008604c00314a08120c09c008c08c004dd5260122d87a9f581cbbc4afc85c16f76eadb475ad44fc864870bb016006eac4afeb89738bff004084604800460400026ea8004c07cc084dd5180f8011980b19ba548008cc0600492f5c06602e03c6eacc07c030cdd780099ba548008cc0600492f5c0660320026eacc07cc080c080c08003229345901d45660026602a6602c66e952002330180124bd701980b80f1bab301f00c33019301f00f3756603e604060406040019149a2c80e901d180f007180f9baa301d001375860380166eb4c074008c064004c966002003168992cc00400626036005164060603600280b8cc0548966002003168acc004cdd7980d180d80080244dd5980e980d800c4c008c07000501920300013756603000c602e003164054603000460280026ea8c050c054010c050c050004c04cc04c004c048c048c04cdd51808801180880098089baa00123002375200244446601644b30010018802c56600266ebcc040c04400401a2600860266022003130023012001403c80700048a6002005001a5040284600c44b30010018a508acc004cdd79806000801c528c4c008c03400500a2012230052259800800c52f5c11330043003300b0013002300c0014020ae8088cc010896600200314a1159800980299b873009375460126014002900044c008c02c0062b30013375e601860140020091330062259800800c528c56600266e1cc02cdd518059806000a400115980099baf300e300c0010068a50898011806800a0148a5140288048c02c00626600c44b30010018a508acc004cdc398059baa300b300c001480022b30013375e601c601800200d1330082259800800c528c56600266e1cc034dd518069807000a400115980099baf3010300e0010088a50898011807800a0188a5140308058c03400626004601a0028052294100a2012300b0014020804100700094c004006942945004119180111980100100091801119801001000aab9f5734aae755d0aba25573c98011e581cb8e98f65c3f29808463ab331827b572beb7d9c866ef09505362550860001" +} diff --git a/generated/scripts/preview/transferLogicSpending-4eafef4a1712e464f0efdf287e636932d7fd6a7a43dbb101536218b5.json b/generated/scripts/preview/transferLogicSpending-4eafef4a1712e464f0efdf287e636932d7fd6a7a43dbb101536218b5.json new file mode 100644 index 00000000..971c8989 --- /dev/null +++ b/generated/scripts/preview/transferLogicSpending-4eafef4a1712e464f0efdf287e636932d7fd6a7a43dbb101536218b5.json @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV3", + "description": "Transfer Logic Spending", + "cborHex": "5902c70101003232323232323232322323232598009980319b87300a37546016601800490024c004dd618058014dd618059806000cca6002444b3001001880144cc00cc96600264646464b30013370e900000144c8c8c96600266e1d2000002899baf301700430170018a504054603000460280026ea930122d87a9f581cbbc4afc85c16f76eadb475ad44fc864870bb016006eac4afeb89738bff0089919192cc004cdc3a400400513375e602e008602e00314a080a8c060008c050004dd5260122d87a9f581cbbc4afc85c16f76eadb475ad44fc864870bb016006eac4afeb89738bff004048602a00460220026ea8004c040c048dd5000c4cd5d0180818091baa301030123754602060246ea8c040c044c048dd5000801c400d00e180798089baa300f301030113754601e0026020002806297ae0800a01037586016002911194c0048896600200314a31323232325980099b874801000a264646464b300133015330153372000400666e4000c004c8cc8c060896600200314a1153323301d00114a26006603c00226004603e00280d88cdd7980d80080b0009bab301a008899805980d805180d804c590181bae3019301a003375c60300046eb8c05c018dd6180b000c59014180b80118098009baa301330140013013301437546024602660286ea8c010dd69809180a1baa3012002403d00480120163300a2225980099b8848050006266006602460246024602460246024602460246024602400466e0400520148acc004cdc4240140031330033012301230123012301200233702002900544ccc0348896600266e1c00520008980a00144cc00cc054008cdc0800a4004809000800500f201e00245268b2012300c37546014004601400260146ea8004dd4a451ce24d28a1a5019b59bd930128d2c78af6715bbccf18bfb1ca09ef58a7002298008014006941005119180111980100100091801119801001000aab9f5734aae755d0aba25573c1" +} diff --git a/generated/scripts/unapplied/binds/programmableLogicBase.json b/generated/scripts/unapplied/binds/programmableLogicBase.json index b5bb88fd..112eb7e2 100644 --- a/generated/scripts/unapplied/binds/programmableLogicBase.json +++ b/generated/scripts/unapplied/binds/programmableLogicBase.json @@ -1,5 +1,5 @@ { - "cborHex": "590138590135010100225335738921394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3134345d0013259800992cc00400620031330074911f70726f6772616d6d61626c6520676c6f62616c206e6f7420696e766f6b6564000014018b30013375e600e601200200714a313325330070051323230022330020020012300223300200200122533009007159800800c52845660026006601800314a313002300b0014020aab9f1299803a481394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3135375d0013375e60100020086010002802a2934590051bab357426ae88d5d11aba2357446ae88d5d11aab9e37546ae84d55cf1baa00101", + "cborHex": "590138590135010100225335738921394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3134385d0013259800992cc00400620031330074911f70726f6772616d6d61626c6520676c6f62616c206e6f7420696e766f6b6564000014018b30013375e600e601200200714a313325330070051323230022330020020012300223300200200122533009007159800800c52845660026006601800314a313002300b0014020aab9f1299803a481394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3136315d0013375e60100020086010002802a2934590051bab357426ae88d5d11aba2357446ae88d5d11aab9e37546ae84d55cf1baa00101", "description": "Programmable Logic Base", "type": "PlutusScriptV3" } \ No newline at end of file diff --git a/generated/scripts/unapplied/binds/programmableLogicGlobal.json b/generated/scripts/unapplied/binds/programmableLogicGlobal.json index 001df909..877912ff 100644 --- a/generated/scripts/unapplied/binds/programmableLogicGlobal.json +++ b/generated/scripts/unapplied/binds/programmableLogicGlobal.json @@ -1,5 +1,5 @@ { - "cborHex": "590e3e590e3b010100225335738921394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3235335d00132323232323232533573892012245787472616374696e672070726f746f636f6c20706172616d65746572205554784f0013232325980099b874801000a26464a6608292011a45787472616374696e6720696e766f6b65642073637269707473001323232325980099b874800800a264a6608c920109505365697a6541637400132323232325980099b874801000a2646464646464b30013302a3302a3302a3302a3302a3370ea660a29212a4c5b6c69622f506c7574617263682f436f72652f56616c69646174696f6e4c6f6769632e68733a39365d001980091129982a24812a4c5b6c69622f506c7574617263682f436f72652f56616c69646174696f6e4c6f6769632e68733a39375d00159800800c400a2b30013303501430553057375460aa60ae6ea8c154c158c15cdd5182a800c4cc00ccdc00012400460ac00313300300230560014140826a9000402d048240046606460a460a86ea800c044cdd7998161bac3052018375a60a401866e9520003304200333042374c0026608460a460a600866084980103d87a80004bd701981a1829182a1baa3052305330533053006010325330524901204c5b2e2f506c7574617263682f4275696c74696e2f426f6f6c2e68733a39325d0019800800d28528a09c3375e6e98008dd30009982218290091bab305200a8a4d16413464a660a2921214c5b6c69622f506c7574617263682f436f72652f56616c75652e68733a3433305d00133048225330534901214c5b6c69622f506c7574617263682f436f72652f56616c75652e68733a3433325d00159800800c52f5bded8c115980099baf305230540010038982a800c4cc894cc1541144cc118008004c150004c008c15400504f20980023051005375660a0004609e004609e002609e6ea8c134c138c13cdd5198138031bad304d00837586098003153304b4916c5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3331353a372d32380016411c609a00460900026ea8c124c128004c124c128dd51824182498251baa3302200c375a609060920046eb0c11c03cc11c00626464b3001330203370e608c6ea8c120c12404520043259800800c40062660909211270726f6720746f6b656e732065736361706500001411064a66090921214c5b6c69622f506c7574617263682f436f72652f56616c75652e68733a3434365d001330162533049491214c5b6c69622f506c7574617263682f436f72652f56616c75652e68733a3435315d00133017253304a491214c5b6c69622f506c7574617263682f436f72652f56616c75652e68733a3434375d001337126eb4c134004cc894cc131241264c5b7372632f506c7574617263682f4c65646765724170692f56616c75652e68733a3632345d00198008015200092998268120cc00400a9000494cc1380944dd6800cdd5800a040802a03e375c60920046eb8c124004dd5982600080119299824249394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3132315d00132533049491214c5b6c69622f506c7574617263682f436f72652f56616c75652e68733a3534385d001304b0019800911299825a49394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3132325d00159800800c400a2660066464b30013302e304e3050375400201b198009129982781f099b800020018024dd598271827801206488022092304d001304e37546098002609a002822297adef6c60800a07e3758609001d149a2c8218cc894cc121241394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3137315d0013298009111299826a49394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3137365d00159800801440062646464b30013370e900100144c8c8c8c96600266e1d20040028991919192cc004cc0c4cc0c4c966002003100189982c818800a0aa3372000400664b30010018800c4cc1640c400505519b900030013259800800c40062660b292112696e76616c696420646972206e6f6465206e000014154660960246eacc16402233001305a00e982d006c03100f4590541bae30583059003375c60ae0046eb8c150c158028dd6182a800c54cc1512416d5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3230313a32352d34390016414060ac00460a20026ea8c148c14c004c148c14cdd51828982918299baa3008375a60a20031323232325980099b874801000a264b30013302e3302e3259800800c40062660ac921204d697373696e67207265717569726564207472616e73666572207363726970740000141486607060ac60b06ea8c158c15cc15c004050c966002003100189982b2481186469726563746f72792070726f6f66206d69736d6174636800001414866ebcc158004c150c158028c966002003100189982b248110696e76616c696420646972206e6f64650000141486609001e6eacc15801633001305700b982b8054c94cc1581184cc11c004028c15802900c4590511bac30550018a9982a2496d5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3138353a32352d34360016414060ac00460a20026ea8c148c14c004c148c14cdd51828982918299baa3008375a60a20028260c148008c134004dd51827001a08c8014c12c01297adef6c604100604601a608e00e6eb0c11c008cc894cc11d241384c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a38395d00198009112998252481384c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a39305d00159800800c400a2660066464b30013302d304d304f375400201913232325980099b874800800a2d13232325980099b874800800a2b30013303530530010118cc004894cc15010c4cdc0001000c02a6eacc14cc150021037454cc1492411f4d697373696e6720726571756972656420736372697074207769746e657373001641391598009981a9829800806c6600244a660a8086266e00008006015375660a660a801081ba2a660a49211b4d697373696e6720726571756972656420706b207769746e657373001641388270c150008c13c004dd51828000a0963051002304c0013754609a609e6ea8c134c138c13cdd5000c4011048182600098269baa304b304c304d375460960026098002821a97adef6c60800a07c3758608c608e608e0166eb0c118039041182380118210009baa304300c332533042491264c5b2e2f506c7574617263682f496e7465726e616c2f4c6973744c696b652e68733a3234305d00130392253304403c159800800c52f5c113322533046036133037002001300330450013002304600140f44a66084921394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3237385d00130433045375460820026eacc10801cc104c108004dd61820000c54cc0fd24016c5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3237313a332d3234001640ec6aae78008d55ce8009baa357426ae88d5d11aab9e37546ae84d5d11aab9e3754664a66ae7124011f4c5b6c69622f506c7574617263682f436f72652f4c6973742e68733a38325d00132323002233002002001230022330020020012253357389211f4c5b6c69622f506c7574617263682f436f72652f4c6973742e68733a38325d00159800800c5a2b30013003304000189820000c4c008c10400503b207025335738921394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3236355d001332253357389201214c5b6c69622f506c7574617263682f436f72652f56616c75652e68733a3132385d0013325335738921264c5b2e2f506c7574617263682f496e7465726e616c2f4c6973744c696b652e68733a3233345d0013232300223300200200123002233002002001225335738921264c5b2e2f506c7574617263682f496e7465726e616c2f4c6973744c696b652e68733a3133375d00159800800c528454cc020c00cc03c0044c008c0400050071299ab9c491214c5b6c69622f506c7574617263682f436f72652f56616c75652e68733a3132395d0013375e6aae7400400c004028dd59aba1357446aae78dd51aba1357446aae78dd50008009bac35742006a66ae7124011e4c5b2e2f506c7574617263682f526570722f446174612e68733a3332355d001357446ae88d5d11aba2001357440026ae88004d55cf1baa357420046ae88004d55cf1baa00101", + "cborHex": "591503591500010100225335738921394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3236305d00132323232323232533573892012245787472616374696e672070726f746f636f6c20706172616d65746572205554784f0013232325980099b874801000a26464a6609a92011a45787472616374696e6720696e766f6b65642073637269707473001323232325980099b874800800a264646464646464b30013370e900200144c96600266060660606606064b30010018800c4cc16924011f6d696e692d6c656467657220696e76617269616e74732076696f6c617465640000141593001305a0019bac305a01699982811129982e2481204c5b6c69622f506c7574617263682f436f72652f4c6973742e68733a3134365d0015980099b89480f000a26600666e04009203c301f0018acc004cdc4a40500051330033370200490141810800c56600266e25201400289980199b8100248050c0e000626660a6444a660be921204c5b6c69622f506c7574617263682f436f72652f4c6973742e68733a3133375d0015980099b870024800220031330033370200490011830800a0b6002001416082c10581bad305a007375860b402891129982e2481394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3336365d001323298009111299831249394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3339315d00159800801c56600266052660b244a660c89201394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3337395d00159800800c52f5bded8c11325980099822983318341baa30660010198cc004894cc19c1544cdc0001000cdd598331833800cc00cc19c009049452f5bded8c08308c19cdd51832800a0ba00233053337600126e980052f5bded8c114a31641791323232598009981e192cc004006200313306649141696e707574207061796d656e742063726564656e7469616c206973206e6f74207468652070726f6772616d6d61626c65206c6f6769632063726564656e7469616c0000141886608a60cc60d06ea8c198008064c9660020031001899833248126636f72726573706f6e64696e67206f75747075743a2061646472657373206d69736d617463680000141886644a660ce09026464a6606a6609260d400460d400226644a660d60982646464b30013370e900000144c8c8c96600266e1d2000002899baf307200430720018a5041b460e600460dc0026ea80122646464b30013370e90010014528c52820da3073002306e00137540088350c1c0008c1ac004dd5001183518358011835183580098359baa002306a375400460cc00260cc0051980098338034c19c01730012225330684901394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3333355d00159800801452f5bded8c113259800801452f5bded8c1159800998159bae001375c60d060d40051332253306b05b13305c002001337600026ea0cdc01bad306c306a003375a60d860d40046600860d600660d600515980099b90375c0026eb8c1a0c1a800a26644a660d60b62660b800400260d40066600860d60060051332253306b05b13305c002001306a00233004003306b002419483290621833983480120c2998049833183380118331833800c01105c200e8b20c23067375460ca00860cc6ea8c190c194c198dd500098029831801a0b6805c00e97adef6c604154606e0066444a660be921394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3532335d00198009111299831a49394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3538395d00159800801c52f5bded8c113259800801c52f5bded8c1159800998131bae001375c60c660ca007159800998131bae0010088acc004cdd79ba63066003374c60cc00919800911299833a49394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3533365d0015980080144cc178894cc1a52401394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3439385d00159800800c52f5bded8c11332253306b05b13305c0020013376060d060d40026ea0cdc0a40006eb4c1b0c1a8004c008c1ac005062000c4c9660020051306a0038acc004cdc89bae001375c60ce60d200515980099baf001306730690028acc004cdc399b81375a60d660d20066eb4c1acc1a400920008998021835001983500144cc894cc1a81684cc16c008004cdd80009ba8337026eb4c1acc1a400cdd698359834801198021835001983500120c88999129983502d09982d80100099bb000137506eb4c1acc1a400ccc010c1a800c00906444cc894cc1a81684cc16c008004cdd8183398348011ba83370290001bad306b306900233004003306a00241908308c198c1a00090604dd5983398328024dd598339832801a0b68b20c08acc004cdd79833983280218339832801c6600260cc0093066003801200a8b20c0418115980099baf3067306500430673065003880145906020c0417460c460c800682e26eac00a6eac00697adef6c6041586eb800c64b30010018800c4cc16924123697373756572206c6f67696320736372697074206d75737420626520696e766f6b65640000141586607660b460b86ea8c168c16cc16cc16c004030c966002003100189982d24811b6469726563746f7279206e6f6465206973206e6f742076616c69640000141586609860b401c6eacc168014c966002003100189982d24811c696e70757420696e646578657320617265206e6f7420756e6971756500001415864a660b4921204c5b6c69622f506c7574617263682f436f72652f4c6973742e68733a3236385d0013370e6f526002910120000000000000000000000000000000000000000000000000000000000000000000803d28af40a660b49201204c5b6c69622f506c7574617263682f436f72652f4c6973742e68733a3235335d0019800911129982f249204c5b6c69622f506c7574617263682f436f72652f4c6973742e68733a3235355d0015980099b89480f000e3300133702006901e4cdc0001240793021001401115980099b89480a000e330013370200690144cdc0001240513023001401115980099b894805000e3300133702006900a4cdc000124029303a001401119800911299830a481204c5b6c69622f506c7574617263682f436f72652f4c6973742e68733a3234315d00159800800c400a26600666e000092002306300141694800200282a905a20b44169001a400100741446eb4c168c16c01e2934590551bac30590018a9982c2496c5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3331383a372d32380016415060b400460aa0026ea8c158c15c004c158c15cdd5182a982b182b9baa3302e00d375a60aa008664a660a80862609644a660ac09c2b30010018a5eb0226644a660b0090266092004002600660ae002600460b0002827894cc1512401394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3331345d001375a0026eb0c150008c150004c14c00626464b30013302a3370e60a46ea8c150c15404520043259800800c40062660a89211270726f6720746f6b656e73206573636170650000141406603464a660a89201394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3132355d00132533055491214c5b6c69622f506c7574617263682f436f72652f56616c75652e68733a3534385d0013057001980091129982ba49394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3132365d00159800800c400a2660066464b300133039305a305c375400201b198009129982d824899b800020018024dd5982d182d801207a880220aa3059001305a375460b000260b2002828297adef6c60800a096375860a801c003149a2c8278cc894cc1512401394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3137355d001329800911129982ca49394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3138305d00159800801440062646464b30013370e900000144c8c8c8c96600266e1d20040028992cc004cc0e0cc0e0c96600200310018998312481204d697373696e67207265717569726564207472616e73666572207363726970740000141786608660c460c86ea8c188c18cc18c004050c96600200310018998312481186469726563746f72792070726f6f66206d69736d6174636800001417866ebcc188004c180c188028c9660020031001899831248110696e76616c696420646972206e6f6465000014178660a801e6eacc18801633001306300b98318054c94cc1881484cc14c004028c18802900c45905d1bac30610018a998302496d5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3138393a32352d34360016417060c400460ba0026ea8c178c17c004c178c17cdd5182e982f182f9baa3008375a60ba0031323232325980099b874801000a264646464b30013303b3303b3259800800c40062660ca07c0028308cdc8001001992cc004006200313306503e001418466e4000c004c9660020031001899832a4912696e76616c696420646972206e6f6465206e000014184660ae0246eacc19402233001306600e9833006c03100f4590601bae30643065003375c60c60046eb8c180c188028dd61830800c54cc1812416d5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3230353a32352d34390016417060c400460ba0026ea8c178c17c004c178c17cdd5182e982f182f9baa3008375a60ba00282c0c178008c164004dd5182d001a0a48014c15c01297adef6c604130605c01a60a600e6eb0c14c008cc894cc14d241384c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a39335d001980091129982b2481384c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a39345d00159800800c400a2660066464b3001330383059305b375400201913232325980099b874800800a2d13232325980099b874800800a2b300133040305f0010118cc004894cc1801384cdc0001000c02a6eacc17cc180021042454cc1792411f4d697373696e6720726571756972656420736372697074207769746e6573730016416915980099820182f800806c6600244a660c009c266e00008006015375660be60c001082122a660bc9211b4d697373696e6720726571756972656420706b207769746e6573730016416882d0c180008c16c004dd5182e000a0ae305d0023058001375460b260b66ea8c164c168c16cdd5000c4011054182c000982c9baa305730583059375460ae00260b0002827a97adef6c60800a094375860a460a660a60166eb0c14803904d182980118270009baa304f00c33253304e03d1304522533050048159800800c52f5c113322533052042133043002001300330510013002305200141244a6609c921394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3238355d001304f30513754609a0026eacc13801cc134c138004dd61826000c54cc12d24016c5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3237383a332d32340016411c6aae78008d55ce8009baa357426ae88d5d11aab9e37546ae84d5d11aab9e3754664a66ae7124011f4c5b6c69622f506c7574617263682f436f72652f4c6973742e68733a38325d00132323002233002002001230022330020020012253357389211f4c5b6c69622f506c7574617263682f436f72652f4c6973742e68733a38325d00159800800c5a2b30013003304c00189826000c4c008c134005047208825335738921394c5b6c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3237325d001332253357389201214c5b6c69622f506c7574617263682f436f72652f56616c75652e68733a3132385d0013325335738921264c5b2e2f506c7574617263682f496e7465726e616c2f4c6973744c696b652e68733a3233345d0013232300223300200200123002233002002001225335738921264c5b2e2f506c7574617263682f496e7465726e616c2f4c6973744c696b652e68733a3133375d00159800800c528454cc020c00cc03c0044c008c0400050071299ab9c491214c5b6c69622f506c7574617263682f436f72652f56616c75652e68733a3132395d0013375e6aae7400400c004028dd59aba1357446aae78dd51aba1357446aae78dd50008009bac35742006a66ae7124011e4c5b2e2f506c7574617263682f526570722f446174612e68733a3332355d001357446ae88d5d11aba2001357440026ae88004d55cf1baa357420046ae88004d55cf1baa00101", "description": "Programmable Logic Global", "type": "PlutusScriptV3" } \ No newline at end of file diff --git a/generated/scripts/unapplied/prod/programmableLogicGlobal.json b/generated/scripts/unapplied/prod/programmableLogicGlobal.json index 292c7481..83272fe7 100644 --- a/generated/scripts/unapplied/prod/programmableLogicGlobal.json +++ b/generated/scripts/unapplied/prod/programmableLogicGlobal.json @@ -1,5 +1,5 @@ { - "cborHex": "59056559056201010022323232323232323232325980099b874801000a2646464646464b30013370e900000144c8c9660026603a66e1cc0acdd518161816808a4008646602c46602e466e24dd6981880099914c00400a90004a600200548002eb66eac00501e401501d1bae302e002375c605c0026eacc0c0004008c8c0ba6002444b3001001880144cc00cc8c96600266048606460686ea8004036330017010049bab303230330024081100440bc606200260646ea8c0c0004c0c400502b52f5bded8c1001409c6eb0c0b003a2934590291991194c004888966002005100189919192cc004cdc3a40000051323232325980099b874801000a264b30013302b3302b3302d303a303c375460746076607600202866ebcc0e8004c0e4c0e8028cc0c803cdd5981d002c660026076017303b00a99818981d005004a0188b206e375860720031640d86074004606c0026ea8c0d8c0dc004c0d8c0dcdd5181a981b181b9baa3008375a606a0031323232325980099b874801000a264646464b30013302e3302e3372000400666e4000c004cc0d4048dd5981e804466002607c01d303e00d806201e8b2074375c6078607a0066eb8c0ec008dd7181c981d0051bac30390018b206c303a00230360013754606c606e002606c606e6ea8c0d4c0d8c0dcdd518041bad303500140c8606c00460640026ea8c0c800d02d400a605e0094bd6f7b6302050302100d302b007375860560046645300122259800800c400a2660066464b30013302330313033375400201913232325980099b874800000a2646464b30013370e9000001456600266054606e00201b19800b808054dd5981b981c004204a8b20688acc004cc0a8c0dc0040463300170100a9bab3037303800840951640d081a0c0e0008c0d0004dd5181a000c59031181a80118188009baa3031303337546062606460666ea800620088170c0c0004c0c4dd51817981818189baa302f001303000140a94bd6f7b63040050261bac302a302b302b00b3758605401d13232323232325980099b874801000a2646464646464b300133027330273302733027330273370f300122259800800c400a2b30013302b0143039303b3754607260766ea8c0e4c0e8c0ecdd5181c800c4cc00ccdc0001240046074003133003002303a00140d881a29000402d0302400466050606c60706ea800c044cdd7998151bac3036018375a606c01866e9520003302d0033302d374c0026605a606c606e0086605a98103d87a80004bd7019814981b181c1baa3036303730373037006010329800800d28528a0683375e6e98008dd300099817181b0091bab303600a8a4d1640cc646606044b30010018a5eb7bdb1822b30013375e606e607000200713039001899817981c0009801181c800a06a40cc004606a00a6eacc0d0008c0cc008c0cc004c0ccdd51818981918199baa33025006375a60620106eb0c0c00062c8168c0c4008c0b4004dd518169817000981698171baa302c302d302e3754660400186eb4c0b0c0b4008dd618158079815800a04e302b00230270013754604e018664604244b30010018a5eb82266040600660520026004605400281208c09cc0a4dd518130009bab302600730253026001375860480031640846aae78008d55ce8009baa357426ae88d5d11aab9e37546ae84d5d11aab9e3754664646460044660040040024600446600400400244b30010018b45660026006604800313024001898011812800a042407c46644664646460044660040040024600446600400400244b30010018a508a9980318019805000898011805800a00a23375e6aae7400400c004028dd59aba1357446aae78dd51aba1357446aae78dd50008009bac357420066ae88d5d11aba2357440026ae88004d5d10009aab9e37546ae84008d5d10009aab9e3754003", + "cborHex": "59090d59090a01010022323232323232323232325980099b874801000a2646464646464b30013370e900100144c8c8c8c8c8c8c96600266e1d20040028992cc004cc090cc090cc0926002606c0033758606c02d3330302225980099b89480f000a26600666e04009203c301e0018acc004cdc4a4050005133003337020049014180f800c56600266e25201400289980199b8100248050c0a40062666066444b30013370e00490004400626600666e040092002303d00140e400400281b1036206c375a606c00e6eb0c0d80512223232980091112cc00400e2b300133026330392259800800c52f5bded8c1132598009981a182118221baa30420010198cc005c04dd598211821800cc00cc10c009036452f5bded8c081f8c10cdd51820800a07800233036337600126e980052f5bded8c114a31640f1132323259800998181981a182118221baa30420020193322323253302f3303830460023046001133223232325980099b874800000a2646464b30013370e900000144cdd798270021827000c5282096304f002304b001375400913232325980099b874800800a2946294104b182780118258009baa0044120609800460900026ea8008c118c11c008c118c11c004c11cdd500118231baa002304200130420028cc004c10c01a608600b98009112cc00400a297adef6c608992cc00400a297adef6c608acc004cdc79bae001375c608a608c00513303d337600026ea0cdc01bad30483046003375a6090608c00466008608e006608e00515980099b90375c0026eb8c114c11800a26607a608c00666008608e00600513303d3046002330040033047002410c8219041182218228012080998049821182180118211821800c01103c200e8b207e30433754608200860846ea8c100c104c108dd50009802981f801a074805c00e97adef6c6040d4605200664453001222259800801c52f5bded8c113259800801c52f5bded8c115980099b8f375c0026eb8c100c10400e2b30013371e6eb80040222b30013375e6e98c10800cdd31821002466002444b300100289981f112cc004006297adef6c6089981e99bb030453046001375066e052000375a6090608c0026004608e0028208006264b300100289823001c56600266e44dd70009bae304430450028acc004cdd780098221822801456600266e1ccdc09bad30473045003375a608e608a004900044cc010c11800cc11800a26607866ec0004dd419b81375a608e608a0066eb4c11cc114008cc010c11800cc11800904244cc0f0cdd80009ba8375a608e608a00666008608c006004821226607866ec0c110c114008dd419b8148000dd698239822801198020019823001208441006086608800481fa6eacc10cc1040126eacc10cc10400d03b45903e456600266ebcc10cc104010c10cc10400e3300130420049821001c00900545903e207c8acc004cdd79821982080218219820801c400a2c81f103e2078303f304000340ed375600537560034bd6f7b630206c375c00633029303630383754606c606e606e606e0020186605c606c01c6eacc0d8014c8cdc39bd49800a44120000000000000000000000000000000000000000000000000000000000000000000803d28af41300122225980099b89480f000e3300133702006901e4cdc0001240793020001401115980099b89480a000e330013370200690144cdc0001240513021001401115980099b894805000e3300133702006900a4cdc000124029302b0014011198009112cc0040062005133003337000049001181f800a072a400100140d481c10382070800d2000803a062375a606c606e00f149a2c8198dd6181a800c59032181b00118190009baa303230330013032303337546062606460666ea8cc080034dd6981880219815112cc004006297ac08998149bad30320013002303300140b46eb0c0c0008c0c0004c0bc00626464b30013301e3370e605e6ea8c0c0c0c404520043301732303298009112cc004006200513300332325980099814181b181c1baa00100d8cc005c040126eacc0d8c0dc00902a44011033181a800981b1baa3034001303500140bd4bd6f7b630400502b1bac303000e0018a4d1640b46644653001222259800801440062646464b30013370e900000144c8c8c8c96600266e1d20040028992cc004cc0b0cc0b0cc0c4c0f8c100dd5181f181f981f80080a19baf303e001303d303e00a3303600f3756607c00b19800981f805cc0fc02a6606a607c01401280622c81d8dd6181e800c5903a181f001181d0009baa303a303b001303a303b37546072607460766ea8c020dd6981c800c4c8c8c8c96600266e1d20040028991919192cc004cc0bccc0bccdc800100199b9000300133039012375660820111980098210074c108036018807a2c81f0dd7182018208019bae303f002375c607a607c0146eb0c0f40062c81d0c0f8008c0e8004dd5181d181d800981d181d9baa3039303a303b375460106eb4c0e4005036181d001181b0009baa303600340c5002981980252f5bded8c08160c080034c0bc01cdd6181780119914c00488966002003100289980199192cc004cc09cc0d4c0dcdd500080644c8c8c96600266e1d200000289919192cc004cdc3a400000515980099817181d800806c66002e02015375660766078010817a2c81c22b30013302e303b0010118cc005c0402a6eacc0ecc0f002102f4590382070303c0023038001375460700031640d46072004606a0026ea8c0d4c0dcdd5181a981b181b9baa0018802206430340013035375460666068606a6ea8c0cc004c0d000502e52f5bded8c100140a86eb0c0b8c0bcc0bc02cdd618170072056302f002302b00137546056018664604a44b30010018a5eb822660486006605a0026004605c00281408c0acc0b4dd518150009bab302a0073029302a001375860500031640946aae78008d55ce8009baa357426ae88d5d11aab9e37546ae84d5d11aab9e3754664646460044660040040024600446600400400244b30010018b45660026006605000313028001898011814800a04a408c46644664646460044660040040024600446600400400244b30010018a508a9980318019805000898011805800a00a23375e6aae7400400c004028dd59aba1357446aae78dd51aba1357446aae78dd50008009bac357420066ae88d5d11aba2357440026ae88004d5d10009aab9e37546ae84008d5d10009aab9e3754003", "description": "Programmable Logic Global", "type": "PlutusScriptV3" } \ No newline at end of file diff --git a/generated/scripts/unapplied/tracing/programmableLogicGlobal.json b/generated/scripts/unapplied/tracing/programmableLogicGlobal.json index 67297b68..a8bdf7b2 100644 --- a/generated/scripts/unapplied/tracing/programmableLogicGlobal.json +++ b/generated/scripts/unapplied/tracing/programmableLogicGlobal.json @@ -1,5 +1,5 @@ { - "cborHex": "5908ba5908b7010100223232323232323253357389212245787472616374696e672070726f746f636f6c20706172616d65746572205554784f0013232325980099b874801000a26464a6603e92011a45787472616374696e6720696e766f6b65642073637269707473001323232325980099b874800000a26464b30013301e3370e605a6ea8c0b8c0bc04520043259800800c400626604c9211270726f6720746f6b656e73206573636170650000140b0646602c46602e466e24dd6981980099914c00400a90004a600200548002eb66eac00501e401501d1bae3030002375c60600026eacc0c8004008c8c0c26002444b3001001880144cc00cc8c9660026604a6068606c6ea8004036330017010049bab303430350024085100440c4606600260686ea8c0c8004c0cc00502d52f5bded8c100140a46eb0c0b803a29345902b1991194c004888966002005100189919192cc004cdc3a40000051323232325980099b874801000a264b30013302c3302c3259800800c40062660689201204d697373696e67207265717569726564207472616e73666572207363726970740000140e86605c6078607c6ea8c0f0c0f4c0f4004050c966002003100189981a2481186469726563746f72792070726f6f66206d69736d617463680000140e866ebcc0f0004c0ecc0f0028c966002003100189981a248110696e76616c696420646972206e6f64650000140e86606801e6eacc0f001633001303d00b981e8054cc0c8c0f002802500c4590391bac303b0018a9981924816d5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3138353a32352d3436001640e0607800460700026ea8c0e0c0e4004c0e0c0e4dd5181b981c181c9baa3008375a606e0031323232325980099b874801000a264646464b30013302f3302f3259800800c400626606e05800281e8cdc8001001992cc004006200313303702c00140f466e4000c004c966002003100189981ba4912696e76616c696420646972206e6f6465206e0000140f46606e0246eacc0fc02233001304000e9820006c03100f45903c1bae303e303f003375c607a0046eb8c0ecc0f0028dd6181d800c54cc0c92416d5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3230313a32352d3439001640e0607800460700026ea8c0e0c0e4004c0e0c0e4dd5181b981c181c9baa3008375a606e00281a0c0e0008c0d0004dd5181a001a05e8014c0c401297adef6c6040a8604401a605a00e6eb0c0b4008cc8a6002444b3001001880144cc00cc8c966002660486066606a6ea80040322646464b30013370e900000144c8c8c96600266e1d20000028acc004cc0acc0e40040363300170100a9bab3039303a008409915330304911b4d697373696e6720726571756972656420706b207769746e657373001640d915980099815981c800808c66002e0201537566072607401081322a660609211f4d697373696e6720726571756972656420736372697074207769746e657373001640d881b0c0e8008c0d8004dd5181b000c59033181b80118198009baa30333035375460666068606a6ea800620088180c0c8004c0ccdd51818981918199baa3031001303200140b14bd6f7b63040050281bac302c302d302d00b3758605801d13253302449109505365697a6541637400132323232325980099b874801000a2646464646464b300133028330283302833028330283370f300122259800800c400a2b30013302c014303b303d37546076607a6ea8c0ecc0f0c0f4dd5181d800c4cc00ccdc0001240046078003133003002303c00140e081b29000402d0322400466052607060746ea800c044cdd7998159bac3038018375a607001866e9520003302e0033302e374c0026605c607060720086605c98103d87a80004bd7019815181c181d1baa3038303930393039006010329800800d28528a06c3375e6e98008dd300099818181c0091bab303800a8a4d1640d4646606444b30010018a5eb7bdb1822b30013375e607260740020071303b001899818181d0009801181d800a06e40d4004606e00a6eacc0d8008c0d4008c0d4004c0d4dd51819981a181a9baa33026006375a60660106eb0c0c80062a660529216c5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3331353a372d3238001640bc6066004605e0026ea8c0bcc0c0004c0bcc0c0dd51817181798181baa3302100c375a605c605e0046eb0c0b403cc0b4005029181680118148009baa302900c33230232259800800c52f5c11330213003302b0013002302c00140984605260566ea8c0a0004dd59814003981398140009bac30260018a9980ea4816c5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3237313a332d32340016408c6aae78008d55ce8009baa357426ae88d5d11aab9e37546ae84d5d11aab9e3754664646460044660040040024600446600400400244b30010018b45660026006604c00313026001898011813800a046408446644664646460044660040040024600446600400400244b30010018a508a9980318019805000898011805800a00a23375e6aae7400400c004028dd59aba1357446aae78dd51aba1357446aae78dd50008009bac357420066ae88d5d11aba2357440026ae88004d5d10009aab9e37546ae84008d5d10009aab9e37540021", + "cborHex": "590d9c590d99010100223232323232323253357389212245787472616374696e672070726f746f636f6c20706172616d65746572205554784f0013232325980099b874801000a26464a6604692011a45787472616374696e6720696e766f6b65642073637269707473001323232325980099b874800000a26464b30013301e3370e60626ea8c0c8c0cc04520043259800800c40062660549211270726f6720746f6b656e73206573636170650000140c06602c646069300122259800800c400a2660066464b3001330273038303a375400201b19800b808024dd5981c181c80120428802206a303700130383754606c002606e002818a97adef6c60800a05a3758606401c003149a2c8178cc88ca60024444b30010028800c4c8c8c96600266e1d20000028991919192cc004cdc3a4008005132598009981619816192cc0040062003133038491204d697373696e67207265717569726564207472616e73666572207363726970740000140f866060608060846ea8c100c104c104004050c966002003100189981c2481186469726563746f72792070726f6f66206d69736d617463680000140f866ebcc100004c0fcc100028c966002003100189981c248110696e76616c696420646972206e6f64650000140f86607001e6eacc10001633001304100b98208054cc0d8c10002802500c45903d1bac303f0018a9981b24816d5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3138393a32352d3436001640f0608000460780026ea8c0f0c0f4004c0f0c0f4dd5181d981e181e9baa3008375a60760031323232325980099b874801000a264646464b30013302f3302f3259800800c400626607604e0028208cdc8001001992cc004006200313303b027001410466e4000c004c966002003100189981da4912696e76616c696420646972206e6f6465206e000014104660760246eacc10c02233001304400e9822006c03100f4590401bae30423043003375c60820046eb8c0fcc100028dd6181f800c54cc0d92416d5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3230353a32352d3439001640f0608000460780026ea8c0f0c0f4004c0f0c0f4dd5181d981e181e9baa3008375a607600281c0c0f0008c0e0004dd5181c001a0668014c0d401297adef6c6040b8604c01a606200e6eb0c0c4008cc8a6002444b3001001880144cc00cc8c9660026604c606e60726ea80040322646464b30013370e900000144c8c8c96600266e1d20000028acc004cc0b4c0f40040363300170100a9bab303d303e008409915330344911b4d697373696e6720726571756972656420706b207769746e657373001640e915980099816981e800808c66002e020153756607a607c01081322a660689211f4d697373696e6720726571756972656420736372697074207769746e657373001640e881d0c0f8008c0e8004dd5181d000c59037181d801181b8009baa303730393754606e607060726ea8006200881a0c0d8004c0dcdd5181a981b181b9baa3035001303600140c14bd6f7b630400502c1bac30303031303100b3758606001d1323232323232325980099b874801000a264b30013302433024330243259800800c40062660609211f6d696e692d6c656467657220696e76617269616e74732076696f6c617465640000140d9300130380019bac30380169998191112cc004cdc4a407800513300333702004901e1816000c56600266e25202800289980199b81002480a0c0b40062b300133712900a00144cc00ccdc080124028605e00313330352225980099b87002480022003133003337020049001181f800a07600200140e081c10381bad30380073758607002891119194c004888966002007159800998129981d912cc004006297adef6c608992cc004cc0ccc110c118dd5182200080cc66002e026eacc110c1140066006608a004816a297adef6c604104608a6ea8c10c00503e0011981b99bb0009374c00297adef6c608a518b207c89919192cc004cc0c0c966002003100189981e248141696e707574207061796d656e742063726564656e7469616c206973206e6f74207468652070726f6772616d6d61626c65206c6f6769632063726564656e7469616c000014108660666088608c6ea8c110008064c966002003100189981e248126636f72726573706f6e64696e67206f75747075743a2061646472657373206d69736d6174636800001410866446464a6606a6606e6090004609000226644646464b30013370e900000144c8c8c96600266e1d2000002899baf305000430500018a50413460a2004609a0026ea80122646464b30013370e90010014528c528209a3051002304d00137540088250c138008c128004dd5001182418248011824182480098249baa00230483754004608800260880051980098228034c114017300122259800801452f5bded8c113259800801452f5bded8c115980099b8f375c0026eb8c11cc12000a26607c66ec0004dd419b80375a609460900066eb4c128c120008cc010c12400cc12400a2b3001337206eb8004dd71823982400144cc0f8c12000ccc010c12400c00a26607c609000466008006609200482290452086304630470024109330093044304500230443045001802207c401d164104608a6ea8c10c010c110dd51821182198221baa0013005304100340f100b801d2f5bded8c081b8c0bc00cc88a60024444b30010038a5eb7bdb182264b30010038a5eb7bdb1822b30013371e6eb8004dd718211821801c56600266e3cdd7000804456600266ebcdd318220019ba630440048cc004889660020051330402259800800c52f5bded8c113303e33760608e60900026ea0cdc0a40006eb4c128c120004c008c124005043000c4c966002005130480038acc004cdc89bae001375c608c608e00515980099baf001304630470028acc004cdc399b81375a6092608e0066eb4c124c11c00920008998021824001982400144cc0f4cdd80009ba8337026eb4c124c11c00cdd6982498238011980218240019824001208889981e99bb000137506eb4c124c11c00ccc010c12000c00904444cc0f4cdd8182318238011ba83370290001bad3049304700233004003304800241108210c114c1180090414dd5982298218024dd598229821801a07a8b20808acc004cdd79822982180218229821801c6600260880093044003801200a8b2080410115980099baf30453043004304530430038801459040208040f86082608400681ea6eac00a6eac00697adef6c6040e06eb800c64b30010018800c4cc0c1240123697373756572206c6f67696320736372697074206d75737420626520696e766f6b65640000140d866050607060746ea8c0e0c0e4c0e4c0e4004030c966002003100189981824811b6469726563746f7279206e6f6465206973206e6f742076616c69640000140d866060607001c6eacc0e0014c966002003100189981824811c696e70757420696e646578657320617265206e6f7420756e697175650000140d86466e1cdea4c005220120000000000000000000000000000000000000000000000000000000000000000000803d28af41300122225980099b89480f000e3300133702006901e4cdc000124079302e001401115980099b89480a000e330013370200690144cdc000124051302f001401115980099b894805000e3300133702006900a4cdc00012402930310014011198009112cc00400620051330033370000490011820800a076a400100140dc81d103a2074800d2000803a066375a6070607200f149a2c81a8dd6181b800c54cc0b924016c5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3331383a372d3238001640d0607000460680026ea8c0d0c0d4004c0d0c0d4dd51819981a181a9baa3302600d375a60660086605844b30010018a5eb022660546eb4c0d0004c008c0d400502f1bac30320023032001303100140b46062004605a0026ea8c0b4030cc8c09c896600200314bd7044cc094c00cc0bc004c008c0c000502a1181698179baa302c0013756605800e605660580026eb0c0a80062a660429216c5061747465726e206d61746368206661696c75726520696e207175616c69666965642027646f2720626c6f636b206174206c69622f536d617274546f6b656e732f436f6e7472616374732f50726f6772616d6d61626c654c6f676963426173652e68733a3237383a332d32340016409c6aae78008d55ce8009baa357426ae88d5d11aab9e37546ae84d5d11aab9e3754664646460044660040040024600446600400400244b30010018b4566002600660540031302a001898011815800a04e409446644664646460044660040040024600446600400400244b30010018a508a9980318019805000898011805800a00a23375e6aae7400400c004028dd59aba1357446aae78dd51aba1357446aae78dd50008009bac357420066ae88d5d11aba2357440026ae88004d5d10009aab9e37546ae84008d5d10009aab9e37540021", "description": "Programmable Logic Global", "type": "PlutusScriptV3" } \ No newline at end of file diff --git a/nix/containers.nix b/nix/containers.nix index 0927eb46..fa832b41 100644 --- a/nix/containers.nix +++ b/nix/containers.nix @@ -3,7 +3,7 @@ frontendNpm = pkgs.buildNpmPackage rec { name = "frontend"; src = ../frontend; - npmDepsHash = "sha256-fV2smCrCa4Egb4WZtiQsaDN4egiLYDKJVlNkjAjwETA="; + npmDepsHash = "sha256-l57FlQqrxFLzpSxvwy5vSFElAzdg9om2XlF4fYx807U="; npmPackFlags = [ "--ignore-scripts" ]; npmBuildScript = "export"; installPhase = '' diff --git a/preprod-network/.envrc.local b/preprod-network/.envrc.local new file mode 100644 index 00000000..2b994c7b --- /dev/null +++ b/preprod-network/.envrc.local @@ -0,0 +1 @@ +export NEXT_PUBLIC_BLOCKFROST_API_KEY=preprodDbWTrlobwcl5ilo68prIlLVS82OwDzL7 \ No newline at end of file diff --git a/preprod-network/.envrc.local.example b/preprod-network/.envrc.local.example new file mode 100644 index 00000000..6f96e37a --- /dev/null +++ b/preprod-network/.envrc.local.example @@ -0,0 +1 @@ +export NEXT_PUBLIC_BLOCKFROST_API_KEY=preprod... \ No newline at end of file diff --git a/preprod-network/blockfrost b/preprod-network/blockfrost new file mode 100644 index 00000000..ba3f6de3 --- /dev/null +++ b/preprod-network/blockfrost @@ -0,0 +1 @@ +preprodDbWTrlobwcl5ilo68prIlLVS82OwDzL7 \ No newline at end of file diff --git a/preprod-network/policy-issuers.sqlite b/preprod-network/policy-issuers.sqlite new file mode 100644 index 00000000..0a6da519 Binary files /dev/null and b/preprod-network/policy-issuers.sqlite differ diff --git a/preprod-network/preprod-env.json b/preprod-network/preprod-env.json new file mode 100644 index 00000000..b224956a --- /dev/null +++ b/preprod-network/preprod-env.json @@ -0,0 +1,13 @@ +{ + "blockfrost_key": "preprodDbWTrlobwcl5ilo68prIlLVS82OwDzL7", + "blockfrost_url": "https://cardano-preprod.blockfrost.io/api/v0", + "explorer_url": "https://preprod.cexplorer.io/tx", + "mint_authority": "west gun guitar chicken sample lend manage swap auto box hero artwork close pottery glare genuine develop next when erupt other spin anchor exhaust", + "minting_policy": "96096844c3a3f6800da3cd8cc1aa42203fe3f74faa1dd31564d98e6c", + "network": "Preprod", + "prog_logic_base_hash": "ca50fbaee265bbd04ef5e7498b104953e8a7f70d14c8a5b5f1f7e597", + "token_name": "575354", + "transfer_logic_address": "addr_test1vzgxw8ufrkdrm40tredarr33cfdylhs2p7q4a6dqfnx9k5q62pvja", + "user_a": "during dolphin crop lend pizza guilt hen earn easy direct inhale deputy detect season army inject exhaust apple hard front bubble emotion short portion", + "user_b": "silver legal flame powder fence kiss stable margin refuse hold unknown valid wolf kangaroo zero able waste jewel find salad sadness exhibit hello tape" + } \ No newline at end of file diff --git a/preprod-network/preprod_script_root.json b/preprod-network/preprod_script_root.json new file mode 100644 index 00000000..f76e2c3e --- /dev/null +++ b/preprod-network/preprod_script_root.json @@ -0,0 +1,5 @@ +{ + "tx_in": "80a1c1c3cc9b5437d4343cf2a9f2e04a1a390b0c7a82ff1ce9c5ce9bde86766e#3", + "issuance_cbor_hex_tx_in": "f4229c4b13cb0b2d943991766c096732b4732b57f6b78ed16a8e062607299032#3", + "target": "Production" + } \ No newline at end of file diff --git a/src/examples/aiken/haskell/lib/Wst/Cli.hs b/src/examples/aiken/haskell/lib/Wst/Cli.hs index 48a0b835..46e3aaaa 100644 --- a/src/examples/aiken/haskell/lib/Wst/Cli.hs +++ b/src/examples/aiken/haskell/lib/Wst/Cli.hs @@ -18,7 +18,7 @@ import Convex.Wallet.Operator qualified as Operator import Data.Aeson qualified as Aeson import Data.ByteString.Base16 qualified as Base16 import Data.ByteString.Lazy qualified as ByteString -import Data.Foldable (traverse_) +import Data.Foldable (for_) import Data.Functor (void) import Data.String (IsString (..)) import Data.Text qualified as Text @@ -91,8 +91,8 @@ runCommand com = do logInfo $ "registry" :# [ "nodes.count" .= length nodes ] - flip traverse_ paymentCred $ \addr_ -> do - outputs <- Query.userProgrammableOutputs addr_ + for_ paymentCred $ \addr_ -> do + outputs <- Query.userProgrammableOutputs (addr_, Nothing) userAddr <- traverse Directory.programmableTokenReceivingAddress paymentCred let val = foldMap (txOutValue . UTxODat.uOut) outputs logInfo $ "User account" :# diff --git a/src/examples/aiken/haskell/test/Wst/Aiken/Test.hs b/src/examples/aiken/haskell/test/Wst/Aiken/Test.hs index 8bed2eb0..196983d8 100644 --- a/src/examples/aiken/haskell/test/Wst/Aiken/Test.hs +++ b/src/examples/aiken/haskell/test/Wst/Aiken/Test.hs @@ -133,7 +133,7 @@ transferAikenPolicy = do let paymentCred = C.PaymentCredentialByKey (Wallet.verificationKeyHash Wallet.w2) runAsAdmin $ - Query.userProgrammableOutputs paymentCred + Query.userProgrammableOutputs (paymentCred, Nothing) >>= void . Test.expectN 0 "user programmable outputs" runAsAdmin $ do @@ -141,7 +141,7 @@ transferAikenPolicy = do >>= void . sendTx . signTxOperator Test.admin runAsAdmin $ - Query.userProgrammableOutputs paymentCred + Query.userProgrammableOutputs (paymentCred, Nothing) >>= void . Test.expectN 1 "user programmable outputs" runAsAdmin $ do @@ -149,6 +149,6 @@ transferAikenPolicy = do >>= void . sendTx . signTxOperator Test.admin runAsAdmin $ - Query.userProgrammableOutputs paymentCred + Query.userProgrammableOutputs (paymentCred, Nothing) >>= void . Test.expectN 2 "user programmable outputs" diff --git a/src/examples/regulated-stablecoin/exe/calculate-hashes/Main.hs b/src/examples/regulated-stablecoin/exe/calculate-hashes/Main.hs index 86a4bfbb..2e502c00 100644 --- a/src/examples/regulated-stablecoin/exe/calculate-hashes/Main.hs +++ b/src/examples/regulated-stablecoin/exe/calculate-hashes/Main.hs @@ -19,6 +19,9 @@ main = System.Environment.getArgs >>= \case [fp, addr] -> do putStrLn $ "Calculating hashes using " <> fp <> " with adddress " <> addr operator <- decodeAddress addr + let stakeCred = case operator of + (C.ShelleyAddress _ntw _pmt (C.fromShelleyStakeReference -> C.StakeAddressByValue sCred)) -> Just sCred + _ -> Nothing (nid, pkh) <- paymentHashAndNetworkId operator dirEnv <- Env.mkDirectoryEnv <$> loadFromFile fp let scriptRoot = @@ -26,6 +29,7 @@ main = System.Environment.getArgs >>= \case (srTarget $ Env.dsScriptRoot dirEnv) dirEnv pkh + stakeCred let transferLogicEnv = Env.mkTransferLogicEnv scriptRoot blacklistEnv = Env.mkBlacklistEnv scriptRoot diff --git a/src/examples/regulated-stablecoin/exe/export-smart-tokens/Main.hs b/src/examples/regulated-stablecoin/exe/export-smart-tokens/Main.hs index fadbf402..e3c05919 100644 --- a/src/examples/regulated-stablecoin/exe/export-smart-tokens/Main.hs +++ b/src/examples/regulated-stablecoin/exe/export-smart-tokens/Main.hs @@ -1,30 +1,31 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} + module Main (main) where import Cardano.Api qualified as C import Cardano.Api.Shelley qualified as C import Cardano.Binary qualified as CBOR +import Control.Applicative (optional) import Control.Monad (when) import Control.Monad.IO.Class import Control.Monad.Reader (asks) -import Data.Aeson (KeyValue ((.=)), eitherDecode, object) +import Data.Aeson (KeyValue ((.=)), object) import Data.Aeson.Encode.Pretty (encodePretty) import Data.Bifunctor (first) import Data.ByteString.Base16 qualified as Base16 import Data.ByteString.Lazy qualified as LBS -import Data.ByteString.Lazy.Char8 qualified as LBS8 +import Data.Data (Proxy (..)) import Data.Foldable (traverse_) import Data.String (IsString (..)) import Data.Text (Text, pack) import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Data.Text.IO qualified as TIO -import Options.Applicative (Parser, argument, customExecParser, disambiguate, - eitherReader, help, helper, idm, info, metavar, - optional, prefs, showHelpOnEmpty, showHelpOnError, - strArgument) -import Options.Applicative.Builder (ReadM) +import Options.Applicative (Parser, ReadM, argument, disambiguate, eitherReader, + help, idm, info, metavar, prefs, showHelpOnEmpty, + showHelpOnError, strArgument) +import Options.Applicative.Extra (customExecParser, helper) import Plutarch.Evaluate (applyArguments, evalScript) import Plutarch.Internal.Term (Config (..), LogLevel (LogInfo), Script, TracingMode (DoTracing, DoTracingAndBinds), @@ -126,18 +127,18 @@ breakCborHex toSplitOn cborHex = main :: IO () main = do - let progLogicBase = V3.ScriptCredential (V3.ScriptHash "deadbeef") - mintingLogicA = V3.ScriptHash $ stringToBuiltinByteStringHex "deadbeefdeadbeef" - mintingLogicB = V3.ScriptHash $ stringToBuiltinByteStringHex "deadbeefcafebabe" - (prefixIssuerCborHex, postfixIssuerCborHex) = issuerPrefixPostfixBytes progLogicBase - let baseCompiled = case compile NoTracing (mkProgrammableLogicMinting # pconstant progLogicBase) of - Right compiledScript -> compiledScript - Left err -> error $ "Failed to compile base issuance script: " <> show err - - TIO.writeFile ("generated" "unapplied" "test" "prefixIssuerCborHex.txt") prefixIssuerCborHex - TIO.writeFile ("generated" "unapplied" "test" "postfixIssuerCborHex.txt") postfixIssuerCborHex - _writePlutusScriptWithArgs "Issuance" ("generated" "unapplied" "test" "issuance1.json") [toData mintingLogicA] baseCompiled - _writePlutusScriptWithArgs "Issuance" ("generated" "unapplied" "test" "issuance2.json") [toData mintingLogicB] baseCompiled + -- let progLogicBase = V3.ScriptCredential (V3.ScriptHash "deadbeef") + -- mintingLogicA = V3.ScriptHash $ stringToBuiltinByteStringHex "deadbeefdeadbeef" + -- mintingLogicB = V3.ScriptHash $ stringToBuiltinByteStringHex "deadbeefcafebabe" + -- (prefixIssuerCborHex, postfixIssuerCborHex) = issuerPrefixPostfixBytes progLogicBase + -- let baseCompiled = case compile NoTracing (mkProgrammableLogicMinting # pconstant progLogicBase) of + -- Right compiledScript -> compiledScript + -- Left err -> error $ "Failed to compile base issuance script: " <> show err + + -- TIO.writeFile ("generated" "unapplied" "test" "prefixIssuerCborHex.txt") prefixIssuerCborHex + -- TIO.writeFile ("generated" "unapplied" "test" "postfixIssuerCborHex.txt") postfixIssuerCborHex + -- _writePlutusScriptWithArgs "Issuance" ("generated" "unapplied" "test" "issuance1.json") [toData mintingLogicA] baseCompiled + -- _writePlutusScriptWithArgs "Issuance" ("generated" "unapplied" "test" "issuance2.json") [toData mintingLogicB] baseCompiled runMain @@ -153,8 +154,11 @@ writeAppliedScripts baseFolder AppliedScriptArgs{asaTxIn, asaIssuerCborHexTxIn, let opkh = case issuerAddr of (C.ShelleyAddress _ntw (C.fromShelleyPaymentCredential -> C.PaymentCredentialByKey pmt) _stakeRef) -> pmt _ -> error "Expected public key address" -- FIXME: proper error + stakeCred = case issuerAddr of + (C.ShelleyAddress _ntw _pmt (C.fromShelleyStakeReference -> C.StakeAddressByValue sCred)) -> Just sCred + _ -> Nothing dirRoot = DirectoryScriptRoot asaTxIn asaIssuerCborHexTxIn Production - blacklistTransferRoot = BlacklistTransferLogicScriptRoot Production (mkDirectoryEnv dirRoot) opkh + blacklistTransferRoot = BlacklistTransferLogicScriptRoot Production (mkDirectoryEnv dirRoot) opkh stakeCred putStrLn "Writing applied Plutus scripts to files" createDirectoryIfMissing True baseFolder withEnv $ @@ -195,7 +199,10 @@ writeAppliedScripts baseFolder AppliedScriptArgs{asaTxIn, asaIssuerCborHexTxIn, writeAppliedScript (baseFolder "transferLogicSpending") "Transfer Logic Spending" tleTransferScript writeAppliedScript (baseFolder "transferLogicIssuerSpending") "Transfer Logic Issuer Spending" tleIssuerScript writeAppliedScript (baseFolder "programmableTokenMinting") "Programmable Token Minting" programmableMinting - + -- blacklist address + let nid = C.Testnet (C.NetworkMagic 1) + blacklistSpendingHash = C.hashScript $ C.PlutusScript C.PlutusScriptV3 bleSpendingScript + liftIO $ print $ "Blacklist address: " <> show (C.serialiseAddress (C.makeShelleyAddressInEra C.ShelleyBasedEraConway nid (C.PaymentCredentialByScript blacklistSpendingHash) C.NoStakeAddress)) runExportCommand :: ExportCommand -> IO () runExportCommand ExportCommand{exBaseFolder, exAppliedScript} = case exAppliedScript of @@ -281,19 +288,25 @@ parseAppliedScriptArgs :: Parser AppliedScriptArgs parseAppliedScriptArgs = AppliedScriptArgs <$> Cli.Command.parseTxIn <*> parseIssuerTxIn <*> parseAddress parseAddress :: Parser (SerialiseAddress (C.Address C.ShelleyAddr)) -parseAddress = argument (eitherReader (eitherDecode . LBS8.pack)) (help "The address to use for the issuer" <> metavar "ISSUER_ADDRESS") +parseAddress = argument (eitherReader parseAddressString) (help "The address to use for the issuer" <> metavar "ISSUER_ADDRESS") + where + parseAddressString :: String -> Either String (SerialiseAddress (C.Address C.ShelleyAddr)) + parseAddressString str' = + maybe (Left $ "Failed to deserialise address: " ++ str') + (Right . SerialiseAddress) + (C.deserialiseAddress (C.proxyToAsType (Proxy @(C.Address C.ShelleyAddr))) (Text.pack str')) parseIssuerTxIn :: Parser C.TxIn parseIssuerTxIn = argument txInReader - (help "The reference utxo with the prefix and postfix cborhex of the issuance script. Format: ." <> metavar "ISSUER_TX_IN") + (help "The reference utxo with the prefix and postfix cborhex of the issuance script. Format: #" <> metavar "ISSUER_TX_IN") txInReader :: ReadM C.TxIn -txInReader = eitherReader $ \str -> do - (txId, txIx) <- case break (== '.') str of +txInReader = eitherReader $ \str' -> do + (txId, txIx) <- case break (== '#') str' of (txId, _:txIx) -> Right (txId, txIx) - _ -> Left "Expected ." + _ -> Left "Expected #" when (length txId /= 64) $ Left "Expected tx ID with 64 characters" ix <- case readMaybe @Word txIx of Nothing -> Left "Expected tx index" diff --git a/src/examples/regulated-stablecoin/lib/Wst/App.hs b/src/examples/regulated-stablecoin/lib/Wst/App.hs index 85012ab7..bb5f567b 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/App.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/App.hs @@ -22,7 +22,15 @@ import Servant.Server qualified as S import Wst.AppError (AppError (..)) newtype WstApp env era a = WstApp { unWstApp :: ReaderT env (ExceptT (AppError era) (BlockfrostT IO)) a } - deriving newtype (Monad, Applicative, Functor, MonadIO, MonadReader env, MonadError (AppError era), MonadUtxoQuery, MonadBlockchain C.ConwayEra) + deriving newtype ( Monad + , Applicative + , Functor + , MonadIO + , MonadReader env + , MonadError (AppError era) + , MonadUtxoQuery + , MonadBlockchain C.ConwayEra + ) deriving (MonadLogger, MonadLoggerIO) via (WithLogger env (ExceptT (AppError era) (BlockfrostT IO))) diff --git a/src/examples/regulated-stablecoin/lib/Wst/AppError.hs b/src/examples/regulated-stablecoin/lib/Wst/AppError.hs index af44e877..214ddf88 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/AppError.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/AppError.hs @@ -14,6 +14,7 @@ module Wst.AppError( ) where import Blockfrost.Client.Core (BlockfrostError) +import Cardano.Api (PolicyId) import Control.Lens (makeClassyPrisms) import Convex.Class (AsValidationError (..), ValidationError) import Convex.CoinSelection (AsBalanceTxError (..), AsCoinSelectionError (..)) @@ -27,6 +28,7 @@ data RegulatedStablecoinError = | 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 + | PolicyIssuerNotFound PolicyId -- ^ Attempting to use a programmable token whose issuer is unknown deriving stock (Show) makeClassyPrisms ''RegulatedStablecoinError diff --git a/src/examples/regulated-stablecoin/lib/Wst/Cli.hs b/src/examples/regulated-stablecoin/lib/Wst/Cli.hs index 02bebcc5..6a639583 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/Cli.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/Cli.hs @@ -4,12 +4,13 @@ {-# HLINT ignore "Move brackets to avoid $" #-} module Wst.Cli(runMain) where -import Blammo.Logging.Simple (Message ((:#)), MonadLogger, logDebug, logError, - logInfo, runLoggerLoggingT, (.=)) +import Blammo.Logging.Simple (HasLogger, Message ((:#)), MonadLogger, logDebug, + logError, logInfo, runLoggerLoggingT, (.=)) import Cardano.Api qualified as C +import Control.Monad (when) import Control.Monad.Except (MonadError, liftEither) import Control.Monad.IO.Class (MonadIO (..)) -import Control.Monad.Reader (MonadReader, asks) +import Control.Monad.Reader (MonadReader, ask, asks) import Convex.Class (MonadBlockchain (sendTx), MonadUtxoQuery) import Convex.Wallet.Operator (Operator (Operator, oPaymentKey), OperatorConfigSigning, @@ -21,6 +22,7 @@ import Data.Proxy (Proxy (..)) import Data.String (IsString (..)) import Options.Applicative (customExecParser, disambiguate, helper, idm, info, prefs, showHelpOnEmpty, showHelpOnError) +import ProgrammableTokens.OffChain.Endpoints qualified as Endpoints import ProgrammableTokens.OffChain.Env.Operator qualified as Env import ProgrammableTokens.OffChain.Env.Runtime qualified as Env import ProgrammableTokens.OffChain.Env.Utils qualified as Utils @@ -29,10 +31,12 @@ import Wst.App (runWstApp) import Wst.AppError (AppError) import Wst.Cli.Command (Command (..), ManageCommand (StartServer, Status), parseCommand) -import Wst.Offchain.Endpoints.Deployment qualified as Endpoints +import Wst.Offchain.Endpoints.Deployment (deployBlacklistTx) import Wst.Offchain.Env qualified as Env import Wst.Server (ServerArgs (..)) import Wst.Server qualified as Server +import Wst.Server.PolicyIssuerStore (HasPolicyIssuerStore, addPolicyIssuerStore, + withPolicyIssuerStore) runMain :: IO () runMain = do @@ -46,16 +50,24 @@ runCommand com = do env <- Env.addRuntimeEnv <$> Env.loadRuntimeEnv <*> pure Utils.empty result <- case com of Deploy config -> runWstApp env (deploy config) + DeployIssuanceCborHex config txIn issuanceCborHexTxIn -> runWstApp env (deployIssuanceCborHex config txIn issuanceCborHexTxIn) + RegisterPolicyStakeScripts config txIn issuanceCborHexTxIn -> runWstApp env (registerPolicyStakeScripts config txIn issuanceCborHexTxIn) + BlacklistInit config txIn issuanceCborHexTxIn -> runWstApp env (deployBlacklist config txIn issuanceCborHexTxIn) Manage txIn issuanceCborHexTxIn com_ -> do let env' = Env.addDirectoryEnvFor (Env.DirectoryScriptRoot txIn issuanceCborHexTxIn Production) env - runWstApp env' $ case com_ of - Status -> do - -- TODO: status check (call the query endpoints and print out a summary of the results) - logInfo "Manage" - StartServer options -> - Server.staticFilesFromEnv options >>= - Server.demoFileFromEnv >>= - startServer env' + case com_ of + Status -> + runWstApp env' $ do + -- TODO: status check (call the query endpoints and print out a summary of the results) + logInfo "Manage" + StartServer options -> do + serverArgs <- + Server.staticFilesFromEnv options >>= + Server.demoFileFromEnv >>= + Server.policyIssuerStoreFromEnv + withPolicyIssuerStore (saPolicyIssuerStore serverArgs) $ \store -> do + let envWithStore = addPolicyIssuerStore store env' + runWstApp envWithStore (startServer serverArgs) case result of Left err -> runLoggerLoggingT env $ logError (fromString $ show err) Right a -> pure a @@ -79,9 +91,23 @@ deploy config = do let env = Env.addOperatorEnv opEnv $ Utils.singleton runEnv + utxos <- liftIO (runWstApp env (Env.selectOperatorUTxOs @(Utils.HSet '[Env.OperatorEnv C.ConwayEra, Env.RuntimeEnv]) @_ @C.ConwayEra )) >>= liftEither + when (length utxos < 2) $ do + frackTx <- liftIO (runWstApp env Endpoints.frackUtxosTx) >>= liftEither + logInfo "Created frack Tx" + let signedFrackTx = signTxOperator operator frackTx + logInfo $ "Signed frack Tx" :# ["fracktx" .= show frackTx] + sendTx signedFrackTx >>= \case + Left err -> logError $ "Error sending frack Tx" :# ["err" .= show err] + Right txid -> logInfo $ "Frack tx submitted successfully" :# ["txid" .= show txid] + + -- reload the operator env + refreshedOpEnv <- Env.reloadOperatorEnv @_ @C.ConwayEra opEnv + let env' = Env.replaceOperatorEnv refreshedOpEnv env + -- Use blockfrost backend to run Wst.Offchain.Endpoints.Deployment with the operator's funds - (tx, root) <- liftIO (runWstApp env $ do - Endpoints.deployFullTx Production) >>= liftEither + (tx, root) <- liftIO (runWstApp env' $ do + Endpoints.deployCip143RegistryTx Production) >>= liftEither logInfo $ "Created deployment Tx" :# ["root" .= root] @@ -96,7 +122,69 @@ deploy config = do logInfo $ "Tx submitted successfully" :# ["txid" .= show txid] (liftIO $ C.writeFileJSON "deployment-root.json" root) >>= either (error . show) pure -startServer :: (Utils.Elem Env.RuntimeEnv els, Utils.Elem Env.DirectoryEnv els, Utils.HMonoModifiable els Env.RuntimeEnv, MonadIO m, MonadLogger m) => Utils.HSet els -> Server.ServerArgs -> m () -startServer env' serverArgs@ServerArgs{saPort, saStaticFiles} = do - logInfo $ "starting server" :# ["port" .= saPort, "static_files" .= fromMaybe "(no static files)" saStaticFiles] - liftIO (Server.runServer env' serverArgs) +deployIssuanceCborHex :: forall env m. (MonadLogger m, MonadIO m, MonadUtxoQuery m, MonadReader env m, Env.HasRuntimeEnv env, MonadError (AppError C.ConwayEra) m, MonadBlockchain C.ConwayEra m) => OperatorConfigSigning -> C.TxIn -> C.TxIn -> m () +deployIssuanceCborHex config txin issuanceCborHexTxIn = do + signingKey <- liftIO + $ C.readFileTextEnvelope (C.proxyToAsType $ Proxy @(C.SigningKey C.PaymentExtendedKey)) (C.File $ Operator.ocSigningKeyFile config) + >>= either (error . show) pure + let operator = Operator (PESigningEx signingKey) Nothing + operatorPaymentHash = C.verificationKeyHash . verificationKey . oPaymentKey $ operator + opEnv <- Env.loadOperatorEnv @_ @C.ConwayEra operatorPaymentHash C.NoStakeAddress + runEnv <- asks Env.runtimeEnv + + let env = Env.addDirectoryEnvFor (Env.DirectoryScriptRoot txin issuanceCborHexTxIn Production) $ Env.addOperatorEnv opEnv $ Utils.singleton runEnv + issuanceCborHexTx <- liftIO (runWstApp env Endpoints.deployIssuanceCborHex) >>= liftEither + let signedIssuanceCborHexTx = signTxOperator operator issuanceCborHexTx + sendTx signedIssuanceCborHexTx >>= \case + Left err -> logError $ "Error sending issuance cbor hex Tx" :# ["err" .= show err] + Right issuanceCborHexTxid -> + logInfo $ "Issuance cbor hex tx submitted successfully" :# ["txid" .= show issuanceCborHexTxid] + +deployBlacklist :: forall env m. (MonadLogger m, MonadIO m, MonadUtxoQuery m, MonadReader env m, Env.HasRuntimeEnv env, MonadError (AppError C.ConwayEra) m, MonadBlockchain C.ConwayEra m) => OperatorConfigSigning -> C.TxIn -> C.TxIn -> m () +deployBlacklist config txin issuanceCborHexTxIn = do + signingKey <- liftIO + $ C.readFileTextEnvelope (C.proxyToAsType $ Proxy @(C.SigningKey C.PaymentExtendedKey)) (C.File $ Operator.ocSigningKeyFile config) + >>= either (error . show) pure + let operator = Operator (PESigningEx signingKey) Nothing + operatorPaymentHash = C.verificationKeyHash . verificationKey . oPaymentKey $ operator + opEnv <- Env.loadOperatorEnv @_ @C.ConwayEra operatorPaymentHash C.NoStakeAddress + runEnv <- asks Env.runtimeEnv + let env = Env.addDirectoryEnvFor (Env.DirectoryScriptRoot txin issuanceCborHexTxIn Production) $ Env.addOperatorEnv opEnv $ Utils.singleton runEnv + (transferEnv, _) <- liftIO (runWstApp env $ Env.transferLogicForDirectory operatorPaymentHash Nothing) >>= liftEither + let env' = Env.addTransferEnv transferEnv env + policyTx <- liftIO (runWstApp env' deployBlacklistTx) >>= liftEither + let signedPolicyTx = signTxOperator operator policyTx + sendTx signedPolicyTx >>= \case + Left err -> logError $ "Error sending blacklist Tx" :# ["err" .= show err] + Right txid -> logInfo $ "Blacklist tx submitted successfully" :# ["txid" .= show txid] + +registerPolicyStakeScripts :: forall env m. (MonadLogger m, MonadIO m, MonadUtxoQuery m, MonadReader env m, Env.HasRuntimeEnv env, MonadError (AppError C.ConwayEra) m, MonadBlockchain C.ConwayEra m) => OperatorConfigSigning -> C.TxIn -> C.TxIn -> m () +registerPolicyStakeScripts config txin issuanceCborHexTxIn = do + signingKey <- liftIO + $ C.readFileTextEnvelope (C.proxyToAsType $ Proxy @(C.SigningKey C.PaymentExtendedKey)) (C.File $ Operator.ocSigningKeyFile config) + >>= either (error . show) pure + let operator = Operator (PESigningEx signingKey) Nothing + operatorPaymentHash = C.verificationKeyHash . verificationKey . oPaymentKey $ operator + opEnv <- Env.loadOperatorEnv @_ @C.ConwayEra operatorPaymentHash C.NoStakeAddress + runEnv <- asks Env.runtimeEnv + let env = Env.addDirectoryEnvFor (Env.DirectoryScriptRoot txin issuanceCborHexTxIn Production) $ Env.addOperatorEnv opEnv $ Utils.singleton runEnv + (transferEnv, _) <- liftIO (runWstApp env $ Env.transferLogicForDirectory operatorPaymentHash Nothing) >>= liftEither + let env' = Env.addTransferEnv transferEnv env + policyTx <- liftIO (runWstApp env' Endpoints.registerCip143PolicyTransferScripts) >>= liftEither + let signedPolicyTx = signTxOperator operator policyTx + sendTx signedPolicyTx >>= \case + Left err -> logError $ "Error sending policy stake scripts Tx" :# ["err" .= show err] + Right txid -> logInfo $ "Policy stake scripts tx submitted successfully" :# ["txid" .= show txid] + +startServer :: ( MonadReader env m + , Env.HasRuntimeEnv env + , Env.HasDirectoryEnv env + , HasLogger env + , HasPolicyIssuerStore env + , MonadIO m + , MonadLogger m + ) => Server.ServerArgs -> m () +startServer serverArgs@ServerArgs{saPort, saStaticFiles, saPolicyIssuerStore} = do + logInfo $ "starting server" :# ["port" .= saPort, "static_files" .= fromMaybe "(no static files)" saStaticFiles, "policy_issuer_store" .= saPolicyIssuerStore] + env <- ask + liftIO (Server.runServer env serverArgs) diff --git a/src/examples/regulated-stablecoin/lib/Wst/Cli/Command.hs b/src/examples/regulated-stablecoin/lib/Wst/Cli/Command.hs index 88917c54..32777219 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/Cli/Command.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/Cli/Command.hs @@ -6,7 +6,7 @@ module Wst.Cli.Command( ManageCommand(..), -- * Other parsers - parseTxIn + parseTxIn, ) where import Cardano.Api (TxIn (..), TxIx (..)) @@ -17,7 +17,7 @@ import Data.String (IsString (..)) import Options.Applicative (CommandFields, Mod, Parser, ReadM, argument, auto, command, eitherReader, fullDesc, help, info, long, metavar, option, optional, progDesc, short, - subparser, value) + showDefault, subparser, value) import Options.Applicative.Builder (strOption) import Text.Read (readMaybe) import Wst.Server (ServerArgs (..)) @@ -28,10 +28,16 @@ parseCommand = mconcat [ parseDeploy , parseManage + , parseDeployIssuanceCborHex + , parseRegisterPolicyStakeScripts + , parseBlacklistInit ] data Command = Deploy OperatorConfigSigning + | DeployIssuanceCborHex OperatorConfigSigning TxIn TxIn + | RegisterPolicyStakeScripts OperatorConfigSigning TxIn TxIn + | BlacklistInit OperatorConfigSigning TxIn TxIn | Manage TxIn TxIn ManageCommand deriving Show @@ -46,6 +52,21 @@ parseDeploy = command "deploy" $ info (Deploy <$> parseOperatorConfigSigning) (fullDesc <> progDesc "Deploy the directory and global params") +parseBlacklistInit :: Mod CommandFields Command +parseBlacklistInit = + command "blacklist-init" $ + info (BlacklistInit <$> parseOperatorConfigSigning <*> parseTxIn <*> parseTxIn) (fullDesc <> progDesc "Deploy the blacklist") + +parseRegisterPolicyStakeScripts :: Mod CommandFields Command +parseRegisterPolicyStakeScripts = + command "register-policy-stake-scripts" $ + info (RegisterPolicyStakeScripts <$> parseOperatorConfigSigning <*> parseTxIn <*> parseTxIn) (fullDesc <> progDesc "Register the stake scripts for the programmable token policy") + +parseDeployIssuanceCborHex :: Mod CommandFields Command +parseDeployIssuanceCborHex = + command "issuance-cbor-hex" $ + info (DeployIssuanceCborHex <$> parseOperatorConfigSigning <*> parseTxIn <*> parseTxIn) (fullDesc <> progDesc "Deploy the issuance cbor hex") + parseManage :: Mod CommandFields Command parseManage = command "manage" $ @@ -70,18 +91,23 @@ parseServerArgs = <$> option auto (help "The port" <> value 8080 <> long "port" <> short 'p') <*> optional (strOption (help "Folder to serve static files from" <> long "static-files")) <*> optional (strOption (help "JSON file with demo environment" <> long "demo-environment")) + <*> strOption (help "SQLite file for persisting policy issuer metadata" + <> long "policy-issuer-store" + <> metavar "FILE" + <> value "policy-issuers.sqlite" + <> showDefault) parseTxIn :: Parser TxIn parseTxIn = argument txInReader - (help "The TxIn that was selected when deploying the system. Format: ." <> metavar "TX_IN") + (help "The TxIn that was selected when deploying the system. Format: #" <> metavar "TX_IN") txInReader :: ReadM TxIn txInReader = eitherReader $ \str -> do - (txId, txIx) <- case break ((==) '.') str of + (txId, txIx) <- case break ((==) '#') str of (txId, _:txIx) -> Right (txId, txIx) - _ -> Left "Expected ." + _ -> Left "Expected #" when (length txId /= 64) $ Left "Expected tx ID with 64 characters" ix <- case readMaybe @Word txIx of Nothing -> Left "Expected tx index" diff --git a/src/examples/regulated-stablecoin/lib/Wst/Client.hs b/src/examples/regulated-stablecoin/lib/Wst/Client.hs index 646befa2..c4d24ff0 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/Client.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/Client.hs @@ -13,7 +13,10 @@ module Wst.Client ( postTransferProgrammableTokenTx, postAddToBlacklistTx, postRemoveFromBlacklistTx, - postSeizeFundsTx + postSeizeFundsTx, + postSeizeMultiFundsTx, + postRegisterTransferScriptsTx, + postBlacklistInitTx ) where import Cardano.Api qualified as C @@ -23,8 +26,9 @@ 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, BlacklistNodeArgs, - IssueProgrammableTokenArgs (..), SeizeAssetsArgs, +import Wst.Server.Types (API, APIInEra, BlacklistInitArgs, BlacklistNodeArgs, + IssueProgrammableTokenArgs (..), MultiSeizeAssetsArgs, + RegisterTransferScriptsArgs, SeizeAssetsArgs, TextEnvelopeJSON, TransferProgrammableTokenArgs (..)) getHealthcheck :: ClientEnv -> IO (Either ClientError NoContent) @@ -59,5 +63,20 @@ postRemoveFromBlacklistTx env args = do 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 + +postSeizeMultiFundsTx :: forall era. C.IsShelleyBasedEra era => ClientEnv -> MultiSeizeAssetsArgs -> IO (Either ClientError (TextEnvelopeJSON (C.Tx era))) +postSeizeMultiFundsTx env args = do + let _ :<|> _ :<|> ((_ :<|> _ :<|> _ :<|> _ :<|> _ :<|> seizeMultiFunds :<|> _) :<|> _) = client (Proxy @(API era)) + runClientM (seizeMultiFunds args) env + +postRegisterTransferScriptsTx :: forall era. C.IsShelleyBasedEra era => ClientEnv -> RegisterTransferScriptsArgs -> IO (Either ClientError (TextEnvelopeJSON (C.Tx era))) +postRegisterTransferScriptsTx env args = do + let _ :<|> _ :<|> ((_ :<|> _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> registerTransferScripts :<|> _) :<|> _) = client (Proxy @(API era)) + runClientM (registerTransferScripts args) env + +postBlacklistInitTx :: forall era. C.IsShelleyBasedEra era => ClientEnv -> BlacklistInitArgs -> IO (Either ClientError (TextEnvelopeJSON (C.Tx era))) +postBlacklistInitTx env args = do + let _ :<|> _ :<|> ((_ :<|> _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> blacklistInit) :<|> _) = client (Proxy @(API era)) + runClientM (blacklistInit args) env diff --git a/src/examples/regulated-stablecoin/lib/Wst/Offchain/BuildTx/ProgrammableLogic.hs b/src/examples/regulated-stablecoin/lib/Wst/Offchain/BuildTx/ProgrammableLogic.hs index 5e2673d9..d313ad4a 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/Offchain/BuildTx/ProgrammableLogic.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/Offchain/BuildTx/ProgrammableLogic.hs @@ -11,19 +11,23 @@ module Wst.Offchain.BuildTx.ProgrammableLogic where import Cardano.Api qualified as C +import Cardano.Api.Internal.Tx.Body qualified as C import Cardano.Api.Shelley qualified as C import Control.Lens ((^.)) +import Control.Lens qualified as L +import Control.Monad (forM_) import Control.Monad.Reader (MonadReader, asks) -import Convex.BuildTx (MonadBuildTx, addReference, addWithdrawalWithTxBody, - buildScriptWitness, findIndexReference, - findIndexSpending, prependTxOut, spendPlutusInlineDatum) +import Convex.BuildTx (MonadBuildTx, addOutput, addReference, + addWithdrawalWithTxBody, buildScriptWitness, + findIndexReference, findIndexSpending, + spendPlutusInlineDatum) import Convex.CardanoApi.Lenses as L import Convex.Class (MonadBlockchain (queryNetworkId)) import Convex.PlutusLedger.V1 (transPolicyId) import Convex.Utils qualified as Utils import Data.Foldable (find) -import Data.List (partition) -import Data.Maybe (fromJust) +import Data.List (findIndex, partition) +import Data.Maybe (fromMaybe) import GHC.Exts (IsList (..)) import PlutusLedgerApi.V3 (CurrencySymbol (..)) import ProgrammableTokens.OffChain.Env qualified as Env @@ -41,16 +45,40 @@ import Wst.Offchain.Query (UTxODat (..)) ensure that the specific issuer logic stake script witness is included in the final transaction. - NOTE: Seems the issuer is only able to seize 1 UTxO at a time. - In the future we should allow multiple UTxOs in 1 Tx. + * @UTxODat era ProgrammableLogicGlobalParams@: The reference input + containing the global programmable-logic parameters. Used to anchor the + global stake script during the seize. + * @[UTxODat era a]@: The input UTxOs to be seized. Each entry is a + programmable-token UTxO that will be spent and redirected. + * @C.PolicyId@: The policy ID of the programmable token being seized. This is + used to locate the corresponding directory node and to filter the value + being removed from each seized UTxO. + * @[UTxODat era DirectorySetNode]@: The directory entries that map programmable + policies to the relevant transfer / issuer logic scripts. The function searches this list to find the node + for the supplied policy ID so it can include the correct reference input. + -} -seizeProgrammableToken :: forall a env era m. (MonadReader env m, Env.HasDirectoryEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m) => UTxODat era ProgrammableLogicGlobalParams -> UTxODat era a -> C.PolicyId -> [UTxODat era DirectorySetNode] -> m () -seizeProgrammableToken UTxODat{uIn = paramsTxIn} UTxODat{uIn = seizingTxIn, uOut = seizingTxOut} seizingTokenPolicyId directoryList = Utils.inBabbage @era $ do +seizeProgrammableToken :: + forall a env era m. + ( MonadReader env m, + Env.HasDirectoryEnv env, + C.IsBabbageBasedEra era, + MonadBlockchain era m, + C.HasScriptLanguageInEra C.PlutusScriptV3 era, + MonadBuildTx era m + ) => + UTxODat era ProgrammableLogicGlobalParams -> + [UTxODat era a] -> + C.PolicyId -> + [UTxODat era DirectorySetNode] -> + m () +seizeProgrammableToken UTxODat{uIn = paramsTxIn} seizingUTxOs seizingTokenPolicyId directoryList = Utils.inBabbage @era $ do nid <- queryNetworkId globalStakeScript <- asks (Env.dsProgrammableLogicGlobalScript . Env.directoryEnv) baseSpendingScript <- asks (Env.dsProgrammableLogicBaseScript . Env.directoryEnv) let globalStakeCred = C.StakeCredentialByScript $ C.hashScript $ C.PlutusScript C.PlutusScriptV3 globalStakeScript + programmableLogicBaseCredential = C.PaymentCredentialByScript $ C.hashScript $ C.PlutusScript C.PlutusScriptV3 baseSpendingScript -- Finds the directory node entry that references the programmable token symbol dirNodeRef <- @@ -58,24 +86,24 @@ seizeProgrammableToken UTxODat{uIn = paramsTxIn} UTxODat{uIn = seizingTxIn, uOut find (isNodeWithProgrammableSymbol (transPolicyId seizingTokenPolicyId)) directoryList -- destStakeCred <- either (error . ("Could not unTrans credential: " <>) . show) pure $ unTransStakeCredential $ transCredential seizeDestinationCred - let - -- issuerDestinationAddress = C.makeShelleyAddressInEra C.shelleyBasedEra nid progLogicBaseCred (C.StakeAddressByValue destStakeCred) - - (seizedAddr, remainingValue) = case seizingTxOut of - (C.TxOut a v _ _) -> - let (_seized, other) = - partition - ( \case - (C.AdaAssetId, _q) -> False - (C.AssetId a_ _, _q) -> a_ == seizingTokenPolicyId - ) - $ toList $ C.txOutValueToValue v - in (a, fromList other) - remainingTxOutValue = C.TxOutValueShelleyBased C.shelleyBasedEra $ C.toLedgerValue @era C.maryBasedEra remainingValue - - seizedOutput = C.TxOut seizedAddr remainingTxOutValue C.TxOutDatumNone C.ReferenceScriptNone + forM_ seizingUTxOs $ \UTxODat{uIn = seizingTxIn, uOut = seizingTxOut} -> do + spendPlutusInlineDatum seizingTxIn baseSpendingScript () + let (seizedAddr, remainingValue, seizedDatum, referenceScript) = case seizingTxOut of + (C.TxOut a v dat refScript) -> + let (_seized, other) = + partition + ( \case + (C.AdaAssetId, _q) -> False + (C.AssetId a_ _, _q) -> a_ == seizingTokenPolicyId + ) + $ toList $ C.txOutValueToValue v + in (a, fromList other, dat, refScript) + remainingTxOutValue = C.TxOutValueShelleyBased C.shelleyBasedEra $ C.toLedgerValue @era C.maryBasedEra remainingValue + seizedOutput = C.TxOut seizedAddr remainingTxOutValue seizedDatum referenceScript + addOutput (C.fromCtxUTxOTxOut seizedOutput) + let -- Finds the index of the directory node reference in the transaction ref -- inputs directoryNodeReferenceIndex txBody = @@ -83,26 +111,31 @@ seizeProgrammableToken UTxODat{uIn = paramsTxIn} UTxODat{uIn = seizingTxIn, uOut -- Finds the index of the issuer input in the transaction body seizingInputIndex txBody = - fromIntegral @Int @Integer $ findIndexSpending seizingTxIn txBody - - -- Finds the index of the issuer seized output in the transaction body - seizingOutputIndex txBody = - fromIntegral @Int @Integer $ fst $ fromJust (find ((== seizedOutput) . snd ) $ zip [0 ..] $ txBody ^. L.txOuts) + map (\UTxODat{uIn = seizingTxIn} -> fromIntegral @Int @Integer $ findIndexSpending seizingTxIn txBody) seizingUTxOs + + -- Finds the index of the first output to the programmable logic base credential + firstSeizeContinuationOutputIndex txBody = + fromIntegral @Int @Integer $ + fromMaybe (error "firstSeizeContinuationOutputIndex: No output to programmable logic base credential found") $ + findIndex + ( maybe False ((== programmableLogicBaseCredential) . C.fromShelleyPaymentCredential) + . L.preview (L._TxOut . L._1 . L._AddressInEra . L._Address . L._2) + ) + (txBody ^. L.txOuts) -- The seizing redeemer for the global script programmableLogicGlobalRedeemer txBody = SeizeAct - { plgrSeizeInputIdx = seizingInputIndex txBody, - plgrSeizeOutputIdx = seizingOutputIndex txBody, - plgrDirectoryNodeIdx = directoryNodeReferenceIndex txBody + { plgrDirectoryNodeIdx = directoryNodeReferenceIndex txBody, + plgrInputIdxs = seizingInputIndex txBody, + plgrOutputsStartIdx = firstSeizeContinuationOutputIndex txBody, + plgrLengthInputIdxs = fromIntegral @Int @Integer $ length seizingUTxOs } programmableGlobalWitness txBody = buildScriptWitness globalStakeScript C.NoScriptDatumForStake (programmableLogicGlobalRedeemer txBody) - prependTxOut seizedOutput addReference paramsTxIn -- Protocol Params TxIn addReference dirNodeRef -- Directory Node TxIn - spendPlutusInlineDatum seizingTxIn baseSpendingScript () -- Redeemer is ignored in programmableLogicBase addWithdrawalWithTxBody -- Add the global script witness to the transaction (C.makeStakeAddress nid globalStakeCred) (C.Quantity 0) diff --git a/src/examples/regulated-stablecoin/lib/Wst/Offchain/BuildTx/TransferLogic.hs b/src/examples/regulated-stablecoin/lib/Wst/Offchain/BuildTx/TransferLogic.hs index d3653fbe..84115c58 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/Offchain/BuildTx/TransferLogic.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/Offchain/BuildTx/TransferLogic.hs @@ -9,6 +9,7 @@ module Wst.Offchain.BuildTx.TransferLogic issueSmartTokens, SeizeReason(..), seizeSmartTokens, + multiSeizeSmartTokens, initBlacklist, BlacklistReason(..), insertBlacklistNode, @@ -20,15 +21,17 @@ where import Cardano.Api qualified as C import Cardano.Api.Shelley qualified as C import Control.Lens (at, over, set, (&), (?~), (^.)) -import Control.Monad (when) +import Control.Lens qualified as L +import Control.Monad (forM_, when) import Control.Monad.Error.Lens (throwing_) import Control.Monad.Except (MonadError) import Control.Monad.Reader (MonadReader, asks) import Convex.BuildTx (MonadBuildTx (addTxBuilder), TxBuilder (TxBuilder), - addBtx, addRequiredSignature, addScriptWithdrawal, - addWithdrawalWithTxBody, buildScriptWitness, - findIndexReference, mintPlutus, payToAddress, - prependTxOut, spendPlutusInlineDatum) + addBtx, addOutput, addRequiredSignature, + addScriptWithdrawal, addWithdrawalWithTxBody, + buildScriptWitness, findIndexReference, mintPlutus, + payToAddress, payToAddressTxOut, prependTxOut, + spendPlutusInlineDatum) import Convex.CardanoApi.Lenses qualified as L import Convex.Class (MonadBlockchain (queryNetworkId)) import Convex.PlutusLedger.V1 (transCredential, transPolicyId, @@ -282,7 +285,7 @@ seizeSmartTokens reason paramsTxIn seizingTxo destinationCred directoryList = Ut (toList $ C.txOutValueToValue v) (progTokenPolId, an, q) <- maybe (error "No programmable token found in seizing transaction") pure maybeProgAsset - seizeProgrammableToken paramsTxIn seizingTxo progTokenPolId directoryList + seizeProgrammableToken paramsTxIn [seizingTxo] progTokenPolId directoryList addSeizeWitness progLogicBaseCred <- asks (Env.programmableLogicBaseCredential . Env.directoryEnv) @@ -295,7 +298,56 @@ seizeSmartTokens reason paramsTxIn seizingTxo destinationCred directoryList = Ut addSeizeReason reason -- Send seized funds to destinationCred - payToAddress destinationAddress seizedVal + addOutput $ payToAddressTxOut destinationAddress seizedVal + +{-| + Seize multiple programmable tokens from a user address to an issuer address. The + outputs address will be that of the issuer retrieved from @issuerTxOut@. + Throws if the payment credentials of the issuer output does not match the + programmable logic payment credential. + + * @SeizeReason@: The reason for seizing the funds. + * @UTxODat era ProgrammableLogicGlobalParams@: The reference input + containing the global programmable-logic parameters. Used to anchor the + global stake script during the seize. + * @C.PolicyId@: The policy ID of the programmable token being seized. This is + used to locate the corresponding directory node and to filter the value + being removed from each seized UTxO. + * @[UTxODat era a]@: The input UTxOs to be seized. Each entry is a + programmable-token UTxO that will be spent and redirected. + * @[C.PaymentCredential]@: The payment credentials of the destination address. + * @[UTxODat era DirectorySetNode]@: The directory entries that map programmable + policies to the relevant transfer / issuer logic scripts. The function searches this list to find the node + for the supplied policy ID so it can include the correct reference input. +-} +multiSeizeSmartTokens :: forall env era a m. (MonadReader env m, Env.HasOperatorEnv era env, Env.HasTransferLogicEnv env, Env.HasDirectoryEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m) => SeizeReason -> UTxODat era ProgrammableLogicGlobalParams -> C.PolicyId -> [UTxODat era a] -> [C.PaymentCredential] -> [UTxODat era DirectorySetNode] -> m () +multiSeizeSmartTokens reason paramsTxIn toSeizePolicyId seizingTxos destinationCred directoryList = Utils.inBabbage @era $ do + nid <- queryNetworkId + + let seizedValue = C.policyAssetsToValue toSeizePolicyId $ + foldl (\acc (uOut -> utxoDat) -> + let filteredAssets = foldMap ( \(pid, assets) -> + if pid == toSeizePolicyId then assets else mempty + ) + (toList $ C.valueToPolicyAssets (L.view (L._TxOut . L._2 . L._TxOutValue) utxoDat)) + in acc <> filteredAssets + ) + (mempty :: C.PolicyAssets) + seizingTxos + + seizeProgrammableToken paramsTxIn seizingTxos toSeizePolicyId directoryList + addSeizeWitness + + progLogicBaseCred <- asks (Env.programmableLogicBaseCredential . Env.directoryEnv) + destStakeCredentials <- mapM (either (error . ("Could not unTrans credential: " <>) . show) pure . unTransStakeCredential . transCredential) destinationCred + -- destStakeCred <- either (error . ("Could not unTrans credential: " <>) . show) pure $ unTransStakeCredential $ transCredential destinationCred + let + destinationAddresses = map (C.makeShelleyAddressInEra C.shelleyBasedEra nid progLogicBaseCred . C.StakeAddressByValue) destStakeCredentials + + addSeizeReason reason + + -- Send seized funds to destinationCred + forM_ destinationAddresses $ addOutput . flip payToAddressTxOut seizedValue addIssueWitness :: forall era env m. (MonadReader env m, Env.HasOperatorEnv era env, Env.HasTransferLogicEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m) => m () addIssueWitness = Utils.inBabbage @era $ do diff --git a/src/examples/regulated-stablecoin/lib/Wst/Offchain/Endpoints/Deployment.hs b/src/examples/regulated-stablecoin/lib/Wst/Offchain/Endpoints/Deployment.hs index 6abd983b..739f61e7 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/Offchain/Endpoints/Deployment.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/Offchain/Endpoints/Deployment.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE NamedFieldPuns #-} {-| Deploy the directory and global params -} module Wst.Offchain.Endpoints.Deployment( @@ -9,6 +8,7 @@ module Wst.Offchain.Endpoints.Deployment( insertBlacklistNodeTx, removeBlacklistNodeTx, seizeCredentialAssetsTx, + seizeMultiCredentialAssetsTx ) where import Cardano.Api (Quantity) @@ -25,7 +25,9 @@ import Data.Function (on) import GHC.IsList (IsList (..)) import ProgrammableTokens.OffChain.BuildTx qualified as BuildTx import ProgrammableTokens.OffChain.Env.Operator qualified as Env +import ProgrammableTokens.OffChain.Env.TransferLogic (programmableTokenPolicyId) import ProgrammableTokens.OffChain.Error (AsProgrammableTokensError (..)) +import ProgrammableTokens.OffChain.Query (utxoHasPolicyId) import ProgrammableTokens.OffChain.Query qualified as Query import SmartTokens.Core.Scripts (ScriptTarget (..)) import Wst.AppError (AsRegulatedStablecoinError (..)) @@ -132,12 +134,18 @@ transferSmartTokensTx :: forall era env err m. transferSmartTokensTx policy assetId quantity destCred = do directory <- Query.registryNodeForReference @era blacklist <- Query.blacklistNodes @era - userOutputsAtProgrammable <- Env.operatorPaymentCredential @_ @era >>= Query.userProgrammableOutputs + (opCred, fromStakeAddressReference -> opStakeCred) <- asks (Env.bteOperator . Env.operatorEnv @era) + userOutputsAtProgrammable <- Query.userProgrammableOutputs (C.PaymentCredentialByKey opCred, opStakeCred) paramsTxIn <- Query.globalParamsNode @era (tx, _) <- balanceTxEnvFailing policy $ do BuildTx.transferSmartTokens paramsTxIn blacklist directory userOutputsAtProgrammable (assetId, quantity) destCred pure (Convex.CoinSelection.signBalancedTxBody [] tx) +fromStakeAddressReference :: C.StakeAddressReference -> Maybe C.StakeCredential +fromStakeAddressReference = \case + C.StakeAddressByValue stakeCred -> Just stakeCred + C.StakeAddressByPointer _ -> Nothing + C.NoStakeAddress -> Nothing seizeCredentialAssetsTx :: forall era env err m. ( MonadReader env m @@ -168,10 +176,45 @@ seizeCredentialAssetsTx reason sanctionedCred = do ) 0 $ toList v getNonAdaTokens = nonAda . C.txOutValueToValue . getTxOutValue . uOut - seizeTxo <- maximumBy (compare `on` getNonAdaTokens) <$> Query.userProgrammableOutputs sanctionedCred + seizeTxo <- maximumBy (compare `on` getNonAdaTokens) <$> Query.userProgrammableOutputs (sanctionedCred, Nothing) when (getNonAdaTokens seizeTxo == 0) $ throwing_ _NoTokensToSeize paramsTxIn <- Query.globalParamsNode @era (tx, _) <- Env.balanceTxEnv_ $ do BuildTx.seizeSmartTokens reason paramsTxIn seizeTxo (C.PaymentCredentialByKey opPkh) directory pure (Convex.CoinSelection.signBalancedTxBody [] tx) + +seizeMultiCredentialAssetsTx :: forall era env err m. + ( MonadReader env m + , Env.HasOperatorEnv era env + , Env.HasTransferLogicEnv env + , Env.HasDirectoryEnv env + , MonadBlockchain era m + , MonadError err m + , C.IsBabbageBasedEra era + , C.HasScriptLanguageInEra C.PlutusScriptV3 era + , MonadUtxoQuery m + , AsProgrammableTokensError err + , AsBalancingError err era + , AsCoinSelectionError err + , AsRegulatedStablecoinError err + ) + => BuildTx.SeizeReason + -> Int + -> [C.PaymentCredential] -- ^ Source/User credentials + -> m (C.Tx era) +seizeMultiCredentialAssetsTx reason numUTxOsToSeize sanctionedCreds = do + toSeizePolicyId <- asks programmableTokenPolicyId + opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv @era) + directory <- Query.registryNodes @era + + utxosToSeize <- + take numUTxOsToSeize . concatMap (filter (utxoHasPolicyId toSeizePolicyId)) + <$> traverse (\cred -> Query.userProgrammableOutputs (cred, Nothing)) sanctionedCreds + + when (null utxosToSeize) $ + throwing_ _NoTokensToSeize + paramsTxIn <- Query.globalParamsNode @era + (tx, _) <- Env.balanceTxEnv_ $ do + BuildTx.multiSeizeSmartTokens reason paramsTxIn toSeizePolicyId utxosToSeize [C.PaymentCredentialByKey opPkh] directory + pure (Convex.CoinSelection.signBalancedTxBody [] tx) diff --git a/src/examples/regulated-stablecoin/lib/Wst/Offchain/Env.hs b/src/examples/regulated-stablecoin/lib/Wst/Offchain/Env.hs index ae5c995f..34a93bb9 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/Offchain/Env.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/Offchain/Env.hs @@ -58,6 +58,7 @@ module Wst.Offchain.Env( addRuntimeEnv, withRuntime, addOperatorEnv, + replaceOperatorEnv, withOperator, blacklistNodePolicyId, withBlacklist, @@ -134,6 +135,7 @@ data BlacklistTransferLogicScriptRoot = { tlrTarget :: ScriptTarget , tlrDirEnv :: DirectoryEnv , tlrIssuer :: C.Hash C.PaymentKey + , tlrIssuerStakeCredential :: Maybe C.StakeCredential } mkTransferLogicEnv :: BlacklistTransferLogicScriptRoot -> TransferLogicEnv @@ -186,19 +188,26 @@ withTransferFor = withTransfer . mkTransferLogicEnv {-| Transfer logic scripts for the blacklist managed by the given 'C.PaymentKey' hash -} -transferLogicForDirectory :: (HasDirectoryEnv env, MonadReader env m) => C.Hash C.PaymentKey -> m (TransferLogicEnv, BlacklistEnv) -transferLogicForDirectory pkh = do +transferLogicForDirectory :: (HasDirectoryEnv env, MonadReader env m) => C.Hash C.PaymentKey -> Maybe C.StakeCredential -> m (TransferLogicEnv, BlacklistEnv) +transferLogicForDirectory pkh stakeCred = do env <- ask let dirEnv = directoryEnv env - sr = BlacklistTransferLogicScriptRoot (srTarget $ dsScriptRoot dirEnv) dirEnv pkh + sr = BlacklistTransferLogicScriptRoot (srTarget $ dsScriptRoot dirEnv) dirEnv pkh stakeCred pure (mkTransferLogicEnv sr, mkBlacklistEnv sr) withTransferFromOperator :: forall era els m a. (Utils.NotElem TransferLogicEnv (BlacklistEnv : els), Utils.NotElem BlacklistEnv els, Utils.Elem (OperatorEnv era) els, Utils.Elem DirectoryEnv els, MonadReader (Utils.HSet els) m) => ReaderT (Utils.HSet (TransferLogicEnv : BlacklistEnv : els)) m a -> m a withTransferFromOperator action = do env <- ask let opPkh = fst . bteOperator . operatorEnv @era $ env - (transferEnv, ble) <- transferLogicForDirectory opPkh + stakeCred = toStakeCredential $ snd . bteOperator . operatorEnv @era $ env + (transferEnv, ble) <- transferLogicForDirectory opPkh stakeCred runReaderT action (addTransferEnv transferEnv $ addBlacklistEnv ble env) + where + toStakeCredential :: C.StakeAddressReference -> Maybe C.StakeCredential + toStakeCredential = \case + C.StakeAddressByValue stakeCred -> Just stakeCred + _ -> Nothing + addBlacklistEnv :: (Utils.NotElem BlacklistEnv els) => BlacklistEnv -> Utils.HSet els -> Utils.HSet (BlacklistEnv ': els) addBlacklistEnv = Utils.addEnv @@ -239,5 +248,10 @@ withRuntime runtime_ action = addOperatorEnv :: (Utils.NotElem (OperatorEnv era) els) => OperatorEnv era -> Utils.HSet els -> Utils.HSet (OperatorEnv era ': els) addOperatorEnv = Utils.addEnv +{-| Replace an existing 'OperatorEnv' inside the environment +-} +replaceOperatorEnv :: (Utils.HMonoModifiable els (OperatorEnv era)) => OperatorEnv era -> Utils.HSet els -> Utils.HSet els +replaceOperatorEnv opEnv = Utils.modifyEnv (const opEnv) + withOperator :: (MonadReader (Utils.HSet els) m, Utils.NotElem (OperatorEnv era) els) => OperatorEnv era -> ReaderT (Utils.HSet (OperatorEnv era ': els)) m a -> m a withOperator op action = asks (addOperatorEnv op) >>= runReaderT action diff --git a/src/examples/regulated-stablecoin/lib/Wst/Server.hs b/src/examples/regulated-stablecoin/lib/Wst/Server.hs index 852cc141..df9273c0 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/Server.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/Server.hs @@ -9,18 +9,20 @@ module Wst.Server( ServerArgs(..), staticFilesFromEnv, demoFileFromEnv, + policyIssuerStoreFromEnv, CombinedAPI, - defaultServerArgs + defaultServerArgs, ) where import Blammo.Logging.Simple (HasLogger, Message ((:#)), MonadLogger, logInfo, (.=)) +import Blockfrost.Client.Core (BlockfrostError (..)) import Blockfrost.Client.Types qualified as Blockfrost import Cardano.Api.Shelley qualified as C import Control.Lens qualified as L import Control.Monad.Error.Class (MonadError (throwError)) import Control.Monad.IO.Class (MonadIO (..)) -import Control.Monad.Reader (MonadReader, asks) +import Control.Monad.Reader (MonadReader, ask, asks) import Convex.CardanoApi.Lenses qualified as L import Convex.Class (MonadBlockchain (sendTx), MonadUtxoQuery, utxosByPaymentCredential) @@ -33,8 +35,10 @@ import Data.Maybe (fromMaybe) import Network.Wai.Handler.Warp qualified as Warp import Network.Wai.Middleware.Cors import PlutusTx.Prelude qualified as P +import ProgrammableTokens.OffChain.Endpoints (registerCip143PolicyTransferScripts) import ProgrammableTokens.OffChain.Env.Operator qualified as Env import ProgrammableTokens.OffChain.Env.Runtime qualified as Env +import ProgrammableTokens.OffChain.Env.TransferLogic (programmableTokenPolicyId) import ProgrammableTokens.OffChain.Query qualified as Query import Servant (Server, ServerT) import Servant.API (NoContent (..), Raw, (:<|>) (..)) @@ -42,8 +46,8 @@ import Servant.Server (hoistServer, serve) import Servant.Server.StaticFiles (serveDirectoryWebApp) import SmartTokens.Types.PTokenDirectory (blnKey) import System.Environment qualified -import Wst.App (WstApp, runWstAppServant) -import Wst.AppError (AppError (..)) +import Wst.App (WstApp, runWstApp, runWstAppServant) +import Wst.AppError (AppError (..), RegulatedStablecoinError (..)) import Wst.Offchain.BuildTx.Failing (BlacklistedTransferPolicy (..)) import Wst.Offchain.Endpoints.Deployment qualified as Endpoints import Wst.Offchain.Env qualified as Env @@ -51,9 +55,13 @@ import Wst.Offchain.Query (UTxODat (uDatum)) import Wst.Offchain.Query qualified as Query import Wst.Server.DemoEnvironment (DemoEnvRoute, runDemoEnvRoute) import Wst.Server.DemoEnvironment qualified as DemoEnvironment +import Wst.Server.PolicyIssuerStore (HasPolicyIssuerStore (..), getPolicyIssuer, + insertPolicyIssuer) import Wst.Server.Types (APIInEra, AddVKeyWitnessArgs (..), - BlacklistNodeArgs (..), BuildTxAPI, - IssueProgrammableTokenArgs (..), QueryAPI, + BlacklistInitArgs (..), BlacklistNodeArgs (..), + BuildTxAPI, IssueProgrammableTokenArgs (..), + MultiSeizeAssetsArgs (..), PolicyIdHex (..), QueryAPI, + RegisterTransferScriptsArgs (RegisterTransferScriptsArgs, rsaIssuer), SeizeAssetsArgs (..), SerialiseAddress (..), TextEnvelopeJSON (..), TransferProgrammableTokenArgs (..), @@ -71,6 +79,7 @@ data ServerArgs = { saPort :: !Int , saStaticFiles :: Maybe FilePath , saDemoEnvironmentFile :: Maybe FilePath + , saPolicyIssuerStore :: FilePath } deriving stock (Eq, Show) @@ -91,15 +100,21 @@ demoFileFromEnv sa@ServerArgs{saDemoEnvironmentFile} = case saDemoEnvironmentFil files' <- liftIO (System.Environment.lookupEnv "WST_DEMO_ENV") pure sa{saDemoEnvironmentFile = files'} +policyIssuerStoreFromEnv :: MonadIO m => ServerArgs -> m ServerArgs +policyIssuerStoreFromEnv sa = do + maybeOverride <- liftIO (System.Environment.lookupEnv "WST_POLICY_ISSUER_STORE") + pure $ maybe sa (\path -> sa{saPolicyIssuerStore = path}) maybeOverride + defaultServerArgs :: ServerArgs defaultServerArgs = ServerArgs { saPort = 8080 , saStaticFiles = Nothing , saDemoEnvironmentFile = Nothing + , saPolicyIssuerStore = "policy-issuers.sqlite" } -runServer :: (Env.HasRuntimeEnv env, Env.HasDirectoryEnv env, HasLogger env) => env -> ServerArgs -> IO () +runServer :: (Env.HasRuntimeEnv env, Env.HasDirectoryEnv env, HasLogger env, HasPolicyIssuerStore env) => env -> ServerArgs -> IO () runServer env ServerArgs{saPort, saStaticFiles, saDemoEnvironmentFile} = do let bf = Blockfrost.projectId $ Env.ceBlockfrost $ Env.runtimeEnv env demoEnv <- @@ -112,7 +127,7 @@ runServer env ServerArgs{saPort, saStaticFiles, saDemoEnvironmentFile} = do port = saPort Warp.run port app -server :: forall env. (Env.HasRuntimeEnv env, Env.HasDirectoryEnv env, HasLogger env) => env -> Server APIInEra +server :: forall env. (Env.HasRuntimeEnv env, Env.HasDirectoryEnv env, HasLogger env, HasPolicyIssuerStore env) => env -> Server APIInEra server env = hoistServer (Proxy @APIInEra) (runWstAppServant env) $ healthcheck :<|> queryApi @env @@ -121,21 +136,28 @@ server env = hoistServer (Proxy @APIInEra) (runWstAppServant env) $ healthcheck :: Applicative m => m NoContent healthcheck = pure NoContent -queryApi :: forall env. (Env.HasDirectoryEnv env) => ServerT (QueryAPI C.ConwayEra) (WstApp env C.ConwayEra) +queryApi :: forall env. (Env.HasDirectoryEnv env, Env.HasRuntimeEnv env, HasPolicyIssuerStore env) => ServerT (QueryAPI C.ConwayEra) (WstApp env C.ConwayEra) queryApi = Query.globalParamsNode :<|> queryBlacklistedNodes (Proxy @C.ConwayEra) :<|> queryUserFunds @C.ConwayEra @env (Proxy @C.ConwayEra) :<|> queryAllFunds @C.ConwayEra @env (Proxy @C.ConwayEra) :<|> computeUserAddress (Proxy @C.ConwayEra) + :<|> getFreezeAndSeizeProgrammableTokenPolicyId @C.ConwayEra @env (Proxy @C.ConwayEra) + :<|> getProgrammableTokenStakeScripts @C.ConwayEra @env (Proxy @C.ConwayEra) + :<|> queryUserTotalProgrammableValue @C.ConwayEra @env (Proxy @C.ConwayEra) + :<|> queryPolicyIssuerAddress @C.ConwayEra @env -txApi :: forall env. (Env.HasDirectoryEnv env, HasLogger env) => ServerT (BuildTxAPI C.ConwayEra) (WstApp env C.ConwayEra) +txApi :: forall env. (Env.HasDirectoryEnv env, HasLogger env, HasPolicyIssuerStore env) => ServerT (BuildTxAPI C.ConwayEra) (WstApp env C.ConwayEra) txApi = (issueProgrammableTokenEndpoint @C.ConwayEra @env :<|> transferProgrammableTokenEndpoint @C.ConwayEra @env :<|> addToBlacklistEndpoint :<|> removeFromBlacklistEndpoint :<|> seizeAssetsEndpoint + :<|> seizeMultiAssetsEndpoint + :<|> registerTransferScriptsEndpoint + :<|> blacklistInitEndpoint @C.ConwayEra @env ) :<|> pure . addWitnessEndpoint :<|> submitTxEndpoint @@ -169,7 +191,7 @@ queryBlacklistedNodes :: forall era env m. -> SerialiseAddress (C.Address C.ShelleyAddr) -> m [C.Hash C.PaymentKey] queryBlacklistedNodes _ (SerialiseAddress addr) = do - (transferLogic, ble) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress addr) + (transferLogic, ble) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress addr) (stakeCredentialFromAddress addr) let getHash = either (error "deserialiseFromRawBytes failed") id . C.deserialiseFromRawBytes (C.proxyToAsType $ Proxy @(C.Hash C.PaymentKey)) @@ -187,15 +209,24 @@ queryUserFunds :: forall era env m. , C.IsBabbageBasedEra era , MonadReader env m , Env.HasDirectoryEnv env + , Env.HasRuntimeEnv env , MonadBlockchain era m + , MonadError (AppError era) m + , MonadIO m ) => Proxy era -> SerialiseAddress (C.Address C.ShelleyAddr) -> m UserBalanceResponse queryUserFunds _ (SerialiseAddress addr) = do let credential = paymentCredentialFromAddress addr - ubrProgrammableTokens <- foldMap (txOutValue . Query.uOut) <$> Query.userProgrammableOutputs @era @env credential - otherUTxOs <- utxosByPaymentCredential credential + stakeCredential = stakeCredentialFromAddress addr + ubrProgrammableTokens <- foldMap (txOutValue . Query.uOut) <$> Query.userProgrammableOutputs @era @env (credential, stakeCredential) + env <- ask + result <- liftIO $ runWstApp env (utxosByPaymentCredential credential) + otherUTxOs <- case result of + Left (BlockfrostErr BlockfrostNotFound) -> pure mempty + Left err -> throwError err + Right utxos -> pure utxos let userBalance = C.selectLovelace (Utxos.totalBalance otherUTxOs) adaOnly = Map.size $ Utxos._utxos $ Utxos.onlyAda otherUTxOs pure @@ -205,6 +236,52 @@ queryUserFunds _ (SerialiseAddress addr) = do , ubrAdaOnlyOutputs = adaOnly } +queryUserTotalProgrammableValue :: forall era env m. + ( MonadReader env m + , Env.HasDirectoryEnv env + , MonadUtxoQuery m + , C.IsBabbageBasedEra era + , MonadBlockchain era m + ) + => Proxy era + -> SerialiseAddress (C.Address C.ShelleyAddr) -> m C.Value +queryUserTotalProgrammableValue _ (SerialiseAddress userAddress) = do + let credential = paymentCredentialFromAddress userAddress + stakeCredential = stakeCredentialFromAddress userAddress + Query.userTotalProgrammableValue @era @env (credential, stakeCredential) + +getFreezeAndSeizeProgrammableTokenPolicyId :: forall era env m. + ( MonadReader env m + , Env.HasDirectoryEnv env + ) + => Proxy era + -> SerialiseAddress (C.Address C.ShelleyAddr) + -> m C.PolicyId +getFreezeAndSeizeProgrammableTokenPolicyId _ (SerialiseAddress userAddress) = do + dirEnv <- asks Env.directoryEnv + (logic, _) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress userAddress) (stakeCredentialFromAddress userAddress) + Env.withEnv $ Env.withDirectory dirEnv $ Env.withTransfer logic $ do + programmableTokenPolicyId + +getProgrammableTokenStakeScripts :: forall era env m. + ( MonadReader env m + , Env.HasDirectoryEnv env + ) + => Proxy era + -> SerialiseAddress (C.Address C.ShelleyAddr) + -> m [C.ScriptHash] +getProgrammableTokenStakeScripts _ (SerialiseAddress userAddress) = do + dirEnv <- asks Env.directoryEnv + (logic, _) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress userAddress) (stakeCredentialFromAddress userAddress) + Env.withEnv $ Env.withDirectory dirEnv $ Env.withTransfer logic $ do + transferMintingScript <- asks (Env.tleMintingScript . Env.transferLogicEnv) + transferSpendingScript <- asks (Env.tleTransferScript . Env.transferLogicEnv) + transferSeizeSpendingScript <- asks (Env.tleIssuerScript . Env.transferLogicEnv) + let hshMinting = C.hashScript $ C.PlutusScript C.plutusScriptVersion transferMintingScript + hshSpending = C.hashScript $ C.PlutusScript C.plutusScriptVersion transferSpendingScript + hshSeizeSpending = C.hashScript $ C.PlutusScript C.plutusScriptVersion transferSeizeSpendingScript + pure [hshMinting, hshSpending, hshSeizeSpending] + queryAllFunds :: forall era env m. ( MonadUtxoQuery m , C.IsBabbageBasedEra era @@ -215,29 +292,93 @@ queryAllFunds :: forall era env m. -> m C.Value queryAllFunds _ = foldMap (txOutValue . Query.uOut) <$> Query.programmableLogicOutputs @era @env +queryPolicyIssuerAddress :: forall era env m. + ( MonadReader env m + , HasPolicyIssuerStore env + , MonadError (AppError era) m + , MonadIO m + ) + => PolicyIdHex + -> m (SerialiseAddress (C.Address C.ShelleyAddr)) +queryPolicyIssuerAddress (PolicyIdHex policyId) = do + store <- asks policyIssuerStore + result <- liftIO $ getPolicyIssuer store policyId + case result of + Just addr -> pure (SerialiseAddress addr) + Nothing -> throwError (RegStablecoinError (PolicyIssuerNotFound policyId)) + +registerTransferScriptsEndpoint :: forall era env m. + ( MonadReader env m + , Env.HasDirectoryEnv env + , MonadBlockchain era m + , MonadError (AppError era) m + , MonadUtxoQuery m + , C.IsConwayBasedEra era + ) + => RegisterTransferScriptsArgs -> m (TextEnvelopeJSON (C.Tx era)) +registerTransferScriptsEndpoint RegisterTransferScriptsArgs{rsaIssuer} = do + operatorEnv <- Env.loadOperatorEnvFromAddress @_ @era rsaIssuer + dirEnv <- asks Env.directoryEnv + (logic, _) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress rsaIssuer) (stakeCredentialFromAddress rsaIssuer) + Env.withEnv $ Env.withOperator operatorEnv $ Env.withDirectory dirEnv $ Env.withTransfer logic $ do + TextEnvelopeJSON <$> registerCip143PolicyTransferScripts + issueProgrammableTokenEndpoint :: forall era env m. ( MonadReader env m , Env.HasDirectoryEnv env + , HasPolicyIssuerStore env , MonadBlockchain era m , MonadError (AppError era) m , C.IsBabbageBasedEra era , C.HasScriptLanguageInEra C.PlutusScriptV3 era , MonadUtxoQuery m + , MonadIO m ) => IssueProgrammableTokenArgs -> m (TextEnvelopeJSON (C.Tx era)) issueProgrammableTokenEndpoint IssueProgrammableTokenArgs{itaAssetName, itaQuantity, itaIssuer, itaRecipient} = do + store <- asks policyIssuerStore let C.ShelleyAddress _network cred _stake = itaRecipient destinationCredential = C.fromShelleyPaymentCredential cred operatorEnv <- Env.loadOperatorEnvFromAddress @_ @era itaIssuer dirEnv <- asks Env.directoryEnv - (logic, _) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress itaIssuer) + (logic, _) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress itaIssuer) (stakeCredentialFromAddress itaIssuer) + (policyId, envelope) <- + Env.withEnv $ Env.withOperator operatorEnv $ Env.withDirectory dirEnv $ Env.withTransfer logic $ do + pid <- programmableTokenPolicyId + tx <- Endpoints.issueSmartTokensTx itaAssetName itaQuantity destinationCredential + pure (pid, TextEnvelopeJSON (fst tx)) + liftIO $ insertPolicyIssuer store policyId itaIssuer + pure envelope + +blacklistInitEndpoint :: forall era env m. + ( MonadReader env m + , Env.HasDirectoryEnv env + , MonadBlockchain era m + , MonadError (AppError era) m + , C.IsBabbageBasedEra era + , C.HasScriptLanguageInEra C.PlutusScriptV3 era + , MonadUtxoQuery m + ) + => BlacklistInitArgs -> m (TextEnvelopeJSON (C.Tx era)) +blacklistInitEndpoint BlacklistInitArgs{biaIssuer} = do + operatorEnv <- Env.loadOperatorEnvFromAddress @_ @era biaIssuer + dirEnv <- asks Env.directoryEnv + (logic, _) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress biaIssuer) (stakeCredentialFromAddress biaIssuer) Env.withEnv $ Env.withOperator operatorEnv $ Env.withDirectory dirEnv $ Env.withTransfer logic $ do - TextEnvelopeJSON . fst <$> Endpoints.issueSmartTokensTx itaAssetName itaQuantity destinationCredential + TextEnvelopeJSON <$> Endpoints.deployBlacklistTx paymentCredentialFromAddress :: C.Address C.ShelleyAddr -> C.PaymentCredential paymentCredentialFromAddress = \case C.ShelleyAddress _ cred _ -> C.fromShelleyPaymentCredential cred +stakeCredentialFromAddress :: C.Address C.ShelleyAddr -> Maybe C.StakeCredential +stakeCredentialFromAddress = \case + C.ShelleyAddress _ _ (C.fromShelleyStakeReference -> cred) -> + case cred of + C.StakeAddressByValue stakeCred -> Just stakeCred + C.StakeAddressByPointer _ -> Nothing + C.NoStakeAddress -> Nothing + paymentKeyHashFromAddress :: C.Address C.ShelleyAddr -> C.Hash C.PaymentKey paymentKeyHashFromAddress = \case C.ShelleyAddress _ (C.fromShelleyPaymentCredential -> C.PaymentCredentialByKey cred) _ -> cred @@ -257,7 +398,7 @@ transferProgrammableTokenEndpoint :: forall era env m. transferProgrammableTokenEndpoint TransferProgrammableTokenArgs{ttaSender, ttaRecipient, ttaAssetName, ttaQuantity, ttaIssuer, ttaSubmitFailingTx} = do operatorEnv <- Env.loadOperatorEnvFromAddress @_ @era ttaSender dirEnv <- asks Env.directoryEnv - (logic, ble) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress ttaIssuer) + (logic, ble) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress ttaIssuer) (stakeCredentialFromAddress ttaIssuer) let assetId = Env.programmableTokenAssetId dirEnv logic ttaAssetName let policy = if ttaSubmitFailingTx then SubmitFailingTx else DontSubmitFailingTx logInfo $ "Transfer programmable tokens" :# [logPolicy policy, logSender ttaSender, logRecipient ttaRecipient] @@ -278,7 +419,7 @@ addToBlacklistEndpoint BlacklistNodeArgs{bnaIssuer, bnaBlacklistAddress, bnaReas let badCred = paymentCredentialFromAddress bnaBlacklistAddress operatorEnv <- Env.loadOperatorEnvFromAddress @_ @era bnaIssuer dirEnv <- asks Env.directoryEnv - (transferLogic, ble) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress bnaIssuer) + (transferLogic, ble) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress bnaIssuer) (stakeCredentialFromAddress bnaIssuer) Env.withEnv $ Env.withOperator operatorEnv $ Env.withBlacklist ble $ Env.withDirectory dirEnv $ Env.withTransfer transferLogic $ do TextEnvelopeJSON <$> Endpoints.insertBlacklistNodeTx bnaReason badCred @@ -296,7 +437,7 @@ removeFromBlacklistEndpoint BlacklistNodeArgs{bnaIssuer, bnaBlacklistAddress} = let badCred = paymentCredentialFromAddress bnaBlacklistAddress operatorEnv <- Env.loadOperatorEnvFromAddress @_ @era bnaIssuer dirEnv <- asks Env.directoryEnv - (transferLogic, ble) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress bnaIssuer) + (transferLogic, ble) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress bnaIssuer) (stakeCredentialFromAddress bnaIssuer) Env.withEnv $ Env.withOperator operatorEnv $ Env.withBlacklist ble $ Env.withDirectory dirEnv $ Env.withTransfer transferLogic $ do TextEnvelopeJSON <$> Endpoints.removeBlacklistNodeTx badCred @@ -314,10 +455,28 @@ seizeAssetsEndpoint SeizeAssetsArgs{saIssuer, saTarget, saReason} = do let badCred = paymentCredentialFromAddress saTarget operatorEnv <- Env.loadOperatorEnvFromAddress @_ @era saIssuer dirEnv <- asks Env.directoryEnv - (transferLogic, _) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress saIssuer) + (transferLogic, _) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress saIssuer) (stakeCredentialFromAddress saIssuer) Env.withEnv $ Env.withOperator operatorEnv $ Env.withDirectory dirEnv $ Env.withTransfer transferLogic $ do TextEnvelopeJSON <$> Endpoints.seizeCredentialAssetsTx saReason badCred +seizeMultiAssetsEndpoint :: forall era env m. + ( MonadReader env m + , Env.HasDirectoryEnv env + , MonadBlockchain era m + , MonadError (AppError era) m + , C.IsBabbageBasedEra era + , C.HasScriptLanguageInEra C.PlutusScriptV3 era + , MonadUtxoQuery m + ) + => MultiSeizeAssetsArgs -> m (TextEnvelopeJSON (C.Tx era)) +seizeMultiAssetsEndpoint MultiSeizeAssetsArgs{msaIssuer, msaTarget, msaReason, msaNumUTxOsToSeize} = do + let badCreds = map paymentCredentialFromAddress msaTarget + operatorEnv <- Env.loadOperatorEnvFromAddress @_ @era msaIssuer + dirEnv <- asks Env.directoryEnv + (transferLogic, _) <- Env.transferLogicForDirectory (paymentKeyHashFromAddress msaIssuer) (stakeCredentialFromAddress msaIssuer) + Env.withEnv $ Env.withOperator operatorEnv $ Env.withDirectory dirEnv $ Env.withTransfer transferLogic $ do + TextEnvelopeJSON <$> Endpoints.seizeMultiCredentialAssetsTx msaReason msaNumUTxOsToSeize badCreds + addWitnessEndpoint :: forall era. AddVKeyWitnessArgs era -> TextEnvelopeJSON (C.Tx era) addWitnessEndpoint AddVKeyWitnessArgs{avwTx, avwVKeyWitness} = let C.Tx txBody txWits = unTextEnvelopeJSON avwTx diff --git a/src/examples/regulated-stablecoin/lib/Wst/Server/DemoEnvironment.hs b/src/examples/regulated-stablecoin/lib/Wst/Server/DemoEnvironment.hs index 3849920e..01584062 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/Server/DemoEnvironment.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/Server/DemoEnvironment.hs @@ -125,7 +125,7 @@ mkDemoEnv :: C.TxIn -> C.TxIn -> C.Address C.ShelleyAddr -> Either String DemoEn mkDemoEnv txIn issuanceCborHexTxIn (C.ShelleyAddress network (C.fromShelleyPaymentCredential -> C.PaymentCredentialByKey pkh) _) = do let target = Production dirEnv = Env.mkDirectoryEnv (Env.DirectoryScriptRoot txIn issuanceCborHexTxIn target) - rt = Env.BlacklistTransferLogicScriptRoot target dirEnv pkh + rt = Env.BlacklistTransferLogicScriptRoot target dirEnv pkh Nothing transferLogicEnv = Env.mkTransferLogicEnv rt blacklistEnv = Env.mkBlacklistEnv rt dummyText = "REPLACE ME" diff --git a/src/examples/regulated-stablecoin/lib/Wst/Server/PolicyIssuerStore.hs b/src/examples/regulated-stablecoin/lib/Wst/Server/PolicyIssuerStore.hs new file mode 100644 index 00000000..f84d1e47 --- /dev/null +++ b/src/examples/regulated-stablecoin/lib/Wst/Server/PolicyIssuerStore.hs @@ -0,0 +1,131 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE UndecidableInstances #-} + +{- | Lightweight SQLite-backed store for mapping freeze/seize policy identifiers + to their corresponding issuer addresses. This allows off-chain endpoints to + recover the issuer needed to drive future transactions even after process + restarts. +-} +module Wst.Server.PolicyIssuerStore ( + PolicyIssuerStore (..), + HasPolicyIssuerStore (..), + addPolicyIssuerStore, + PolicyIssuer, + openPolicyIssuerStore, + closePolicyIssuerStore, + withPolicyIssuerStore, + insertPolicyIssuer, + getPolicyIssuer, + listPolicyIssuers, +) where + +import Cardano.Api (AsType (..)) +import Cardano.Api qualified as C +import Control.Exception (bracket) +import Data.ByteString (ByteString) +import Data.Maybe (listToMaybe, mapMaybe) +import Data.Text (Text) +import Database.SQLite.Simple qualified as SQL +import Database.SQLite.Simple.FromRow (FromRow (..), field) +import ProgrammableTokens.OffChain.Env.Utils qualified as EnvUtils + +-- | Simple wrapper so we can manage the connection lifecycle explicitly. +newtype PolicyIssuerStore = PolicyIssuerStore + { pisConnection :: SQL.Connection + } + +type PolicyIssuer = (C.PolicyId, C.Address C.ShelleyAddr) + +openPolicyIssuerStore :: FilePath -> IO PolicyIssuerStore +openPolicyIssuerStore path = do + conn <- SQL.open path + initialiseSchema conn + pure $ PolicyIssuerStore conn + +closePolicyIssuerStore :: PolicyIssuerStore -> IO () +closePolicyIssuerStore = SQL.close . pisConnection + +withPolicyIssuerStore :: FilePath -> (PolicyIssuerStore -> IO a) -> IO a +withPolicyIssuerStore path = + bracket (openPolicyIssuerStore path) closePolicyIssuerStore + +insertPolicyIssuer :: PolicyIssuerStore -> C.PolicyId -> C.Address C.ShelleyAddr -> IO () +insertPolicyIssuer PolicyIssuerStore{pisConnection} policyId issuer = + SQL.execute + pisConnection + "INSERT INTO policy_issuers (policy_id, issuer_address) VALUES (?, ?) \ + \ON CONFLICT(policy_id) DO UPDATE SET issuer_address = excluded.issuer_address" + (policyIdToBytes policyId, addressToText issuer) + +getPolicyIssuer :: PolicyIssuerStore -> C.PolicyId -> IO (Maybe (C.Address C.ShelleyAddr)) +getPolicyIssuer PolicyIssuerStore{pisConnection} policyId = do + rows <- + SQL.query + pisConnection + "SELECT issuer_address FROM policy_issuers WHERE policy_id = ? LIMIT 1" + (SQL.Only (policyIdToBytes policyId)) + pure $ listToMaybe rows >>= textToAddress . SQL.fromOnly + +listPolicyIssuers :: PolicyIssuerStore -> IO [PolicyIssuer] +listPolicyIssuers PolicyIssuerStore{pisConnection} = do + rows <- SQL.query_ pisConnection "SELECT policy_id, issuer_address FROM policy_issuers" + pure $ mapMaybe decode rows + where + decode :: PolicyIssuerRow -> Maybe PolicyIssuer + decode PolicyIssuerRow{pirPolicyId, pirIssuerAddress} = do + pid <- policyIdFromBytes pirPolicyId + addr <- textToAddress pirIssuerAddress + pure (pid, addr) + +-- Internal ------------------------------------------------------------------- + +data PolicyIssuerRow = PolicyIssuerRow + { pirPolicyId :: ByteString + , pirIssuerAddress :: Text + } + +instance FromRow PolicyIssuerRow where + fromRow = PolicyIssuerRow <$> field <*> field + +initialiseSchema :: SQL.Connection -> IO () +initialiseSchema conn = do + SQL.execute_ + conn + "CREATE TABLE IF NOT EXISTS policy_issuers ( \ + \ policy_id BLOB PRIMARY KEY, \ + \ issuer_address TEXT NOT NULL, \ + \ inserted_at TEXT NOT NULL DEFAULT (datetime('now')) \ + \)" + SQL.execute_ + conn + "CREATE INDEX IF NOT EXISTS idx_policy_issuers_issuer \ + \ON policy_issuers (issuer_address)" + +policyIdToBytes :: C.PolicyId -> ByteString +policyIdToBytes = C.serialiseToRawBytes + +policyIdFromBytes :: ByteString -> Maybe C.PolicyId +policyIdFromBytes bs = + case C.deserialiseFromRawBytes AsPolicyId bs of + Left _ -> Nothing + Right pid -> Just pid + +addressToText :: C.Address C.ShelleyAddr -> Text +addressToText = C.serialiseAddress + +textToAddress :: Text -> Maybe (C.Address C.ShelleyAddr) +textToAddress = C.deserialiseAddress (AsAddress AsShelleyAddr) + +class HasPolicyIssuerStore env where + policyIssuerStore :: env -> PolicyIssuerStore + +instance HasPolicyIssuerStore PolicyIssuerStore where + policyIssuerStore = id + +instance (EnvUtils.Elem PolicyIssuerStore els) => HasPolicyIssuerStore (EnvUtils.HSet els) where + policyIssuerStore = EnvUtils.hget @_ @PolicyIssuerStore + +addPolicyIssuerStore :: (EnvUtils.NotElem PolicyIssuerStore els) => PolicyIssuerStore -> EnvUtils.HSet els -> EnvUtils.HSet (PolicyIssuerStore ': els) +addPolicyIssuerStore = EnvUtils.addEnv diff --git a/src/examples/regulated-stablecoin/lib/Wst/Server/Types.hs b/src/examples/regulated-stablecoin/lib/Wst/Server/Types.hs index 2ad26a01..799e1fcf 100644 --- a/src/examples/regulated-stablecoin/lib/Wst/Server/Types.hs +++ b/src/examples/regulated-stablecoin/lib/Wst/Server/Types.hs @@ -4,6 +4,8 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeOperators #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +{-# HLINT ignore "Use newtype instead of data" #-} {- | This module contains the relevant types for the server. @@ -19,14 +21,18 @@ module Wst.Server.Types ( TransferProgrammableTokenArgs(..), BlacklistNodeArgs(..), SeizeAssetsArgs(..), + MultiSeizeAssetsArgs(..), AddVKeyWitnessArgs(..), + RegisterTransferScriptsArgs(..), + BlacklistInitArgs(..), -- * Response types UserBalanceResponse(..), -- * Newtypes TextEnvelopeJSON(..), - SerialiseAddress(..) + SerialiseAddress(..), + PolicyIdHex(..) ) where import Cardano.Api (AssetName, Quantity) @@ -43,6 +49,8 @@ import Data.OpenApi.ParamSchema (ToParamSchema (..)) import Data.OpenApi.Schema qualified as Schema import Data.OpenApi.SchemaOptions qualified as SchemaOptions import Data.Proxy (Proxy (..)) +import Data.Text qualified as Text +import Data.Text.Encoding qualified as TextEncoding import GHC.Generics (Generic) import ProgrammableTokens.JSON.Utils qualified as JSON import Servant (FromHttpApiData (..), ToHttpApiData (toUrlPiece)) @@ -96,6 +104,25 @@ instance C.SerialiseAddress a => FromHttpApiData (SerialiseAddress a) where instance C.SerialiseAddress a => ToHttpApiData (SerialiseAddress a) where toUrlPiece = C.serialiseAddress . unSerialiseAddress +instance ToParamSchema PolicyIdHex where + toParamSchema _ = + mempty + & L.type_ ?~ OpenApiString + & L.description ?~ "hex-encoded policy identifier" + & L.example ?~ "4cfd5e2b0c534b4e0cda0f5d84df7e0d3d3c6a74c0e5f3d823a58a38" + +newtype PolicyIdHex = PolicyIdHex { unPolicyIdHex :: C.PolicyId } + deriving stock (Eq, Show) + +instance FromHttpApiData PolicyIdHex where + parseUrlPiece txt = + case C.deserialiseFromRawBytesHex C.AsPolicyId (TextEncoding.encodeUtf8 txt) of + Left err -> Left ("Failed to parse policy id: " <> Text.pack (show err)) + Right pid -> Right (PolicyIdHex pid) + +instance ToHttpApiData PolicyIdHex where + toUrlPiece (PolicyIdHex pid) = C.serialiseToRawBytesHexText pid + type API era = "healthcheck" :> Description "Is the server alive?" :> Get '[JSON] NoContent :<|> "query" :> QueryAPI era @@ -107,6 +134,26 @@ type QueryAPI era = :<|> "user-funds" :> Description "Total value locked in programmable token outputs addressed to the user" :> Capture "address" (SerialiseAddress (C.Address C.ShelleyAddr)) :> Get '[JSON] UserBalanceResponse :<|> "all-funds" :> Description "Total value of all programmable tokens" :> Get '[JSON] C.Value :<|> "address" :> Description "The user's receiving address for programmable tokens" :> Capture "address" (SerialiseAddress (C.Address C.ShelleyAddr)) :> Get '[JSON] (C.Address C.ShelleyAddr) + :<|> "freeze-policy-id" :> Description "The policy ID for the freeze and seize programmable token policy associated with this user" :> Capture "address" (SerialiseAddress (C.Address C.ShelleyAddr)) :> Get '[JSON] C.PolicyId + :<|> "stake-scripts" :> Description "The stake scripts for the programmable token" :> Capture "address" (SerialiseAddress (C.Address C.ShelleyAddr)) :> Get '[JSON] [C.ScriptHash] + :<|> "user-total-programmable-value" :> Description "Total value of all programmable tokens addressed to the user" :> Capture "address" (SerialiseAddress (C.Address C.ShelleyAddr)) :> Get '[JSON] C.Value + :<|> "policy-issuer" :> Description "Issuer address associated with a freeze/seize policy id" :> Capture "policy_id" PolicyIdHex :> Get '[JSON] (SerialiseAddress (C.Address C.ShelleyAddr)) + +data RegisterTransferScriptsArgs = + RegisterTransferScriptsArgs + { rsaIssuer :: C.Address C.ShelleyAddr + } + deriving stock (Eq, Show, Generic) + +instance ToJSON RegisterTransferScriptsArgs where + toJSON = JSON.genericToJSON jsonOptions3 + toEncoding = JSON.genericToEncoding jsonOptions3 + +instance ToSchema RegisterTransferScriptsArgs where + declareNamedSchema = Schema.genericDeclareNamedSchema (SchemaOptions.fromAesonOptions jsonOptions3) + +instance FromJSON RegisterTransferScriptsArgs where + parseJSON = JSON.genericParseJSON jsonOptions3 {-| Arguments for the programmable-token endpoint. The asset name can be something like "USDW" for the regulated stablecoin. -} @@ -192,6 +239,25 @@ instance FromJSON SeizeAssetsArgs where instance ToSchema SeizeAssetsArgs where declareNamedSchema = Schema.genericDeclareNamedSchema (SchemaOptions.fromAesonOptions jsonOptions2) +data MultiSeizeAssetsArgs = + MultiSeizeAssetsArgs + { msaIssuer :: C.Address C.ShelleyAddr + , msaTarget :: [C.Address C.ShelleyAddr] + , msaReason :: SeizeReason + , msaNumUTxOsToSeize :: Int + } + deriving stock (Eq, Show, Generic) + +instance ToJSON MultiSeizeAssetsArgs where + toJSON = JSON.genericToJSON jsonOptions2 + toEncoding = JSON.genericToEncoding jsonOptions2 + +instance FromJSON MultiSeizeAssetsArgs where + parseJSON = JSON.genericParseJSON jsonOptions2 + +instance ToSchema MultiSeizeAssetsArgs where + declareNamedSchema = Schema.genericDeclareNamedSchema (SchemaOptions.fromAesonOptions jsonOptions2) + data AddVKeyWitnessArgs era = AddVKeyWitnessArgs { avwTx :: TextEnvelopeJSON (C.Tx era) @@ -216,12 +282,33 @@ type BuildTxAPI era = :<|> "blacklist" :> Description "Add a credential to the blacklist" :> ReqBody '[JSON] BlacklistNodeArgs :> Post '[JSON] (TextEnvelopeJSON (C.Tx era)) :<|> "unblacklist" :> Description "Remove a credential from the blacklist" :> ReqBody '[JSON] BlacklistNodeArgs :> Post '[JSON] (TextEnvelopeJSON (C.Tx era)) :<|> "seize" :> Description "Seize a user's funds" :> ReqBody '[JSON] SeizeAssetsArgs :> Post '[JSON] (TextEnvelopeJSON (C.Tx era)) + :<|> "seize-multi" :> Description "Seize multiple user's funds" :> ReqBody '[JSON] MultiSeizeAssetsArgs :> Post '[JSON] (TextEnvelopeJSON (C.Tx era)) + :<|> "register-transfer-scripts" :> Description "Register the transfer scripts" :> ReqBody '[JSON] RegisterTransferScriptsArgs :> Post '[JSON] (TextEnvelopeJSON (C.Tx era)) + :<|> "blacklist-init" :> Description "Initialize the blacklist" :> ReqBody '[JSON] BlacklistInitArgs :> Post '[JSON] (TextEnvelopeJSON (C.Tx era)) ) :<|> "add-vkey-witness" :> Description "Add a VKey witness to a transaction" :> ReqBody '[JSON] (AddVKeyWitnessArgs era) :> Post '[JSON] (TextEnvelopeJSON (C.Tx era)) :<|> "submit" :> Description "Submit a transaction to the blockchain" :> ReqBody '[JSON] (TextEnvelopeJSON (C.Tx era)) :> Post '[JSON] C.TxId +{-| Arguments for the blacklist-init endpoint. +-} +data BlacklistInitArgs = + BlacklistInitArgs + { biaIssuer :: C.Address C.ShelleyAddr + } + deriving stock (Eq, Show, Generic) + +instance ToJSON BlacklistInitArgs where + toJSON = JSON.genericToJSON jsonOptions3 + toEncoding = JSON.genericToEncoding jsonOptions3 + +instance FromJSON BlacklistInitArgs where + parseJSON = JSON.genericParseJSON jsonOptions3 + +instance ToSchema BlacklistInitArgs where + declareNamedSchema = Schema.genericDeclareNamedSchema (SchemaOptions.fromAesonOptions jsonOptions3) + {-| Response to the user-balance query -} data UserBalanceResponse = diff --git a/src/examples/regulated-stablecoin/regulated-stablecoin.cabal b/src/examples/regulated-stablecoin/regulated-stablecoin.cabal index 08932981..3fab8dd6 100644 --- a/src/examples/regulated-stablecoin/regulated-stablecoin.cabal +++ b/src/examples/regulated-stablecoin/regulated-stablecoin.cabal @@ -64,6 +64,7 @@ library Wst.Offchain.Scripts Wst.Server Wst.Server.DemoEnvironment + Wst.Server.PolicyIssuerStore Wst.Server.Types hs-source-dirs: lib @@ -96,6 +97,7 @@ library , servant-client , servant-client-core , servant-server + , sqlite-simple , text , wai-cors , warp diff --git a/src/examples/regulated-stablecoin/test/unit/Wst/Test/UnitTest.hs b/src/examples/regulated-stablecoin/test/unit/Wst/Test/UnitTest.hs index 646ef01a..b01160ca 100644 --- a/src/examples/regulated-stablecoin/test/unit/Wst/Test/UnitTest.hs +++ b/src/examples/regulated-stablecoin/test/unit/Wst/Test/UnitTest.hs @@ -5,7 +5,7 @@ module Wst.Test.UnitTest( import Cardano.Api qualified as C import Cardano.Api.Shelley qualified as C -import Control.Monad (void) +import Control.Monad (void, when) import Control.Monad.Except (MonadError) import Control.Monad.Reader (MonadReader (ask), ReaderT (runReaderT), asks) import Convex.BuildTx qualified as BuildTx @@ -22,6 +22,7 @@ import Data.List (isPrefixOf) import Data.String (IsString (..)) import GHC.Exception (SomeException, throw) import ProgrammableTokens.OffChain.Endpoints qualified as Endpoints +import ProgrammableTokens.OffChain.Env (programmableTokenPolicyId) import ProgrammableTokens.OffChain.Env.Operator qualified as Env import ProgrammableTokens.OffChain.Query qualified as Query import ProgrammableTokens.Test (deployDirectorySet) @@ -55,6 +56,7 @@ scriptTargetTests target = , testCase "blacklisted transfer" (mockchainFails (blacklistTransfer DontSubmitFailingTx) assertBlacklistedAddressException) , testCase "blacklisted transfer (failing tx)" (Test.mockchainSucceedsWithTarget @(AppError C.ConwayEra) target (blacklistTransfer SubmitFailingTx >>= Test.assertFailingTx)) , testCase "seize user output" (Test.mockchainSucceedsWithTarget @(AppError C.ConwayEra) target $ deployDirectorySet admin >>= seizeUserOutput) + , testCase "seize multi user outputs" (Test.mockchainSucceedsWithTarget @(AppError C.ConwayEra) target $ deployDirectorySet admin >>= seizeMultiUserOutputs) , testCase "deploy all" (Test.mockchainSucceedsWithTarget @(AppError C.ConwayEra) target deployAll) ] ] @@ -121,9 +123,9 @@ transferSmartTokens scriptRoot = Env.withEnv $ do Query.programmableLogicOutputs @C.ConwayEra >>= void . Test.expectN 2 "programmable logic outputs" - Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh) + Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh, Nothing) >>= void . Test.expectN 1 "user programmable outputs" - Query.userProgrammableOutputs (C.PaymentCredentialByKey opPkh) + Query.userProgrammableOutputs (C.PaymentCredentialByKey opPkh, Nothing) >>= void . Test.expectN 1 "user programmable outputs" blacklistCredential :: (MonadUtxoQuery m, MonadFail m, MonadError (AppError C.ConwayEra) m, MonadMockchain C.ConwayEra m) => DirectoryScriptRoot -> m C.PaymentCredential @@ -189,7 +191,7 @@ blacklistTransfer policy = failOnError @_ @(AppError C.ConwayEra) $ Env.withEnv >>= void . sendTx . signTxOperator admin pure opPkh - (transferLogic, ble) <- Env.withDirectoryFor scriptRoot $ Env.transferLogicForDirectory (C.verificationKeyHash . Operator.verificationKey . Operator.oPaymentKey $ admin) + (transferLogic, ble) <- Env.withDirectoryFor scriptRoot $ Env.transferLogicForDirectory (C.verificationKeyHash . Operator.verificationKey . Operator.oPaymentKey $ admin) Nothing asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator @C.ConwayEra $ Endpoints.insertBlacklistNodeTx "" userPaymentCred >>= void . sendTx . signTxOperator admin @@ -212,7 +214,7 @@ seizeUserOutput scriptRoot = Env.withEnv $ do >>= void . sendTx . signTxOperator admin Query.programmableLogicOutputs @C.ConwayEra >>= void . Test.expectN 2 "programmable logic outputs" - Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh) + Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh, Nothing) >>= void . Test.expectN 1 "user programmable outputs" asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator @C.ConwayEra $ do @@ -221,11 +223,51 @@ seizeUserOutput scriptRoot = Env.withEnv $ do >>= void . sendTx . signTxOperator admin Query.programmableLogicOutputs @C.ConwayEra >>= void . Test.expectN 3 "programmable logic outputs" - Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh) + Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh, Nothing) >>= void . Test.expectN 1 "user programmable outputs" - Query.userProgrammableOutputs (C.PaymentCredentialByKey opPkh) + Query.userProgrammableOutputs (C.PaymentCredentialByKey opPkh, Nothing) + >>= void . Test.expectN 2 "operator programmable outputs" + +seizeMultiUserOutputs :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m, MonadError (AppError C.ConwayEra) m) => DirectoryScriptRoot -> m () +seizeMultiUserOutputs scriptRoot = Env.withEnv $ do + userPkh <- asWallet @C.ConwayEra Wallet.w2 $ asks (fst . Env.bteOperator . Env.operatorEnv @C.ConwayEra) + let userPaymentCred = C.PaymentCredentialByKey userPkh + + aid <- issueTransferLogicProgrammableToken scriptRoot + + asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator @C.ConwayEra $ Endpoints.deployBlacklistTx + >>= void . sendTx . signTxOperator admin + + asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator @C.ConwayEra $ do + Endpoints.transferSmartTokensTx DontSubmitFailingTx aid 50 (C.PaymentCredentialByKey userPkh) + >>= void . sendTx . signTxOperator admin + Query.programmableLogicOutputs @C.ConwayEra + >>= void . Test.expectN 2 "programmable logic outputs" + Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh, Nothing) + >>= void . Test.expectN 1 "user programmable outputs" + + asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator @C.ConwayEra $ do + Endpoints.transferSmartTokensTx DontSubmitFailingTx aid 50 (C.PaymentCredentialByKey userPkh) + >>= void . sendTx . signTxOperator admin + Query.programmableLogicOutputs @C.ConwayEra + >>= void . Test.expectN 3 "programmable logic outputs" + Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh, Nothing) >>= void . Test.expectN 2 "user programmable outputs" + asAdmin @C.ConwayEra $ Env.withDirectoryFor scriptRoot $ Env.withTransferFromOperator @C.ConwayEra $ do + opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv @C.ConwayEra) + toSeizePolicyId <- asks programmableTokenPolicyId + Endpoints.seizeMultiCredentialAssetsTx mempty 2 [userPaymentCred] + >>= void . sendTx . signTxOperator admin + Query.programmableLogicOutputs @C.ConwayEra + >>= void . Test.expectN 4 "programmable logic outputs" + userOutputs <- Query.userProgrammableOutputs (C.PaymentCredentialByKey userPkh, Nothing) + Test.expectN 2 "user programmable outputs" userOutputs + mapM_ (\utxo -> when (Query.utxoHasPolicyId toSeizePolicyId utxo) $ fail "User should not have any UTxOs with the programmable token policy ID") userOutputs + + Query.userProgrammableOutputs (C.PaymentCredentialByKey opPkh, Nothing) + >>= void . Test.expectN 2 "operator programmable outputs" + -- TODO: registration to be moved to the endpoints registerTransferScripts :: (MonadFail m, MonadError (AppError C.ConwayEra) m, MonadReader env m, Env.HasTransferLogicEnv env, MonadMockchain C.ConwayEra m) => C.Hash C.PaymentKey -> m C.TxId registerTransferScripts pkh = do diff --git a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/BuildTx/ProgrammableLogic.hs b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/BuildTx/ProgrammableLogic.hs index 633749d6..0a9ba8f9 100644 --- a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/BuildTx/ProgrammableLogic.hs +++ b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/BuildTx/ProgrammableLogic.hs @@ -157,7 +157,6 @@ registerTransferScripts = do hshSeizeSpending = C.hashScript $ C.PlutusScript C.plutusScriptVersion transferSeizeSpendingScript credSeizeSpending = C.StakeCredentialByScript hshSeizeSpending - Utils.addConwayStakeCredentialCertificate credSpending Utils.addConwayStakeCredentialCertificate credMinting Utils.addConwayStakeCredentialCertificate credSeizeSpending diff --git a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Endpoints.hs b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Endpoints.hs index e22c0592..c7ce8e75 100644 --- a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Endpoints.hs +++ b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Endpoints.hs @@ -36,7 +36,7 @@ import SmartTokens.Core.Scripts (ScriptTarget (..)) -} frackUtxosTx :: (MonadReader env m, Env.HasOperatorEnv era env, MonadBlockchain era m, MonadError err m, C.IsBabbageBasedEra era, AsBalancingError err era, AsCoinSelectionError err) => m (C.Tx era) frackUtxosTx = do - (tx, _) <- Env.balanceTxEnv_ $ BuildTx.frackUTxOs + (tx, _) <- Env.balanceTxEnv_ BuildTx.frackUTxOs pure (Convex.CoinSelection.signBalancedTxBody [] tx) {-| Build a transaction that deploys the cbox hex outputs @@ -145,8 +145,8 @@ transferTokens assetName quantity target redeemer = do paramsNode <- Query.globalParamsNode @era policyId <- programmableTokenPolicyId op <- Env.operatorPaymentCredential @env @era - opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv @era) - (inputs, change) <- Query.selectProgammableOutputsFor op assetName quantity + (opPkh, fromStakeAddressReference -> opStakeCred) <- asks (Env.bteOperator . Env.operatorEnv @era) + (inputs, change) <- Query.selectProgammableOutputsFor (op, opStakeCred) assetName quantity (tx, _) <- Env.balanceTxEnv_ $ do BuildTx.transferProgrammableToken paramsNode inputs (transPolicyId policyId) headNode BuildTx.paySmartTokensToDestination (assetName, quantity) policyId target @@ -155,3 +155,9 @@ transferTokens assetName quantity target redeemer = do Env.operatorPaymentCredential @env @era >>= BuildTx.paySmartTokensToDestination (assetName, change) policyId BuildTx.addRequiredSignature opPkh pure (Convex.CoinSelection.signBalancedTxBody [] tx) + +fromStakeAddressReference :: C.StakeAddressReference -> Maybe C.StakeCredential +fromStakeAddressReference = \case + C.StakeAddressByValue stakeCred -> Just stakeCred + C.StakeAddressByPointer _ -> Nothing + C.NoStakeAddress -> Nothing diff --git a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/Operator.hs b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/Operator.hs index 9167afca..4f17dfea 100644 --- a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/Operator.hs +++ b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/Operator.hs @@ -9,10 +9,12 @@ module ProgrammableTokens.OffChain.Env.Operator( -- ** Creating operator env values operatorPaymentCredential, loadOperatorEnv, + reloadOperatorEnv, loadOperatorEnvFromAddress, loadConvexOperatorEnv, selectOperatorOutput, selectTwoOperatorOutputs, + selectOperatorUTxOs, runAs, -- ** Transaction balancing @@ -27,12 +29,12 @@ import Control.Monad.Except (MonadError) import Control.Monad.Reader (MonadReader, ReaderT, asks) import Convex.BuildTx (BuildTxT) import Convex.BuildTx qualified as BuildTx -import Convex.Class (MonadBlockchain, MonadUtxoQuery (..), +import Convex.CardanoApi.Lenses (emptyTxOut) +import Convex.Class (MonadBlockchain, MonadUtxoQuery (..), queryNetworkId, queryProtocolParameters, utxosByPaymentCredential) import Convex.CoinSelection qualified as CoinSelection import Convex.Utxos (BalanceChanges) import Convex.Utxos qualified as Utxos -import Convex.Wallet.Operator (returnOutputFor) import Convex.Wallet.Operator qualified as Op import Data.Map qualified as Map import Data.Maybe (listToMaybe) @@ -71,6 +73,12 @@ loadOperatorEnv paymentCredential stakeCredential = do bteOperatorUtxos <- Utxos.toApiUtxo <$> utxosByPaymentCredential (C.PaymentCredentialByKey paymentCredential) pure OperatorEnv{bteOperator, bteOperatorUtxos} +{-| Refresh an existing 'OperatorEnv' by re-querying the chain for its UTxOs +-} +reloadOperatorEnv :: (MonadUtxoQuery m, C.IsBabbageBasedEra era) => OperatorEnv era -> m (OperatorEnv era) +reloadOperatorEnv OperatorEnv{bteOperator = (paymentCredential, stakeCredential)} = + loadOperatorEnv paymentCredential stakeCredential + {-| Glue code for creating an 'OperatorEnv' from the convex type -} -- TODO: This entire module should be merged with the one from convex! @@ -100,6 +108,25 @@ selectTwoOperatorOutputs = do (k1, v1) : (k2, v2) : _rest -> pure ((k1, v1), (k2, v2)) _ -> throwing_ _OperatorNoUTxOs +{-| Select all UTxOs owned by the operator +-} +selectOperatorUTxOs :: (MonadReader env m, HasOperatorEnv era env, MonadError err m, AsProgrammableTokensError err) => m [(C.TxIn, C.TxOut C.CtxUTxO era)] +selectOperatorUTxOs = do + asks (C.unUTxO . bteOperatorUtxos . operatorEnv) >>= (\case + [] -> throwing_ _OperatorNoUTxOs + utxos -> pure utxos) . Map.toList + +{- An empty output to address produced by the payment credential and stake credential. +-} +returnOutputForWithStakeReference :: (MonadBlockchain era m, C.IsShelleyBasedEra era) => (C.Hash C.PaymentKey, C.StakeAddressReference) -> m (C.TxOut ctx era) +returnOutputForWithStakeReference (paymentCredential, stakeReference) = do + addr <- + C.makeShelleyAddress + <$> queryNetworkId + <*> pure (C.PaymentCredentialByKey paymentCredential) + <*> pure stakeReference + pure $ emptyTxOut $ C.AddressInEra (C.ShelleyAddressInEra C.shelleyBasedEra) addr + {-| Balance a transaction using the operator's funds and return output -} balanceTxEnv_ :: forall era env err a m. (MonadBlockchain era m, MonadReader env m, HasOperatorEnv era env, MonadError err m, C.IsBabbageBasedEra era, CoinSelection.AsCoinSelectionError err, CoinSelection.AsBalancingError err era) => BuildTxT era m a -> m (C.BalancedTxBody era, BalanceChanges) @@ -107,9 +134,7 @@ balanceTxEnv_ btx = do OperatorEnv{bteOperatorUtxos, bteOperator} <- asks (operatorEnv @era) params <- queryProtocolParameters txBuilder <- BuildTx.execBuildTxT $ btx >> BuildTx.setMinAdaDepositAll params - -- TODO: change returnOutputFor to consider the stake address reference - -- (needs to be done in sc-tools) - output <- returnOutputFor (C.PaymentCredentialByKey $ fst bteOperator) + output <- returnOutputForWithStakeReference bteOperator CoinSelection.balanceTx mempty output (Utxos.fromApiUtxo bteOperatorUtxos) txBuilder CoinSelection.TrailingChange {-| Balance a transaction using the operator's funds and return output @@ -119,9 +144,7 @@ balanceTxEnv btx = do OperatorEnv{bteOperatorUtxos, bteOperator} <- asks (operatorEnv @era) 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) - output <- returnOutputFor (C.PaymentCredentialByKey $ fst bteOperator) + output <- returnOutputForWithStakeReference bteOperator (balBody, balChanges) <- CoinSelection.balanceTx mempty output (Utxos.fromApiUtxo bteOperatorUtxos) txBuilder CoinSelection.TrailingChange pure ((balBody, balChanges), r) diff --git a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/TransferLogic.hs b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/TransferLogic.hs index 6c608f5b..4f5cda7d 100644 --- a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/TransferLogic.hs +++ b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/TransferLogic.hs @@ -66,4 +66,4 @@ programmableTokenMintingScript dirEnv@DirectoryEnv{dsScriptRoot} TransferLogicEn -- | The minting policy ID of the programmable token programmableTokenPolicyId :: (MonadReader env m, HasTransferLogicEnv env, HasDirectoryEnv env) => m C.PolicyId programmableTokenPolicyId = - fmap Scripts.scriptPolicyIdV3 (programmableTokenMintingScript <$> asks directoryEnv <*> asks transferLogicEnv) + fmap Scripts.scriptPolicyIdV3 (asks (programmableTokenMintingScript . directoryEnv) <*> asks transferLogicEnv) diff --git a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/Utils.hs b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/Utils.hs index fcd3c14f..a3e214ca 100644 --- a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/Utils.hs +++ b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Env/Utils.hs @@ -10,6 +10,7 @@ module ProgrammableTokens.OffChain.Env.Utils( empty, singleton, addEnv, + modifyEnv, hget, -- * ReaderT @@ -21,6 +22,7 @@ module ProgrammableTokens.OffChain.Env.Utils( import Control.Monad.Reader (MonadReader, ReaderT, asks, runReaderT) import Data.HSet.Get (HGettable, hget) import Data.HSet.Modify (HMonoModifiable) +import Data.HSet.Modify qualified as HModify import Data.HSet.Type (HSet) import Data.HSet.Type qualified as HSet import TypeFun.Data.List (NotElem) @@ -44,6 +46,10 @@ singleton elm = HSet.HSCons elm HSet.HSNil addEnv :: (NotElem elm els) => elm -> HSet els -> HSet (elm ': els) addEnv = HSet.HSCons +-- | Modify an existing element in the environment +modifyEnv :: (HMonoModifiable els elm) => (elm -> elm) -> HSet els -> HSet els +modifyEnv = HModify.hmodify + -- | Add an element to the environment and run a 'ReaderT' action with the extended environment withEnv :: forall elm els m a. (NotElem elm els, MonadReader (HSet els) m) => elm -> ReaderT (HSet (elm ': els)) m a -> m a withEnv elm action = do diff --git a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Orphans.hs b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Orphans.hs index e544d0f9..64a77cb7 100644 --- a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Orphans.hs +++ b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Orphans.hs @@ -110,3 +110,19 @@ instance ToSchema C.TxId where $ NamedSchema (Just "TxId") $ mempty & L.type_ ?~ OpenApiString + +instance ToSchema C.PolicyId where + declareNamedSchema _ = pure + $ NamedSchema (Just "PolicyId") + $ mempty + & L.type_ ?~ OpenApiString + & L.description ?~ "Policy ID" + & L.example ?~ "01f4b788593d4f70de2a45c2e1e87088bfbdfa29577ae1b62aba60e095e3ab53" + +instance ToSchema C.ScriptHash where + declareNamedSchema _ = pure + $ NamedSchema (Just "ScriptHash") + $ mempty + & L.type_ ?~ OpenApiString + & L.description ?~ "Script hash" + & L.example ?~ "01f4b788593d4f70de2a45c2e1e87088bfbdfa29577ae1b62aba60e095e3ab53" diff --git a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Query.hs b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Query.hs index 2d590700..b01997ac 100644 --- a/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Query.hs +++ b/src/programmable-tokens-offchain/lib/ProgrammableTokens/OffChain/Query.hs @@ -13,8 +13,12 @@ module ProgrammableTokens.OffChain.Query( issuanceCborHexUTxO, globalParamsNode, programmableLogicOutputs, - selectProgammableOutputsFor + selectProgammableOutputsFor, + utxoHasPolicyId, + hasPolicyId, + extractValue, + userTotalProgrammableValue, ) where import Cardano.Api qualified as C @@ -25,12 +29,12 @@ import Control.Monad.Reader (MonadReader, asks) import Convex.CardanoApi.Lenses qualified as L import Convex.Class (MonadBlockchain (..), MonadUtxoQuery, utxosByPaymentCredential) -import Convex.PlutusLedger.V1 (transCredential, transPolicyId, +import Convex.PlutusLedger.V1 (transCredential, transPolicyId, unTransPolicyId, unTransStakeCredential) import Convex.Utxos (UtxoSet (UtxoSet)) import Convex.Wallet (selectMixedInputsCovering) import Data.List (sortOn) -import Data.Maybe (listToMaybe) +import Data.Maybe (listToMaybe, mapMaybe) import Data.Ord (Down (..)) import GHC.Exts (IsList (..)) import ProgrammableTokens.OffChain.Env.Directory (DirectoryEnv (..), @@ -101,17 +105,40 @@ registryNode op' = do x : _ -> pure x -- | CIP-143 outputs addressed to the given payment credential -userProgrammableOutputs :: forall era env m. (MonadReader env m, HasDirectoryEnv env, MonadUtxoQuery m, C.IsBabbageBasedEra era, MonadBlockchain era m) => C.PaymentCredential -> m [UTxODat era ()] -userProgrammableOutputs userCred = do +userProgrammableOutputs :: forall era env m. (MonadReader env m, HasDirectoryEnv env, MonadUtxoQuery m, C.IsBabbageBasedEra era, MonadBlockchain era m) => (C.PaymentCredential, Maybe C.StakeCredential) -> m [UTxODat era ()] +userProgrammableOutputs (userPayCred, userStakeCred) = do nid <- queryNetworkId baseCred <- asks (C.PaymentCredentialByScript . C.hashScript . C.PlutusScript C.PlutusScriptV3 . dsProgrammableLogicBaseScript . directoryEnv) - userStakeCred <- either (error . ("Could not unTrans credential: " <>) . show) pure $ unTransStakeCredential $ transCredential userCred - let expectedAddress = C.makeShelleyAddressInEra C.shelleyBasedEra nid baseCred (C.StakeAddressByValue userStakeCred) + userPayCredAsStakeCred <- either (error . ("Could not unTrans credential: " <>) . show) pure $ unTransStakeCredential $ transCredential userPayCred + let expectedAddress = C.makeShelleyAddressInEra C.shelleyBasedEra nid baseCred (C.StakeAddressByValue userPayCredAsStakeCred) isUserUtxo UTxODat{uOut=(C.TxOut addr _ _ _)} = addr == expectedAddress + || maybe False (\stakeCred -> addr == C.makeShelleyAddressInEra C.shelleyBasedEra nid baseCred (C.StakeAddressByValue stakeCred)) userStakeCred filter isUserUtxo <$> programmableLogicOutputs +-- | Get the total of a user's programmable tokens +userTotalProgrammableValue :: forall era env m. (MonadReader env m, HasDirectoryEnv env, MonadUtxoQuery m, C.IsBabbageBasedEra era, MonadBlockchain era m) => (C.PaymentCredential, Maybe C.StakeCredential) -> m C.Value +userTotalProgrammableValue (userCred, userStakeCred) = do + baseCred <- asks (C.PaymentCredentialByScript . C.hashScript . C.PlutusScript C.PlutusScriptV3 . dsProgrammableLogicBaseScript . directoryEnv) + nid <- queryNetworkId + userPayCredAsStakeCred <- either (error . ("Could not unTrans credential: " <>) . show) pure $ unTransStakeCredential $ transCredential userCred + let programmableAddressByPayCred = C.makeShelleyAddressInEra C.shelleyBasedEra nid baseCred (C.StakeAddressByValue userPayCredAsStakeCred) + isUserUtxo UTxODat{uOut=(C.TxOut addr _ _ _)} = + addr == programmableAddressByPayCred + || maybe False (\stakeCred -> addr == C.makeShelleyAddressInEra C.shelleyBasedEra nid baseCred (C.StakeAddressByValue stakeCred)) userStakeCred + plOutputs <- filter isUserUtxo <$> programmableLogicOutputs @era @env + + directoryPolicyIds <- mapMaybe (either (const Nothing) Just . unTransPolicyId . key . uDatum) <$> registryNodes @era + pure $ foldMap (filterValueByPolicyIds directoryPolicyIds . extractValue . uOut) plOutputs + where + filterValueByPolicyIds :: [C.PolicyId] -> C.Value -> C.Value + filterValueByPolicyIds policyIds val = + let isPolicy (C.AssetId pid _, _) = pid `elem` policyIds + isPolicy _ = False + in fromList $ filter isPolicy (toList val) + + {-| Select enough programmable outputs to cover the desired amount of the token. Returns the 'TxIn's and the leftover (change) quantity -} @@ -123,12 +150,12 @@ selectProgammableOutputsFor :: forall era env m. , MonadBlockchain era m , HasTransferLogicEnv env ) - => C.PaymentCredential + => (C.PaymentCredential, Maybe C.StakeCredential) -> C.AssetName -> C.Quantity -> m ([C.TxIn], C.Quantity) -selectProgammableOutputsFor owner assetname quantity = do - userOutputs <- userProgrammableOutputs owner +selectProgammableOutputsFor (owner, ownerStakeCred) assetname quantity = do + userOutputs <- userProgrammableOutputs (owner, ownerStakeCred) policyId <- programmableTokenPolicyId -- Find sufficient inputs to cover the transfer let assetId = C.AssetId policyId assetname diff --git a/src/programmable-tokens-onchain/lib/SmartTokens/Contracts/ProgrammableLogicBase.hs b/src/programmable-tokens-onchain/lib/SmartTokens/Contracts/ProgrammableLogicBase.hs index 34be9b1a..7f71ce2b 100644 --- a/src/programmable-tokens-onchain/lib/SmartTokens/Contracts/ProgrammableLogicBase.hs +++ b/src/programmable-tokens-onchain/lib/SmartTokens/Contracts/ProgrammableLogicBase.hs @@ -19,9 +19,11 @@ module SmartTokens.Contracts.ProgrammableLogicBase ( import Generics.SOP qualified as SOP import GHC.Generics (Generic) +import Plutarch.Builtin.Integer (pconstantInteger) import Plutarch.Core.Context (paddressCredential, pscriptContextTxInfo, ptxInInfoResolved, ptxOutDatum, ptxOutValue) import Plutarch.Core.Integrity (pisRewardingScript) +import Plutarch.Core.Internal.Builtins (pmapData, ppairDataBuiltinRaw) import Plutarch.Core.List import Plutarch.Core.Utils import Plutarch.Core.ValidationLogic hiding (pemptyLedgerValue, pvalueFromCred, @@ -46,21 +48,23 @@ paddressStakingCredential addr = pmatch addr $ \addr' -> pjustData $ paddress'stakingCredential addr' -pconstructExpectedOutputWithOutputDatum :: Term s PAddress -> Term s (PAsData (PValue 'Sorted 'Positive)) -> Term s POutputDatum -> Term s (PAsData PTxOut) -pconstructExpectedOutputWithOutputDatum address value datum = - pdata $ pcon $ - PTxOut - { ptxOut'address = address - , ptxOut'value = value - , ptxOut'datum = datum - , ptxOut'referenceScript = pconstant Nothing - } - +-- TODO: Replace current corresponding input / output comparison (which compares address, reference script and datum) for multi-seize +-- with constructing the expected output from the input with this function and comparing it to the actual output. +-- Further optimize this with the optimization in the "Everything is possible" UPLC fest presentation. +-- pconstructExpectedOutputWithOutputDatum :: Term s PAddress -> Term s (PAsData (PValue 'Sorted 'Positive)) -> Term s POutputDatum -> Term s (PAsData PTxOut) +-- pconstructExpectedOutputWithOutputDatum address value datum = +-- pdata $ pcon $ +-- PTxOut +-- { ptxOut'address = address +-- , ptxOut'value = value +-- , ptxOut'datum = datum +-- , ptxOut'referenceScript = pconstant Nothing +-- } -- TODO: -- The current implementation of the contracts in this module are not designed to be maximally efficient. --- In the future, this should be optimized to use the redeemer indexing design pattern to identify and validate --- the programmable inputs. +-- In the future, this should be optimized to use the redeemer indexing design pattern to not just index the directory nodes in the reference inputs, +-- but also to index the programmable inputs and outputs. data TokenProof = TokenExists Integer | TokenDoesNotExist Integer @@ -226,9 +230,10 @@ pcheckTransferLogicAndGetProgrammableValue = plam $ \directoryNodeCS refInputs p data ProgrammableLogicGlobalRedeemer = TransferAct [TokenProof] | SeizeAct { - plgrSeizeInputIdx :: Integer, - plgrSeizeOutputIdx :: Integer, plgrDirectoryNodeIdx :: Integer + , plgrInputIdxs :: [Integer] + , plgrOutputsStartIdx :: Integer + , plgrLengthInputIdxs :: Integer } deriving (Show, Eq, Generic) @@ -237,10 +242,12 @@ PlutusTx.makeIsDataIndexed ''ProgrammableLogicGlobalRedeemer data PProgrammableLogicGlobalRedeemer (s :: S) = PTransferAct {pproofs :: Term s (PAsData (PBuiltinList (PAsData PTokenProof)))} + -- The proofs are the list of proofs that the token exists in the directory | PSeizeAct - { pseizeInputIdx :: Term s (PAsData PInteger) - , pseizeOutputIdx :: Term s (PAsData PInteger) - , pdirectoryNodeIdx :: Term s (PAsData PInteger) + { pdirectoryNodeIdx :: Term s (PAsData PInteger) + , pinputIdxs :: Term s (PAsData (PBuiltinList (PAsData PInteger))) + , poutputsStartIdx :: Term s (PAsData PInteger) + , plengthInputIdxs :: Term s (PAsData PInteger) } deriving stock (Generic) deriving anyclass (SOP.Generic, PIsData, PEq, PShow) @@ -283,6 +290,7 @@ mkProgrammableLogicGlobal = plam $ \protocolParamsCS ctx -> P.do pmatch red $ \case PTransferAct proofs -> P.do + -- TODO: Consider minted values, right now minted value can be smuggled out of the programmable assets mini-ledger. totalProgTokenValue <- plet $ pvalueFromCred # progLogicCred @@ -302,50 +310,298 @@ mkProgrammableLogicGlobal = plam $ \protocolParamsCS ctx -> P.do , ptraceInfoIfFalse "prog tokens escape" $ pvalueContains # (pvalueToCred # progLogicCred # pfromData ptxInfo'outputs) # totalProgTokenValue_ ] - PSeizeAct {pseizeInputIdx, pseizeOutputIdx, pdirectoryNodeIdx} -> P.do - -- TODO: - -- Possibly enforce that the seized assets must be seized to the programmable logic contract - -- just under different ownership (staking credential changed) - ptraceInfo "PSeizeAct" - txInputs <- plet $ pfromData ptxInfo'inputs - let seizeInput = ptxInInfoResolved $ pfromData (pelemAtFast @PBuiltinList # txInputs # pfromData pseizeInputIdx) - seizeOutput = pelemAtFast @PBuiltinList # pfromData ptxInfo'outputs # pfromData pseizeOutputIdx - directoryNodeUTxO = pelemAtFast @PBuiltinList # referenceInputs # pfromData pdirectoryNodeIdx + PSeizeAct {pdirectoryNodeIdx, pinputIdxs, poutputsStartIdx, plengthInputIdxs} -> P.do + inputIdxs <- plet $ pmap @PBuiltinList @(PAsData PInteger) # plam pfromData # pfromData pinputIdxs + let remainingOutputs = pdropFast # pfromData poutputsStartIdx # pfromData ptxInfo'outputs + let directoryNodeUTxO = pelemAtFast @PBuiltinList # referenceInputs # pfromData pdirectoryNodeIdx PTxOut {ptxOut'value=seizeDirectoryNodeValue, ptxOut'datum=seizeDirectoryNodeDatum} <- pmatch (ptxInInfoResolved $ pfromData directoryNodeUTxO) POutputDatum seizeDat' <- pmatch seizeDirectoryNodeDatum PDirectorySetNode { pkey=directoryNodeDatumFKey , pissuerLogicScript=directoryNodeDatumFIssuerLogicScript } <- pmatch (pfromData $ punsafeCoerce @(PAsData PDirectorySetNode) (pto seizeDat')) - PTxOut{ptxOut'address=seizeInputAddress', ptxOut'value=seizeInputValue', ptxOut'datum=seizeInputDatum} <- pmatch seizeInput - seizeInputAddress <- plet seizeInputAddress' - - seizeInputValue <- plet $ pfromData seizeInputValue' - expectedSeizeOutputValue <- plet $ pfilterCSFromValue # seizeInputValue # directoryNodeDatumFKey - - let expectedSeizeOutput = - pconstructExpectedOutputWithOutputDatum - seizeInputAddress - (pdata expectedSeizeOutputValue) - seizeInputDatum - - -- For ease of implementation of POC we only allow one UTxO to be seized per transaction. - -- This can be easily modified to support seizure of multiple UTxOs. let issuerLogicScriptHash = punsafeCoerce @(PAsData PByteString) $ phead #$ psndBuiltin #$ pasConstr # pforgetData directoryNodeDatumFIssuerLogicScript - pvalidateConditions - [ pcountInputsFromCred # progLogicCred # txInputs #== pconstant 1 - , paddressCredential seizeInputAddress #== progLogicCred - , seizeOutput #== expectedSeizeOutput - , pelem # issuerLogicScriptHash # invokedScripts - -- Prevent DDOS greifing attacks via the seize action - -- i.e. the issuer logic script being used to spend a programmable token UTxO that does not have the given programmable token - -- back to the mkProgrammableLogicBase script without modifying it (thus preventing any others from spending - -- that UTxO in that block). Or using it to repeatedly spend a programmable token UTxO that does have the programmable token back back to - -- the mkProgrammableLogicBase script without removing the programmable token associated with the `issuerLogicCredential`. - , pnot # (pdata seizeInputValue #== pdata expectedSeizeOutputValue) - -- seize node is valid (presence of state token) - , phasDataCS # pdirectoryNodeCS # pfromData seizeDirectoryNodeValue - ] - - + let conditions = + [ ptraceInfoIfFalse "mini-ledger invariants violated" $ processThirdPartyTransfer # directoryNodeDatumFKey # progLogicCred # pfromData ptxInfo'inputs # remainingOutputs # inputIdxs + , ptraceInfoIfFalse "issuer logic script must be invoked" $ pelem # issuerLogicScriptHash # invokedScripts + -- directory node is valid (presence of state token) + , ptraceInfoIfFalse "directory node is not valid" $ phasDataCS # pdirectoryNodeCS # pfromData seizeDirectoryNodeValue + -- input indexes are unique. + , ptraceInfoIfFalse "input indexes are not unique" $ pisUniqueSet # pfromData plengthInputIdxs # inputIdxs + ] + pvalidateConditions conditions + +punionTokens :: Term s (PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger)) :--> PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger)) :--> PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger))) +punionTokens = pfix #$ plam $ \self tokensA tokensB -> + pelimList + (\tokenPairA tokensRestA -> + plet (pfstBuiltin # tokenPairA) $ \tokenNameA -> + pelimList + (\tokenPairB tokensRestB -> + pif (pfromData tokenNameA #== pfromData (pfstBuiltin # tokenPairB)) + ( -- both entries have the same token so we add quantities + let quantityA = pfromData (psndBuiltin # tokenPairA) + quantityB = pfromData (psndBuiltin # tokenPairB) + in pcons # (ppairDataBuiltin # tokenNameA # pdata (quantityA + quantityB)) # (self # tokensRestA # tokensRestB) + ) + ( + pif (pfromData tokenNameA #< pfromData (pfstBuiltin # tokenPairB)) + -- entry A has a token that entry B does not so we add the token and quantity from entry A. + (pcons # tokenPairA # (self # tokensRestA # tokensB)) + -- entry B has a token that entry A does not so we add the token and quantity from entry B. + (pcons # tokenPairB # (self # tokensA # tokensRestB)) + ) + ) + pnil tokensB + ) + pnil tokensA + +processThirdPartyTransfer :: Term s + (PAsData PCurrencySymbol + :--> PCredential + :--> PBuiltinList (PAsData PTxInInfo) + :--> PBuiltinList (PAsData PTxOut) + :--> PBuiltinList PInteger + :--> PBool) +processThirdPartyTransfer = plam $ \programmableCS progLogicCred inputs progOutputs inputIdxs' -> + plet (pvalueEqualsDeltaCurrencySymbol # pfromData programmableCS) $ \pvalueEqualsDeltaCurrencySymbol' -> + plet (pelemAtFast @PBuiltinList # inputs) $ \patInputIdx -> + let + checkBalanceInvariant :: Term _ (PBuiltinList (PAsData PTxOut)) -> Term _ (PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger))) -> Term _ PBool + checkBalanceInvariant remainingOutputs deltaAccumulatorResult = + let outputAccumulatorResult = go2 # remainingOutputs + deltaResultValue = punsafeCoerce @(PValue 'Sorted 'Positive) (pconsBuiltin # (ppairDataBuiltinRaw # pforgetData programmableCS # (pmapData # punsafeCoerce deltaAccumulatorResult)) # pnil) + in pif (pvalueContains # outputAccumulatorResult # deltaResultValue) + (pconstant True) + perror + + go2 :: Term _ (PBuiltinList (PAsData PTxOut) :--> PValue 'Sorted 'Positive) + go2 = pfix #$ plam $ \self programmableOutputs -> + pelimList + (\programmableOutput programmableOutputsRest -> + pmatch (pfromData programmableOutput) $ \(PTxOut {ptxOut'address=programmableOutputAddress, ptxOut'value=programmableOutputValue}) -> + pif ( paddressCredential programmableOutputAddress #== progLogicCred ) + ( pfromData programmableOutputValue #<> (self # programmableOutputsRest) ) + pmempty + ) + pmempty + programmableOutputs + go :: Term _ (PBuiltinList PInteger :--> PBuiltinList (PAsData PTxOut) :--> PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger)) :--> PBool) + go = pfix #$ plam $ \self inputIdxs programmableOutputs deltaAccumulator -> + pelimList + (\txInIdx txInsIdxs -> + plet (patInputIdx # txInIdx) $ \programmableInput -> + pmatch (ptxInInfoResolved $ pfromData programmableInput) $ \(PTxOut {ptxOut'address=programmableInputAddress, ptxOut'value=programmableInputValue}) -> + pmatch (pfromData $ phead # programmableOutputs) $ \(PTxOut {ptxOut'address=programmableOutputAddress, ptxOut'value=programmableOutputValue}) -> + let conditions = + pand'List + [ -- this credential check is likely not necessary as the fact that the value has a programmable CS is proof that that input must come from the programmable logic base script. + -- otherwise that means programmable tokens have been smuggled out of the programmable assets mini-ledger. + ptraceInfoIfFalse "input payment credential is not the programmable logic credential" $ paddressCredential programmableInputAddress #== progLogicCred + , ptraceInfoIfFalse "corresponding output: address mismatch" $ programmableOutputAddress #== programmableInputAddress + ] + in pif conditions + ( + let delta = pvalueEqualsDeltaCurrencySymbol' # programmableInputValue # programmableOutputValue + in self # txInsIdxs # (ptail # programmableOutputs) # (punionTokens # delta # deltaAccumulator) + ) + perror + ) + (checkBalanceInvariant programmableOutputs deltaAccumulator) + inputIdxs + in go # inputIdxs' # progOutputs # pnil + + +------------------------------------------------------------------------------- +-- Corresponding inputs and outputs from and to the programmable token spending script (mini-ledger where all programmable tokens live). +-- Example Inputs: +-- inputA = { +-- progCS: { Foo: 120, Bar: 80 }, +-- ADA: { "": 3_000_000 }, +-- usdCS: { USDT: 50 }, +-- nftCS: { ArtNFT: 1 } +-- } + +-- inputB = { +-- progCS: { Foo: 70 }, +-- ADA: { "": 2_000_000 }, +-- usdCS: { USDT: 10 } +-- } + +-- inputC = { +-- progCS: { Foo: 40, Bar: 30 }, +-- ADA: { "": 1_500_000 } +-- } + +------------------------------------------------------------------------------- +-- Corresponding Outputs: +-- Corresponding outputs are the continuing outputs for their corresponding inputs. They must have the same address as their input (as indicated by their label as continuing outputs) +-- they must also have the same datum and the same reference script hash (if present) as their input. +-- Finally, they must also have the same value as their input except for the balance of tokens with the progCS currency symbol, for tokens of that currency symbol. + +-- Example outputs: +-- correspondingOutputA = { +-- progCS: { Foo: 140, Bar: 60 }, +-- ADA: { "": 3_000_000 }, +-- usdCS: { USDT: 50 }, +-- nftCS: { ArtNFT: 1 } +-- } + +-- correspondingOutputB = { +-- progCS: { Foo: 20 }, +-- ADA: { "": 2_000_000 }, +-- usdCS: { USDT: 10 } +-- } + +-- correspondingOutputC = { +-- progCS: { Foo: 10, Bar: 10 }, +-- ADA: { "": 1_500_000 } +-- } + +------------------------------------------------------------------------------- +-- Remaining outputs: +-- Remaining programmable token outputs - these are outputs to the programmable token spending script (mini-ledger where all programmable tokens live) +-- that are not corresponding to any inputs to the programmable token spending script. The accumulated value of these outputs must contain +-- the delta between the amount of programmable asset in the inputs and the amount of programmable asset in the corresponding outputs thus assuring all +-- programmable assets must stay within the programmable token spending script. +-- Example remaining outputs: +-- remainingOutputA = { +-- progCS: { Foo: 40, Bar: 25 }, +-- ADA: { "": 2_000_000 } +-- } + +-- remainingOutputB = { +-- progCS: { Foo: 20, Bar: 15 }, +-- ADA: { "": 2_000_000 } +-- } + +------------------------------------------------------------------------------- +-- The below calculation checks that the total amount of programmable tokens spent from the script is equal to the amount sent to the script, +-- and that each correspondingOutput is equal to it's input except for the balance of tokens with the progCS currency symbol, for tokens of that currency symbol +-- each corresponding output contains either more or less than the amount of the tokens in the input. + +-- accumulatedValue = amount of programmable asset in input - amount of programmable asset in corresponding output + +-- outputValueAccumulator = emptyValue +-- if accumulatedValue > 0 +-- for each remainingOutput: +-- outputValueAccumulator = outputValueAccumulator <> remainingOutputValue + +-- if (valueContains outputValueAccumulator accumulatedValue) +-- constant True + +-- | Negates the quantity of each token in a list of token quantity pairs (ie. the inner map of a `PValue`). +-- Example: +-- pnegateTokens [("FooToken", 10), ("BarToken", 20)] = [("FooToken", -10), ("BarToken", -20)] +pnegateTokens :: Term _ (PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger)) :--> PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger))) +pnegateTokens = pfix #$ plam $ \self tokens -> + pelimList + (\tokenPair tokensRest -> + let tokenName = pfstBuiltin # tokenPair + tokenAmount = psndBuiltin # tokenPair + in pcons # (ppairDataBuiltin # tokenName # pdata (pconstantInteger 0 - pfromData tokenAmount)) # (self # tokensRest) + ) + pnil + tokens + +-- | +-- `pvalueEqualsDeltaCurrencySymbol # progCS # inputUTxOValue # outputUTxOValue` MUST check that inputUTxOValue is equal to outputUTxOValue for all tokens except those of currency symbol progCS. +-- The function should return a value consisting of only tokens with the currency symbol progCS, this value is as follows: For each token t of currency symbol progCS, the quantity of the token +-- in the return value rValue is the quantity of token t in inputUTxOValue minus the quantity of token t in outputUTxOValue. +-- for the purposes of the subtraction ie. if inputUTxOValue has 0 FooToken and outputUTxOValue has 10 FooToken then rValue should have 0 - 10 = -10 FooToken. +-- +pvalueEqualsDeltaCurrencySymbol :: + forall anyOrder anyAmount s. + Term + s + ( PCurrencySymbol + :--> PAsData (PValue anyOrder anyAmount) + :--> PAsData (PValue anyOrder anyAmount) + :--> PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger)) + ) +pvalueEqualsDeltaCurrencySymbol = plam $ \progCS inputUTxOValue outputUTxOValue -> + let innerInputValue :: Term _ (PBuiltinList (PBuiltinPair (PAsData PCurrencySymbol) (PAsData (PMap anyOrder PTokenName PInteger)))) + innerInputValue = pto (pto $ pfromData inputUTxOValue) + innerOutputValue :: Term _ (PBuiltinList (PBuiltinPair (PAsData PCurrencySymbol) (PAsData (PMap anyOrder PTokenName PInteger)))) + innerOutputValue = pto (pto $ pfromData outputUTxOValue) + + psubtractTokens :: + Term _ ( + PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger)) + :--> PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger)) + :--> PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger)) + ) + psubtractTokens = + pfix #$ plam $ \self inputTokens outputTokens -> + pelimList + (\inputPair inputRest -> + plet (pfstBuiltin # inputPair) $ \inputTokenName -> + let inputTokenAmount = psndBuiltin # inputPair + in pelimList + (\outputPair outputRest -> + let outputTokenName = pfstBuiltin # outputPair + outputTokenAmount = psndBuiltin # outputPair + in + pif (pfromData inputTokenName #<= pfromData outputTokenName) + ( -- inputTokenName <= outputTokenName + pif (inputTokenName #== outputTokenName) + ( -- names equal → diff = input − output; skip if zero + let diff = pfromData inputTokenAmount - pfromData outputTokenAmount + in pif (diff #== 0) + (self # inputRest # outputRest) + (pcons + # (ppairDataBuiltin # inputTokenName # pdata diff) + # (self # inputRest # outputRest)) + ) + ( -- outputTokenName > inputTokenName → token only in input (nonzero by invariant) + let diff = pfromData inputTokenAmount + in pcons + # (ppairDataBuiltin # inputTokenName # pdata diff) + # (self # inputRest # outputTokens) + ) + ) + ( -- outputTokenName < inputTokenName → token only in output (nonzero by invariant) + let diff = pconstantInteger 0 - pfromData outputTokenAmount + in pcons + # (ppairDataBuiltin # outputTokenName # pdata diff) + # (self # inputTokens # outputRest) + ) + ) + -- output exhausted → emit remaining input tokens as positive (nonzero by invariant) + inputRest + outputTokens + ) + -- input exhausted → emit remaining output tokens as negative (nonzero by invariant) + (pnegateTokens # outputTokens) + inputTokens + + -- no need to check for progCs in "everything should be same" parts + -- input : |- everything should be same -| |-progCs-| |-everything should be same-| + -- output : |- everything should be same -| |-progCs-| |-everything should be same-| + goOuter :: + Term _ + ( PBuiltinList (PBuiltinPair (PAsData PCurrencySymbol) (PAsData (PMap anyOrder PTokenName PInteger))) + :--> PBuiltinList (PBuiltinPair (PAsData PCurrencySymbol) (PAsData (PMap anyOrder PTokenName PInteger))) + :--> PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger)) -- accumulator (delta for progCS) + :--> PBuiltinList (PBuiltinPair (PAsData PTokenName) (PAsData PInteger)) + ) + goOuter = pfix #$ plam $ \self inputValuePairs outputValuePairs diffAccumulator -> + pelimList + (\inputValueEntry inputValueEntries -> + plet (pfstBuiltin # inputValueEntry) $ \inputValueEntryCS -> + pelimList + (\outputValueEntry outputValueEntries -> + pif (pfromData inputValueEntryCS #== pfromData (pfstBuiltin # outputValueEntry)) + (pif (pfromData inputValueEntryCS #== progCS) + (pif (pmapData # punsafeCoerce outputValueEntries #== pmapData # punsafeCoerce inputValueEntries) + (psubtractTokens # pto (pfromData (psndBuiltin # inputValueEntry)) # pto (pfromData @(PMap anyOrder PTokenName PInteger) (psndBuiltin # outputValueEntry))) + perror + ) + (pif (psndBuiltin # inputValueEntry #== psndBuiltin # outputValueEntry) (self # inputValueEntries # outputValueEntries # diffAccumulator) perror) + ) + (pif (psndBuiltin # inputValueEntry #== psndBuiltin # outputValueEntry) diffAccumulator perror) + + ) pnil outputValuePairs + ) pnil inputValuePairs + in goOuter # innerInputValue # innerOutputValue # pnil diff --git a/src/programmable-tokens-onchain/lib/SmartTokens/Core/Scripts.hs b/src/programmable-tokens-onchain/lib/SmartTokens/Core/Scripts.hs index 567143e8..42d6e7ca 100644 --- a/src/programmable-tokens-onchain/lib/SmartTokens/Core/Scripts.hs +++ b/src/programmable-tokens-onchain/lib/SmartTokens/Core/Scripts.hs @@ -26,7 +26,7 @@ data ScriptTarget -} targetConfig :: ScriptTarget -> Config targetConfig = \case - Debug -> _tracingConfig + Debug -> _tracingAndBindsConfig Production -> prodConfig tryCompile :: ScriptTarget -> ClosedTerm a -> Script diff --git a/src/programmable-tokens-test/lib/ProgrammableTokens/Test.hs b/src/programmable-tokens-test/lib/ProgrammableTokens/Test.hs index dc0691bb..4fda7051 100644 --- a/src/programmable-tokens-test/lib/ProgrammableTokens/Test.hs +++ b/src/programmable-tokens-test/lib/ProgrammableTokens/Test.hs @@ -134,13 +134,11 @@ deployDirectorySet op = do >>= void . sendTx . signTxOperator op operatorEnv_ <- Env.loadConvexOperatorEnv @_ @era op - dirScriptRoot <- flip runReaderT operatorEnv_ $ do + flip runReaderT operatorEnv_ $ do (tx, scriptRoot) <- Endpoints.deployCip143RegistryTx target void $ sendTx $ signTxOperator op tx pure scriptRoot - pure dirScriptRoot - {-| Build a transaction that issues a progammable token -} issueProgrammableTokenTx :: forall era env redeemer err m.