diff --git a/.gitignore b/.gitignore index acce4214d..ab62b78bd 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ runner/public/*.wasm !runner/public/codegen-wasm_exec.js skills/.registry.json .superpowers/ +.nx/ diff --git a/ai/chat/Dockerfile b/ai/chat/Dockerfile index 50b04e27f..6c8e24a66 100644 --- a/ai/chat/Dockerfile +++ b/ai/chat/Dockerfile @@ -4,7 +4,7 @@ RUN npm install -g bun WORKDIR /app # Workspace root config + lockfile -COPY package.json bun.lock ./ +COPY package.json bun.lock nx.json ./ # Shared monorepo packages (needed for workspace:* resolution) COPY packages/ ./packages/ @@ -18,11 +18,8 @@ RUN node -e "const p=require('./package.json');p.workspaces=['packages/*','ai/ch # Install all workspace deps RUN cd ai/chat/web && bun install -# Build shared packages (generate dist/ via tsup) -RUN cd packages/flow-passkey && bun run build \ - && cd ../auth-core && bun run build \ - && cd ../flowtoken && bun run build \ - && cd ../flow-ui && bun run build +# Build shared packages (nx resolves dependency order via tag:package) +RUN npx nx run-many -t build --projects=tag:package # Copy app source and build COPY ai/chat/web/ ./ai/chat/web/ diff --git a/ai/chat/web/app/api/runner-chat/route.ts b/ai/chat/web/app/api/runner-chat/route.ts index c0a158ea4..381db9457 100644 --- a/ai/chat/web/app/api/runner-chat/route.ts +++ b/ai/chat/web/app/api/runner-chat/route.ts @@ -12,6 +12,8 @@ import { buildSkillsPrompt, createLoadSkillTool } from "@/lib/skills"; const CADENCE_MCP_URL = process.env.CADENCE_MCP_URL || "https://cadence-mcp.up.railway.app/mcp"; +const FLOW_EVM_MCP_URL = + process.env.FLOW_EVM_MCP_URL || "https://flow-evm-mcp.up.railway.app/mcp"; // Mode -> model + thinking config (mirrors main chat) const MODE_CONFIG = { @@ -166,8 +168,8 @@ const walletTools = { }), }; -const SYSTEM_PROMPT = `You are a Cadence programming assistant embedded in Cadence Runner. -Your primary job is to help users write, edit, and debug Cadence smart contract code for Flow. +const SYSTEM_PROMPT = `You are a Cadence & Solidity programming assistant embedded in Cadence Runner. +Your primary job is to help users write, edit, and debug smart contract code for Flow — both Cadence and Solidity (Flow EVM). ## CRITICAL: Always use editor tools for code changes @@ -223,6 +225,16 @@ Always call \`get_wallet_info\` first to check if a signer is available before a - FlowToken: 0x1654653399040a61 - FUSD: 0x3c5959b568896393 +## Solidity / Flow EVM guidelines + +- Flow EVM is a full EVM environment on Flow — Solidity contracts deploy and run natively. +- Flow EVM Mainnet chain ID: 747, Testnet chain ID: 545. +- Use \`pragma solidity ^0.8.24;\` or later. The runner bundles solc 0.8.24. +- .sol files compile client-side via solc WASM. When an EVM wallet is connected, contracts auto-deploy. +- After deployment, the Interact tab lets users call read/write functions on the deployed contract. +- You have Flow EVM MCP tools to query on-chain EVM data (balances, transactions, contracts, tokens, etc.). +- For cross-VM patterns, users can call Solidity contracts from Cadence via \`EVM.run()\`. + Keep responses concise and implementation-focused.${buildSkillsPrompt()}`; function sanitizeProjectFiles(files?: RunnerProjectFile[]): RunnerProjectFile[] { @@ -305,12 +317,16 @@ export async function POST(req: Request) { projectFiles: sanitizeProjectFiles(projectFiles), })}`; - const cadenceMcp = await safeMcpTools(CADENCE_MCP_URL); + const [cadenceMcp, flowEvmMcp] = await Promise.all([ + safeMcpTools(CADENCE_MCP_URL), + safeMcpTools(FLOW_EVM_MCP_URL), + ]); const allTools = { ...editorTools, ...walletTools, ...cadenceMcp.tools, + ...flowEvmMcp.tools, loadSkill: createLoadSkillTool(), }; @@ -340,7 +356,10 @@ export async function POST(req: Request) { tools: allTools, stopWhen: stepCountIs(10), onFinish: async () => { - await cadenceMcp.client?.close(); + await Promise.all([ + cadenceMcp.client?.close(), + flowEvmMcp.client?.close(), + ]); }, }); diff --git a/backend/internal/api/blockscout_proxy.go b/backend/internal/api/blockscout_proxy.go index b16782ca9..11187cca4 100644 --- a/backend/internal/api/blockscout_proxy.go +++ b/backend/internal/api/blockscout_proxy.go @@ -17,6 +17,14 @@ func (s *Server) proxyBlockscout(w http.ResponseWriter, r *http.Request, upstrea if q := r.URL.RawQuery; q != "" { target += "?" + q } + // Append API key for rate limit bypass + if s.blockscoutAPIKey != "" { + sep := "?" + if r.URL.RawQuery != "" { + sep = "&" + } + target += sep + "apikey=" + s.blockscoutAPIKey + } req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target, nil) if err != nil { diff --git a/backend/internal/api/routes_registration.go b/backend/internal/api/routes_registration.go index 28f547baf..6b90168af 100644 --- a/backend/internal/api/routes_registration.go +++ b/backend/internal/api/routes_registration.go @@ -179,9 +179,18 @@ func registerFlowRoutes(r *mux.Router, s *Server) { r.HandleFunc("/flow/contract/{identifier}/{id}", s.handleFlowGetContractVersion).Methods("GET", "OPTIONS") r.HandleFunc("/flow/evm/transaction", s.handleFlowListEVMTransactions).Methods("GET", "OPTIONS") r.HandleFunc("/flow/evm/transaction/{hash}", s.handleFlowGetEVMTransaction).Methods("GET", "OPTIONS") + r.HandleFunc("/flow/evm/transaction/{hash}/internal-transactions", s.handleFlowGetEVMTransactionInternalTxs).Methods("GET", "OPTIONS") + r.HandleFunc("/flow/evm/transaction/{hash}/logs", s.handleFlowGetEVMTransactionLogs).Methods("GET", "OPTIONS") + r.HandleFunc("/flow/evm/transaction/{hash}/token-transfers", s.handleFlowGetEVMTransactionTokenTransfers).Methods("GET", "OPTIONS") r.HandleFunc("/flow/evm/token", s.handleFlowListEVMTokens).Methods("GET", "OPTIONS") r.HandleFunc("/flow/evm/token/{address}", s.handleFlowGetEVMToken).Methods("GET", "OPTIONS") r.HandleFunc("/flow/evm/address/{address}/token", s.handleFlowGetEVMAddressTokens).Methods("GET", "OPTIONS") + r.HandleFunc("/flow/evm/address/{address}/nft", s.handleFlowGetEVMAddressNFTs).Methods("GET", "OPTIONS") + r.HandleFunc("/flow/evm/address/{address}/transactions", s.handleFlowGetEVMAddressTransactions).Methods("GET", "OPTIONS") + r.HandleFunc("/flow/evm/address/{address}/internal-transactions", s.handleFlowGetEVMAddressInternalTxs).Methods("GET", "OPTIONS") + r.HandleFunc("/flow/evm/address/{address}/token-transfers", s.handleFlowGetEVMAddressTokenTransfers).Methods("GET", "OPTIONS") + r.HandleFunc("/flow/evm/address/{address}", s.handleFlowGetEVMAddress).Methods("GET", "OPTIONS") + r.HandleFunc("/flow/evm/search", cachedHandler(30*time.Second, s.handleFlowEVMSearch)).Methods("GET", "OPTIONS") r.HandleFunc("/flow/node", s.handleListNodes).Methods("GET", "OPTIONS") r.HandleFunc("/flow/node/{node_id}", s.handleGetNode).Methods("GET", "OPTIONS") r.HandleFunc("/flow/node/{node_id}/reward/delegation", s.handleNotImplemented).Methods("GET", "OPTIONS") @@ -194,6 +203,7 @@ func registerFlowRoutes(r *mux.Router, s *Server) { r.HandleFunc("/flow/coa/{address}", s.handleGetCOAMapping).Methods("GET", "OPTIONS") r.HandleFunc("/flow/account/{address}/labels", s.handleFlowAccountLabels).Methods("GET", "OPTIONS") r.HandleFunc("/flow/search", cachedHandler(30*time.Second, s.handleSearch)).Methods("GET", "OPTIONS") + r.HandleFunc("/flow/search/preview", s.handleSearchPreview).Methods("GET", "OPTIONS") } func registerAccountingRoutes(r *mux.Router, s *Server) { diff --git a/backend/internal/api/routes_test.go b/backend/internal/api/routes_test.go index 4f3289255..d87612922 100644 --- a/backend/internal/api/routes_test.go +++ b/backend/internal/api/routes_test.go @@ -92,12 +92,23 @@ var specExcludedRoutes = map[string]bool{ // Alias: /contract/{id}/version/{id} same as /contract/{id}/{id} "/flow/contract/{identifier}/version/{id}": true, // Analytics aliases (content blockers block "analytics") - "/analytics/daily": true, - "/analytics/daily/module/{module}": true, - "/analytics/transfers/daily": true, - "/analytics/big-transfers": true, - "/analytics/top-contracts": true, - "/analytics/token-volume": true, + "/analytics/daily": true, + "/analytics/daily/module/{module}": true, + "/analytics/transfers/daily": true, + "/analytics/big-transfers": true, + "/analytics/top-contracts": true, + "/analytics/token-volume": true, + // EVM proxy routes (proxied to Blockscout, not our own API) + "/flow/evm/transaction/{hash}/internal-transactions": true, + "/flow/evm/transaction/{hash}/logs": true, + "/flow/evm/transaction/{hash}/token-transfers": true, + "/flow/evm/address/{address}/transactions": true, + "/flow/evm/address/{address}/internal-transactions": true, + "/flow/evm/address/{address}/token-transfers": true, + "/flow/evm/address/{address}": true, + "/flow/evm/address/{address}/nft": true, + "/flow/evm/search": true, + "/flow/search/preview": true, } // TestAllRoutesInSpec ensures every registered public route has an OpenAPI spec entry. diff --git a/backend/internal/api/server_bootstrap.go b/backend/internal/api/server_bootstrap.go index 5e11ff71f..5de323f0a 100644 --- a/backend/internal/api/server_bootstrap.go +++ b/backend/internal/api/server_bootstrap.go @@ -126,6 +126,7 @@ type Server struct { httpServer *http.Server startBlock uint64 blockscoutURL string // e.g. "https://evm.flowindex.dev" + blockscoutAPIKey string // optional API key for Blockscout rate limit bypass backfillProgress *BackfillProgress priceCache *market.PriceCache webhookHandlers WebhookRouteRegistrar @@ -162,7 +163,8 @@ func NewServer(repo *repository.Repository, client FlowClient, port string, star repo: repo, client: client, startBlock: startBlock, - blockscoutURL: bsURL, + blockscoutURL: bsURL, + blockscoutAPIKey: os.Getenv("BLOCKSCOUT_API_KEY"), priceCache: market.NewPriceCache(), } for _, opt := range opts { diff --git a/backend/internal/api/v1_handlers_evm.go b/backend/internal/api/v1_handlers_evm.go index fd2dedda2..ae8c2dfbc 100644 --- a/backend/internal/api/v1_handlers_evm.go +++ b/backend/internal/api/v1_handlers_evm.go @@ -1,6 +1,9 @@ package api import ( + "encoding/json" + "io" + "log" "net/http" "strings" @@ -35,3 +38,108 @@ func (s *Server) handleFlowGetEVMToken(w http.ResponseWriter, r *http.Request) { } s.proxyBlockscout(w, r, "/api/v2/tokens/0x"+address) } + +func (s *Server) handleFlowGetEVMTransactionInternalTxs(w http.ResponseWriter, r *http.Request) { + hash := strings.ToLower(strings.TrimPrefix(mux.Vars(r)["hash"], "0x")) + s.proxyBlockscout(w, r, "/api/v2/transactions/0x"+hash+"/internal-transactions") +} + +func (s *Server) handleFlowGetEVMTransactionLogs(w http.ResponseWriter, r *http.Request) { + hash := strings.ToLower(strings.TrimPrefix(mux.Vars(r)["hash"], "0x")) + s.proxyBlockscout(w, r, "/api/v2/transactions/0x"+hash+"/logs") +} + +func (s *Server) handleFlowGetEVMTransactionTokenTransfers(w http.ResponseWriter, r *http.Request) { + hash := strings.ToLower(strings.TrimPrefix(mux.Vars(r)["hash"], "0x")) + s.proxyBlockscout(w, r, "/api/v2/transactions/0x"+hash+"/token-transfers") +} + +func (s *Server) handleFlowGetEVMAddressNFTs(w http.ResponseWriter, r *http.Request) { + address := normalizeAddr(mux.Vars(r)["address"]) + s.proxyBlockscout(w, r, "/api/v2/addresses/0x"+address+"/nft") +} + +func (s *Server) handleFlowGetEVMAddress(w http.ResponseWriter, r *http.Request) { + addr := normalizeAddr(mux.Vars(r)["address"]) + + // Build upstream request manually so we can read + enrich the response body. + target := s.blockscoutURL + "/api/v2/addresses/0x" + addr + if q := r.URL.RawQuery; q != "" { + target += "?" + q + } + if s.blockscoutAPIKey != "" { + sep := "?" + if strings.Contains(target, "?") { + sep = "&" + } + target += sep + "apikey=" + s.blockscoutAPIKey + } + + req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, target, nil) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, "failed to build upstream request") + return + } + req.Header.Set("Accept", "application/json") + + resp, err := blockscoutClient.Do(req) + if err != nil { + log.Printf("blockscout proxy error: %v", err) + writeAPIError(w, http.StatusBadGateway, "upstream blockscout unavailable") + return + } + defer resp.Body.Close() + + // Non-200: stream through unchanged. + if resp.StatusCode != http.StatusOK { + w.Header().Set("Content-Type", resp.Header.Get("Content-Type")) + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) + return + } + + // Read full body for potential enrichment. + body, err := io.ReadAll(resp.Body) + if err != nil { + writeAPIError(w, http.StatusBadGateway, "failed to read upstream response") + return + } + + // Attempt COA enrichment; any failure falls through to returning original body. + enriched := false + if coaRow, coaErr := s.repo.GetFlowAddressByCOA(r.Context(), addr); coaErr == nil && coaRow != nil { + var data map[string]interface{} + if jsonErr := json.Unmarshal(body, &data); jsonErr == nil { + data["flow_address"] = "0x" + coaRow.FlowAddress + data["is_coa"] = true + if out, marshalErr := json.Marshal(data); marshalErr == nil { + body = out + enriched = true + } + } + } + _ = enriched // not needed further; kept for clarity + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(body) +} + +func (s *Server) handleFlowGetEVMAddressTransactions(w http.ResponseWriter, r *http.Request) { + addr := normalizeAddr(mux.Vars(r)["address"]) + s.proxyBlockscout(w, r, "/api/v2/addresses/0x"+addr+"/transactions") +} + +func (s *Server) handleFlowGetEVMAddressInternalTxs(w http.ResponseWriter, r *http.Request) { + addr := normalizeAddr(mux.Vars(r)["address"]) + s.proxyBlockscout(w, r, "/api/v2/addresses/0x"+addr+"/internal-transactions") +} + +func (s *Server) handleFlowGetEVMAddressTokenTransfers(w http.ResponseWriter, r *http.Request) { + addr := normalizeAddr(mux.Vars(r)["address"]) + s.proxyBlockscout(w, r, "/api/v2/addresses/0x"+addr+"/token-transfers") +} + +func (s *Server) handleFlowEVMSearch(w http.ResponseWriter, r *http.Request) { + s.proxyBlockscout(w, r, "/api/v2/search") +} diff --git a/backend/internal/api/v1_handlers_search_preview.go b/backend/internal/api/v1_handlers_search_preview.go new file mode 100644 index 000000000..20295a024 --- /dev/null +++ b/backend/internal/api/v1_handlers_search_preview.go @@ -0,0 +1,544 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "regexp" + "strings" + "sync" + "time" + + "flowscan-clone/internal/models" +) + +// --------------------------------------------------------------------------- +// Response types +// --------------------------------------------------------------------------- + +// SearchPreviewTxResponse is the response for type=tx search preview. +type SearchPreviewTxResponse struct { + Query string `json:"query"` + Cadence *SearchPreviewCadence `json:"cadence"` + EVM *SearchPreviewEVM `json:"evm"` + Link *SearchPreviewTxLink `json:"link"` +} + +// SearchPreviewCadence holds Cadence transaction details. +type SearchPreviewCadence struct { + ID string `json:"id"` + Status string `json:"status"` + BlockHeight uint64 `json:"block_height"` + Timestamp string `json:"timestamp"` + Authorizers []string `json:"authorizers"` + IsEVM bool `json:"is_evm"` + ExecutionStatus string `json:"execution_status"` + GasUsed uint64 `json:"gas_used"` +} + +// SearchPreviewEVM holds EVM transaction details from Blockscout. +type SearchPreviewEVM struct { + Hash string `json:"hash"` + Status interface{} `json:"status"` + From interface{} `json:"from"` + To interface{} `json:"to"` + Value interface{} `json:"value"` + GasUsed interface{} `json:"gas_used"` + Method interface{} `json:"method"` + Block interface{} `json:"block"` + TxTypes interface{} `json:"tx_types,omitempty"` +} + +// SearchPreviewTxLink connects Cadence and EVM transactions. +type SearchPreviewTxLink struct { + CadenceTxID *string `json:"cadence_tx_id"` + EVMHash *string `json:"evm_hash"` +} + +// SearchPreviewAddressResponse is the response for type=address search preview. +type SearchPreviewAddressResponse struct { + Query string `json:"query"` + Cadence *SearchPreviewAddrCadence `json:"cadence"` + EVM *SearchPreviewAddrEVM `json:"evm"` + Link *SearchPreviewAddrLink `json:"link"` +} + +// SearchPreviewAddrCadence holds Cadence address details. +type SearchPreviewAddrCadence struct { + Address string `json:"address"` + ContractCount int `json:"contract_count"` + HasActiveKeys bool `json:"has_active_keys"` +} + +// SearchPreviewAddrEVM holds EVM address details from Blockscout. +type SearchPreviewAddrEVM struct { + Address interface{} `json:"address"` + IsContract interface{} `json:"is_contract"` + Name interface{} `json:"name"` + TokenName interface{} `json:"token_name,omitempty"` + TokenSymbol interface{} `json:"token_symbol,omitempty"` + TxCount interface{} `json:"tx_count,omitempty"` + Balance interface{} `json:"balance,omitempty"` +} + +// SearchPreviewAddrLink connects Flow and EVM addresses via COA. +type SearchPreviewAddrLink struct { + FlowAddress *string `json:"flow_address"` + COAAddress *string `json:"coa_address"` +} + +// --------------------------------------------------------------------------- +// Regex helpers +// --------------------------------------------------------------------------- + +var ( + hexPattern = regexp.MustCompile(`^(0x)?[0-9a-fA-F]+$`) +) + +// isFlowAddress returns true for 16 hex-char Flow addresses (with optional 0x prefix). +func isFlowAddress(s string) bool { + clean := strings.TrimPrefix(strings.ToLower(s), "0x") + return len(clean) == 16 && hexPattern.MatchString(clean) +} + +// isEVMAddress returns true for 40 hex-char EVM addresses (with optional 0x prefix). +func isEVMAddress(s string) bool { + clean := strings.TrimPrefix(strings.ToLower(s), "0x") + return len(clean) == 40 && hexPattern.MatchString(clean) +} + +// --------------------------------------------------------------------------- +// Handler +// --------------------------------------------------------------------------- + +func (s *Server) handleSearchPreview(w http.ResponseWriter, r *http.Request) { + if s.repo == nil { + writeAPIError(w, http.StatusInternalServerError, "repository unavailable") + return + } + + q := strings.TrimSpace(r.URL.Query().Get("q")) + if q == "" { + writeAPIError(w, http.StatusBadRequest, "q parameter is required") + return + } + if len(q) > 130 { + writeAPIError(w, http.StatusBadRequest, "query too long") + return + } + + searchType := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type"))) + + switch searchType { + case "tx": + s.handleSearchPreviewTx(w, r, q) + case "address": + s.handleSearchPreviewAddress(w, r, q) + default: + writeAPIError(w, http.StatusBadRequest, "type must be 'tx' or 'address'") + } +} + +// --------------------------------------------------------------------------- +// type=tx +// --------------------------------------------------------------------------- + +func (s *Server) handleSearchPreviewTx(w http.ResponseWriter, r *http.Request, query string) { + ctx := r.Context() + hash := normalizeAddr(query) // strips 0x, lowercases + + var ( + mu sync.Mutex + cadenceTx *models.Transaction + evmParentID string // Cadence tx ID resolved from EVM hash lookup + bsData map[string]interface{} + wg sync.WaitGroup + ) + + // 1) Local DB: direct Cadence tx lookup + wg.Add(1) + go func() { + defer wg.Done() + tx, err := s.repo.GetTransactionByID(ctx, hash) + if err != nil { + log.Printf("search-preview tx cadence lookup: %v", err) + return + } + mu.Lock() + cadenceTx = tx + mu.Unlock() + }() + + // 2) Local DB: EVM hash -> parent Cadence tx + wg.Add(1) + go func() { + defer wg.Done() + parentID, err := s.repo.LookupCadenceTxByEVMHash(ctx, hash) + if err != nil { + log.Printf("search-preview evm->cadence lookup: %v", err) + return + } + mu.Lock() + evmParentID = parentID + mu.Unlock() + }() + + // 3) Blockscout: EVM tx details + wg.Add(1) + go func() { + defer wg.Done() + if s.blockscoutURL == "" { + return + } + bsCtx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + + url := fmt.Sprintf("%s/api/v2/transactions/0x%s", s.blockscoutURL, hash) + if s.blockscoutAPIKey != "" { + url += "?apikey=" + s.blockscoutAPIKey + } + req, err := http.NewRequestWithContext(bsCtx, http.MethodGet, url, nil) + if err != nil { + return + } + req.Header.Set("Accept", "application/json") + + resp, err := blockscoutClient.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) + if err != nil { + return + } + + var data map[string]interface{} + if err := json.Unmarshal(body, &data); err != nil { + return + } + + mu.Lock() + bsData = data + mu.Unlock() + }() + + wg.Wait() + + // Follow-up: if EVM hash resolved to a Cadence parent but we didn't find it directly + if evmParentID != "" && cadenceTx == nil { + tx, err := s.repo.GetTransactionByID(ctx, evmParentID) + if err != nil { + log.Printf("search-preview follow-up cadence lookup: %v", err) + } else { + cadenceTx = tx + } + } + + // Build response + resp := SearchPreviewTxResponse{Query: query} + + if cadenceTx != nil { + resp.Cadence = &SearchPreviewCadence{ + ID: cadenceTx.ID, + Status: cadenceTx.Status, + BlockHeight: cadenceTx.BlockHeight, + Timestamp: cadenceTx.Timestamp.UTC().Format(time.RFC3339), + Authorizers: cadenceTx.Authorizers, + IsEVM: cadenceTx.IsEVM, + ExecutionStatus: cadenceTx.ExecutionStatus, + GasUsed: cadenceTx.GasUsed, + } + + // If cadence tx is EVM, try to find the EVM hash + if cadenceTx.IsEVM { + evmHash := cadenceTx.EVMHash + if evmHash == "" { + // Lookup from evm_tx_hashes table + h, err := s.repo.LookupEVMHashByCadenceTx(ctx, cadenceTx.ID) + if err != nil { + log.Printf("search-preview evm hash lookup: %v", err) + } else { + evmHash = h + } + } + if evmHash != "" { + prefixed := "0x" + strings.TrimPrefix(evmHash, "0x") + resp.Link = &SearchPreviewTxLink{ + CadenceTxID: strPtr(cadenceTx.ID), + EVMHash: &prefixed, + } + } + } + } + + if bsData != nil { + resp.EVM = &SearchPreviewEVM{ + Hash: safeString(bsData, "hash"), + Status: bsData["status"], + From: extractNestedField(bsData, "from", "hash"), + To: extractNestedField(bsData, "to", "hash"), + Value: bsData["value"], + GasUsed: bsData["gas_used"], + Method: bsData["method"], + Block: bsData["block"], + TxTypes: bsData["tx_types"], + } + } + + // Build link from EVM parent resolution (if we found an EVM->Cadence mapping) + if resp.Link == nil && evmParentID != "" { + prefixed := "0x" + hash + resp.Link = &SearchPreviewTxLink{ + CadenceTxID: &evmParentID, + EVMHash: &prefixed, + } + } + + writeAPIResponse(w, resp, nil, nil) +} + +// --------------------------------------------------------------------------- +// type=address +// --------------------------------------------------------------------------- + +func (s *Server) handleSearchPreviewAddress(w http.ResponseWriter, r *http.Request, query string) { + ctx := r.Context() + addr := normalizeAddr(query) + + isFlow := isFlowAddress(addr) + isEVM := isEVMAddress(addr) + + if !isFlow && !isEVM { + writeAPIError(w, http.StatusBadRequest, "invalid address format: expected 16 hex (Flow) or 40 hex (EVM)") + return + } + + var ( + mu sync.Mutex + coaLink *SearchPreviewAddrLink + cadenceData *SearchPreviewAddrCadence + evmData *SearchPreviewAddrEVM + wg sync.WaitGroup + ) + + // 1) COA link lookup + wg.Add(1) + go func() { + defer wg.Done() + var link SearchPreviewAddrLink + if isFlow { + coa, err := s.repo.GetCOAByFlowAddress(ctx, addr) + if err != nil { + log.Printf("search-preview coa-by-flow lookup: %v", err) + return + } + if coa != nil { + link.FlowAddress = &coa.FlowAddress + link.COAAddress = &coa.COAAddress + } + } else { + coa, err := s.repo.GetFlowAddressByCOA(ctx, addr) + if err != nil { + log.Printf("search-preview flow-by-coa lookup: %v", err) + return + } + if coa != nil { + link.FlowAddress = &coa.FlowAddress + link.COAAddress = &coa.COAAddress + } + } + if link.FlowAddress != nil || link.COAAddress != nil { + mu.Lock() + coaLink = &link + mu.Unlock() + } + }() + + // 2) Cadence data (if Flow address) + if isFlow { + wg.Add(1) + go func() { + defer wg.Done() + var cd SearchPreviewAddrCadence + cd.Address = addr + + var wg2 sync.WaitGroup + wg2.Add(2) + go func() { + defer wg2.Done() + cnt, err := s.repo.GetAddressContractCount(ctx, addr) + if err != nil { + log.Printf("search-preview contract count: %v", err) + return + } + cd.ContractCount = cnt + }() + go func() { + defer wg2.Done() + has, err := s.repo.GetAddressHasActiveKeys(ctx, addr) + if err != nil { + log.Printf("search-preview active keys: %v", err) + return + } + cd.HasActiveKeys = has + }() + wg2.Wait() + + mu.Lock() + cadenceData = &cd + mu.Unlock() + }() + } + + // 3) EVM data from Blockscout (if EVM address) + if isEVM { + wg.Add(1) + go func() { + defer wg.Done() + evmInfo := fetchBlockscoutAddress(ctx, s.blockscoutURL, s.blockscoutAPIKey, addr) + if evmInfo != nil { + mu.Lock() + evmData = evmInfo + mu.Unlock() + } + }() + } + + wg.Wait() + + // Follow-up: if COA link resolved, fetch the other side's data + if coaLink != nil { + if isFlow && coaLink.COAAddress != nil && evmData == nil { + // We have a Flow address with a COA link, fetch EVM data for the COA + evmInfo := fetchBlockscoutAddress(ctx, s.blockscoutURL, s.blockscoutAPIKey, *coaLink.COAAddress) + if evmInfo != nil { + evmData = evmInfo + } + } + if isEVM && coaLink.FlowAddress != nil && cadenceData == nil { + // We have an EVM address with a COA link, fetch Cadence data for the Flow address + flowAddr := *coaLink.FlowAddress + var cd SearchPreviewAddrCadence + cd.Address = flowAddr + + var wg2 sync.WaitGroup + wg2.Add(2) + go func() { + defer wg2.Done() + cnt, err := s.repo.GetAddressContractCount(ctx, flowAddr) + if err == nil { + cd.ContractCount = cnt + } + }() + go func() { + defer wg2.Done() + has, err := s.repo.GetAddressHasActiveKeys(ctx, flowAddr) + if err == nil { + cd.HasActiveKeys = has + } + }() + wg2.Wait() + cadenceData = &cd + } + } + + resp := SearchPreviewAddressResponse{ + Query: query, + Cadence: cadenceData, + EVM: evmData, + Link: coaLink, + } + + writeAPIResponse(w, resp, nil, nil) +} + +// --------------------------------------------------------------------------- +// Blockscout helpers +// --------------------------------------------------------------------------- + +func fetchBlockscoutAddress(ctx context.Context, blockscoutURL, apiKey, addr string) *SearchPreviewAddrEVM { + if blockscoutURL == "" { + return nil + } + + bsCtx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + + url := fmt.Sprintf("%s/api/v2/addresses/0x%s", blockscoutURL, strings.TrimPrefix(addr, "0x")) + if apiKey != "" { + url += "?apikey=" + apiKey + } + req, err := http.NewRequestWithContext(bsCtx, http.MethodGet, url, nil) + if err != nil { + return nil + } + req.Header.Set("Accept", "application/json") + + resp, err := blockscoutClient.Do(req) + if err != nil { + return nil + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) + if err != nil { + return nil + } + + var data map[string]interface{} + if err := json.Unmarshal(body, &data); err != nil { + return nil + } + + return &SearchPreviewAddrEVM{ + Address: extractNestedField(data, "hash", ""), + IsContract: data["is_contract"], + Name: data["name"], + TokenName: extractNestedField(data, "token", "name"), + TokenSymbol: extractNestedField(data, "token", "symbol"), + TxCount: data["transactions_count"], + Balance: data["coin_balance"], + } +} + +// --------------------------------------------------------------------------- +// JSON helpers +// --------------------------------------------------------------------------- + +func safeString(m map[string]interface{}, key string) string { + if v, ok := m[key]; ok { + if s, ok := v.(string); ok { + return s + } + } + return "" +} + +func extractNestedField(m map[string]interface{}, key1, key2 string) interface{} { + if key2 == "" { + return m[key1] + } + if nested, ok := m[key1]; ok { + if nm, ok := nested.(map[string]interface{}); ok { + return nm[key2] + } + } + return nil +} + +func strPtr(s string) *string { + return &s +} diff --git a/backend/internal/repository/query_search_preview.go b/backend/internal/repository/query_search_preview.go new file mode 100644 index 000000000..0842e5d73 --- /dev/null +++ b/backend/internal/repository/query_search_preview.go @@ -0,0 +1,67 @@ +package repository + +import ( + "context" + + "github.com/jackc/pgx/v5" +) + +// LookupCadenceTxByEVMHash finds the parent Cadence transaction ID for a given EVM hash. +// Returns the hex-encoded transaction_id or empty string if not found. +func (r *Repository) LookupCadenceTxByEVMHash(ctx context.Context, evmHash string) (string, error) { + var txID string + err := r.db.QueryRow(ctx, + `SELECT encode(transaction_id, 'hex') FROM app.evm_tx_hashes WHERE evm_hash = $1 LIMIT 1`, + hexToBytes(evmHash), + ).Scan(&txID) + if err == pgx.ErrNoRows { + return "", nil + } + if err != nil { + return "", err + } + return txID, nil +} + +// LookupEVMHashByCadenceTx finds the EVM hash(es) for a given Cadence transaction ID. +// Returns the first hex-encoded evm_hash or empty string if not found. +func (r *Repository) LookupEVMHashByCadenceTx(ctx context.Context, cadenceTxID string) (string, error) { + var evmHash string + err := r.db.QueryRow(ctx, + `SELECT encode(evm_hash, 'hex') FROM app.evm_tx_hashes WHERE transaction_id = $1 LIMIT 1`, + hexToBytes(cadenceTxID), + ).Scan(&evmHash) + if err == pgx.ErrNoRows { + return "", nil + } + if err != nil { + return "", err + } + return evmHash, nil +} + +// GetAddressContractCount returns the number of smart contracts deployed at an address. +func (r *Repository) GetAddressContractCount(ctx context.Context, address string) (int, error) { + var count int + err := r.db.QueryRow(ctx, + `SELECT COUNT(*) FROM app.smart_contracts WHERE address = $1`, + hexToBytes(address), + ).Scan(&count) + if err != nil { + return 0, err + } + return count, nil +} + +// GetAddressHasActiveKeys checks if the address has at least one non-revoked key. +func (r *Repository) GetAddressHasActiveKeys(ctx context.Context, address string) (bool, error) { + var exists bool + err := r.db.QueryRow(ctx, + `SELECT EXISTS(SELECT 1 FROM app.account_keys WHERE address = $1 AND revoked = false)`, + hexToBytes(address), + ).Scan(&exists) + if err != nil { + return false, err + } + return exists, nil +} diff --git a/bun.lock b/bun.lock index 24c09cbcd..0e6c9a8b9 100644 --- a/bun.lock +++ b/bun.lock @@ -362,6 +362,7 @@ "@outblock/flowtoken": "workspace:*", "@outblock/wallet-core-lite": "^0.1.0", "@supabase/supabase-js": "^2.98.0", + "@tanstack/react-query": "^5.90.21", "ai": "^6.0.101", "axios": "^1.13.4", "boring-avatars": "^2.0.4", @@ -382,10 +383,13 @@ "recharts": "^3.7.0", "remark-gfm": "^4.0.1", "shiki": "^4.0.1", + "solc": "^0.8.34", "tailwind-merge": "^2.2.0", + "viem": "^2.47.4", "vscode-jsonrpc": "8.2.1", "vscode-oniguruma": "^2.0.1", "vscode-textmate": "^9.3.2", + "wagmi": "^3.5.0", }, "devDependencies": { "@playwright/test": "^1.58.2", @@ -399,6 +403,7 @@ "tailwindcss": "^3.4.19", "typescript": "^5.9.3", "vite": "^7.3.1", + "vite-plugin-node-polyfills": "^0.25.0", "vitest": "^4.0.18", }, }, @@ -1277,6 +1282,10 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + "@rollup/plugin-inject": ["@rollup/plugin-inject@5.0.5", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "estree-walker": "^2.0.2", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], @@ -1449,6 +1458,10 @@ "@tanstack/history": ["@tanstack/history@1.161.4", "", {}, "sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="], + "@tanstack/react-router": ["@tanstack/react-router@1.166.2", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.166.2", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-pKhUtrvVLlhjWhsHkJSuIzh1J4LcP+8ErbIqRLORX9Js8dUFMKoT0+8oFpi+P8QRpuhm/7rzjYiWfcyTsqQZtA=="], "@tanstack/react-start": ["@tanstack/react-start@1.166.2", "", { "dependencies": { "@tanstack/react-router": "1.166.2", "@tanstack/react-start-client": "1.166.2", "@tanstack/react-start-server": "1.166.2", "@tanstack/router-utils": "^1.161.4", "@tanstack/start-client-core": "1.166.2", "@tanstack/start-plugin-core": "1.166.2", "@tanstack/start-server-core": "1.166.2", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-ryeDIITTVmGmOkTrdg4dL4Sl+LXK5w8BZtzLtsr3YxNhQaPwxqX4r69iuBt5M8jyXEsWwbJJdToN3xLr7CO5XQ=="], @@ -1701,6 +1714,10 @@ "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], + "@wagmi/connectors": ["@wagmi/connectors@7.2.1", "", { "peerDependencies": { "@base-org/account": "^2.5.1", "@coinbase/wallet-sdk": "^4.3.6", "@metamask/sdk": "~0.33.1", "@safe-global/safe-apps-provider": "~0.18.6", "@safe-global/safe-apps-sdk": "^9.1.0", "@wagmi/core": "3.4.0", "@walletconnect/ethereum-provider": "^2.21.1", "porto": "~0.2.35", "typescript": ">=5.7.3", "viem": "2.x" }, "optionalPeers": ["@base-org/account", "@coinbase/wallet-sdk", "@metamask/sdk", "@safe-global/safe-apps-provider", "@safe-global/safe-apps-sdk", "@walletconnect/ethereum-provider", "porto", "typescript"] }, "sha512-/tyDepUMDM8eNzNX3ofjqHNRFZ6XcZ3u0+cQp5x0/LHCpMA8tRh7A1/e7dTrYiIJeL7iLgHzfHUXCsU02OKMLQ=="], + + "@wagmi/core": ["@wagmi/core@3.4.0", "", { "dependencies": { "eventemitter3": "5.0.1", "mipd": "0.0.7", "zustand": "5.0.0" }, "peerDependencies": { "@tanstack/query-core": ">=5.0.0", "ox": ">=0.11.1", "typescript": ">=5.7.3", "viem": "2.x" }, "optionalPeers": ["@tanstack/query-core", "ox", "typescript"] }, "sha512-EU5gDsUp5t7+cuLv12/L8hfyWfCIKsBNiiBqpOqxZJxvAcAiQk4xFe2jMgaQPqApc3Omvxrk032M8AQ4N0cQeg=="], + "@walletconnect/core": ["@walletconnect/core@2.23.7", "", { "dependencies": { "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/jsonrpc-ws-connection": "1.0.16", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "3.0.2", "@walletconnect/relay-api": "1.0.11", "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.23.7", "@walletconnect/utils": "2.23.7", "@walletconnect/window-getters": "1.0.1", "es-toolkit": "1.44.0", "events": "3.3.0", "uint8arrays": "3.1.1" } }, "sha512-yTyymn9mFaDZkUfLfZ3E9VyaSDPeHAXlrPxQRmNx2zFsEt/25GmTU2A848aomimLxZnAG2jNLhxbJ8I0gyNV+w=="], "@walletconnect/environment": ["@walletconnect/environment@1.0.1", "", { "dependencies": { "tslib": "1.14.1" } }, "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg=="], @@ -1855,6 +1872,10 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + "asn1.js": ["asn1.js@4.10.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw=="], + + "assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], @@ -1905,6 +1926,20 @@ "browser-headers": ["browser-headers@0.4.1", "", {}, "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg=="], + "browser-resolve": ["browser-resolve@2.0.0", "", { "dependencies": { "resolve": "^1.17.0" } }, "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ=="], + + "browserify-aes": ["browserify-aes@1.2.0", "", { "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.3", "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA=="], + + "browserify-cipher": ["browserify-cipher@1.0.1", "", { "dependencies": { "browserify-aes": "^1.0.4", "browserify-des": "^1.0.0", "evp_bytestokey": "^1.0.0" } }, "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w=="], + + "browserify-des": ["browserify-des@1.0.2", "", { "dependencies": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A=="], + + "browserify-rsa": ["browserify-rsa@4.1.1", "", { "dependencies": { "bn.js": "^5.2.1", "randombytes": "^2.1.0", "safe-buffer": "^5.2.1" } }, "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ=="], + + "browserify-sign": ["browserify-sign@4.2.5", "", { "dependencies": { "bn.js": "^5.2.2", "browserify-rsa": "^4.1.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "elliptic": "^6.6.1", "inherits": "^2.0.4", "parse-asn1": "^5.1.9", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1" } }, "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw=="], + + "browserify-zlib": ["browserify-zlib@0.2.0", "", { "dependencies": { "pako": "~1.0.5" } }, "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA=="], + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], @@ -1913,6 +1948,10 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "buffer-xor": ["buffer-xor@1.0.3", "", {}, "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ=="], + + "builtin-status-codes": ["builtin-status-codes@3.0.0", "", {}, "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], @@ -1973,6 +2012,8 @@ "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], + "cipher-base": ["cipher-base@1.0.7", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.2" } }, "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA=="], + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], @@ -2011,7 +2052,9 @@ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "command-exists": ["command-exists@1.2.9", "", {}, "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="], + + "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], @@ -2023,6 +2066,10 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "console-browserify": ["console-browserify@1.2.0", "", {}, "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA=="], + + "constants-browserify": ["constants-browserify@1.0.0", "", {}, "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ=="], + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -2043,6 +2090,14 @@ "cosmiconfig": ["cosmiconfig@9.0.1", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="], + "create-ecdh": ["create-ecdh@4.0.4", "", { "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" } }, "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A=="], + + "create-hash": ["create-hash@1.2.0", "", { "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "md5.js": "^1.3.4", "ripemd160": "^2.0.1", "sha.js": "^2.4.0" } }, "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg=="], + + "create-hmac": ["create-hmac@1.1.7", "", { "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg=="], + + "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], + "cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="], "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], @@ -2051,6 +2106,8 @@ "crossws": ["crossws@0.4.4", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg=="], + "crypto-browserify": ["crypto-browserify@3.12.1", "", { "dependencies": { "browserify-cipher": "^1.0.1", "browserify-sign": "^4.2.3", "create-ecdh": "^4.0.4", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "diffie-hellman": "^5.0.3", "hash-base": "~3.0.4", "inherits": "^2.0.4", "pbkdf2": "^3.1.2", "public-encrypt": "^4.0.3", "randombytes": "^2.1.0", "randomfill": "^1.0.4" } }, "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ=="], + "css-background-parser": ["css-background-parser@0.1.0", "", {}, "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA=="], "css-box-shadow": ["css-box-shadow@1.0.0-3", "", {}, "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg=="], @@ -2201,6 +2258,8 @@ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="], + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "detect-browser": ["detect-browser@5.3.0", "", {}, "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w=="], @@ -2215,6 +2274,8 @@ "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "diffie-hellman": ["diffie-hellman@5.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" } }, "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg=="], + "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], @@ -2223,6 +2284,8 @@ "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + "domain-browser": ["domain-browser@4.22.0", "", {}, "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw=="], + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], @@ -2353,6 +2416,8 @@ "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "evp_bytestokey": ["evp_bytestokey@1.0.3", "", { "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" } }, "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA=="], + "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], @@ -2517,6 +2582,8 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hash-base": ["hash-base@3.0.5", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1" } }, "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg=="], + "hash.js": ["hash.js@1.1.7", "", { "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA=="], "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], @@ -2577,6 +2644,8 @@ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "https-browserify": ["https-browserify@1.0.0", "", {}, "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], @@ -2623,6 +2692,8 @@ "is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="], + "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], @@ -2667,6 +2738,8 @@ "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + "is-nan": ["is-nan@1.3.2", "", { "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" } }, "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w=="], + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], @@ -2713,6 +2786,8 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "isomorphic-timers-promises": ["isomorphic-timers-promises@1.0.1", "", {}, "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ=="], + "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], @@ -2731,6 +2806,8 @@ "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-sha3": ["js-sha3@0.8.0", "", {}, "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], @@ -2861,6 +2938,8 @@ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "md5.js": ["md5.js@1.3.5", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], @@ -2901,6 +2980,8 @@ "memfs": ["memfs@3.4.3", "", { "dependencies": { "fs-monkey": "1.0.3" } }, "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg=="], + "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], @@ -2977,6 +3058,8 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "miller-rabin": ["miller-rabin@4.0.1", "", { "dependencies": { "bn.js": "^4.0.0", "brorand": "^1.0.1" }, "bin": { "miller-rabin": "bin/miller-rabin" } }, "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], @@ -2993,6 +3076,8 @@ "minimist": ["minimist@1.2.6", "", {}, "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="], + "mipd": ["mipd@0.0.7", "", { "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg=="], + "mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="], "monaco-editor": ["monaco-editor@0.50.0", "", {}, "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA=="], @@ -3041,6 +3126,8 @@ "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + "node-stdlib-browser": ["node-stdlib-browser@1.3.1", "", { "dependencies": { "assert": "^2.0.0", "browser-resolve": "^2.0.0", "browserify-zlib": "^0.2.0", "buffer": "^5.7.1", "console-browserify": "^1.1.0", "constants-browserify": "^1.0.0", "create-require": "^1.1.1", "crypto-browserify": "^3.12.1", "domain-browser": "4.22.0", "events": "^3.0.0", "https-browserify": "^1.0.0", "isomorphic-timers-promises": "^1.0.1", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "pkg-dir": "^5.0.0", "process": "^0.11.10", "punycode": "^1.4.1", "querystring-es3": "^0.2.1", "readable-stream": "^3.6.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "string_decoder": "^1.0.0", "timers-browserify": "^2.0.4", "tty-browserify": "0.0.1", "url": "^0.11.4", "util": "^0.12.4", "vm-browserify": "^1.0.1" } }, "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], @@ -3059,6 +3146,8 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="], + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], "object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="], @@ -3097,6 +3186,10 @@ "ora": ["ora@5.3.0", "", { "dependencies": { "bl": "^4.0.3", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "log-symbols": "^4.0.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g=="], + "os-browserify": ["os-browserify@0.3.0", "", {}, "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], @@ -3119,6 +3212,8 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse-asn1": ["parse-asn1@5.1.9", "", { "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", "evp_bytestokey": "^1.0.3", "pbkdf2": "^3.1.5", "safe-buffer": "^5.2.1" } }, "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg=="], + "parse-css-color": ["parse-css-color@0.2.1", "", { "dependencies": { "color-name": "^1.1.4", "hex-rgb": "^4.1.0" } }, "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg=="], "parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="], @@ -3151,6 +3246,8 @@ "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + "pbkdf2": ["pbkdf2@3.1.5", "", { "dependencies": { "create-hash": "^1.2.0", "create-hmac": "^1.1.7", "ripemd160": "^2.0.3", "safe-buffer": "^5.2.1", "sha.js": "^2.4.12", "to-buffer": "^1.2.1" } }, "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ=="], + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], @@ -3171,6 +3268,8 @@ "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "pkg-dir": ["pkg-dir@5.0.0", "", { "dependencies": { "find-up": "^5.0.0" } }, "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA=="], + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], @@ -3229,6 +3328,8 @@ "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], @@ -3247,9 +3348,11 @@ "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "public-encrypt": ["public-encrypt@4.0.3", "", { "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" } }, "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "punycode": ["punycode@1.4.1", "", {}, "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="], "qrcode": ["qrcode@1.5.3", "", { "dependencies": { "dijkstrajs": "^1.0.1", "encode-utf8": "^1.0.3", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg=="], @@ -3257,6 +3360,8 @@ "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + "querystring-es3": ["querystring-es3@0.2.1", "", {}, "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], @@ -3265,6 +3370,10 @@ "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + + "randomfill": ["randomfill@1.0.4", "", { "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" } }, "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], @@ -3393,6 +3502,8 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "ripemd160": ["ripemd160@2.0.3", "", { "dependencies": { "hash-base": "^3.1.2", "inherits": "^2.0.4" } }, "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA=="], + "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], @@ -3451,6 +3562,8 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + "sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" } }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="], + "sha3": ["sha3@2.1.4", "", { "dependencies": { "buffer": "6.0.3" } }, "sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg=="], "shadcn": ["shadcn@3.8.5", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA=="], @@ -3485,6 +3598,8 @@ "slow-redact": ["slow-redact@0.3.2", "", {}, "sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw=="], + "solc": ["solc@0.8.34", "", { "dependencies": { "command-exists": "^1.2.8", "commander": "^8.1.0", "follow-redirects": "^1.12.1", "js-sha3": "0.8.0", "memorystream": "^0.3.1", "semver": "^5.5.0", "tmp": "0.0.33" }, "bin": { "solcjs": "solc.js" } }, "sha512-qf8HajA1sHhXRV0hMSDXLjVbc4v3Q+SQbL9zok+1WmgVj7Z4oMjMHxaysCzfGtFVqjZdfDDJWyZI+tcx5bO7Dw=="], + "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], @@ -3515,6 +3630,10 @@ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], + + "stream-http": ["stream-http@3.2.0", "", { "dependencies": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.4", "readable-stream": "^3.6.0", "xtend": "^4.0.2" } }, "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A=="], + "streamdown": ["streamdown@2.4.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.2.2", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-fRk4HEYNznRLmxoVeT8wsGBwHF6/Yrdey6k+ZrE1Qtp4NyKwm7G/6e2Iw8penY4yLx31TlAHWT5Bsg1weZ9FZg=="], "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], @@ -3535,7 +3654,7 @@ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], - "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], @@ -3601,6 +3720,8 @@ "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], + "timers-browserify": ["timers-browserify@2.0.12", "", { "dependencies": { "setimmediate": "^1.0.4" } }, "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ=="], + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], @@ -3625,6 +3746,8 @@ "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -3659,6 +3782,8 @@ "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "tty-browserify": ["tty-browserify@0.0.1", "", {}, "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="], + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -3729,6 +3854,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "url": ["url@0.11.4", "", { "dependencies": { "punycode": "^1.4.1", "qs": "^6.12.3" } }, "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], @@ -3737,6 +3864,8 @@ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], @@ -3763,12 +3892,16 @@ "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + "vite-plugin-node-polyfills": ["vite-plugin-node-polyfills@0.25.0", "", { "dependencies": { "@rollup/plugin-inject": "^5.0.5", "node-stdlib-browser": "^1.3.1" }, "peerDependencies": { "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg=="], + "vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="], "vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="], "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + "vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="], + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="], "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], @@ -3785,6 +3918,8 @@ "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "wagmi": ["wagmi@3.5.0", "", { "dependencies": { "@wagmi/connectors": "7.2.1", "@wagmi/core": "3.4.0", "use-sync-external-store": "1.4.0" }, "peerDependencies": { "@tanstack/react-query": ">=5.0.0", "react": ">=18", "typescript": ">=5.7.3", "viem": "2.x" }, "optionalPeers": ["typescript"] }, "sha512-39uiY6Vkc28NiAHrxJzVTodoRgSVGG97EewwUxRf+jcFMTe8toAnaM8pJZA3Zw/6snMg4tSgWLJAtMnOacLe7w=="], + "watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="], "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], @@ -3899,6 +4034,8 @@ "@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "@hey-api/openapi-ts/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "@hey-api/shared/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "@hey-api/shared/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -4105,6 +4242,10 @@ "@remotion/zod-types/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@rollup/plugin-inject/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@rspack/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], "@shuding/opentype.js/fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="], @@ -4159,6 +4300,10 @@ "@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "@wagmi/core/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "@wagmi/core/zustand": ["zustand@5.0.0", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ=="], + "@walletconnect/core/es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], "@walletconnect/environment/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -4207,6 +4352,10 @@ "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "browserify-rsa/bn.js": ["bn.js@5.2.3", "", {}, "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w=="], + + "browserify-sign/bn.js": ["bn.js@5.2.3", "", {}, "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w=="], + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "c12/dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], @@ -4221,6 +4370,8 @@ "cadence-runner/tailwind-merge": ["tailwind-merge@2.6.1", "", {}, "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ=="], + "cadence-runner/viem": ["viem@2.47.4", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.5", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-h0Wp/SYmJO/HB4B/em1OZ3W1LaKrmr7jzaN7talSlZpo0LCn0V6rZ5g923j6sf4VUSrqp/gUuWuHFc7UcoIp8A=="], + "cadence-runner/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "cli-truncate/string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], @@ -4315,10 +4466,10 @@ "js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - "linebreak/base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], + "lint-staged/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "lint-staged/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "listr2/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], @@ -4333,6 +4484,8 @@ "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "md5.js/hash-base": ["hash-base@3.1.2", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.1" } }, "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "mdast-util-mdx-jsx/parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], @@ -4351,6 +4504,10 @@ "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "node-stdlib-browser/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "node-stdlib-browser/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], "nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], @@ -4407,10 +4564,14 @@ "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "refractor/prismjs": ["prismjs@1.27.0", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="], + "ripemd160/hash-base": ["hash-base@3.1.2", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.1" } }, "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg=="], + "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "safe-array-concat/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], @@ -4419,6 +4580,8 @@ "schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "shadcn/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "shadcn/execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], "shadcn/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], @@ -4439,11 +4602,17 @@ "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "solc/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "solc/tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "streamdown/marked": ["marked@17.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ=="], + "stream-browserify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "stream-http/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "streamdown/marked": ["marked@17.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ=="], "stringify-entities/character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], @@ -4459,10 +4628,16 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "to-buffer/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "tr46/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "tsconfig-paths/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + "uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "valtio/use-sync-external-store": ["use-sync-external-store@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="], "videos/@types/three": ["@types/three@0.167.2", "", { "dependencies": { "@tweenjs/tween.js": "~23.1.2", "@types/stats.js": "*", "@types/webxr": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.18.1" } }, "sha512-onxnIUNYpXcZJ5DTiIsxfnr4F9kAWkkxAUWx5yqzz/u0a4IygCLCjMuOl2DEeCxyJdJ2nOJZvKpu48sBMqfmkQ=="], @@ -4489,6 +4664,8 @@ "vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + "wagmi/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], + "web/react-syntax-highlighter": ["react-syntax-highlighter@16.1.1", "", { "dependencies": { "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^5.0.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA=="], "web/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], @@ -4719,14 +4896,22 @@ "@walletconnect/utils/ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "cadence-runner/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], "cadence-runner/@vitejs/plugin-react/react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + "cadence-runner/viem/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], + + "cadence-runner/viem/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "cadence-runner/viem/@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], + + "cadence-runner/viem/@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "cadence-runner/viem/ox": ["ox@0.14.5", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-HgmHmBveYO40H/R3K6TMrwYtHsx/u6TAB+GpZlgJCoW0Sq5Ttpjih0IZZiwGQw7T6vdW4IAyobYrE2mdAvyF8Q=="], + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "cmdk/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -4857,8 +5042,6 @@ "tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "videos/@types/three/meshoptimizer": ["meshoptimizer@0.18.1", "", {}, "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw=="], "viem/@scure/bip32/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], @@ -4969,6 +5152,14 @@ "@walletconnect/utils/ox/@scure/bip32/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + "cadence-runner/viem/@scure/bip32/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "cadence-runner/viem/@scure/bip32/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "cadence-runner/viem/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "cadence-runner/viem/ox/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "cross-fetch/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], diff --git a/docs/superpowers/plans/2026-03-15-evm-account-tx-detail.md b/docs/superpowers/plans/2026-03-15-evm-account-tx-detail.md new file mode 100644 index 000000000..53d979a60 --- /dev/null +++ b/docs/superpowers/plans/2026-03-15-evm-account-tx-detail.md @@ -0,0 +1,2238 @@ +# EVM Account & Transaction Detail Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add native EVM account detail, transaction detail, and activity pages to FlowIndex — reusing Blockscout API via proxy. + +**Architecture:** Go backend adds ~10 proxy routes forwarding to Blockscout `/api/v2/`. Frontend detects address/hash type in existing route loaders and renders EVM-specific components. No new DB tables. COA enrichment via existing `coa_accounts` table. + +**Tech Stack:** Go (Gorilla Mux), React 19, TanStack Start/Router, TypeScript, TailwindCSS, Shadcn/UI + +**Spec:** `docs/superpowers/specs/2026-03-15-evm-account-tx-detail-design.md` + +### Import Conventions (reference for all tasks) + +These are the correct imports used throughout the plan. If any task code differs from these, follow these: + +```typescript +// CopyButton — NOT from '../ui/CopyButton' +import { CopyButton } from '@/components/animate-ui/components/buttons/copy'; + +// Relative time — NO TimeAgo component exists. Use the function: +import { formatRelativeTime } from '@/lib/time'; +// Usage: {formatRelativeTime(tx.timestamp)} + +// Address display — use existing AddressLink for consistent look: +import { AddressLink } from '@/components/AddressLink'; +// Usage: + +// API base URL +import { resolveApiBaseUrl } from '@/api'; + +// EVM API client (created in Task 6) +import { getEVMAddress, getEVMAddressTransactions, ... } from '@/api/evm'; + +// Blockscout types (created in Task 5) +import type { BSAddress, BSTransaction, ... } from '@/types/blockscout'; +``` + +--- + +## Chunk 1: Backend Proxy Routes + +### Task 1: Add EVM Address Proxy Endpoints + +**Files:** +- Modify: `backend/internal/api/v1_handlers_evm.go` +- Modify: `backend/internal/api/routes_registration.go` + +- [ ] **Step 1: Add address detail handler** + +In `v1_handlers_evm.go`, add after the existing handlers: + +```go +func (s *Server) handleFlowGetEVMAddress(w http.ResponseWriter, r *http.Request) { + addr := normalizeAddr(mux.Vars(r)["address"]) + s.proxyBlockscout(w, r, "/api/v2/addresses/0x"+addr) +} +``` + +- [ ] **Step 2: Add address transactions handler** + +```go +func (s *Server) handleFlowGetEVMAddressTransactions(w http.ResponseWriter, r *http.Request) { + addr := normalizeAddr(mux.Vars(r)["address"]) + s.proxyBlockscout(w, r, "/api/v2/addresses/0x"+addr+"/transactions") +} +``` + +- [ ] **Step 3: Add address internal transactions handler** + +```go +func (s *Server) handleFlowGetEVMAddressInternalTxs(w http.ResponseWriter, r *http.Request) { + addr := normalizeAddr(mux.Vars(r)["address"]) + s.proxyBlockscout(w, r, "/api/v2/addresses/0x"+addr+"/internal-transactions") +} +``` + +- [ ] **Step 4: Add address token transfers handler** + +```go +func (s *Server) handleFlowGetEVMAddressTokenTransfers(w http.ResponseWriter, r *http.Request) { + addr := normalizeAddr(mux.Vars(r)["address"]) + s.proxyBlockscout(w, r, "/api/v2/addresses/0x"+addr+"/token-transfers") +} +``` + +- [ ] **Step 5: Register address routes** + +In `routes_registration.go`, after line 184 (existing EVM routes), add: + +```go +r.HandleFunc("/flow/evm/address/{address}", s.handleFlowGetEVMAddress).Methods("GET", "OPTIONS") +r.HandleFunc("/flow/evm/address/{address}/transactions", s.handleFlowGetEVMAddressTransactions).Methods("GET", "OPTIONS") +r.HandleFunc("/flow/evm/address/{address}/internal-transactions", s.handleFlowGetEVMAddressInternalTxs).Methods("GET", "OPTIONS") +r.HandleFunc("/flow/evm/address/{address}/token-transfers", s.handleFlowGetEVMAddressTokenTransfers).Methods("GET", "OPTIONS") +``` + +Note: existing `/flow/evm/address/{address}/token` route already handles token balances. + +- [ ] **Step 6: Verify build** + +Run: `cd backend && go build ./...` +Expected: No errors + +- [ ] **Step 7: Commit** + +```bash +git add backend/internal/api/v1_handlers_evm.go backend/internal/api/routes_registration.go +git commit -m "feat(api): add EVM address proxy endpoints for Blockscout" +``` + +### Task 2: Add EVM Transaction Sub-resource Proxy Endpoints + +**Files:** +- Modify: `backend/internal/api/v1_handlers_evm.go` +- Modify: `backend/internal/api/routes_registration.go` + +- [ ] **Step 1: Add transaction internal txs handler** + +```go +func (s *Server) handleFlowGetEVMTransactionInternalTxs(w http.ResponseWriter, r *http.Request) { + hash := strings.ToLower(strings.TrimPrefix(mux.Vars(r)["hash"], "0x")) + s.proxyBlockscout(w, r, "/api/v2/transactions/0x"+hash+"/internal-transactions") +} +``` + +- [ ] **Step 2: Add transaction logs handler** + +```go +func (s *Server) handleFlowGetEVMTransactionLogs(w http.ResponseWriter, r *http.Request) { + hash := strings.ToLower(strings.TrimPrefix(mux.Vars(r)["hash"], "0x")) + s.proxyBlockscout(w, r, "/api/v2/transactions/0x"+hash+"/logs") +} +``` + +- [ ] **Step 3: Add transaction token transfers handler** + +```go +func (s *Server) handleFlowGetEVMTransactionTokenTransfers(w http.ResponseWriter, r *http.Request) { + hash := strings.ToLower(strings.TrimPrefix(mux.Vars(r)["hash"], "0x")) + s.proxyBlockscout(w, r, "/api/v2/transactions/0x"+hash+"/token-transfers") +} +``` + +- [ ] **Step 4: Register transaction sub-resource routes** + +In `routes_registration.go`, after the existing `/flow/evm/transaction/{hash}` route: + +```go +r.HandleFunc("/flow/evm/transaction/{hash}/internal-transactions", s.handleFlowGetEVMTransactionInternalTxs).Methods("GET", "OPTIONS") +r.HandleFunc("/flow/evm/transaction/{hash}/logs", s.handleFlowGetEVMTransactionLogs).Methods("GET", "OPTIONS") +r.HandleFunc("/flow/evm/transaction/{hash}/token-transfers", s.handleFlowGetEVMTransactionTokenTransfers).Methods("GET", "OPTIONS") +``` + +- [ ] **Step 5: Verify build** + +Run: `cd backend && go build ./...` +Expected: No errors + +- [ ] **Step 6: Commit** + +```bash +git add backend/internal/api/v1_handlers_evm.go backend/internal/api/routes_registration.go +git commit -m "feat(api): add EVM transaction sub-resource proxy endpoints" +``` + +### Task 3: Add EVM Search Proxy Endpoint + +**Files:** +- Modify: `backend/internal/api/v1_handlers_evm.go` +- Modify: `backend/internal/api/routes_registration.go` + +- [ ] **Step 1: Add search handler** + +```go +func (s *Server) handleFlowEVMSearch(w http.ResponseWriter, r *http.Request) { + s.proxyBlockscout(w, r, "/api/v2/search") +} +``` + +- [ ] **Step 2: Register with caching** + +In `routes_registration.go`: + +```go +r.HandleFunc("/flow/evm/search", cachedHandler(30*time.Second, s.handleFlowEVMSearch)).Methods("GET", "OPTIONS") +``` + +Check how `cachedHandler` is used for existing search routes in the same file to match the pattern. + +- [ ] **Step 3: Verify build** + +Run: `cd backend && go build ./...` +Expected: No errors + +- [ ] **Step 4: Commit** + +```bash +git add backend/internal/api/v1_handlers_evm.go backend/internal/api/routes_registration.go +git commit -m "feat(api): add cached EVM search proxy endpoint" +``` + +### Task 4: Add COA Enrichment to EVM Address Detail + +**Files:** +- Modify: `backend/internal/api/v1_handlers_evm.go` + +The `handleFlowGetEVMAddress` handler currently does a pure proxy. Enhance it to check `coa_accounts` and inject `flow_address` into the response. + +- [ ] **Step 1: Update handler to enrich with COA data** + +Replace the simple proxy handler with: + +```go +func (s *Server) handleFlowGetEVMAddress(w http.ResponseWriter, r *http.Request) { + addr := normalizeAddr(mux.Vars(r)["address"]) + upstreamPath := "/api/v2/addresses/0x" + addr + + // Fetch from Blockscout + target := s.blockscoutURL + upstreamPath + if r.URL.RawQuery != "" { + target += "?" + r.URL.RawQuery + } + req, err := http.NewRequestWithContext(r.Context(), "GET", target, nil) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, "failed to create request") + return + } + req.Header.Set("Accept", "application/json") + + resp, err := blockscoutClient.Do(req) + if err != nil { + writeAPIError(w, http.StatusBadGateway, "blockscout unavailable") + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + writeAPIError(w, http.StatusBadGateway, "failed to read blockscout response") + return + } + + // Try to enrich with COA mapping + if resp.StatusCode == http.StatusOK && s.repo != nil { + coaRow, _ := s.repo.GetFlowAddressByCOA(r.Context(), addr) + if coaRow != nil && coaRow.FlowAddress != "" { + // Inject flow_address into JSON response + var parsed map[string]interface{} + if json.Unmarshal(body, &parsed) == nil { + parsed["flow_address"] = "0x" + coaRow.FlowAddress + parsed["is_coa"] = true + if enriched, err := json.Marshal(parsed); err == nil { + body = enriched + } + } + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.StatusCode) + w.Write(body) +} +``` + +Add `"encoding/json"` and `"io"` imports if not already present. + +- [ ] **Step 2: Verify build** + +Run: `cd backend && go build ./...` +Expected: No errors + +- [ ] **Step 3: Commit** + +```bash +git add backend/internal/api/v1_handlers_evm.go +git commit -m "feat(api): enrich EVM address detail with COA mapping" +``` + +--- + +## Chunk 2: TypeScript Types + EVM API Client + +### Task 5: Define Blockscout TypeScript Types + +**Files:** +- Create: `frontend/app/types/blockscout.ts` + +- [ ] **Step 1: Create Blockscout type definitions** + +```typescript +// Blockscout API v2 response types + +export interface BSAddress { + hash: string; + is_contract: boolean; + is_verified: boolean | null; + name: string | null; + coin_balance: string | null; // wei string + exchange_rate: string | null; + block_number_balance_updated_at: number | null; + transactions_count: number; + token_transfers_count: number; + has_custom_methods_read: boolean; + has_custom_methods_write: boolean; + // COA enrichment (added by our backend) + flow_address?: string; + is_coa?: boolean; +} + +export interface BSTransaction { + hash: string; + block_number: number; + timestamp: string; // ISO 8601 + from: { hash: string; name?: string | null; is_contract: boolean }; + to: { hash: string; name?: string | null; is_contract: boolean } | null; + value: string; // wei string + gas_limit: string; + gas_used: string; + gas_price: string; // wei string + status: string; // "ok" | "error" + result: string; + nonce: number; + type: number; // 0=legacy, 1=access_list, 2=EIP-1559 + method: string | null; // decoded method name + raw_input: string; // hex calldata + decoded_input: BSDecodedInput | null; + token_transfers: BSTokenTransfer[] | null; + fee: { type: string; value: string }; + tx_types: string[]; // ["coin_transfer", "token_transfer", "contract_call", etc.] + confirmations: number; + revert_reason: string | null; + has_error_in_internal_txs: boolean; +} + +export interface BSDecodedInput { + method_call: string; + method_id: string; + parameters: BSDecodedParam[]; +} + +export interface BSDecodedParam { + name: string; + type: string; + value: string; +} + +export interface BSInternalTransaction { + index: number; + transaction_hash: string; + block_number: number; + timestamp: string; + type: string; // "call" | "create" | "selfdestruct" | "reward" + call_type: string | null; // "call" | "delegatecall" | "staticcall" | "callcode" + from: { hash: string; name?: string | null; is_contract: boolean }; + to: { hash: string; name?: string | null; is_contract: boolean } | null; + value: string; // wei string + gas_limit: string; + gas_used: string; + input: string; + output: string; + error: string | null; + created_contract: { hash: string; name?: string | null } | null; + success: boolean; +} + +export interface BSTokenTransfer { + block_hash: string; + block_number: number; + log_index: number; + timestamp: string; + from: { hash: string; name?: string | null; is_contract: boolean }; + to: { hash: string; name?: string | null; is_contract: boolean }; + token: BSToken; + total: { value: string; decimals: string } | null; + tx_hash: string; + type: string; // "token_transfer" + method: string | null; +} + +export interface BSToken { + address: string; + name: string | null; + symbol: string | null; + decimals: string | null; + type: string; // "ERC-20" | "ERC-721" | "ERC-1155" + icon_url: string | null; + exchange_rate: string | null; +} + +export interface BSTokenBalance { + token: BSToken; + token_id: string | null; + value: string; + token_instance: any | null; +} + +export interface BSLog { + index: number; + address: { hash: string; name?: string | null; is_contract: boolean }; + data: string; // hex + topics: string[]; // array of topic hex strings + decoded: BSDecodedLog | null; + tx_hash: string; + block_number: number; +} + +export interface BSDecodedLog { + method_call: string; + method_id: string; + parameters: BSDecodedParam[]; +} + +export interface BSSearchResult { + items: BSSearchItem[]; + next_page_params: BSPageParams | null; +} + +export interface BSSearchItem { + type: string; // "address" | "transaction" | "token" | "contract" | "block" + name: string | null; + address: string | null; + url: string; + symbol: string | null; + token_type: string | null; + is_smart_contract_verified: boolean | null; + exchange_rate: string | null; +} + +/** Cursor-based pagination — pass as query params to fetch next page */ +export interface BSPageParams { + [key: string]: string | number; +} + +/** Wrapper for paginated Blockscout responses */ +export interface BSPaginatedResponse { + items: T[]; + next_page_params: BSPageParams | null; +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/app/types/blockscout.ts +git commit -m "feat(frontend): add Blockscout API v2 TypeScript types" +``` + +### Task 6: Create EVM API Client + +**Files:** +- Create: `frontend/app/api/evm.ts` + +- [ ] **Step 1: Create EVM API client module** + +```typescript +import { resolveApiBaseUrl } from '@/api'; +import type { + BSAddress, + BSTransaction, + BSInternalTransaction, + BSTokenTransfer, + BSTokenBalance, + BSLog, + BSSearchResult, + BSPageParams, + BSPaginatedResponse, +} from '@/types/blockscout'; + +async function evmFetch(path: string, params?: Record, signal?: AbortSignal): Promise { + const baseUrl = await resolveApiBaseUrl(); + const url = new URL(`${baseUrl}/flow/evm${path}`); + if (params) { + Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)); + } + const res = await fetch(url.toString(), { signal }); + if (!res.ok) throw new Error(`EVM API error: ${res.status}`); + return res.json(); +} + +function pageParamsToRecord(params?: BSPageParams): Record | undefined { + if (!params) return undefined; + const record: Record = {}; + Object.entries(params).forEach(([k, v]) => { record[k] = String(v); }); + return record; +} + +// --- Address endpoints --- + +export async function getEVMAddress(address: string, signal?: AbortSignal): Promise { + return evmFetch(`/address/${address}`, undefined, signal); +} + +export async function getEVMAddressTransactions( + address: string, pageParams?: BSPageParams, signal?: AbortSignal +): Promise> { + return evmFetch(`/address/${address}/transactions`, pageParamsToRecord(pageParams), signal); +} + +export async function getEVMAddressInternalTxs( + address: string, pageParams?: BSPageParams, signal?: AbortSignal +): Promise> { + return evmFetch(`/address/${address}/internal-transactions`, pageParamsToRecord(pageParams), signal); +} + +export async function getEVMAddressTokenTransfers( + address: string, pageParams?: BSPageParams, signal?: AbortSignal +): Promise> { + return evmFetch(`/address/${address}/token-transfers`, pageParamsToRecord(pageParams), signal); +} + +export async function getEVMAddressTokenBalances( + address: string, signal?: AbortSignal +): Promise { + // Blockscout returns array directly for current token balances + return evmFetch(`/address/${address}/token`, undefined, signal); +} + +// --- Transaction endpoints --- + +export async function getEVMTransaction(hash: string, signal?: AbortSignal): Promise { + return evmFetch(`/transaction/${hash}`, undefined, signal); +} + +export async function getEVMTransactionInternalTxs( + hash: string, pageParams?: BSPageParams, signal?: AbortSignal +): Promise> { + return evmFetch(`/transaction/${hash}/internal-transactions`, pageParamsToRecord(pageParams), signal); +} + +export async function getEVMTransactionLogs( + hash: string, pageParams?: BSPageParams, signal?: AbortSignal +): Promise> { + return evmFetch(`/transaction/${hash}/logs`, pageParamsToRecord(pageParams), signal); +} + +export async function getEVMTransactionTokenTransfers( + hash: string, pageParams?: BSPageParams, signal?: AbortSignal +): Promise> { + return evmFetch(`/transaction/${hash}/token-transfers`, pageParamsToRecord(pageParams), signal); +} + +// --- Search --- + +export async function searchEVM(query: string, signal?: AbortSignal): Promise { + return evmFetch(`/search`, { q: query }, signal); +} +``` + +- [ ] **Step 2: Verify frontend build** + +Run: `cd frontend && bun run build` +Expected: No TypeScript errors (types are imported but not yet used by components) + +- [ ] **Step 3: Commit** + +```bash +git add frontend/app/api/evm.ts +git commit -m "feat(frontend): add EVM API client for Blockscout proxy" +``` + +--- + +## Chunk 3: Shared Components + +### Task 7: Create LoadMorePagination Component + +**Files:** +- Create: `frontend/app/components/LoadMorePagination.tsx` + +Blockscout uses cursor-based pagination. Instead of adapting the existing page-number `Pagination.tsx`, create a simple "Load More" button. + +- [ ] **Step 1: Create component** + +```typescript +import type { BSPageParams } from '@/types/blockscout'; + +interface LoadMorePaginationProps { + nextPageParams: BSPageParams | null; + isLoading: boolean; + onLoadMore: (params: BSPageParams) => void; +} + +export function LoadMorePagination({ nextPageParams, isLoading, onLoadMore }: LoadMorePaginationProps) { + if (!nextPageParams) return null; + + return ( +
+ +
+ ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/app/components/LoadMorePagination.tsx +git commit -m "feat(frontend): add LoadMorePagination for cursor-based pagination" +``` + +### Task 8: Create EVM Utility Helpers + +**Files:** +- Create: `frontend/app/lib/evmUtils.ts` + +- [ ] **Step 1: Create utility functions** + +```typescript +/** Format wei string to human-readable FLOW value */ +export function formatWei(wei: string | null | undefined, decimals = 18, precision = 4): string { + if (!wei || wei === '0') return '0'; + try { + const num = BigInt(wei); + const divisor = BigInt(10 ** decimals); + const whole = num / divisor; + const remainder = num % divisor; + const fracStr = remainder.toString().padStart(decimals, '0').slice(0, precision); + const result = `${whole}.${fracStr}`.replace(/\.?0+$/, ''); + return result || '0'; + } catch { + return wei; + } +} + +/** Format gas number with commas */ +export function formatGas(gas: string | number | null | undefined): string { + if (!gas) return '0'; + return Number(gas).toLocaleString(); +} + +/** Truncate hex string: 0xAbCd...1234 */ +export function truncateHash(hash: string, startLen = 6, endLen = 4): string { + if (!hash || hash.length <= startLen + endLen + 3) return hash; + return `${hash.slice(0, startLen)}...${hash.slice(-endLen)}`; +} + +/** Normalize EVM address to lowercase with 0x prefix */ +export function normalizeEVMAddress(addr: string): string { + const clean = addr.toLowerCase().replace(/^0x/, ''); + return `0x${clean}`; +} + +/** Check if a hex string (without 0x) is a 40-char EVM address */ +export function isEVMAddress(hexOnly: string): boolean { + return /^[0-9a-fA-F]{40}$/.test(hexOnly); +} + +/** Map Blockscout tx status to display */ +export function txStatusLabel(status: string): { label: string; color: string } { + if (status === 'ok') return { label: 'Success', color: 'text-green-600 dark:text-green-400' }; + return { label: 'Failed', color: 'text-red-600 dark:text-red-400' }; +} + +/** Map internal tx type + call_type to display label */ +export function internalTxTypeLabel(type: string, callType: string | null): string { + if (type === 'create') return 'CREATE'; + if (type === 'selfdestruct') return 'SELFDESTRUCT'; + if (callType === 'delegatecall') return 'DELEGATECALL'; + if (callType === 'staticcall') return 'STATICCALL'; + return 'CALL'; +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/app/lib/evmUtils.ts +git commit -m "feat(frontend): add EVM utility helpers (formatting, normalization)" +``` + +--- + +## Chunk 4: EVM Account Page + +### Task 9: Refactor Address Detection in Account Route + +**Files:** +- Modify: `frontend/app/routes/accounts/$address.tsx` + +This is the critical routing change. Currently the loader only handles Cadence addresses and COA redirect. We need to add an EVM branch. + +- [ ] **Step 1: Update the loader to detect EVM addresses** + +In `accounts/$address.tsx`, replace the existing COA detection block (lines ~63-87) with: + +```typescript +loader: async ({ params, search }: any) => { + try { + const address = params.address; + const normalized = address.toLowerCase().startsWith('0x') ? address.toLowerCase() : `0x${address.toLowerCase()}`; + const hexOnly = normalized.replace(/^0x/, ''); + + // EVM address: 40 hex chars + if (hexOnly.length === 40) { + const base = await resolveApiBaseUrl(); + // Check if this is a COA (has linked Flow address) + const coaRes = await fetch(`${base}/flow/v1/coa/${normalized}`).catch(() => null); + let flowAddress: string | null = null; + if (coaRes?.ok) { + const json = await coaRes.json().catch(() => null); + flowAddress = json?.data?.[0]?.flow_address ?? null; + } + return { + account: null, + initialTransactions: [], + initialNextCursor: '', + isEVM: true, + isCOA: !!flowAddress, + evmAddress: normalized, + flowAddress, + }; + } + + // Cadence address: <= 16 hex chars — existing logic below (unchanged) + // ... rest of existing loader code ... +``` + +- [ ] **Step 2: Update the component to render EVMAccountPage for EVM addresses** + +In the main component function, add a branch at the top: + +```typescript +function AccountPage() { + const data = Route.useLoaderData(); + + // EVM address → render EVM account page + if (data.isEVM) { + return ( + + ); + } + + // Existing Cadence account rendering below (unchanged) + // ... +} +``` + +Add import at top: `import { EVMAccountPage } from '@/components/evm/EVMAccountPage';` + +- [ ] **Step 3: Verify frontend builds** + +Run: `cd frontend && npx tsc --noEmit` +Expected: May fail because `EVMAccountPage` doesn't exist yet. That's OK — create a placeholder in the next task. + +- [ ] **Step 4: Commit** + +```bash +git add frontend/app/routes/accounts/\$address.tsx +git commit -m "feat(frontend): detect EVM addresses in account route loader" +``` + +### Task 10: Create EVMAccountPage Component + +**Files:** +- Create: `frontend/app/components/evm/EVMAccountPage.tsx` + +- [ ] **Step 1: Create the main EVM account page component** + +```typescript +import { useState, useEffect, useCallback } from 'react'; +import { Link } from '@tanstack/react-router'; +import { Copy, ExternalLink } from 'lucide-react'; +import { getEVMAddress } from '@/api/evm'; +import type { BSAddress } from '@/types/blockscout'; +import { formatWei, truncateHash } from '@/lib/evmUtils'; +import { CopyButton } from '@/components/animate-ui/components/buttons/copy'; +import { EVMTransactionList } from './EVMTransactionList'; +import { EVMInternalTxList } from './EVMInternalTxList'; +import { EVMTokenTransfers } from './EVMTokenTransfers'; +import { EVMTokenHoldings } from './EVMTokenHoldings'; + +type EVMTab = 'transactions' | 'internal' | 'token-transfers' | 'holdings'; + +interface EVMAccountPageProps { + address: string; + flowAddress?: string; + isCOA: boolean; +} + +export function EVMAccountPage({ address, flowAddress, isCOA }: EVMAccountPageProps) { + const [addressInfo, setAddressInfo] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState('transactions'); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + getEVMAddress(address) + .then((data) => { if (!cancelled) setAddressInfo(data); }) + .catch((err) => { if (!cancelled) setError('EVM data temporarily unavailable'); }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, [address]); + + const tabs: { key: EVMTab; label: string; show?: boolean }[] = [ + { key: 'transactions', label: 'Transactions' }, + { key: 'internal', label: 'Internal Txs' }, + { key: 'token-transfers', label: 'Token Transfers' }, + { key: 'holdings', label: 'Token Holdings' }, + ]; + + return ( +
+ {/* Header */} +
+
+

+ EVM Address +

+ {isCOA && ( + + COA + + )} + {addressInfo?.is_contract && ( + + Contract + + )} +
+ +
+ {address} + +
+ + {/* COA link to Flow address */} + {flowAddress && ( +
+ Linked Flow Address: + + {flowAddress} + +
+ )} + + {/* Balance & stats */} + {loading ? ( +
+
+
+
+ ) : error ? ( +
+ {error} +
+ ) : addressInfo && ( +
+ Balance: {formatWei(addressInfo.coin_balance)} FLOW + Transactions: {addressInfo.transactions_count.toLocaleString()} +
+ )} +
+ + {/* Tabs */} +
+
+ {tabs.filter(t => t.show !== false).map((tab) => ( + + ))} +
+
+ + {/* Tab content */} + {activeTab === 'transactions' && } + {activeTab === 'internal' && } + {activeTab === 'token-transfers' && } + {activeTab === 'holdings' && } +
+ ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/app/components/evm/EVMAccountPage.tsx +git commit -m "feat(frontend): create EVMAccountPage with header, tabs, and skeleton loading" +``` + +### Task 11: Create EVMTransactionList Component + +**Files:** +- Create: `frontend/app/components/evm/EVMTransactionList.tsx` + +- [ ] **Step 1: Create component** + +```typescript +import { useState, useEffect, useCallback } from 'react'; +import { Link } from '@tanstack/react-router'; +import { getEVMAddressTransactions } from '@/api/evm'; +import type { BSTransaction, BSPageParams } from '@/types/blockscout'; +import { formatWei, truncateHash, txStatusLabel } from '@/lib/evmUtils'; +import { LoadMorePagination } from '@/components/LoadMorePagination'; +import { formatRelativeTime } from '@/lib/time'; + +interface EVMTransactionListProps { + address: string; +} + +export function EVMTransactionList({ address }: EVMTransactionListProps) { + const [txs, setTxs] = useState([]); + const [nextPage, setNextPage] = useState(null); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + getEVMAddressTransactions(address) + .then((res) => { + if (!cancelled) { + setTxs(res.items); + setNextPage(res.next_page_params); + } + }) + .catch(() => { if (!cancelled) setError('Failed to load transactions'); }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, [address]); + + const loadMore = useCallback(async (params: BSPageParams) => { + setLoadingMore(true); + try { + const res = await getEVMAddressTransactions(address, params); + setTxs((prev) => [...prev, ...res.items]); + setNextPage(res.next_page_params); + } catch { + setError('Failed to load more transactions'); + } finally { + setLoadingMore(false); + } + }, [address]); + + if (loading) { + return ( +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ ))} +
+ ); + } + + if (error) { + return
{error}
; + } + + if (txs.length === 0) { + return
No transactions found.
; + } + + return ( +
+
+ + + + + + + + + + + + + + + {txs.map((tx) => { + const status = txStatusLabel(tx.status); + return ( + + + + + + + + + + + ); + })} + +
Tx HashMethodBlockAgeFromToValueStatus
+ + {truncateHash(tx.hash)} + + + {tx.method ? ( + + {tx.method} + + ) : ( + + )} + + {tx.block_number} + + {formatRelativeTime(tx.timestamp)} + + + {truncateHash(tx.from.hash)} + + + {tx.to ? ( + + {truncateHash(tx.to.hash)} + + ) : ( + Contract Create + )} + + {formatWei(tx.value)} FLOW + + + {status.label} + +
+
+ +
+ ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/app/components/evm/EVMTransactionList.tsx +git commit -m "feat(frontend): create EVMTransactionList with load-more pagination" +``` + +### Task 12: Create EVMInternalTxList Component + +**Files:** +- Create: `frontend/app/components/evm/EVMInternalTxList.tsx` + +- [ ] **Step 1: Create component** + +```typescript +import { useState, useEffect, useCallback } from 'react'; +import { Link } from '@tanstack/react-router'; +import { getEVMAddressInternalTxs, getEVMTransactionInternalTxs } from '@/api/evm'; +import type { BSInternalTransaction, BSPageParams } from '@/types/blockscout'; +import { formatWei, truncateHash, internalTxTypeLabel } from '@/lib/evmUtils'; +import { LoadMorePagination } from '@/components/LoadMorePagination'; + +interface EVMInternalTxListProps { + address?: string; + txHash?: string; +} + +export function EVMInternalTxList({ address, txHash }: EVMInternalTxListProps) { + const [items, setItems] = useState([]); + const [nextPage, setNextPage] = useState(null); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + + const fetchFn = address + ? () => getEVMAddressInternalTxs(address) + : txHash + ? () => getEVMTransactionInternalTxs(txHash) + : null; + + if (!fetchFn) return; + + fetchFn() + .then((res) => { + if (!cancelled) { + setItems(res.items); + setNextPage(res.next_page_params); + } + }) + .catch(() => { if (!cancelled) setError('Failed to load internal transactions'); }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, [address, txHash]); + + const loadMore = useCallback(async (params: BSPageParams) => { + setLoadingMore(true); + try { + const fetchFn = address + ? () => getEVMAddressInternalTxs(address, params) + : txHash + ? () => getEVMTransactionInternalTxs(txHash, params) + : null; + if (!fetchFn) return; + const res = await fetchFn(); + setItems((prev) => [...prev, ...res.items]); + setNextPage(res.next_page_params); + } catch { + setError('Failed to load more'); + } finally { + setLoadingMore(false); + } + }, [address, txHash]); + + if (loading) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+ ); + } + + if (error) return
{error}
; + if (items.length === 0) return
No internal transactions found.
; + + return ( +
+
+ + + + + + + + + + + + + {items.map((itx, idx) => ( + + + + + + + + + ))} + +
TypeFromToValueGas UsedResult
+ + {internalTxTypeLabel(itx.type, itx.call_type)} + + + + {truncateHash(itx.from.hash)} + + + {itx.to ? ( + + {truncateHash(itx.to.hash)} + + ) : itx.created_contract ? ( + + {truncateHash(itx.created_contract.hash)} (new) + + ) : ( + + )} + {formatWei(itx.value)} FLOW{Number(itx.gas_used).toLocaleString()} + {itx.error ? ( + {itx.error} + ) : ( + Success + )} +
+
+ +
+ ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/app/components/evm/EVMInternalTxList.tsx +git commit -m "feat(frontend): create EVMInternalTxList component" +``` + +### Task 13: Create EVMTokenTransfers + EVMTokenHoldings Components + +**Files:** +- Create: `frontend/app/components/evm/EVMTokenTransfers.tsx` +- Create: `frontend/app/components/evm/EVMTokenHoldings.tsx` + +- [ ] **Step 1: Create EVMTokenTransfers** + +```typescript +import { useState, useEffect, useCallback } from 'react'; +import { Link } from '@tanstack/react-router'; +import { getEVMAddressTokenTransfers, getEVMTransactionTokenTransfers } from '@/api/evm'; +import type { BSTokenTransfer, BSPageParams } from '@/types/blockscout'; +import { formatWei, truncateHash } from '@/lib/evmUtils'; +import { LoadMorePagination } from '@/components/LoadMorePagination'; +import { formatRelativeTime } from '@/lib/time'; + +interface EVMTokenTransfersProps { + address?: string; + txHash?: string; +} + +export function EVMTokenTransfers({ address, txHash }: EVMTokenTransfersProps) { + const [items, setItems] = useState([]); + const [nextPage, setNextPage] = useState(null); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + + const fetchFn = address + ? () => getEVMAddressTokenTransfers(address) + : txHash + ? () => getEVMTransactionTokenTransfers(txHash) + : null; + + if (!fetchFn) return; + + fetchFn() + .then((res) => { + if (!cancelled) { setItems(res.items); setNextPage(res.next_page_params); } + }) + .catch(() => { if (!cancelled) setError('Failed to load token transfers'); }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, [address, txHash]); + + const loadMore = useCallback(async (params: BSPageParams) => { + setLoadingMore(true); + try { + const fetchFn = address + ? () => getEVMAddressTokenTransfers(address, params) + : txHash + ? () => getEVMTransactionTokenTransfers(txHash, params) + : null; + if (!fetchFn) return; + const res = await fetchFn(); + setItems((prev) => [...prev, ...res.items]); + setNextPage(res.next_page_params); + } catch { + setError('Failed to load more'); + } finally { + setLoadingMore(false); + } + }, [address, txHash]); + + if (loading) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+ ); + } + if (error) return
{error}
; + if (items.length === 0) return
No token transfers found.
; + + return ( +
+
+ + + + + + + + + + + + + {items.map((transfer, idx) => { + const decimals = Number(transfer.token.decimals ?? '18'); + const amount = transfer.total?.value + ? formatWei(transfer.total.value, decimals, 6) + : '—'; + return ( + + + + + + + + + ); + })} + +
Tx HashAgeFromToTokenAmount
+ + {truncateHash(transfer.tx_hash)} + + {formatRelativeTime(transfer.timestamp)} + + {truncateHash(transfer.from.hash)} + + + + {truncateHash(transfer.to.hash)} + + +
+ {transfer.token.icon_url && } + {transfer.token.symbol ?? transfer.token.name ?? '?'} + {transfer.token.type} +
+
{amount}
+
+ +
+ ); +} +``` + +- [ ] **Step 2: Create EVMTokenHoldings** + +```typescript +import { useState, useEffect } from 'react'; +import { Link } from '@tanstack/react-router'; +import { getEVMAddressTokenBalances } from '@/api/evm'; +import type { BSTokenBalance } from '@/types/blockscout'; +import { formatWei } from '@/lib/evmUtils'; + +interface EVMTokenHoldingsProps { + address: string; +} + +export function EVMTokenHoldings({ address }: EVMTokenHoldingsProps) { + const [balances, setBalances] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + getEVMAddressTokenBalances(address) + .then((data) => { if (!cancelled) setBalances(data); }) + .catch(() => { if (!cancelled) setError('Failed to load token holdings'); }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, [address]); + + if (loading) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+ ); + } + if (error) return
{error}
; + if (balances.length === 0) return
No token holdings found.
; + + return ( +
+ + + + + + + + + + {balances.map((bal, idx) => { + const decimals = Number(bal.token.decimals ?? '18'); + return ( + + + + + + ); + })} + +
TokenTypeBalance
+
+ {bal.token.icon_url && } +
+ {bal.token.name ?? 'Unknown'} + {bal.token.symbol && ({bal.token.symbol})} +
+
+
+ + {bal.token.type} + + + {bal.token.type === 'ERC-20' + ? formatWei(bal.value, decimals, 6) + : bal.value} +
+
+ ); +} +``` + +- [ ] **Step 3: Verify frontend builds** + +Run: `cd frontend && npx tsc --noEmit` +Expected: Pass (or only unrelated errors) + +- [ ] **Step 4: Commit** + +```bash +git add frontend/app/components/evm/EVMTokenTransfers.tsx frontend/app/components/evm/EVMTokenHoldings.tsx +git commit -m "feat(frontend): create EVMTokenTransfers and EVMTokenHoldings components" +``` + +--- + +## Chunk 5: EVM Transaction Detail Page + +### Task 14: Update TX Route to Detect EVM Hashes + +**Files:** +- Modify: `frontend/app/routes/txs/$txId.tsx` + +- [ ] **Step 1: Add parallel EVM lookup to the loader** + +In the `txs/$txId.tsx` loader, after the existing Cadence transaction fetch, add a fallback EVM lookup. The existing loader tries to fetch from the local API. If it returns 404 and the hash looks like an EVM hash (`0x` + 64 hex), try the EVM endpoint. + +Find where the loader handles a failed/empty Cadence transaction response and add: + +```typescript +// If Cadence lookup failed and this looks like an EVM hash, try Blockscout +if (!transaction && /^0x[0-9a-fA-F]{64}$/.test(txId)) { + try { + const baseUrl = await resolveApiBaseUrl(); + const evmRes = await fetch(`${baseUrl}/flow/evm/transaction/${txId}`); + if (evmRes.ok) { + const evmTx = await evmRes.json(); + return { transaction: null, evmTransaction: evmTx, isEVM: true, error: null }; + } + } catch {} +} + +// Note: For better performance, consider firing both Cadence and EVM lookups +// in parallel with Promise.allSettled when the hash is 0x-prefixed. +// The sequential approach above is simpler but adds latency for EVM-only txs. +``` + +- [ ] **Step 2: Add EVM rendering branch in the component** + +At the top of the component function, before existing Cadence rendering: + +```typescript +if (data.isEVM && data.evmTransaction) { + return ; +} +``` + +Add import: `import { EVMTxDetail } from '@/components/evm/EVMTxDetail';` + +- [ ] **Step 3: Commit** + +```bash +git add frontend/app/routes/txs/\$txId.tsx +git commit -m "feat(frontend): add EVM transaction fallback in tx route loader" +``` + +### Task 15: Create EVMTxDetail Component + +**Files:** +- Create: `frontend/app/components/evm/EVMTxDetail.tsx` + +- [ ] **Step 1: Create component** + +```typescript +import { useState } from 'react'; +import { Link } from '@tanstack/react-router'; +import type { BSTransaction } from '@/types/blockscout'; +import { formatWei, formatGas, truncateHash, txStatusLabel } from '@/lib/evmUtils'; +import { CopyButton } from '@/components/animate-ui/components/buttons/copy'; +import { formatRelativeTime } from '@/lib/time'; +import { EVMInternalTxList } from './EVMInternalTxList'; +import { EVMLogsList } from './EVMLogsList'; +import { EVMTokenTransfers } from './EVMTokenTransfers'; + +type TxTab = 'internal' | 'logs' | 'token-transfers'; + +interface EVMTxDetailProps { + tx: BSTransaction; +} + +export function EVMTxDetail({ tx }: EVMTxDetailProps) { + const [activeTab, setActiveTab] = useState('internal'); + const status = txStatusLabel(tx.status); + + return ( +
+ {/* Header */} +
+

+ EVM Transaction +

+
+ {tx.hash} + +
+
+ + {/* Overview */} +
+
+
+ Status: + {status.label} +
+
+ Block: + {tx.block_number} +
+
+ Timestamp: + {formatRelativeTime(tx.timestamp)} +
+
+ Type: + {tx.type === 2 ? 'EIP-1559' : tx.type === 1 ? 'Access List' : 'Legacy'} +
+
+ From: + + {truncateHash(tx.from.hash, 10, 8)} + + +
+
+ To: + {tx.to ? ( + <> + + {truncateHash(tx.to.hash, 10, 8)} + + + + ) : ( + Contract Creation + )} +
+
+ Value: + {formatWei(tx.value)} FLOW +
+
+ Gas: + {formatGas(tx.gas_used)} / {formatGas(tx.gas_limit)} +
+
+ Gas Price: + {formatWei(tx.gas_price, 9, 4)} Gwei +
+
+ Nonce: + {tx.nonce} +
+
+ + {/* Decoded Input */} + {tx.decoded_input && ( +
+
Input Data (Decoded):
+
+
{tx.decoded_input.method_call}
+ {tx.decoded_input.parameters.map((p, i) => ( +
+ {p.name}: {p.value} +
+ ))} +
+
+ )} + + {/* Raw Input (when not decoded) */} + {!tx.decoded_input && tx.raw_input && tx.raw_input !== '0x' && ( +
+
Input Data (Raw):
+
+ {tx.raw_input} +
+
+ )} + + {/* Revert Reason */} + {tx.revert_reason && ( +
+
Revert Reason:
+
+ {tx.revert_reason} +
+
+ )} +
+ + {/* Sub-tabs */} +
+
+ {[ + { key: 'internal' as TxTab, label: 'Internal Transactions' }, + { key: 'logs' as TxTab, label: 'Logs' }, + { key: 'token-transfers' as TxTab, label: 'Token Transfers' }, + ].map((tab) => ( + + ))} +
+
+ + {activeTab === 'internal' && } + {activeTab === 'logs' && } + {activeTab === 'token-transfers' && } +
+ ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/app/components/evm/EVMTxDetail.tsx +git commit -m "feat(frontend): create EVMTxDetail page with overview, decoded input, and sub-tabs" +``` + +### Task 16: Create EVMLogsList Component + +**Files:** +- Create: `frontend/app/components/evm/EVMLogsList.tsx` + +- [ ] **Step 1: Create component** + +```typescript +import { useState, useEffect, useCallback } from 'react'; +import { Link } from '@tanstack/react-router'; +import { getEVMTransactionLogs } from '@/api/evm'; +import type { BSLog, BSPageParams } from '@/types/blockscout'; +import { truncateHash } from '@/lib/evmUtils'; +import { LoadMorePagination } from '@/components/LoadMorePagination'; + +interface EVMLogsListProps { + txHash: string; +} + +export function EVMLogsList({ txHash }: EVMLogsListProps) { + const [logs, setLogs] = useState([]); + const [nextPage, setNextPage] = useState(null); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + getEVMTransactionLogs(txHash) + .then((res) => { + if (!cancelled) { setLogs(res.items); setNextPage(res.next_page_params); } + }) + .catch(() => { if (!cancelled) setError('Failed to load logs'); }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, [txHash]); + + const loadMore = useCallback(async (params: BSPageParams) => { + setLoadingMore(true); + try { + const res = await getEVMTransactionLogs(txHash, params); + setLogs((prev) => [...prev, ...res.items]); + setNextPage(res.next_page_params); + } catch { + setError('Failed to load more logs'); + } finally { + setLoadingMore(false); + } + }, [txHash]); + + if (loading) { + return ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+ ); + } + if (error) return
{error}
; + if (logs.length === 0) return
No logs found.
; + + return ( +
+ {logs.map((log) => ( +
+
+ Log Index: {log.index} + + Address:{' '} + + {truncateHash(log.address.hash)} + + {log.address.name && ({log.address.name})} + +
+ + {/* Decoded log */} + {log.decoded && ( +
+
{log.decoded.method_call}
+ {log.decoded.parameters.map((p, i) => ( +
+ {p.name} ({p.type}): {p.value} +
+ ))} +
+ )} + + {/* Topics */} +
+ Topics: + {log.topics.map((topic, i) => ( +
+ [{i}] {topic} +
+ ))} +
+ + {/* Data */} + {log.data && log.data !== '0x' && ( +
+ Data: +
+ {log.data} +
+
+ )} +
+ ))} + +
+ ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/app/components/evm/EVMLogsList.tsx +git commit -m "feat(frontend): create EVMLogsList component with decoded event display" +``` + +--- + +## Chunk 6: Search Enhancement + +### Task 17: Update Search Hook for EVM + +**Files:** +- Modify: `frontend/app/hooks/useSearch.ts` +- Modify: `frontend/app/api.ts` (add EVM search types) + +- [ ] **Step 1: Add EVM address pattern to useSearch** + +In `useSearch.ts`, add a new pattern after the existing `HEX_40`: + +```typescript +const EVM_ADDR = /^0x[0-9a-fA-F]{40}$/; // 0x-prefixed EVM address +``` + +Update the `detectPattern` function. Before the existing `HEX_40` check, add: + +```typescript +// EVM address with 0x prefix +if (EVM_ADDR.test(q)) { + return { mode: 'idle', matches: [{ type: 'evm-addr', label: 'EVM Address', value: q, route: `/accounts/${q}` }] }; +} +``` + +Update the existing `HEX_40` match to label as "EVM Address" instead of "COA Address": + +```typescript +if (HEX_40.test(q)) { + return { mode: 'idle', matches: [{ type: 'evm-addr', label: 'EVM Address', value: q, route: `/accounts/0x${q}` }] }; +} +``` + +- [ ] **Step 2: Add parallel EVM search to fuzzy mode** + +In the fuzzy search section of `useSearch.ts`, fire both local and EVM searches in parallel: + +```typescript +// Inside the debounced fuzzy search callback: +const [localResults, evmResults] = await Promise.allSettled([ + searchAll(q, 3, controller.signal), + searchEVM(q, controller.signal), +]); + +const fuzzy = localResults.status === 'fulfilled' ? localResults.value : { contracts: [], tokens: [], nft_collections: [] }; +const evm = evmResults.status === 'fulfilled' ? evmResults.value : { items: [] }; + +setState({ + mode: 'fuzzy', + fuzzyResults: fuzzy, + evmResults: evm.items ?? [], + // ... +}); +``` + +Add `searchEVM` import: `import { searchEVM } from '@/api/evm';` + +- [ ] **Step 3: Update SearchState type to include EVM results** + +```typescript +interface SearchState { + mode: 'idle' | 'quick-match' | 'fuzzy'; + quickMatches: QuickMatchItem[]; + fuzzyResults: SearchAllResponse | null; + evmResults: BSSearchItem[]; // NEW + isLoading: boolean; + error: string | null; +} +``` + +- [ ] **Step 4: Commit** + +```bash +git add frontend/app/hooks/useSearch.ts +git commit -m "feat(frontend): add EVM address pattern and parallel Blockscout search" +``` + +### Task 18: Update SearchDropdown for EVM Results + +**Files:** +- Modify: `frontend/app/components/SearchDropdown.tsx` + +- [ ] **Step 1: Add EVM results section to fuzzy mode rendering** + +In the fuzzy results rendering section, after the existing sections (Contracts, Tokens, NFT Collections), add: + +```typescript +{/* EVM Results */} +{state.evmResults && state.evmResults.length > 0 && ( +
+
+ EVM +
+ {state.evmResults.map((item, i) => { + const route = item.type === 'address' + ? `/accounts/${item.address}` + : item.type === 'transaction' + ? `/txs/${item.address}` + : item.url; // fallback to Blockscout URL + return ( + + ); + })} +
+)} +``` + +- [ ] **Step 2: Update Header.tsx for EVM address handling** + +In `Header.tsx`, update the search-result navigation logic for the `coa` / `evm-addr` type. Remove the old COA resolution logic and navigate directly: + +```typescript +// Replace the COA resolution block with: +if (match.type === 'evm-addr') { + navigate({ to: '/accounts/$address', params: { address: match.value.startsWith('0x') ? match.value : `0x${match.value}` } }); + return; +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add frontend/app/components/SearchDropdown.tsx frontend/app/components/Header.tsx +git commit -m "feat(frontend): display EVM search results with badge and update address navigation" +``` + +--- + +## Chunk 7: COA Account Page (Dual View) + +### Task 19: Create COAAccountPage Component + +**Files:** +- Create: `frontend/app/components/evm/COAAccountPage.tsx` + +This is the dual-view page for Cadence Owned Accounts — shows both Cadence and EVM tabs. + +- [ ] **Step 1: Create component** + +```typescript +import { useState, useEffect } from 'react'; +import { Link } from '@tanstack/react-router'; +import { CopyButton } from '@/components/animate-ui/components/buttons/copy'; +import { EVMTransactionList } from './EVMTransactionList'; +import { EVMInternalTxList } from './EVMInternalTxList'; +import { EVMTokenTransfers } from './EVMTokenTransfers'; +import { EVMTokenHoldings } from './EVMTokenHoldings'; +import { getEVMAddress } from '@/api/evm'; +import type { BSAddress } from '@/types/blockscout'; +import { formatWei } from '@/lib/evmUtils'; + +// Import existing Cadence tab components +import { AccountActivityTab } from '@/components/account/AccountActivityTab'; +import { AccountTokensTab } from '@/components/account/AccountTokensTab'; +import { AccountNFTsTab } from '@/components/account/AccountNFTsTab'; +import { AccountContractsTab } from '@/components/account/AccountContractsTab'; + +type ViewMode = 'cadence' | 'evm'; +type CadenceTab = 'activity' | 'tokens' | 'nfts' | 'contracts'; +type EVMTab = 'transactions' | 'internal' | 'token-transfers' | 'holdings'; + +interface COAAccountPageProps { + evmAddress: string; + flowAddress: string; + cadenceAccount: any; // existing Cadence account data from loader +} + +export function COAAccountPage({ evmAddress, flowAddress, cadenceAccount }: COAAccountPageProps) { + const [viewMode, setViewMode] = useState('cadence'); + const [cadenceTab, setCadenceTab] = useState('activity'); + const [evmTab, setEVMTab] = useState('transactions'); + const [evmInfo, setEVMInfo] = useState(null); + + useEffect(() => { + getEVMAddress(evmAddress) + .then(setEVMInfo) + .catch(() => {}); + }, [evmAddress]); + + return ( +
+ {/* Dual Address Header */} +
+
+

+ COA Account +

+ + Cadence Owned Account + +
+ +
+
+ Flow: + + {flowAddress} + + +
+
+ EVM: + {evmAddress} + +
+
+ + {evmInfo && ( +
+ EVM Balance: {formatWei(evmInfo.coin_balance)} FLOW +
+ )} +
+ + {/* View Mode Switcher */} +
+ + +
+ + {/* Cadence View */} + {viewMode === 'cadence' && ( + <> +
+
+ {(['activity', 'tokens', 'nfts', 'contracts'] as CadenceTab[]).map((tab) => ( + + ))} +
+
+ {cadenceTab === 'activity' && } + {cadenceTab === 'tokens' && } + {cadenceTab === 'nfts' && } + {cadenceTab === 'contracts' && } + + )} + + {/* EVM View */} + {viewMode === 'evm' && ( + <> +
+
+ {([ + { key: 'transactions', label: 'Transactions' }, + { key: 'internal', label: 'Internal Txs' }, + { key: 'token-transfers', label: 'Token Transfers' }, + { key: 'holdings', label: 'Token Holdings' }, + ] as { key: EVMTab; label: string }[]).map((tab) => ( + + ))} +
+
+ {evmTab === 'transactions' && } + {evmTab === 'internal' && } + {evmTab === 'token-transfers' && } + {evmTab === 'holdings' && } + + )} +
+ ); +} +``` + +- [ ] **Step 2: Wire COAAccountPage into the account route** + +In `accounts/$address.tsx`, update the EVM rendering branch to use COAAccountPage for COA addresses: + +```typescript +if (data.isEVM) { + if (data.isCOA && data.flowAddress) { + return ( + + ); + } + return ( + + ); +} +``` + +Add import: `import { COAAccountPage } from '@/components/evm/COAAccountPage';` + +- [ ] **Step 3: Verify full frontend build** + +Run: `cd frontend && NODE_OPTIONS="--max-old-space-size=8192" bun run build` +Expected: Build succeeds + +- [ ] **Step 4: Commit** + +```bash +git add frontend/app/components/evm/COAAccountPage.tsx frontend/app/routes/accounts/\$address.tsx +git commit -m "feat(frontend): create COAAccountPage with dual Cadence/EVM view" +``` + +--- + +## Chunk 8: Verification & Cleanup + +### Task 20: Verify Full Stack Build + +- [ ] **Step 1: Backend build** + +Run: `cd backend && go build ./...` +Expected: No errors + +- [ ] **Step 2: Frontend build** + +Run: `cd frontend && NODE_OPTIONS="--max-old-space-size=8192" bun run build` +Expected: Build succeeds + +- [ ] **Step 3: Frontend lint** + +Run: `cd frontend && bun run lint` +Expected: No new lint errors (fix any that appear) + +- [ ] **Step 4: Verify all new files are committed** + +```bash +git status +``` + +Expected: Clean working tree + +### Task 21: Manual Smoke Test Checklist + +These are manual verification steps for after deployment: + +- [ ] Navigate to `/accounts/0x<40-hex EOA address>` → should show EVMAccountPage +- [ ] Navigate to `/accounts/0x<40-hex COA address>` → should show COAAccountPage with dual view +- [ ] Navigate to `/accounts/0x<16-hex Flow address>` → should show existing Cadence page (unchanged) +- [ ] Navigate to `/txs/0x` → should show EVMTxDetail +- [ ] Navigate to `/txs/` → should show existing Cadence detail (unchanged) +- [ ] Search for an EVM address → should show "EVM Address" quick match +- [ ] Search for free text → should show EVM results with `[EVM]` badge alongside local results +- [ ] Click tabs on EVMAccountPage → each tab loads data +- [ ] Click "Load More" → cursor-based pagination works +- [ ] EVMTxDetail shows internal txs, logs, and token transfers diff --git a/docs/superpowers/plans/2026-03-15-evm-contract-interact.md b/docs/superpowers/plans/2026-03-15-evm-contract-interact.md new file mode 100644 index 000000000..9315902af --- /dev/null +++ b/docs/superpowers/plans/2026-03-15-evm-contract-interact.md @@ -0,0 +1,791 @@ +# EVM Contract Interact Page — Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a standalone `/interact` page where users can load any deployed EVM contract by address, fetch its ABI from Blockscout, and call read/write methods. + +**Architecture:** New route `/interact` with lazy-loaded `InteractPage` component. Reuses existing `ContractInteraction` + `SolidityParamInput` for method execution. Backend Blockscout proxy extended to return ABI and support testnet. Recent contracts persisted in localStorage. + +**Tech Stack:** React, viem, wagmi, react-router-dom, Express (server proxy) + +--- + +## Chunk 1: Backend + Type Changes + +### Task 1: Extend Blockscout proxy to return ABI and support testnet + +**Files:** +- Modify: `runner/server/src/http.ts:37-87` + +- [ ] **Step 1: Add testnet Blockscout URL constant** + +In `runner/server/src/http.ts`, after line 38 (`const BLOCKSCOUT_BASE = ...`), add: + +```typescript +const BLOCKSCOUT_TESTNET_BASE = process.env.BLOCKSCOUT_TESTNET_URL || 'https://evm-testnet.flowscan.io'; +``` + +- [ ] **Step 2: Update the endpoint handler to accept `?network` and return `abi`** + +Replace the handler body of `app.get('/api/evm-contracts/:address', ...)` to: + +```typescript +app.get('/api/evm-contracts/:address', async (req, res) => { + const { address } = req.params; + const network = req.query.network === 'testnet' ? 'testnet' : 'mainnet'; + const base = network === 'testnet' ? BLOCKSCOUT_TESTNET_BASE : BLOCKSCOUT_BASE; + + try { + const addrRes = await fetch(`${base}/api/v2/addresses/${address}`); + if (!addrRes.ok) { + res.json({ verified: false }); + return; + } + const addrData = await addrRes.json() as Record; + if (!addrData.is_verified) { + res.json({ verified: false }); + return; + } + + const scRes = await fetch(`${base}/api/v2/smart-contracts/${address}`); + if (!scRes.ok) { + res.json({ verified: false }); + return; + } + const scData = await scRes.json() as { + name?: string; + abi?: unknown[]; + source_code?: string; + file_path?: string; + additional_sources?: { file_path: string; source_code: string }[]; + }; + + const files: { path: string; content: string }[] = []; + const mainName = scData.file_path || `${scData.name || 'Contract'}.sol`; + if (scData.source_code) { + files.push({ path: mainName.split('/').pop() || mainName, content: scData.source_code }); + } + if (scData.additional_sources) { + for (const src of scData.additional_sources) { + files.push({ + path: src.file_path.split('/').pop() || src.file_path, + content: src.source_code, + }); + } + } + + res.json({ + verified: true, + name: scData.name || 'Contract', + abi: scData.abi || null, + files, + }); + } catch (e) { + console.error('Blockscout proxy error:', e); + res.status(500).json({ error: 'Failed to fetch from Blockscout' }); + } +}); +``` + +- [ ] **Step 3: Verify server compiles** + +Run: `cd runner/server && npx tsc --noEmit` + +- [ ] **Step 4: Commit** + +```bash +git add runner/server/src/http.ts +git commit -m "feat(runner): extend Blockscout proxy with ABI and testnet support" +``` + +--- + +### Task 2: Make `DeployedContract.deployTxHash` optional + +**Files:** +- Modify: `runner/src/flow/evmContract.ts:17-23` +- Modify: `runner/src/components/ContractInteraction.tsx:45-49` + +- [ ] **Step 1: Make `deployTxHash` optional in the interface** + +In `runner/src/flow/evmContract.ts`, change: + +```typescript + deployTxHash: string; +``` + +to: + +```typescript + deployTxHash?: string; +``` + +- [ ] **Step 2: Guard the tx hash display in ResultDisplay** + +In `runner/src/components/ContractInteraction.tsx`, the `ResultDisplay` already conditionally renders `{result.txHash && (...)}` so no change needed there. But verify with: + +Run: `cd runner && npx tsc --noEmit 2>&1 | grep -i deployTxHash` + +Expected: no errors + +- [ ] **Step 3: Commit** + +```bash +git add runner/src/flow/evmContract.ts +git commit -m "feat(runner): make DeployedContract.deployTxHash optional" +``` + +--- + +### Task 1.5: Add Vite dev proxy and nginx location for `/api/evm-contracts` + +**Files:** +- Modify: `runner/vite.config.ts:27-47` +- Modify: `runner/nginx.conf` + +- [ ] **Step 1: Add Vite dev proxy** + +In `runner/vite.config.ts`, inside the `proxy` object, add before the closing `}`: + +```typescript + '/api/evm-contracts': { + target: 'http://localhost:3003', + }, +``` + +- [ ] **Step 2: Add nginx location for production** + +In `runner/nginx.conf`, add this block **before** the catch-all `location /api/` block (before line 73): + +```nginx + # EVM contract proxy — Node.js server (Blockscout ABI fetch) + location /api/evm-contracts/ { + proxy_pass http://127.0.0.1:3003; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +``` + +- [ ] **Step 3: Commit** + +```bash +git add runner/vite.config.ts runner/nginx.conf +git commit -m "fix(runner): add proxy rules for /api/evm-contracts endpoint" +``` + +--- + +## Chunk 2: Routing + Sidebar Entry + +### Task 3: Add `interact` to ActivityBar + +**Files:** +- Modify: `runner/src/components/ActivityBar.tsx` +- Modify: `runner/src/App.tsx:2083-2085` + +- [ ] **Step 1: Add the tab to ActivityBar** + +In `runner/src/components/ActivityBar.tsx`: + +1. Update import to include `Terminal`: +```typescript +import { Files, Search, GitBranch, Rocket, Terminal, Settings } from 'lucide-react'; +``` + +2. Add `'interact'` to the union type: +```typescript +export type SidebarTab = 'files' | 'search' | 'github' | 'deploy' | 'interact' | 'settings'; +``` + +3. Add the tab entry after deploy (before settings): +```typescript + { id: 'deploy', icon: Rocket, label: 'Deploy' }, + { id: 'interact', icon: Terminal, label: 'Interact' }, + { id: 'settings', icon: Settings, label: 'Settings' }, +``` + +- [ ] **Step 2: Intercept `interact` tab click in App.tsx** + +In `runner/src/App.tsx`, find the `onTabChange` handler (around line 2083): + +```typescript +if (tab === 'deploy') { window.location.href = '/deploy'; return; } +``` + +Add after it: + +```typescript +if (tab === 'interact') { window.location.href = '/interact'; return; } +``` + +- [ ] **Step 3: Add route to Router.tsx** + +In `runner/src/Router.tsx`: + +1. Add lazy import: +```typescript +const InteractPage = lazy(() => import('./interact/InteractPage')); +``` + +2. Add route before the catch-all: +```typescript +} /> +``` + +- [ ] **Step 4: Verify TypeScript compiles (will fail — InteractPage doesn't exist yet, that's OK)** + +Run: `cd runner && npx tsc --noEmit 2>&1 | grep InteractPage` + +Expected: error about missing module (this is expected; we'll create it in the next task) + +- [ ] **Step 5: Commit** + +```bash +git add runner/src/components/ActivityBar.tsx runner/src/App.tsx runner/src/Router.tsx +git commit -m "feat(runner): add interact tab to sidebar and /interact route" +``` + +--- + +## Chunk 3: InteractPage + ContractLoader + +### Task 4: Create the InteractPage component + +**Files:** +- Create: `runner/src/interact/InteractPage.tsx` + +This is the main page component. It orchestrates: header, ContractLoader, ContractInteraction, and RecentContracts. + +- [ ] **Step 1: Create the file** + +```tsx +// runner/src/interact/InteractPage.tsx +import { useState, useEffect, useCallback } from 'react'; +import { Link } from 'react-router-dom'; +import { ArrowLeft, Terminal } from 'lucide-react'; +import type { Abi } from 'viem'; +import type { Chain } from 'viem/chains'; +import type { DeployedContract } from '../flow/evmContract'; +import { flowEvmMainnet, flowEvmTestnet } from '../flow/evmChains'; +import ContractInteraction from '../components/ContractInteraction'; +import ContractLoader from './ContractLoader'; +import RecentContracts, { type RecentContract, loadRecentContracts, saveRecentContract, removeRecentContract } from './RecentContracts'; + +function getChain(network: string): Chain { + return network === 'testnet' ? flowEvmTestnet : flowEvmMainnet; +} + +export default function InteractPage() { + // Read URL params + const params = new URLSearchParams(window.location.search); + const initialAddress = params.get('address') || ''; + const initialNetwork = params.get('network') || localStorage.getItem('runner:network') || 'mainnet'; + + const [network, setNetwork] = useState<'mainnet' | 'testnet'>( + initialNetwork === 'testnet' ? 'testnet' : 'mainnet', + ); + const [contract, setContract] = useState(null); + const [recentContracts, setRecentContracts] = useState(loadRecentContracts); + + // Sync URL when contract loads + useEffect(() => { + if (contract) { + const url = new URL(window.location.href); + url.searchParams.set('address', contract.address); + url.searchParams.set('network', network); + window.history.replaceState({}, '', url.toString()); + } + }, [contract, network]); + + const handleContractLoaded = useCallback((address: `0x${string}`, name: string, abi: Abi) => { + setContract({ + address, + name, + abi, + chainId: network === 'testnet' ? 545 : 747, + }); + const entry = saveRecentContract({ address, network, name, timestamp: Date.now() }); + setRecentContracts(entry); + }, [network]); + + // Navigate with URL params so ContractLoader auto-fetches on page load + const handleSelectRecent = useCallback((recent: RecentContract) => { + const url = new URL(window.location.href); + url.searchParams.set('address', recent.address); + url.searchParams.set('network', recent.network); + window.location.href = url.toString(); + }, []); + + const handleRemoveRecent = useCallback((c: RecentContract) => { + const updated = removeRecentContract(c.address, c.network); + setRecentContracts(updated); + }, []); + + const chain = getChain(network); + + return ( +
+ {/* Header */} +
+ { e.preventDefault(); window.location.href = '/editor'; }} + > + + Editor + +
+ +

Contract Interact

+
+ + {/* Content */} +
+
+ {/* Contract Loader */} + + + {/* Recent Contracts (only shown when no contract loaded) */} + {!contract && recentContracts.length > 0 && ( + + )} + + {/* Contract Interaction */} + {contract && ( + + )} +
+
+
+ ); +} +``` + +- [ ] **Step 2: Verify file exists** + +Run: `ls runner/src/interact/InteractPage.tsx` + +- [ ] **Step 3: Commit (will not compile yet — dependencies missing)** + +```bash +git add runner/src/interact/InteractPage.tsx +git commit -m "feat(runner): add InteractPage shell component" +``` + +--- + +### Task 5: Create the ContractLoader component + +**Files:** +- Create: `runner/src/interact/ContractLoader.tsx` + +Handles address input, network selector, Blockscout fetch, and manual ABI paste fallback. + +- [ ] **Step 1: Create the file** + +```tsx +// runner/src/interact/ContractLoader.tsx +import { useState, useCallback, useEffect } from 'react'; +import { Loader2, Download, AlertCircle, ChevronDown } from 'lucide-react'; +import type { Abi } from 'viem'; + +interface ContractLoaderProps { + initialAddress: string; + network: 'mainnet' | 'testnet'; + onNetworkChange: (n: 'mainnet' | 'testnet') => void; + onContractLoaded: (address: `0x${string}`, name: string, abi: Abi) => void; +} + +const SERVER_BASE = ''; // Same-origin proxy + +function validateAbi(json: unknown): json is Abi { + if (!Array.isArray(json)) return false; + return json.every((item: any) => item && typeof item.type === 'string'); +} + +export default function ContractLoader({ + initialAddress, + network, + onNetworkChange, + onContractLoaded, +}: ContractLoaderProps) { + const [address, setAddress] = useState(initialAddress); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [showManualAbi, setShowManualAbi] = useState(false); + const [manualAbi, setManualAbi] = useState(''); + + // Auto-fetch if initialAddress is provided + useEffect(() => { + if (initialAddress) { + handleFetch(initialAddress); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const handleFetch = useCallback(async (addr?: string) => { + const target = (addr || address).trim(); + if (!target) return; + + // Validate address format (40 hex chars) + const clean = target.startsWith('0x') ? target.slice(2) : target; + if (clean.length !== 40 || !/^[0-9a-fA-F]+$/.test(clean)) { + setError('Invalid EVM address. Must be 40 hex characters.'); + return; + } + + const fullAddr = target.startsWith('0x') ? target : `0x${target}`; + + setLoading(true); + setError(''); + setShowManualAbi(false); + + try { + const res = await fetch(`${SERVER_BASE}/api/evm-contracts/${fullAddr}?network=${network}`); + if (!res.ok) throw new Error('Server error'); + const data = await res.json(); + + if (data.verified && data.abi) { + onContractLoaded(fullAddr as `0x${string}`, data.name || 'Contract', data.abi); + } else if (data.verified && !data.abi) { + setError('Contract is verified but ABI not available. Paste ABI manually.'); + setShowManualAbi(true); + } else { + setError('No verified contract found at this address. You can paste an ABI manually.'); + setShowManualAbi(true); + } + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Failed to fetch contract'); + } finally { + setLoading(false); + } + }, [address, network, onContractLoaded]); + + const handleManualAbiSubmit = useCallback(() => { + try { + const parsed = JSON.parse(manualAbi); + if (!validateAbi(parsed)) { + setError('Invalid ABI format. Paste a valid JSON ABI array.'); + return; + } + const fullAddr = address.trim().startsWith('0x') ? address.trim() : `0x${address.trim()}`; + onContractLoaded(fullAddr as `0x${string}`, 'Custom Contract', parsed); + } catch { + setError('Invalid JSON. Please check your ABI.'); + } + }, [manualAbi, address, onContractLoaded]); + + return ( +
+
+ {/* Address input */} + setAddress(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleFetch()} + placeholder="0x... (EVM contract address)" + className="flex-1 bg-zinc-800 text-sm text-zinc-200 px-3 py-2.5 rounded-lg border border-zinc-600 focus:border-zinc-500 focus:outline-none placeholder:text-zinc-600 font-mono" + autoFocus + /> + + {/* Network selector */} +
+ + +
+ + {/* Load button */} + +
+ + {/* Error */} + {error && ( +
+ + {error} +
+ )} + + {/* Manual ABI input */} + {showManualAbi && ( +
+