diff --git a/.github/workflows/build-and-lint.yml b/.github/workflows/build-and-lint.yml index 3a36790..0693d93 100644 --- a/.github/workflows/build-and-lint.yml +++ b/.github/workflows/build-and-lint.yml @@ -4,9 +4,6 @@ on: push: branches: - '**' - pull_request: - branches: - - main jobs: build-and-lint: diff --git a/.github/workflows/deploy-to-pages.yml b/.github/workflows/deploy-to-pages.yml index 4f1c1b5..a771eb2 100644 --- a/.github/workflows/deploy-to-pages.yml +++ b/.github/workflows/deploy-to-pages.yml @@ -1,7 +1,7 @@ name: Deploy to GitHub Pages on: - workflow_dispatch: + workflow_dispatch: jobs: build-and-deploy: @@ -26,7 +26,7 @@ jobs: run: | npm install npm run build - working-directory: src + working-directory: ./src # Step 4: Deploy to GitHub Pages - name: Deploy to GitHub Pages diff --git a/src/app/components/transaction.tsx b/src/app/components/transaction.tsx index 992ae6e..b6f9a02 100644 --- a/src/app/components/transaction.tsx +++ b/src/app/components/transaction.tsx @@ -2,22 +2,14 @@ import { useState,useEffect } from "react"; import { useWallet } from "@meshsdk/react"; -import { BlockfrostProvider, deserializeAddress } from "@meshsdk/core"; -import { Button, TextField, Box, Typography, Container, Table, TableBody, TableCell, TableContainer, TableRow, Paper } from "@mui/material"; +import { deserializeAddress } from "@meshsdk/core"; +import { Button, TextField, Box, Typography, Container, Table, TableBody, TableCell, TableContainer, TableRow, Paper, Link } from "@mui/material"; import * as CLS from "@emurgo/cardano-serialization-lib-browser"; import ReactJsonPretty from 'react-json-pretty'; - -// Function to decode an unasigned transaction -const decodeTransaction = (unsignedTransactionHex: string) => { - try { - const unsignedTransaction = CLS.Transaction.from_hex(unsignedTransactionHex); - console.log("signers list", unsignedTransaction.body().required_signers()?.to_json()); - return unsignedTransaction; - } catch (error) { - console.error("Error decoding transaction:", error); - return null; - } -}; +import * as txValidationUtils from "../utils/txValidationUtils"; +import { TransactionChecks } from "./validationChecks"; +import { decodeHextoTx,convertGAToBech,getCardanoScanURL} from "../utils/txUtils"; +import { VotingDetails } from "./votingDetails"; export const TransactionButton = () => { @@ -26,163 +18,82 @@ export const TransactionButton = () => { const [unsignedTransaction, setUnsignedTransaction] = useState(null); const { wallet, connected, name, connect, disconnect } = useWallet(); const [signature, setsignature] = useState(""); - const [isPartOfSigners, setIsPartOfSigners] = useState(false); - const [isOneVote, setIsOneVote] = useState(false); - const [hasCertificates, setHasCertificates] = useState(true); - const [isSameNetwork, setIsSameNetwork] = useState(false); - const [hasICCCredentials, setHasICCCredentials] = useState(false); - const [isInOutputPlutusData , setIsInOutputPlutusData] = useState(false); - const [voteResult, setVoteResult] = useState(); - const [voteID, setVoteID] = useState(); - const [cardanoscan, setCardanoscan] = useState(); - const [metadataAnchorURL, setmetadataAnchorURL] = useState(); - const [metadataAnchorHash, setMetadataAnchorHash] = useState(); - - + const [voteChoice, setvoteChoice] = useState(""); + const [govActionID, setgovActionID] = useState(""); + const [cardanoscan, setCardanoscan] = useState(""); + const [metadataAnchorURL, setmetadataAnchorURL] = useState(""); + const [metadataAnchorHash, setMetadataAnchorHash] = useState(""); + const [validationState, setValidationState] = useState({ + isPartOfSigners: false, + isOneVote: false, + hasCertificates: true, + isSameNetwork: false, + hasICCCredentials: false, + isInOutputPlutusData: false, + }); + const resetValidationState = () => { + setValidationState((prev) => ({ + ...prev, + isPartOfSigners: false, + isOneVote: false, + hasCertificates: true, + isSameNetwork: false, + hasICCCredentials: false, + isInOutputPlutusData: false, + })); + }; const checkTransaction = async () => { if (!connected) { - setIsPartOfSigners(false); - setIsOneVote(false); - setHasCertificates(true); - setIsSameNetwork(false); - setHasICCCredentials(false); - setIsInOutputPlutusData(false); - setVoteResult(""); - setVoteID(""); - setMessage("Please connect your wallet first."); - return; + resetValidationState(); + setvoteChoice(""); + setgovActionID(""); + return setMessage("Please connect your wallet first."); } - - const network = await wallet.getNetworkId(); - console.log("Connected wallet network ID:", network); - console.log("isPartOfSigners:", isPartOfSigners); - - const unsignedTransaction = decodeTransaction(unsignedTransactionHex); - setUnsignedTransaction(unsignedTransaction); - - console.log("unsignedTransaction:", unsignedTransaction); - - const changeAddress = await wallet.getChangeAddress(); - const stakeCred = deserializeAddress(changeAddress).stakeCredentialHash; - - console.log("Stake Credential:", stakeCred); - - //**************************************Transaction Validation Checks**************************************** - - const transactionBody = unsignedTransaction?.body(); - const voting_procedures= transactionBody?.to_js_value().voting_procedures; - try{ - if (!transactionBody) { - throw new Error("Transaction body is null."); - } - //wallet needs to sign - // Check if signer part of plutus output data - const requiredSigners = transactionBody.required_signers(); - if (!requiredSigners || requiredSigners.len() === 0) { - console.log("No required signers in the transaction."); + const network = await wallet.getNetworkId(); + const unsignedTransaction = decodeHextoTx(unsignedTransactionHex); + setUnsignedTransaction(unsignedTransaction); + if (!unsignedTransaction) throw new Error("Invalid transaction format."); + + const changeAddress = await wallet.getChangeAddress(); + const stakeCred = deserializeAddress(changeAddress).stakeCredentialHash; + + console.log("Connected wallet network ID:", network); + console.log("unsignedTransaction:", unsignedTransaction); + console.log("Stake Credential:", stakeCred); + + //**************************************Transaction Validation Checks**************************************** + + const transactionBody = unsignedTransaction.body(); + if (!transactionBody) throw new Error("Transaction body is null."); + const voting_procedures= transactionBody.to_js_value().voting_procedures; + if (!voting_procedures) throw new Error("Transaction has no voting procedures."); + const votes=voting_procedures[0].votes; + const hasOneVote = txValidationUtils.hasOneVoteOnTransaction(transactionBody); + const vote = voting_procedures[0].votes[0].voting_procedure.vote; + + setValidationState({ + isPartOfSigners: await txValidationUtils.isPartOfSigners(transactionBody, stakeCred), + isOneVote: hasOneVote, + hasCertificates: txValidationUtils.hasCertificates(transactionBody), + isSameNetwork: txValidationUtils.isSameNetwork(transactionBody, network), + hasICCCredentials: txValidationUtils.hasValidICCCredentials(transactionBody, network), + isInOutputPlutusData: txValidationUtils.isSignerInPlutusData(transactionBody, stakeCred), + }); - } else if (requiredSigners?.to_json().includes(stakeCred) ) { - console.log("Required signers in the transaction:", requiredSigners?.to_json()); - setIsPartOfSigners(true); - } - - //one vote - const votes=voting_procedures?.[0]?.votes; - const votesNumber = votes?.length; - - if(votesNumber === 1){ - setIsOneVote(true); - setVoteResult(votes?.[0].voting_procedure.vote); - setVoteID(votes?.[0].action_id.transaction_id); - setmetadataAnchorURL(votes?.[0].voting_procedure.anchor?.anchor_url); - setMetadataAnchorHash(votes?.[0].voting_procedure.anchor?.anchor_data_hash); - console.log("Transaction has one vote set to:",voteResult); - }else if (!votesNumber){ - throw new Error("Transaction has no votes."); - }else{ - //throw new Error("You are signing more than one vote. Number of votes: "+ votesNumber); - } - - // Check to see if the transactions has any certificates in it - const certificates = transactionBody?.certs(); - console.log("certificates:", certificates); - if (!certificates) { - console.log("No certificates in the transaction."); - setHasCertificates(false); - } - - //Same network - const transactionNetworkID= transactionBody.outputs().get(0).address().to_bech32().startsWith("addr_test1")?0:1; - console.log('transactionNetwork:',transactionNetworkID); - if (network === transactionNetworkID ) { - setIsSameNetwork(true); - } + //********************************************Voting Details *********************************************************************/ + const transactionNetworkID = transactionBody.outputs().get(0).address().to_bech32().startsWith("addr_test1") ? 0 : 1; - //Is Intersect CC credential - const voterJSON = voting_procedures?.[0]?.voter; - console.log("voterJSON:", voterJSON); - let script; - // Function to check if the voterJSON has ConstitutionalCommitteeHotCred to avoid type error - function isConstitutionalCommitteeHotCred(voter: CLS.VoterJSON): voter is { ConstitutionalCommitteeHotCred: { Script: string } } { - return (voter as { ConstitutionalCommitteeHotCred: any }).ConstitutionalCommitteeHotCred !== undefined; - } - - if (voterJSON && isConstitutionalCommitteeHotCred(voterJSON)) { - // If it has ConstitutionalCommitteeHotCred, extract the Script hex - const credType = voterJSON.ConstitutionalCommitteeHotCred; - script = credType.Script; - console.log("ConstitutionalCommitteeHotCred Script:", script); + if (votes && hasOneVote) { - } - //If in Testnet and scrit matches preview ICC credential ; else if in mainnet and script matches mainnet ICC credential - if (network === 0 && script === "4f00984fa72e265b8ff8ffce4405da562cd3d6b16a4a38de3372eeea") { - console.log("Intersect CC Credential found in testnet"); - setHasICCCredentials(true); - } else if (network === 1 && script === "85c47dd4b9a2e70e88965d91dd69be182d5605b23bb5250b1c94bf64") { - console.log("Intersect CC Credential found in mainnet"); - setHasICCCredentials(true); - } else { - console.error("Incorrect Intersect CC Credentials"); - } - //check if signer is in plutus data - const plutusScripts = transactionBody?.outputs().to_js_value(); - console.log("plutusScripts:", plutusScripts); - console.log("stakeCred:", stakeCred); - - if (Array.isArray(plutusScripts) && stakeCred) { - - const regex = new RegExp(stakeCred); - - plutusScripts.forEach((output, index) => { - if (output.plutus_data && typeof output.plutus_data === 'object' && 'Data' in output.plutus_data) { - const plutusData = output.plutus_data.Data; - console.log("plutusData:", plutusData); - - if (regex.test(plutusData)) { - console.log(`Stake credential found in output for address ${output.address}`); - setIsInOutputPlutusData(true); - } - } else if (!output.plutus_data) { - console.log(`No plutus data found in output`); - } else { - console.log(`Stake credential not found on plutus script data for address ${output.address}`); - } - }); - - } else { - console.error("Transaction outputs are not available "); - } - - - //for future add context of some of the - - //********************************************Voting Details *********************************************************************/ - if (transactionNetworkID === 0) { - setCardanoscan("https://preprod.cardanoscan.io/transaction/"); - } else if (transactionNetworkID === 1) { - setCardanoscan("https://cardanoscan.io/transaction/"); + const govActionID = convertGAToBech(votes[0].action_id.transaction_id, votes[0].action_id.index); + setvoteChoice(vote === 'Yes' ? 'Constitutional' : vote === 'No' ? 'Unconstitutional' : 'Abstain'); + setgovActionID(govActionID); + if(!votes[0].voting_procedure.anchor) throw new Error("Vote has no anchor."); + setmetadataAnchorURL(votes[0].voting_procedure.anchor.anchor_url); + setMetadataAnchorHash(votes[0].voting_procedure.anchor.anchor_data_hash); + setCardanoscan(getCardanoScanURL(govActionID,transactionNetworkID)); } @@ -194,13 +105,13 @@ export const TransactionButton = () => { }; const signTransaction = async () => { - console.log("isPartOfSigners:", isPartOfSigners); + console.log("isPartOfSigners:", validationState.isPartOfSigners); try { - if (isPartOfSigners) { + if (validationState.isPartOfSigners) { const signedTx = await wallet.signTx(unsignedTransactionHex, true); console.log("Transaction signed successfully:", signedTx); - const signature = await decodeTransaction(signedTx); + const signature = await decodeHextoTx(signedTx); setsignature(signature?.witness_set().vkeys()?.get(0)?.signature()?.to_hex() || ''); console.log("signature:", signature?.witness_set().vkeys()?.get(0).signature().to_hex()); @@ -230,7 +141,6 @@ export const TransactionButton = () => { }, [signature,unsignedTransaction]); return ( - {/* Transaction Input & Button */} @@ -240,16 +150,12 @@ export const TransactionButton = () => { variant="outlined" fullWidth value={unsignedTransactionHex} - onChange={(e) => {setUnsignedTransactionHex(e.target.value); - setIsPartOfSigners(false); - setIsOneVote(false); - setHasCertificates(true); - setIsSameNetwork(false); - setHasICCCredentials(false); - setIsInOutputPlutusData(false); - setVoteResult(""); - setVoteID("") - }} + onChange={(e) => { + setUnsignedTransactionHex(e.target.value); + resetValidationState(); + setvoteChoice(""); + setgovActionID(""); + }} />