From 01e878f5ce7aeea373d2165e223251acd52c1e9b Mon Sep 17 00:00:00 2001 From: dhebp <152355273+DHEBP@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:33:30 -0400 Subject: [PATCH 01/16] fix(sc): unify SC invocation path and decode hex strings in API responses - Add sc_response_normalizer.go to decode printable ASCII strings from DERO.GetSC while preserving numeric values and binary hashes - Apply normalization to all GetSC exit paths (XSWD, Explorer, direct) - Refactor InvokeSCFunction to delegate to InternalWalletCall scinvoke path, ensuring Studio and dApp calls use identical TX building - Fix parseXSWDScArgs to handle both []interface{} and []map[string]interface{} input types and support uint64/int numeric values - Add 0-transfer fallback in scinvoke to prevent index-out-of-range panic when no explicit transfers are provided --- app.go | 1 + explorer_service.go | 38 +++---- sc_function_parser.go | 201 +++++++++----------------------------- sc_response_normalizer.go | 123 +++++++++++++++++++++++ wallet.go | 110 +++++++++++++++------ xswd_router.go | 3 + 6 files changed, 271 insertions(+), 205 deletions(-) create mode 100644 sc_response_normalizer.go diff --git a/app.go b/app.go index ca95801..cee0b98 100644 --- a/app.go +++ b/app.go @@ -834,6 +834,7 @@ func (a *App) DaemonGetSC(scid string) map[string]interface{} { if err != nil { return ErrorResponse(err) } + res = normalizeDEROGetSCResult(res) return map[string]interface{}{"success": true, "result": res} } diff --git a/explorer_service.go b/explorer_service.go index fca7685..6f31e49 100644 --- a/explorer_service.go +++ b/explorer_service.go @@ -312,9 +312,9 @@ func (a *App) GetCoinbaseMiner(txid string) map[string]interface{} { if txHex == "" { return map[string]interface{}{ - "success": true, + "success": true, "isCoinbase": false, - "message": "No raw transaction hex available", + "message": "No raw transaction hex available", } } @@ -547,7 +547,7 @@ func (a *App) SearchAddress(address string) map[string]interface{} { if a.gnomonClient.IsRunning() { // Search through Gnomon for SCIDs owned by this address ownedSCIDs := a.getOwnedSCIDs(address) - + return map[string]interface{}{ "success": true, "address": address, @@ -910,7 +910,7 @@ func (a *App) GetBlockchainStats() map[string]interface{} { // Extract relevant stats - info is already map[string]interface{} stats := map[string]interface{}{} - + stats["height"] = info["height"] stats["topoheight"] = info["topoheight"] stats["difficulty"] = info["difficulty"] @@ -1192,7 +1192,7 @@ func (a *App) GetMempoolExtended(maxCount int) map[string]interface{} { func (a *App) GetSCInfo(scid string) map[string]interface{} { // Normalize SCID to lowercase (DERO requires lowercase hex) normalizedSCID := strings.ToLower(strings.TrimSpace(scid)) - + a.logToConsole(fmt.Sprintf("[...] Getting SC info: %s", normalizedSCID[:16]+"...")) params := map[string]interface{}{ @@ -1205,6 +1205,7 @@ func (a *App) GetSCInfo(scid string) map[string]interface{} { if err != nil { return ErrorResponse(err) } + result = normalizeDEROGetSCResult(result) scData := map[string]interface{}{} if resultMap, ok := result.(map[string]interface{}); ok { @@ -1248,7 +1249,7 @@ func (a *App) GetRingMembers(txid string) map[string]interface{} { "index": idx, "members": []string{}, } - + if addresses, ok := payload.([]interface{}); ok { members := []string{} for _, addr := range addresses { @@ -1258,12 +1259,12 @@ func (a *App) GetRingMembers(txid string) map[string]interface{} { } payloadRing["members"] = members payloadRing["count"] = len(members) - + if len(members) > ringSize { ringSize = len(members) } } - + ringData = append(ringData, payloadRing) } } @@ -1274,10 +1275,10 @@ func (a *App) GetRingMembers(txid string) map[string]interface{} { a.logToConsole(fmt.Sprintf("[OK] Found %d ring groups, max size %d", len(ringData), ringSize)) return map[string]interface{}{ - "success": true, - "txid": txid, - "rings": ringData, - "ringCount": len(ringData), + "success": true, + "txid": txid, + "rings": ringData, + "ringCount": len(ringData), "maxRingSize": ringSize, } } @@ -1294,15 +1295,14 @@ func (a *App) GetTransactionWithRings(txid string) map[string]interface{} { // Get ring members ringResult := a.GetRingMembers(txid) - + // Combine results return map[string]interface{}{ - "success": true, - "txid": txid, - "tx": txResult["tx"], - "rings": ringResult["rings"], - "ringCount": ringResult["ringCount"], + "success": true, + "txid": txid, + "tx": txResult["tx"], + "rings": ringResult["rings"], + "ringCount": ringResult["ringCount"], "maxRingSize": ringResult["maxRingSize"], } } - diff --git a/sc_function_parser.go b/sc_function_parser.go index 0a27bc2..724491e 100644 --- a/sc_function_parser.go +++ b/sc_function_parser.go @@ -223,7 +223,11 @@ func (a *App) InvokeSCFunction(paramsJSON string) map[string]interface{} { } } - a.logToConsole(fmt.Sprintf("[SC] Invoking %s on %s...", params.Function, params.SCID[:16])) + scidPrefix := params.SCID + if len(scidPrefix) > 16 { + scidPrefix = scidPrefix[:16] + } + a.logToConsole(fmt.Sprintf("[SC] Invoking %s on %s...", params.Function, scidPrefix)) // Check wallet wallet := GetWallet() @@ -238,186 +242,73 @@ func (a *App) InvokeSCFunction(paramsJSON string) map[string]interface{} { } } - // Build SC arguments - scArgs := rpc.Arguments{ - {Name: rpc.SCACTION, DataType: rpc.DataUint64, Value: uint64(rpc.SC_CALL)}, - {Name: rpc.SCID, DataType: rpc.DataHash, Value: crypto.HashHexToHash(params.SCID)}, - {Name: "entrypoint", DataType: rpc.DataString, Value: params.Function}, + // Route through the wallet's built-in scinvoke path (same path used by dApps), + // which keeps SC arg handling consistent across Studio and XSWD calls. + scRPC := []interface{}{ + map[string]interface{}{"name": "entrypoint", "datatype": "S", "value": params.Function}, } - - // Add function parameters for name, value := range params.Params { switch v := value.(type) { case string: - scArgs = append(scArgs, rpc.Argument{ - Name: name, - DataType: rpc.DataString, - Value: v, + scRPC = append(scRPC, map[string]interface{}{ + "name": name, "datatype": "S", "value": v, }) - case float64: // JSON numbers come as float64 - scArgs = append(scArgs, rpc.Argument{ - Name: name, - DataType: rpc.DataUint64, - Value: uint64(v), + case float64: + scRPC = append(scRPC, map[string]interface{}{ + "name": name, "datatype": "U", "value": v, }) case int: - scArgs = append(scArgs, rpc.Argument{ - Name: name, - DataType: rpc.DataUint64, - Value: uint64(v), + scRPC = append(scRPC, map[string]interface{}{ + "name": name, "datatype": "U", "value": float64(v), + }) + case uint64: + scRPC = append(scRPC, map[string]interface{}{ + "name": name, "datatype": "U", "value": float64(v), }) } } - // Validate SC arguments before building the transaction - if err := scArgs.Validate_Arguments(); err != nil { - a.logToConsole(fmt.Sprintf("[ERR] SC argument validation failed: %v", err)) - return map[string]interface{}{ - "success": false, - "error": "Invalid SC arguments: " + err.Error(), - } - } - if a.simulatorManager != nil && a.simulatorManager.isInitialized { - return a.withSimulatorTransactionConnectivity(wallet, "scinvoke", func() map[string]interface{} { - return a.invokeSCFunctionSimulator(params, wallet, scArgs) - }) - } - - // Sync wallet with daemon before building the transaction - // (mirrors InternalWalletCall scinvoke path which does this and works correctly) - if syncErr := wallet.Sync_Wallet_Memory_With_Daemon(); syncErr != nil { - a.logToConsole(fmt.Sprintf("[WARN] Pre-invoke wallet sync failed: %v", syncErr)) - } - - // Check daemon connectivity - if wallet.Get_Daemon_Height() == 0 { - return map[string]interface{}{ - "success": false, - "error": "Wallet is not connected to the daemon. Please check your connection and try again.", - } - } - - // Check wallet has balance for gas fees - mature, _ := wallet.Get_Balance() - if mature == 0 { - return map[string]interface{}{ - "success": false, - "error": "Insufficient balance. Smart contract calls require gas fees — please fund your wallet first.", - } - } - - // Build transfers - transfers := []rpc.Transfer{} - - // Ensure daemon connection before Random_ring_members (needs daemon to fetch decoy outputs) - a.ensureWalletDaemonConnectivity(a.getDaemonEndpointForWallet()) - - // Get random destination for the transfer - randos := wallet.Random_ring_members(crypto.ZEROHASH) - if len(randos) == 0 { - return map[string]interface{}{ - "success": false, - "error": "Could not get ring members - check daemon connection", - } + walletParams := map[string]interface{}{ + "scid": params.SCID, + "sc_rpc": scRPC, + "ringsize": float64(2), } - destination := randos[0] - if destination == wallet.GetAddress().String() && len(randos) > 1 { - destination = randos[1] + if params.Anonymous { + walletParams["ringsize"] = float64(16) } - - // Add DERO transfer if needed if params.DeroAmount > 0 { - transfers = append(transfers, rpc.Transfer{ - Destination: destination, - Amount: 0, - Burn: params.DeroAmount, - }) + walletParams["sc_dero_deposit"] = float64(params.DeroAmount) } - - // Add asset transfer if needed if params.AssetSCID != "" && params.AssetAmount > 0 { - transfers = append(transfers, rpc.Transfer{ - Destination: destination, - SCID: crypto.HashHexToHash(params.AssetSCID), - Amount: 0, - Burn: params.AssetAmount, - }) - } - - // If no transfers, add a minimal one - if len(transfers) == 0 { - transfers = append(transfers, rpc.Transfer{ - Destination: destination, - Amount: 0, - }) + walletParams["sc_token_deposit"] = float64(params.AssetAmount) + walletParams["sc_token_deposit_scid"] = params.AssetSCID } - // Set ringsize - ringsize := uint64(2) - if params.Anonymous { - ringsize = 16 - } - - // Estimate gas via daemon RPC (same as Engram and InternalWalletCall paths) - gasStorage := uint64(0) - gasRPC := []map[string]interface{}{ - {"name": "SC_ACTION", "datatype": "U", "value": uint64(0)}, - {"name": "SC_ID", "datatype": "H", "value": params.SCID}, - {"name": "entrypoint", "datatype": "S", "value": params.Function}, - } - for name, value := range params.Params { - switch v := value.(type) { - case string: - gasRPC = append(gasRPC, map[string]interface{}{"name": name, "datatype": "S", "value": v}) - case float64: - gasRPC = append(gasRPC, map[string]interface{}{"name": name, "datatype": "U", "value": uint64(v)}) + a.logToConsole(fmt.Sprintf("[SC] Forwarding invoke through wallet scinvoke path (args=%d)", len(scRPC)-1)) + invokeResult := a.InternalWalletCall("scinvoke", walletParams, "") + if success, _ := invokeResult["success"].(bool); !success { + errorMessage := "Transaction failed" + if errText, ok := invokeResult["error"].(string); ok && errText != "" { + errorMessage = errText } - } - gasParams := map[string]interface{}{ - "sc_rpc": gasRPC, - "ringsize": ringsize, - "signer": wallet.GetAddress().String(), - } - if a.daemonClient != nil { - gasResult, gasErr := a.daemonClient.Call("DERO.GetGasEstimate", gasParams) - if gasErr != nil { - a.logToConsole(fmt.Sprintf("[WARN] Gas estimate failed (proceeding with 0): %v", gasErr)) - } else if resultMap, ok := gasResult.(map[string]interface{}); ok { - if gs, ok := resultMap["gasstorage"].(float64); ok { - gasStorage = uint64(gs) - } - } - } - - a.logToConsole(fmt.Sprintf("[SC] Building TX: ringsize=%d gasStorage=%d transfers=%d", ringsize, gasStorage, len(transfers))) - - // Build and send transaction - tx, err := wallet.TransferPayload0(transfers, ringsize, false, scArgs, gasStorage, false) - if err != nil { - // Retry after resync (mirrors InternalWalletCall pattern) - a.logToConsole(fmt.Sprintf("[WARN] SC invoke build failed, retrying after resync: %v", err)) - if syncErr := wallet.Sync_Wallet_Memory_With_Daemon(); syncErr != nil { - a.logToConsole(fmt.Sprintf("[WARN] Retry sync failed: %v", syncErr)) + resp := map[string]interface{}{ + "success": false, + "error": errorMessage, } - tx, err = wallet.TransferPayload0(transfers, ringsize, false, scArgs, gasStorage, false) - if err != nil { - return map[string]interface{}{ - "success": false, - "error": "Transaction failed: " + err.Error(), - } + if technicalError, ok := invokeResult["technicalError"].(string); ok && technicalError != "" { + resp["technicalError"] = technicalError } + return resp } - if err := wallet.SendTransaction(tx); err != nil { - return map[string]interface{}{ - "success": false, - "error": "Failed to send transaction: " + err.Error(), + txid := "" + if resultMap, ok := invokeResult["result"].(map[string]interface{}); ok { + if tx, ok := resultMap["txid"].(string); ok { + txid = tx } } - txid := tx.GetHash().String() - a.logToConsole(fmt.Sprintf("[OK] SC invoked successfully: %s...", txid[:16])) - + a.logToConsole(fmt.Sprintf("[OK] SC invoked successfully: %s...", txid)) return map[string]interface{}{ "success": true, "txid": txid, diff --git a/sc_response_normalizer.go b/sc_response_normalizer.go new file mode 100644 index 0000000..d6622ae --- /dev/null +++ b/sc_response_normalizer.go @@ -0,0 +1,123 @@ +package main + +import ( + "encoding/hex" + "strings" +) + +// normalizeDEROGetSCResult decodes printable ASCII string values from +// DERO.GetSC variable maps to preserve legacy client behavior. +func normalizeDEROGetSCResult(result interface{}) interface{} { + resultMap, ok := result.(map[string]interface{}) + if !ok { + return result + } + + normalizeGetSCMapField(resultMap, "stringkeys") + normalizeGetSCMapField(resultMap, "uint64keys") + return resultMap +} + +func normalizeGetSCMapField(resultMap map[string]interface{}, field string) { + raw, ok := resultMap[field] + if !ok { + return + } + + values, ok := raw.(map[string]interface{}) + if !ok { + return + } + + normalized := make(map[string]interface{}, len(values)) + for key, value := range values { + if strValue, ok := value.(string); ok { + normalized[key] = decodePrintableHexString(strValue) + } else { + normalized[key] = value + } + } + + resultMap[field] = normalized +} + +func decodePrintableHexString(value string) string { + trimmed := strings.TrimSpace(value) + if !shouldDecodeHexString(trimmed) { + return value + } + decimalOnly := isDecimalString(trimmed) + + decoded, err := hex.DecodeString(trimmed) + if err != nil { + return value + } + + if !isPrintableASCII(decoded) { + return value + } + + decodedText := strings.TrimRight(string(decoded), "\x00 \t\n\r") + if decodedText == "" { + return value + } + // Keep short decimal strings as numbers ("50" should stay numeric). + if decimalOnly && len(decodedText) == 1 { + return value + } + // Decimal-only hex should decode only when it clearly becomes text. + if decimalOnly && !containsASCIIAlpha(decodedText) { + return value + } + + return decodedText +} + +func shouldDecodeHexString(value string) bool { + if value == "" || len(value)%2 != 0 || len(value) < 4 { + return false + } + + for _, c := range value { + if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F') { + return false + } + } + + return true +} + +func isPrintableASCII(data []byte) bool { + if len(data) == 0 { + return false + } + + for _, b := range data { + if b < 32 || b > 126 { + return false + } + } + + return true +} + +func isDecimalString(value string) bool { + if value == "" { + return false + } + for _, c := range value { + if c < '0' || c > '9' { + return false + } + } + return true +} + +func containsASCIIAlpha(value string) bool { + for _, c := range value { + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { + return true + } + } + return false +} diff --git a/wallet.go b/wallet.go index 58294fd..e6184fd 100644 --- a/wallet.go +++ b/wallet.go @@ -1338,42 +1338,60 @@ func parseXSWDScArgs(params map[string]interface{}, scid string) rpc.Arguments { hasEntrypointInScRpc := false entrypointFromScRpc := "" + appendScArg := func(argMap map[string]interface{}) { + name, _ := argMap["name"].(string) + dtype, _ := argMap["datatype"].(string) + val := argMap["value"] + if name == "" { + return + } + if name == "entrypoint" { + hasEntrypointInScRpc = true + if ep, ok := val.(string); ok { + entrypointFromScRpc = ep + } + } + switch dtype { + case "U": + switch v := val.(type) { + case float64: + scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "U", Value: uint64(v)}) + case uint64: + scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "U", Value: v}) + case int: + scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "U", Value: uint64(v)}) + } + case "I": + switch v := val.(type) { + case float64: + scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "I", Value: int64(v)}) + case int64: + scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "I", Value: v}) + case int: + scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "I", Value: int64(v)}) + } + case "S": + if v, ok := val.(string); ok { + scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "S", Value: v}) + } + case "H": + if v, ok := val.(string); ok { + scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "H", Value: crypto.HashHexToHash(v)}) + } + } + } + if args, ok := params["sc_rpc"].([]interface{}); ok { for _, arg := range args { - a, ok := arg.(map[string]interface{}) + argMap, ok := arg.(map[string]interface{}) if !ok { continue } - name, _ := a["name"].(string) - dtype, _ := a["datatype"].(string) - val := a["value"] - if name == "" { - continue - } - if name == "entrypoint" { - hasEntrypointInScRpc = true - if ep, ok := val.(string); ok { - entrypointFromScRpc = ep - } - } - switch dtype { - case "U": - if v, ok := val.(float64); ok { - scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "U", Value: uint64(v)}) - } - case "I": - if v, ok := val.(float64); ok { - scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "I", Value: int64(v)}) - } - case "S": - if v, ok := val.(string); ok { - scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "S", Value: v}) - } - case "H": - if v, ok := val.(string); ok { - scArgs = append(scArgs, rpc.Argument{Name: name, DataType: "H", Value: crypto.HashHexToHash(v)}) - } - } + appendScArg(argMap) + } + } else if args, ok := params["sc_rpc"].([]map[string]interface{}); ok { + for _, argMap := range args { + appendScArg(argMap) } } @@ -1650,6 +1668,12 @@ func (a *App) InternalWalletCall(method string, params map[string]interface{}, p // Parse SC arguments (shared helper handles all datatypes including I, U, S, H) scArgs := parseXSWDScArgs(params, scid) + if len(scArgs) == 0 { + return map[string]interface{}{ + "success": false, + "error": "Invalid smart contract call. Missing or malformed 'entrypoint'/'sc_rpc' parameters.", + } + } // sc_dero_deposit / sc_token_deposit -- amount attached to the SC call. // These are top-level params distinct from the transfers array. @@ -1729,6 +1753,30 @@ func (a *App) InternalWalletCall(method string, params map[string]interface{}, p if f, ok := params["fees"].(float64); ok && f > 0 { fees = uint64(f) } + + // Guard against wallet library panic path: scinvoke with no transfers at all. + if len(transfers) == 0 { + destination := "" + if a.IsInSimulatorMode() { + destination = a.getSimulatorTransferDestination(wallet.GetAddress().String()) + } else { + randos := wallet.Random_ring_members(crypto.ZEROHASH) + if len(randos) == 0 { + return map[string]interface{}{ + "success": false, + "error": "Could not get ring members for SC call. Please check daemon connection and retry.", + } + } + destination = randos[0] + if destination == wallet.GetAddress().String() && len(randos) > 1 { + destination = randos[1] + } + } + transfers = append(transfers, rpc.Transfer{ + Destination: destination, + Amount: 0, + }) + } a.logToConsole(fmt.Sprintf("[XSWD] Building scinvoke TX with ringsize=%d fees=%d", ringsize, fees)) tx, err := wallet.TransferPayload0(transfers, ringsize, false, scArgs, fees, false) diff --git a/xswd_router.go b/xswd_router.go index 16caeb9..2e061a6 100644 --- a/xswd_router.go +++ b/xswd_router.go @@ -46,6 +46,9 @@ func (a *App) routeDaemonCall(method string, params map[string]interface{}) XSWD log.Printf("[ERR] Daemon call failed: %v", err) return xswdError(FriendlyError(err), err.Error()) } + if method == "DERO.GetSC" { + result = normalizeDEROGetSCResult(result) + } return xswdSuccess(result) } From a1d2af2d0df49107ae46fb18dccfd780aab9e1d3 Mon Sep 17 00:00:00 2001 From: dhebp <152355273+DHEBP@users.noreply.github.com> Date: Fri, 1 May 2026 21:15:10 -0400 Subject: [PATCH 02/16] fix(explorer): decode SC string values consistently Normalize smart contract search results before Explorer renders them and move SC variable display decoding into a focused helper. This keeps scores numeric while allowing printable string values to display as text with raw hex still available. --- frontend/src/lib/utils/scValueDisplay.js | 74 +++++++++++++++++++ frontend/src/lib/utils/scValueDisplay.test.js | 72 ++++++++++++++++++ frontend/src/routes/Explorer.svelte | 50 ++++--------- search_service.go | 3 + 4 files changed, 164 insertions(+), 35 deletions(-) create mode 100644 frontend/src/lib/utils/scValueDisplay.js create mode 100644 frontend/src/lib/utils/scValueDisplay.test.js diff --git a/frontend/src/lib/utils/scValueDisplay.js b/frontend/src/lib/utils/scValueDisplay.js new file mode 100644 index 0000000..6f601fb --- /dev/null +++ b/frontend/src/lib/utils/scValueDisplay.js @@ -0,0 +1,74 @@ +const HEX_RE = /^[0-9a-fA-F]+$/; +const PRINTABLE_TEXT_RE = /^[\t\n\r -~]*$/; + +function toDisplayString(value) { + return value == null ? '' : String(value); +} + +function decodePrintableHexString(value) { + if (typeof value !== 'string' || value.length === 0 || value.length % 2 !== 0) { + return null; + } + + if (!HEX_RE.test(value)) { + return null; + } + + const bytes = value.match(/.{2}/g).map((pair) => parseInt(pair, 16)); + let decoded; + + try { + decoded = new TextDecoder('utf-8', { fatal: true }).decode(new Uint8Array(bytes)); + } catch { + return null; + } + + decoded = decoded.replace(/\0+$/, ''); + + if (decoded.length === 0 || !PRINTABLE_TEXT_RE.test(decoded)) { + return null; + } + + // Scores/counters such as "40", "50", and "60" are valid one-byte hex too. + if (/^\d+$/.test(value) && decoded.length === 1) { + return null; + } + + return decoded; +} + +function formatSCDisplayString(value) { + const raw = toDisplayString(value); + const decoded = decodePrintableHexString(value); + + if (decoded == null || decoded === raw) { + return { + display: raw, + raw, + wasDecoded: false, + }; + } + + return { + display: decoded, + raw, + wasDecoded: true, + }; +} + +export function formatSCDisplayValue(value) { + if (typeof value !== 'string') { + const raw = toDisplayString(value); + return { + display: raw, + raw, + wasDecoded: false, + }; + } + + return formatSCDisplayString(value); +} + +export function formatSCDisplayKey(key) { + return formatSCDisplayString(toDisplayString(key)); +} diff --git a/frontend/src/lib/utils/scValueDisplay.test.js b/frontend/src/lib/utils/scValueDisplay.test.js new file mode 100644 index 0000000..a84acfb --- /dev/null +++ b/frontend/src/lib/utils/scValueDisplay.test.js @@ -0,0 +1,72 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { formatSCDisplayKey, formatSCDisplayValue } from './scValueDisplay.js'; + +test('decodes printable hex string values shown in SC variables', () => { + const cases = [ + ['53696567', 'Sieg'], + ['536965676672696564', 'Siegfried'], + ['5465737453696567', 'TestSieg'], + ['736563726574', 'secret'], + ['5369656746', 'SiegF'], + ]; + + for (const [raw, display] of cases) { + assert.deepEqual(formatSCDisplayValue(raw), { + display, + raw, + wasDecoded: true, + }); + } +}); + +test('keeps numeric score and time values numeric-looking', () => { + for (const value of [40, 50, 60, 55, 132]) { + assert.deepEqual(formatSCDisplayValue(value), { + display: String(value), + raw: String(value), + wasDecoded: false, + }); + } + + for (const value of ['40', '50', '60', '55', '132', '6962919']) { + assert.deepEqual(formatSCDisplayValue(value), { + display: value, + raw: value, + wasDecoded: false, + }); + } +}); + +test('trims null padding after decoding printable hex strings', () => { + assert.deepEqual(formatSCDisplayValue('48656c6c6f00'), { + display: 'Hello', + raw: '48656c6c6f00', + wasDecoded: true, + }); +}); + +test('leaves invalid or non-printable hex values raw', () => { + for (const value of ['not-hex', '123', '00ff', '48656c6c6g']) { + assert.deepEqual(formatSCDisplayValue(value), { + display: value, + raw: value, + wasDecoded: false, + }); + } +}); + +test('formats keys independently from values', () => { + assert.deepEqual(formatSCDisplayKey('6e616d65'), { + display: 'name', + raw: '6e616d65', + wasDecoded: true, + }); + + assert.deepEqual(formatSCDisplayKey('name_0'), { + display: 'name_0', + raw: 'name_0', + wasDecoded: false, + }); +}); diff --git a/frontend/src/routes/Explorer.svelte b/frontend/src/routes/Explorer.svelte index 290f7cb..ec8dd4e 100644 --- a/frontend/src/routes/Explorer.svelte +++ b/frontend/src/routes/Explorer.svelte @@ -18,6 +18,7 @@ import SearchHistory from '../lib/components/SearchHistory.svelte'; import VersionHistory from '../lib/components/VersionHistory.svelte'; import SCFunctionInteractor from '../lib/components/SCFunctionInteractor.svelte'; + import { formatSCDisplayKey, formatSCDisplayValue } from '../lib/utils/scValueDisplay.js'; import { Package, FileText, Coins, Clock, Copy, ArrowLeft, Home, X, ChevronLeft, ChevronRight, FileCode, User, Globe, Lock, Info, AlertTriangle, Check, Loader2, Shield, Pickaxe, @@ -88,26 +89,12 @@ // Hex toggle: tracks which string var keys are currently showing raw hex instead of decoded text let hexViewKeys = {}; function toggleHexView(key) { hexViewKeys[key] = !hexViewKeys[key]; hexViewKeys = hexViewKeys; } - function tryHexDecode(hex) { - if (typeof hex !== 'string') return String(hex); - try { - const bytes = hex.match(/.{1,2}/g); - if (!bytes) return hex; - const decoded = new TextDecoder('utf-8', { fatal: true }).decode( - new Uint8Array(bytes.map(b => parseInt(b, 16))) - ); - return decoded.replace(/\0+$/, ''); - } catch { return hex; } - } - function isHexEncoded(str) { - if (typeof str !== 'string' || str.length === 0 || str.length % 2 !== 0) return false; - if (!/^[0-9a-fA-F]+$/.test(str)) return false; - // DERO.GetSC can return uint64 values under stringkeys when the variable key - // itself is a string (e.g. score_0 = "50"). Do not decode decimal numerics - // as ASCII hex ("50" -> "P"). - if (/^\d+$/.test(str)) return false; - const decoded = tryHexDecode(str); - return decoded !== str; + function getSCDisplayName(data, fallback = 'Smart Contract') { + const stringkeys = data?.stringkeys || {}; + const rawName = stringkeys.nameHdr ?? stringkeys.var_header_name ?? stringkeys.dURL; + if (rawName == null || rawName === '') return fallback; + + return formatSCDisplayValue(rawName).display || fallback; } // SC Discovery state @@ -1276,9 +1263,7 @@ watchingInProgress = true; try { - const scName = searchResult?.data?.stringkeys?.nameHdr || - searchResult?.data?.stringkeys?.dURL || - searchQuery.substring(0, 16); + const scName = getSCDisplayName(searchResult?.data, searchQuery.substring(0, 16)); const result = await WatchSmartContract(searchQuery, scName); if (result.success) { toast.success('Now watching this smart contract'); @@ -2341,19 +2326,16 @@
Write down these 25 words in order. This is the ONLY way to recover your wallet.
-{revealedSecretKey}
- {revealedPublicKey}
-