diff --git a/.docker/Dockerfile.optimized b/.docker/Dockerfile.optimized deleted file mode 100644 index ba8205d43..000000000 --- a/.docker/Dockerfile.optimized +++ /dev/null @@ -1,78 +0,0 @@ -# syntax=docker/dockerfile:1 - -FROM golang:1.24.0-alpine AS builder - -# Install build dependencies -RUN apk update && apk add --no-cache make bash nodejs npm git - -ARG EXPLORER_BASE_PATH -ARG WALLET_BASE_PATH - -WORKDIR /go/src/github.com/canopy-network/canopy - -# ============================================ -# OPTIMIZATION 1: Cache Go module dependencies -# Copy only go.mod and go.sum first to cache dependencies layer -# This layer only invalidates when dependencies change -# ============================================ -COPY go.mod go.sum ./ -RUN --mount=type=cache,target=/go/pkg/mod \ - go mod download - -# ============================================ -# OPTIMIZATION 2: Cache NPM dependencies for wallet -# Copy only package.json files first to cache npm install layer -# ============================================ -COPY cmd/rpc/web/wallet/package*.json ./cmd/rpc/web/wallet/ -RUN --mount=type=cache,target=/root/.npm \ - npm install --prefix ./cmd/rpc/web/wallet - -# ============================================ -# OPTIMIZATION 3: Cache NPM dependencies for explorer -# ============================================ -COPY cmd/rpc/web/explorer/package*.json ./cmd/rpc/web/explorer/ -RUN --mount=type=cache,target=/root/.npm \ - npm install --prefix ./cmd/rpc/web/explorer - -# ============================================ -# OPTIMIZATION 4: Copy web source and build BEFORE copying all source -# This caches web builds - only invalidates when web files change -# ============================================ -COPY cmd/rpc/web/wallet/ ./cmd/rpc/web/wallet/ -COPY cmd/rpc/web/explorer/ ./cmd/rpc/web/explorer/ - -ENV EXPLORER_BASE_PATH=${EXPLORER_BASE_PATH} -ENV WALLET_BASE_PATH=${WALLET_BASE_PATH} - -# Build web assets (cached unless web source changes) -RUN --mount=type=cache,target=/root/.npm \ - npm run build --prefix ./cmd/rpc/web/wallet -RUN --mount=type=cache,target=/root/.npm \ - npm run build --prefix ./cmd/rpc/web/explorer - -# ============================================ -# OPTIMIZATION 5: Copy remaining source code -# Web builds are already cached above -# ============================================ -COPY . . - -# ============================================ -# OPTIMIZATION 6: Use build cache mounts for Go -# Preserves Go build cache and module cache between builds -# This can speed up rebuilds by 5-10x -# ============================================ -RUN --mount=type=cache,target=/root/.cache/go-build \ - --mount=type=cache,target=/go/pkg/mod \ - go build -a -o bin ./cmd/main/... - -# ============================================ -# Final stage - minimal runtime image -# ============================================ -FROM alpine:3.19 - -WORKDIR /app - -# Copy only the built binary (web assets are embedded) -COPY --from=builder /go/src/github.com/canopy-network/canopy/bin ./bin - -ENTRYPOINT ["/app/bin"] diff --git a/.gitignore b/.gitignore index be69dc699..ea588d4e2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.dll *.so *.dylib +/main .cache # Test binary, built with `go test -c` *.test @@ -28,6 +29,9 @@ go.work # Zed .zed +# Emacs +.projectile + # Visual Studio Code .vscode @@ -46,4 +50,7 @@ cmd/web/explorer/.idea /cmd/tps/data rag -node_modules \ No newline at end of file +node_modules + +# Hardhat +cache/solidity-files-cache.json \ No newline at end of file diff --git a/.projectile b/.projectile deleted file mode 100644 index 472153629..000000000 --- a/.projectile +++ /dev/null @@ -1,3 +0,0 @@ --/node_modules --/rag --rag \ No newline at end of file diff --git a/cache/solidity-files-cache.json b/cache/solidity-files-cache.json deleted file mode 100644 index cd0b68bf5..000000000 --- a/cache/solidity-files-cache.json +++ /dev/null @@ -1 +0,0 @@ -{"_format":"","paths":{"artifacts":"out","build_infos":"out/build-info","sources":"src","tests":"test","scripts":"script","libraries":["lib","node_modules"]},"files":{"cmd/rpc/oracle/testing/contracts/USDC.sol":{"lastModificationDate":1752574865334,"contentHash":"f3edf42ae79e8f47","interfaceReprHash":null,"sourceName":"cmd/rpc/oracle/testing/contracts/USDC.sol","imports":[],"versionRequirement":"^0.8.0","artifacts":{"USDC":{"0.8.20":{"default":{"path":"USDC.sol/USDC.json","build_id":"bea85887a954e3be"}}}},"seenByCompiler":true}},"builds":["bea85887a954e3be"],"profiles":{"default":{"solc":{"optimizer":{"enabled":false,"runs":200},"metadata":{"useLiteralContent":false,"bytecodeHash":"ipfs","appendCBOR":true},"outputSelection":{"*":{"*":["abi","evm.bytecode.object","evm.bytecode.sourceMap","evm.bytecode.linkReferences","evm.deployedBytecode.object","evm.deployedBytecode.sourceMap","evm.deployedBytecode.linkReferences","evm.deployedBytecode.immutableReferences","evm.methodIdentifiers","metadata"]}},"evmVersion":"cancun","viaIR":false,"libraries":{}},"vyper":{"evmVersion":"cancun","outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode"]}}}}},"preprocessed":false,"mocks":[]} \ No newline at end of file diff --git a/cmd/rpc/README.md b/cmd/rpc/README.md index 49e0e2a7d..bb4cc67fc 100644 --- a/cmd/rpc/README.md +++ b/cmd/rpc/README.md @@ -58,10 +58,8 @@ - /v1/query/validator-set - /v1/query/checkpoint - /v1/subscribe-rc-info -- /debug/blocked -- /debug/heap -- /debug/cpu -- /debug/routine +- /debug/pprof +- /debug/pprof/*name - /v1/eth - /v1/admin/keystore - /v1/admin/keystore-new-key @@ -5050,11 +5048,9 @@ Jun 11 09:47:09.521 INFO: Reset BFT (NEW_HEIGHT) ## Golang Profiling Debug **Route:** -- DebugBlockedRoutePath = "/debug/blocked" -- DebugHeapRoutePath = "/debug/heap" -- DebugCPURoutePath = "/debug/cpu" -- DebugRoutineRoutePath = "/debug/routine" +- /debug/pprof +- /debug/pprof/*name -**Description**: returns an HTTP handler that serves the named profile. Available profiles can be found in [runtime/pprof.Profile]. See https://pkg.go.dev/net/http/pprof +**Description**: serves the Go pprof index and named pprof handlers. These routes are exposed on the profiling server bound to `ProfilingPort`, not the main RPC port. Available profiles can be found in `net/http/pprof`. See https://pkg.go.dev/net/http/pprof **HTTP Method**: `GET` diff --git a/cmd/rpc/query.go b/cmd/rpc/query.go index c0a510d25..53db96678 100644 --- a/cmd/rpc/query.go +++ b/cmd/rpc/query.go @@ -400,8 +400,34 @@ func (s *Server) Blocks(w http.ResponseWriter, r *http.Request, _ httprouter.Par // TransactionByHash responds with a transaction with the hash h func (s *Server) TransactionByHash(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - // Invoke helper with the HTTP request, response writer and an inline callback - s.hashIndexer(w, r, func(s lib.StoreI, h lib.HexBytes) (any, lib.ErrorI) { return s.GetTxByHash(h) }) + req := new(hashRequest) + if ok := unmarshal(w, r, req); !ok { + return + } + hashBytes, err := lib.StringToBytes(req.Hash) + if err != nil { + write(w, err, http.StatusBadRequest) + return + } + st, ok := s.setupStore(w) + if !ok { + return + } + defer st.Discard() + tx, err := st.GetTxByHash(hashBytes) + if err != nil { + write(w, err, http.StatusBadRequest) + return + } + if tx != nil && tx.GetTxHash() != "" { + write(w, tx, http.StatusOK) + return + } + if pendingTx, found := s.controller.GetPendingTxByHash(req.Hash); found { + write(w, pendingTx, http.StatusOK) + return + } + write(w, map[string]string{"error": "transaction not found"}, http.StatusNotFound) } // TransactionsByHeight response with the transactions at block height h diff --git a/cmd/rpc/web/explorer/src/components/Footer.tsx b/cmd/rpc/web/explorer/src/components/Footer.tsx index b82c4dd3d..14be5b8c6 100644 --- a/cmd/rpc/web/explorer/src/components/Footer.tsx +++ b/cmd/rpc/web/explorer/src/components/Footer.tsx @@ -6,13 +6,6 @@ const Footer: React.FC = () => {
{/* Desktop Layout */}
- {/* Left side - Logo and Copyright */} -
- - © 2025 Canopy Foundation. All rights reserved. - -
- {/* Right side - Links */}
{ Terms
- - {/* Copyright */} -
- - © 2025 Canopy Foundation. All rights reserved. - -
diff --git a/cmd/rpc/web/explorer/src/components/transaction/TransactionDetailPage.tsx b/cmd/rpc/web/explorer/src/components/transaction/TransactionDetailPage.tsx index aad0cc1f6..a9991b896 100644 --- a/cmd/rpc/web/explorer/src/components/transaction/TransactionDetailPage.tsx +++ b/cmd/rpc/web/explorer/src/components/transaction/TransactionDetailPage.tsx @@ -243,8 +243,10 @@ const TransactionDetailPage: React.FC = () => { } // Extract data from the API response (using transaction already extracted above) - const status = transaction?.status || 'success' const blockHeight = transaction?.height || transaction?.blockHeight || transaction?.block || 0 + const rawStatus = transaction?.status || '' + const isPending = blockHeight === 0 || rawStatus.toLowerCase() === 'pending' + const status = isPending ? 'pending' : (rawStatus || 'success') const timestamp = transaction?.transaction?.time || transaction?.timestamp || transaction?.time || new Date().toISOString() const fee = formatFee(transactionFeeMicro) @@ -297,11 +299,11 @@ const TransactionDetailPage: React.FC = () => {
- - {status === 'success' || status === 'Success' ? 'Success' : 'Pending'} + + {isPending ? 'Pending' : 'Success'} - Confirmed {getTimeAgo(timestamp)} + {isPending ? 'Awaiting block inclusion' : `Confirmed ${getTimeAgo(timestamp)}`}
@@ -368,8 +370,8 @@ const TransactionDetailPage: React.FC = () => {
Status - - {status === 'success' || status === 'Success' ? 'Success' : 'Pending'} + + {isPending ? 'Pending' : 'Success'}
diff --git a/cmd/rpc/web/explorer/src/components/transaction/TransactionsPage.tsx b/cmd/rpc/web/explorer/src/components/transaction/TransactionsPage.tsx index f27651a41..5a48c2681 100644 --- a/cmd/rpc/web/explorer/src/components/transaction/TransactionsPage.tsx +++ b/cmd/rpc/web/explorer/src/components/transaction/TransactionsPage.tsx @@ -6,6 +6,8 @@ import { extractAmountMicro } from '../../lib/utils' import transactionsTexts from '../../data/transactions.json' import ExplorerOverviewCards from '../ExplorerOverviewCards' +type ActiveTab = 'confirmed' | 'pending' + interface TransactionRow { hash: string type: string @@ -36,10 +38,16 @@ const LiveIndicator = () => ( ) const TransactionsPage: React.FC = () => { - const [currentPage, setCurrentPage] = React.useState(1) - const [pageSize, setPageSize] = React.useState(10) - const { data: transactionsData, isLoading: isTransactionsLoading } = useTransactionsWithRealPagination(currentPage, pageSize) - const { data: pendingData, isLoading: isPendingLoading } = usePending(1) + const [activeTab, setActiveTab] = React.useState('confirmed') + + const [confirmedPage, setConfirmedPage] = React.useState(1) + const [confirmedPageSize, setConfirmedPageSize] = React.useState(10) + + const [pendingPage, setPendingPage] = React.useState(1) + const [pendingPageSize, setPendingPageSize] = React.useState(10) + + const { data: transactionsData, isLoading: isTransactionsLoading } = useTransactionsWithRealPagination(confirmedPage, confirmedPageSize) + const { data: pendingData, isLoading: isPendingLoading } = usePending(pendingPage, pendingPageSize) const normalizeConfirmedTransactions = React.useMemo(() => { const payload = transactionsData as Record | undefined @@ -66,7 +74,7 @@ const TransactionsPage: React.FC = () => { }, [transactionsData]) const pendingTransactions = React.useMemo(() => { - if (currentPage !== 1 || !pendingData) return [] + if (!pendingData) return [] const payload = pendingData as Record const list = payload.results ?? payload.transactions ?? payload.txs ?? pendingData @@ -82,46 +90,64 @@ const TransactionsPage: React.FC = () => { to: String(txRecord.recipient ?? txRecord.to ?? 'N/A'), amount: extractAmountMicro(txRecord), fee: Number(txRecord.fee ?? 0), - status: 'pending', + status: 'pending' as const, blockHeight: undefined, timestamp: undefined, } }) - }, [currentPage, pendingData]) + }, [pendingData]) + + const totalConfirmed = Number((transactionsData as Record | undefined)?.totalCount ?? 0) + const totalPending = Number((pendingData as Record | undefined)?.totalCount ?? 0) - const totalTransactions = Number((transactionsData as Record | undefined)?.totalCount ?? 0) const overviewCards = [ { - title: 'Visible Transactions', - value: normalizeConfirmedTransactions.length.toLocaleString(), - subValue: 'Current page', - icon: 'fa-solid fa-arrow-right-arrow-left', + title: 'Indexed Transactions', + value: totalConfirmed.toLocaleString(), + subValue: 'Confirmed total', + icon: 'fa-solid fa-cubes', }, { title: 'Pending', - value: pendingTransactions.length.toLocaleString(), + value: totalPending.toLocaleString(), subValue: 'Awaiting block', icon: 'fa-solid fa-clock', }, { title: 'Confirmed', value: normalizeConfirmedTransactions.length.toLocaleString(), - subValue: 'In blocks', + subValue: 'Current page', icon: 'fa-solid fa-circle-check', }, { - title: 'Indexed Transactions', - value: totalTransactions.toLocaleString(), - subValue: 'Confirmed total', - icon: 'fa-solid fa-cubes', + title: 'Visible Transactions', + value: normalizeConfirmedTransactions.length.toLocaleString(), + subValue: 'Current page', + icon: 'fa-solid fa-arrow-right-arrow-left', }, ] - const handlePageSizeChange = (value: number) => { - setPageSize(value) - setCurrentPage(1) + const handleConfirmedPageSizeChange = (value: number) => { + setConfirmedPageSize(value) + setConfirmedPage(1) } + const handlePendingPageSizeChange = (value: number) => { + setPendingPageSize(value) + setPendingPage(1) + } + + const switchTab = (tab: ActiveTab) => { + setActiveTab(tab) + } + + const tabClass = (tab: ActiveTab) => + `px-4 py-2 text-sm font-medium rounded-lg transition-colors ${ + activeTab === tab + ? 'bg-white/10 text-white' + : 'text-gray-400 hover:text-white hover:bg-white/5' + }` + return ( { - +
+ + +
+ + {activeTab === 'confirmed' ? ( + + ) : ( + + )}
) } diff --git a/cmd/rpc/web/explorer/src/components/transaction/TransactionsTable.tsx b/cmd/rpc/web/explorer/src/components/transaction/TransactionsTable.tsx index 1dedb7a1b..d179fc1ae 100644 --- a/cmd/rpc/web/explorer/src/components/transaction/TransactionsTable.tsx +++ b/cmd/rpc/web/explorer/src/components/transaction/TransactionsTable.tsx @@ -4,7 +4,7 @@ import { formatDistanceToNow, isValid, parseISO } from 'date-fns' import { formatPaginationRange, isRowNavigationKey, shouldIgnoreRowNavigation, toCNPY } from '../../lib/utils' import TransactionTypeBadge from './TransactionTypeBadge' import PageSizeSelect from '../shared/PageSizeSelect' -import { GREEN_BADGE_CLASS, GREEN_BADGE_TONE } from '../ui/badgeStyles' +import { BADGE_BASE, GREEN_BADGE_TONE, YELLOW_BADGE_TONE, RED_BADGE_TONE } from '../ui/badgeStyles' import CopyableIdentifier from '../ui/CopyableIdentifier' interface Transaction { @@ -27,6 +27,7 @@ interface TransactionsTableProps { pageSize?: number onPageChange?: (page: number) => void onPageSizeChange?: (value: number) => void + emptyMessage?: string } const desktopHeaderClass = @@ -63,7 +64,14 @@ const formatAge = (timestamp?: string, status?: Transaction['status']) => { } const statusClassName = (status: Transaction['status']) => { - return GREEN_BADGE_TONE + switch (status) { + case 'pending': + return YELLOW_BADGE_TONE + case 'failed': + return RED_BADGE_TONE + default: + return GREEN_BADGE_TONE + } } const statusLabel = (status: Transaction['status']) => { @@ -90,6 +98,7 @@ const TransactionsTable: React.FC = ({ pageSize = 10, onPageChange, onPageSizeChange, + emptyMessage = 'No transactions found', }) => { const navigate = useNavigate() const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)) @@ -157,7 +166,7 @@ const TransactionsTable: React.FC = ({ ) : transactions.length === 0 ? ( - No transactions found in this page of blocks + {emptyMessage} ) : ( @@ -250,7 +259,7 @@ const TransactionsTable: React.FC = ({ {statusLabel(transaction.status)} diff --git a/cmd/rpc/web/explorer/src/components/ui/badgeStyles.ts b/cmd/rpc/web/explorer/src/components/ui/badgeStyles.ts index fe13ce85d..07b97c8c4 100644 --- a/cmd/rpc/web/explorer/src/components/ui/badgeStyles.ts +++ b/cmd/rpc/web/explorer/src/components/ui/badgeStyles.ts @@ -1,3 +1,7 @@ export const GREEN_BADGE_TONE = 'border-[#35cd48]/30 bg-[#35cd48]/12 text-[#35cd48]' +export const YELLOW_BADGE_TONE = 'border-yellow-500/30 bg-yellow-500/12 text-yellow-500' +export const RED_BADGE_TONE = 'border-red-500/30 bg-red-500/12 text-red-500' -export const GREEN_BADGE_CLASS = `inline-flex min-w-[6.25rem] items-center justify-center rounded-md border px-1.5 py-0.5 text-center text-[10px] font-medium tracking-tight ${GREEN_BADGE_TONE}` +export const BADGE_BASE = 'inline-flex min-w-[6.25rem] items-center justify-center rounded-md border px-1.5 py-0.5 text-center text-[10px] font-medium tracking-tight' + +export const GREEN_BADGE_CLASS = `${BADGE_BASE} ${GREEN_BADGE_TONE}` diff --git a/cmd/rpc/web/explorer/src/hooks/useApi.ts b/cmd/rpc/web/explorer/src/hooks/useApi.ts index 8cbb2afc0..975b46b71 100644 --- a/cmd/rpc/web/explorer/src/hooks/useApi.ts +++ b/cmd/rpc/web/explorer/src/hooks/useApi.ts @@ -57,7 +57,7 @@ export const queryKeys = { txByHash: (hash: string) => ['txByHash', hash], transactionsBySender: (page: number, sender: string) => ['transactionsBySender', page, sender], transactionsByRec: (page: number, rec: string) => ['transactionsByRec', page, rec], - pending: (page: number) => ['pending', page], + pending: (page: number, perPage: number) => ['pending', page, perPage], ecoParams: (chainId: number) => ['ecoParams', chainId], orders: (chainId: number) => ['orders', chainId], config: () => ['config'], @@ -393,11 +393,11 @@ export const useTransactionsByRec = (page: number, rec: string) => { }; // Hooks for Pending Transactions -export const usePending = (page: number) => { +export const usePending = (page: number, perPage: number = 10) => { return useQuery({ - queryKey: queryKeys.pending(page), - queryFn: () => Pending(page, 0), - staleTime: 10000, // Shorter stale time for pending transactions + queryKey: queryKeys.pending(page, perPage), + queryFn: () => Pending(page, perPage), + staleTime: 10000, }); }; diff --git a/cmd/rpc/web/wallet/public/plugin/canopy/manifest.json b/cmd/rpc/web/wallet/public/plugin/canopy/manifest.json index aa37f7936..3227ff84a 100644 --- a/cmd/rpc/web/wallet/public/plugin/canopy/manifest.json +++ b/cmd/rpc/web/wallet/public/plugin/canopy/manifest.json @@ -1945,7 +1945,7 @@ "allowCreate": true, "label": "Signer Address", "placeholder": "Select Signer Address", - "value": "{{account.address}}", + "value": "{{form.validatorAddress || account.address}}", "required": true, "autoPopulate": "once", "help": "The address that will sign this transaction", @@ -2215,8 +2215,7 @@ "type": "text", "label": "New Parameter Value", "required": true, - "value": "{{(() => { const root = ds.params?.params ?? ds.params ?? {}; const aliasToRoot = { cons: 'consensus', val: 'validator', fee: 'fee', gov: 'governance', eco: 'economics' }; const selected = form.parameterSpace || ''; const resolved = root[selected] ? selected : (aliasToRoot[selected] || selected); return root?.[resolved]?.[form.parameterKey] ?? ''; })()}}", - "autoPopulate": "once", + "autoPopulate": false, "placeholder": "Enter new value", "help": "The new value for this parameter", "validation": { @@ -2341,23 +2340,23 @@ "value": "{{account.address}}", "coerce": "string" }, - "parameterSpace": { + "paramSpace": { "value": "{{form.parameterSpace}}", "coerce": "string" }, - "parameterKey": { + "paramKey": { "value": "{{form.parameterKey}}", "coerce": "string" }, - "parameterValue": { + "paramValue": { "value": "{{form.parameterValue}}", "coerce": "string" }, - "startHeight": { - "value": "{{resolveHeight(ds.height)}}", + "startBlock": { + "value": "{{(() => { const v = Number(form.startHeight); return v > 0 ? v : resolveHeight(ds.height); })()}}", "coerce": "number" }, - "endHeight": { + "endBlock": { "value": "{{(() => { const s = Number(form.startHeight || resolveHeight(ds.height)); const eRaw = Number(form.endHeight); const e = eRaw > 0 ? eRaw : s + 1; return Math.min(Math.max(e, s + 1), s + 10000); })()}}", "coerce": "number" }, @@ -3480,8 +3479,8 @@ "label": "New Value", "required": true, "showIf": "{{ form.proposalType !== 'treasury' }}", - "value": "{{(() => { const root = ds.params?.params ?? ds.params ?? {}; const aliasToRoot = { cons: 'consensus', val: 'validator', fee: 'fee', gov: 'governance', eco: 'economics' }; const selected = form.paramSpace || 'fee'; const resolved = root[selected] ? selected : (aliasToRoot[selected] || selected); return root?.[resolved]?.[form.paramKey] ?? ''; })()}}", - "autoPopulate": "always", + "autoPopulate": false, + "help": "{{(() => { const root = ds.params?.params ?? ds.params ?? {}; const aliasToRoot = { cons: 'consensus', val: 'validator', fee: 'fee', gov: 'governance', eco: 'economics' }; const selected = form.paramSpace || 'fee'; const resolved = root[selected] ? selected : (aliasToRoot[selected] || selected); const current = root?.[resolved]?.[form.paramKey] ?? ''; return current ? 'Current value: ' + current : ''; })()}}", "span": { "base": 12 } @@ -3626,7 +3625,7 @@ "coerce": "string" }, "startBlock": { - "value": "{{Number(form.startBlock || resolveHeight(ds.height) + 5)}}", + "value": "{{(() => { const v = Number(form.startBlock); return v > 0 ? v : resolveHeight(ds.height) + 5; })()}}", "coerce": "number" }, "endBlock": { @@ -3832,7 +3831,7 @@ "coerce": "string" }, "startBlock": { - "value": "{{Number(form.startBlock || resolveHeight(ds.height) + 5)}}", + "value": "{{(() => { const v = Number(form.startBlock); return v > 0 ? v : resolveHeight(ds.height) + 5; })()}}", "coerce": "number" }, "endBlock": { diff --git a/cmd/rpc/web/wallet/src/actions/fields/TextField.tsx b/cmd/rpc/web/wallet/src/actions/fields/TextField.tsx index 337e2f2ea..cdbcaf213 100644 --- a/cmd/rpc/web/wallet/src/actions/fields/TextField.tsx +++ b/cmd/rpc/web/wallet/src/actions/fields/TextField.tsx @@ -23,6 +23,8 @@ export const TextField: React.FC = ({ // Track whether user has manually edited this field const touchedRef = React.useRef(false) + const isOncePopulate = (field as Record).autoPopulate === 'once' + // Sync field value when the resolved template changes (e.g., table selection) // This allows computed fields to stay in sync while still being editable React.useEffect(() => { @@ -36,8 +38,6 @@ export const TextField: React.FC = ({ } }, [resolvedValue, field.value, onChange]) - const isOncePopulate = (field as Record).autoPopulate === 'once' - // For readOnly fields with a value template, always use the resolved template // For editable fields with autoPopulate=once, respect user clearing the field // For other editable fields, use form value but initialize from template if empty diff --git a/cmd/rpc/web/wallet/src/app/pages/Governance.tsx b/cmd/rpc/web/wallet/src/app/pages/Governance.tsx index 565d3dbe6..1cf34986d 100644 --- a/cmd/rpc/web/wallet/src/app/pages/Governance.tsx +++ b/cmd/rpc/web/wallet/src/app/pages/Governance.tsx @@ -9,6 +9,7 @@ import { ClipboardList, Vote, ChevronDown, + Send, } from "lucide-react"; import { Poll, Proposal, useGovernanceData } from "@/hooks/useGovernance"; import { ProposalTable } from "@/components/governance/ProposalTable"; @@ -180,20 +181,11 @@ export const Governance = () => { actions: [ { id: GOVERNANCE_ACTION_IDS.submitProposal, - title: "1. Generate Payload", - }, - { - id: GOVERNANCE_ACTION_IDS.addProposalVote, - title: "2. Review & Approve", - }, - { - id: GOVERNANCE_ACTION_IDS.submitProposalTx, - title: "3. Submit to Network", }, ], title: "Submit a Proposal", - description: "Generate, review, approve, and submit a protocol-change or treasury-subsidy proposal in one flow.", - help: "Step 1 generates the signed proposal payload. Step 2 records the replica's approve or reject vote. Step 3 broadcasts that same payload to the network.", + description: "Generate a signed protocol-change or treasury-subsidy proposal payload.", + help: "Generates the signed proposal payload for a parameter change or treasury subsidy. Once generated, use Vote on Proposal to approve and Submit to Network to broadcast.", icon: ScrollText, iconClassName: "text-[#35cd48]", }, @@ -209,6 +201,18 @@ export const Governance = () => { icon: Vote, iconClassName: "text-[#35cd48]", }, + { + actions: [ + { + id: GOVERNANCE_ACTION_IDS.submitProposalTx, + }, + ], + title: "Submit to Network", + description: "Broadcast an approved proposal payload to the network as a formal governance transaction.", + help: "Use this after a proposal has been generated and approved. Paste the signed JSON payload to submit it on-chain.", + icon: Send, + iconClassName: "text-[#35cd48]", + }, { actions: [ { @@ -238,7 +242,7 @@ export const Governance = () => { subtitle="Manage polls and proposals with guided, one-step submissions and explicit review details." /> -
+
{criticalActions.map((item) => { const Icon = item.icon; return ( diff --git a/cmd/rpc/web/wallet/src/app/pages/Staking.tsx b/cmd/rpc/web/wallet/src/app/pages/Staking.tsx index 70be97edd..3d4ecedca 100644 --- a/cmd/rpc/web/wallet/src/app/pages/Staking.tsx +++ b/cmd/rpc/web/wallet/src/app/pages/Staking.tsx @@ -16,6 +16,7 @@ import { StatsCards } from "@/components/staking/StatsCards"; import { Toolbar } from "@/components/staking/Toolbar"; import { ValidatorList } from "@/components/staking/ValidatorList"; import { useActionModal } from "@/app/providers/ActionModalProvider"; +import { useSelectedAccount } from "@/app/providers/AccountsProvider"; import { PageHeader } from "@/components/layouts/PageHeader"; type ValidatorRow = { @@ -47,6 +48,7 @@ export default function Staking(): JSX.Element { const { totalStaked } = useAccountData(); const { data: validators = [] } = useValidators(); const { openAction } = useActionModal(); + const { selectedAccount } = useSelectedAccount(); const dsFetch = useDSFetcher(); const csvRef = useRef(null); @@ -168,8 +170,16 @@ export default function Staking(): JSX.Element { // Handler to add stake - opens the "stake" action from manifest const handleAddStake = useCallback(() => { - openAction("stake"); - }, [openAction]); + if (!selectedAccount?.address) { + openAction("stake"); + return; + } + openAction("stake", { + prefilledData: { + operator: selectedAccount.address, + }, + }); + }, [openAction, selectedAccount?.address]); return ( { return; } - openAction("stake"); + openAction("stake", { + prefilledData: { + operator: account.address, + }, + }); }, [openAction, setActiveAccount], ); diff --git a/cmd/rpc/web/wallet/src/manifest/loader.ts b/cmd/rpc/web/wallet/src/manifest/loader.ts index 0f8024087..aeb701c6a 100644 --- a/cmd/rpc/web/wallet/src/manifest/loader.ts +++ b/cmd/rpc/web/wallet/src/manifest/loader.ts @@ -52,8 +52,7 @@ export function useEmbeddedConfig(chain = DEFAULT_CHAIN) { queryKey: ['manifest', base], enabled: !!chainQ.data, queryFn: () => fetchJson(`${base}/manifest.json`), - // Use the global refetch configuration every 20s - // The manifest can change dynamically + staleTime: 0, }) // tiny bridge for places where global ctx is handy (e.g., validators) diff --git a/controller/tx.go b/controller/tx.go index f59de5044..aaf01b572 100644 --- a/controller/tx.go +++ b/controller/tx.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "strings" "sync" "sync/atomic" "time" @@ -332,6 +333,31 @@ func (c *Controller) GetPendingPage(p lib.PageParams) (page *lib.Page, err lib.E return } +// GetPendingTxByHash() returns an unconfirmed mempool transaction by hash. +func (c *Controller) GetPendingTxByHash(hash string) (*lib.TxResult, bool) { + // lock the controller for thread safety + c.Lock() + // unlock the controller when the function completes + defer c.Unlock() + if c.Mempool == nil { + return nil, false + } + normalizedHash := normalizeTxHash(hash) + for _, tx := range c.Mempool.cachedResults { + if tx == nil { + continue + } + if normalizeTxHash(tx.TxHash) == normalizedHash { + return tx, true + } + } + return nil, false +} + +func normalizeTxHash(hash string) string { + return strings.TrimPrefix(strings.ToLower(hash), "0x") +} + // GetFailedTxsPage() returns a list of failed mempool transactions func (c *Controller) GetFailedTxsPage(address string, p lib.PageParams) (page *lib.Page, err lib.ErrorI) { // lock the controller for thread safety diff --git a/controller/tx_test.go b/controller/tx_test.go new file mode 100644 index 000000000..a44864a1a --- /dev/null +++ b/controller/tx_test.go @@ -0,0 +1,35 @@ +package controller + +import ( + "sync" + "testing" + + "github.com/canopy-network/canopy/lib" + "github.com/stretchr/testify/require" +) + +func TestGetPendingTxByHash(t *testing.T) { + c := &Controller{ + Mempool: &Mempool{ + cachedResults: lib.TxResults{ + &lib.TxResult{TxHash: "abcdef1234"}, + &lib.TxResult{TxHash: "1234567890"}, + }, + }, + Mutex: &sync.Mutex{}, + } + + tx, found := c.GetPendingTxByHash("ABCDEF1234") + require.True(t, found) + require.NotNil(t, tx) + require.Equal(t, "abcdef1234", tx.TxHash) + + tx, found = c.GetPendingTxByHash("0x1234567890") + require.True(t, found) + require.NotNil(t, tx) + require.Equal(t, "1234567890", tx.TxHash) + + tx, found = c.GetPendingTxByHash("missing") + require.False(t, found) + require.Nil(t, tx) +} diff --git a/docs/ORACLE_METRICS.md b/docs/ORACLE_METRICS.md deleted file mode 100644 index f194116fe..000000000 --- a/docs/ORACLE_METRICS.md +++ /dev/null @@ -1,308 +0,0 @@ -# Oracle Metrics Reference - -This document describes all Prometheus metrics exposed by the Canopy Oracle system for monitoring and observability. - -## Overview - -The oracle exposes metrics through a Prometheus-compatible endpoint. Metrics are organized into two main categories: - -1. **Oracle Metrics** (`canopy_oracle_*`) - High-level oracle operations, order lifecycle, and validation -2. **Eth Block Provider Metrics** (`canopy_eth_*`) - Ethereum connectivity, block processing, and transaction handling - ---- - -## Oracle Metrics - -These metrics track the oracle's core functionality including order processing, validation, and submission. - -### Block Height Metrics - -| Metric Name | Type | Description | -|-------------|------|-------------| -| `canopy_oracle_safe_height` | Gauge | Current safe block height in the oracle | -| `canopy_oracle_source_chain_height` | Gauge | Current source chain height | -| `canopy_oracle_last_processed_height` | Gauge | Last source chain block height processed | -| `canopy_oracle_confirmation_lag` | Gauge | Gap between source chain height and safe height (blocks awaiting confirmation) | -| `canopy_oracle_orders_awaiting_confirmation` | Gauge | Number of orders witnessed but not yet at safe height | - -### Order Lifecycle Metrics - -| Metric Name | Type | Labels | Description | -|-------------|------|--------|-------------| -| `canopy_oracle_orders_witnessed_total` | Counter | - | Total orders witnessed | -| `canopy_oracle_orders_validated_total` | Counter | - | Total orders validated successfully | -| `canopy_oracle_orders_submitted_total` | Counter | - | Total orders submitted for consensus | -| `canopy_oracle_orders_rejected_total` | Counter | - | Total orders rejected during validation | -| `canopy_oracle_orders_not_in_orderbook_total` | Counter | - | Orders witnessed but not found in order book | -| `canopy_oracle_orders_duplicate_total` | Counter | - | Duplicate orders (already in store) | -| `canopy_oracle_orders_archived_total` | Counter | - | Orders successfully archived | -| `canopy_oracle_lock_orders_committed_total` | Counter | - | Lock orders committed via certificate | -| `canopy_oracle_close_orders_committed_total` | Counter | - | Close orders committed via certificate | - -### Order Store Metrics - -| Metric Name | Type | Description | -|-------------|------|-------------| -| `canopy_oracle_total_orders_stored` | Gauge | Total orders currently stored in order store | -| `canopy_oracle_lock_orders_stored` | Gauge | Total lock orders currently stored | -| `canopy_oracle_close_orders_stored` | Gauge | Total close orders currently stored | - -### Validation Failure Metrics - -| Metric Name | Type | Labels | Description | -|-------------|------|--------|-------------| -| `canopy_oracle_validation_failures_total` | Counter | `reason` | Validation failures by reason | - -**Reason Labels:** -- `order_nil` - Order was nil -- `missing_order_type` - Order missing type specification -- `both_order_types` - Order has both lock and close types -- `lock_id_mismatch` - Lock order ID mismatch -- `lock_chain_mismatch` - Lock order chain ID mismatch -- `close_data_mismatch` - Close order data mismatch -- `close_id_mismatch` - Close order ID mismatch -- `close_chain_mismatch` - Close order chain ID mismatch -- `recipient_mismatch` - Recipient address mismatch -- `amount_nil` - Amount is nil -- `amount_mismatch` - Amount mismatch - -### Submission Tracking Metrics - -| Metric Name | Type | Description | -|-------------|------|-------------| -| `canopy_oracle_orders_held_awaiting_safe_total` | Counter | Orders not submitted due to safe height requirement | -| `canopy_oracle_orders_held_propose_delay_total` | Counter | Orders held by ProposeDelayBlocks | -| `canopy_oracle_orders_held_resubmit_delay_total` | Counter | Orders held by resubmit cooldown | -| `canopy_oracle_lock_order_resubmissions_total` | Counter | Lock orders resubmitted | -| `canopy_oracle_close_order_resubmissions_total` | Counter | Close orders resubmitted | - -### Error and Reorg Metrics - -| Metric Name | Type | Description | -|-------------|------|-------------| -| `canopy_oracle_chain_reorgs_total` | Counter | Total chain reorganizations detected | -| `canopy_oracle_orders_pruned_total` | Counter | Total orders pruned during cleanup | -| `canopy_oracle_block_processing_errors_total` | Counter | Total block processing errors | -| `canopy_oracle_reorg_rollback_depth` | Histogram | How many blocks reorgs roll back | - -### Store Operation Metrics - -| Metric Name | Type | Description | -|-------------|------|-------------| -| `canopy_oracle_store_write_errors_total` | Counter | Order store write failures | -| `canopy_oracle_store_read_errors_total` | Counter | Order store read failures | -| `canopy_oracle_store_remove_errors_total` | Counter | Order store remove failures | - -### Performance Metrics - -| Metric Name | Type | Description | -|-------------|------|-------------| -| `canopy_oracle_order_book_update_time` | Histogram | Time to update order book | -| `canopy_oracle_root_chain_sync_time` | Histogram | Time to sync with root chain | - ---- - -## Eth Block Provider Metrics - -These metrics track Ethereum connectivity, block fetching, and transaction processing. - -### Connection & Sync Status Metrics (High Priority) - -| Metric Name | Type | Labels | Description | -|-------------|------|--------|-------------| -| `canopy_eth_rpc_connection_attempts_total` | Counter | - | Total RPC connection attempts | -| `canopy_eth_rpc_connection_errors_total` | Counter | `error_type` | RPC connection errors by error type | -| `canopy_eth_ws_connection_attempts_total` | Counter | - | Total WebSocket connection attempts | -| `canopy_eth_ws_subscription_errors_total` | Counter | - | WebSocket subscription failures | -| `canopy_eth_connection_state` | Gauge | - | Current connection state | -| `canopy_eth_sync_status` | Gauge | - | Current sync status | - -**Connection State Values:** -- `0` = Disconnected -- `1` = Connecting -- `2` = RPC Connected -- `3` = Fully Connected (RPC + WebSocket) - -**Sync Status Values:** -- `0` = Unsynced -- `1` = Syncing -- `2` = Synced - -### Block Height Metrics - -| Metric Name | Type | Description | -|-------------|------|-------------| -| `canopy_eth_chain_head_height` | Gauge | Latest block height from chain head | -| `canopy_eth_last_processed_height` | Gauge | Last block height successfully processed | -| `canopy_eth_safe_height` | Gauge | Current safe (confirmed) block height | -| `canopy_eth_block_height_lag` | Gauge | Number of blocks behind chain head | - -### Block Processing Metrics (High Priority) - -| Metric Name | Type | Labels | Description | -|-------------|------|--------|-------------| -| `canopy_eth_block_fetch_errors_total` | Counter | `error_type` | Block fetch errors by error type | -| `canopy_eth_block_processing_timeouts_total` | Counter | - | Blocks that timed out during processing | -| `canopy_eth_process_blocks_batch_size` | Histogram | - | Number of blocks processed per batch | -| `canopy_eth_reorg_detected_total` | Counter | - | Chain reorganizations detected | -| `canopy_eth_blocks_processed_total` | Counter | - | Total Ethereum blocks processed | - -**Histogram Buckets for Batch Size:** 1, 5, 10, 25, 50, 100, 250, 500, 1000 - -### Transaction Processing Metrics (Medium Priority) - -| Metric Name | Type | Labels | Description | -|-------------|------|--------|-------------| -| `canopy_eth_transactions_total` | Counter | - | Total transactions encountered in blocks | -| `canopy_eth_transactions_processed_total` | Counter | - | Total Ethereum transactions processed | -| `canopy_eth_transaction_parse_errors_total` | Counter | `error_type` | Transaction parsing errors by error type | -| `canopy_eth_transaction_retry_by_attempt_total` | Counter | `attempt` | Transaction retry attempts by attempt number | -| `canopy_eth_transaction_exhausted_retries_total` | Counter | - | Transactions that exhausted all retry attempts | -| `canopy_eth_transaction_success_status_total` | Counter | `status` | Transaction success/failed/unknown breakdown | -| `canopy_eth_receipt_fetch_errors_total` | Counter | - | Receipt fetch failures | -| `canopy_eth_transaction_retries_total` | Counter | - | Total transaction processing retries | - -**Status Labels:** -- `success` - Transaction succeeded -- `failed` - Transaction failed -- `unknown` - Unable to determine status - -### Order Detection Metrics (Medium Priority) - -| Metric Name | Type | Labels | Description | -|-------------|------|--------|-------------| -| `canopy_eth_erc20_transfer_detected_total` | Counter | - | ERC20 transfers detected | -| `canopy_eth_lock_order_detected_total` | Counter | - | Lock orders successfully parsed | -| `canopy_eth_close_order_detected_total` | Counter | - | Close orders successfully parsed | -| `canopy_eth_order_validation_errors_total` | Counter | `order_type`, `error_type` | Order validation errors | - -**Order Type Labels:** -- `lock` - Lock order -- `close` - Close order - -### Token Cache Error Metrics (Medium Priority) - -| Metric Name | Type | Labels | Description | -|-------------|------|--------|-------------| -| `canopy_eth_token_info_fetch_errors_total` | Counter | `field` | Token info fetch errors by field | -| `canopy_eth_token_contract_call_timeouts_total` | Counter | - | Token contract call timeouts | -| `canopy_eth_token_cache_hits_total` | Counter | - | Token cache hits | -| `canopy_eth_token_cache_misses_total` | Counter | - | Token cache misses | - -**Field Labels:** -- `name` - Error fetching token name -- `symbol` - Error fetching token symbol -- `decimals` - Error fetching token decimals - -### Connection Error Metrics - -| Metric Name | Type | Description | -|-------------|------|-------------| -| `canopy_eth_connection_errors_total` | Counter | Total Ethereum connection errors | - ---- - -## Example Prometheus Queries - -### Oracle Health - -```promql -# Orders pending confirmation -canopy_oracle_orders_awaiting_confirmation - -# Validation failure rate by reason -rate(canopy_oracle_validation_failures_total[5m]) - -# Order processing success rate -rate(canopy_oracle_orders_validated_total[5m]) / rate(canopy_oracle_orders_witnessed_total[5m]) -``` - -### Ethereum Connectivity - -```promql -# Connection state (should be 3 for healthy) -canopy_eth_connection_state - -# Sync status (should be 2 for synced) -canopy_eth_sync_status - -# Block lag (should be low when synced) -canopy_eth_block_height_lag - -# RPC error rate -rate(canopy_eth_rpc_connection_errors_total[5m]) -``` - -### Block Processing - -```promql -# Blocks processed per second -rate(canopy_eth_blocks_processed_total[5m]) - -# Block fetch error rate -rate(canopy_eth_block_fetch_errors_total[5m]) - -# Average batch size -histogram_quantile(0.5, canopy_eth_process_blocks_batch_size) -``` - -### Transaction Processing - -```promql -# Transaction success rate -sum(rate(canopy_eth_transaction_success_status_total{status="success"}[5m])) / -sum(rate(canopy_eth_transaction_success_status_total[5m])) - -# Retry distribution -rate(canopy_eth_transaction_retry_by_attempt_total[5m]) - -# Exhausted retries (potential issues) -rate(canopy_eth_transaction_exhausted_retries_total[5m]) -``` - -### Order Detection - -```promql -# Lock orders detected per minute -rate(canopy_eth_lock_order_detected_total[1m]) * 60 - -# Close orders detected per minute -rate(canopy_eth_close_order_detected_total[1m]) * 60 - -# Order validation error rate -rate(canopy_eth_order_validation_errors_total[5m]) -``` - ---- - -## Alerting Recommendations - -### Critical Alerts - -| Condition | Threshold | Description | -|-----------|-----------|-------------| -| `canopy_eth_connection_state < 3` | > 5 min | Ethereum connection issues | -| `canopy_eth_sync_status < 2` | > 10 min | Oracle not synced | -| `canopy_eth_block_height_lag > 100` | > 5 min | Significant block processing lag | -| `rate(canopy_eth_rpc_connection_errors_total[5m]) > 0.1` | - | High RPC error rate | - -### Warning Alerts - -| Condition | Threshold | Description | -|-----------|-----------|-------------| -| `canopy_eth_block_height_lag > 10` | > 2 min | Block processing falling behind | -| `rate(canopy_oracle_validation_failures_total[5m]) > 0.01` | - | Validation failures occurring | -| `rate(canopy_eth_transaction_exhausted_retries_total[5m]) > 0` | - | Transactions failing after retries | -| `canopy_oracle_orders_awaiting_confirmation > 50` | - | Many orders pending confirmation | - ---- - -## Grafana Dashboard Tips - -1. **Connection Status Panel**: Use stat panel with value mappings for `canopy_eth_connection_state` and `canopy_eth_sync_status` - -2. **Block Heights Panel**: Graph showing `canopy_eth_chain_head_height`, `canopy_eth_last_processed_height`, and `canopy_eth_safe_height` together - -3. **Order Flow Panel**: Stacked graph of `canopy_eth_lock_order_detected_total` and `canopy_eth_close_order_detected_total` rates - -4. **Error Breakdown Panel**: Table showing validation failures grouped by reason label diff --git a/main b/main deleted file mode 100755 index 125bca552..000000000 Binary files a/main and /dev/null differ diff --git a/socket.sh b/socket.sh deleted file mode 100755 index d7f478515..000000000 --- a/socket.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -sudo netstat -np | grep 50002 -sudo ss -K state TIME-WAIT src 127.0.0.1:50002 -sudo ss state time-wait sport = 50002 -K - -#netstat -np | grep 40002 -sudo ss -K state TIME-WAIT src 127.0.0.1:40002 -sudo ss state time-wait sport = 40002 -K - -#netstat -np | grep 60002 -sudo ss -K state TIME-WAIT src 127.0.0.1:60002 -sudo ss state time-wait sport = 60002 -K