Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions packages/r/greg007/TrustFactor/admin.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package trustfactor

import (
"strings"

"gno.land/p/nt/ownable"
)

// =============================================================================
// ADMIN FUNCTIONS (OWNER ONLY)
// =============================================================================
Comment thread
gfanton marked this conversation as resolved.
Outdated

// RemoveUser removes a malicious user from the system (emergency function)
func RemoveUser(cur realm, target address) {
owner.AssertOwnedByCurrent()

trustScores.Remove(string(target))
voteHistory.Remove(string(target))

// Clean up all votes involving the target user
keysToRemove := []string{}
userVotes.Iterate("", "", func(key string, value interface{}) bool {
if strings.Contains(key, string(target)) {
keysToRemove = append(keysToRemove, key)
}
return false
})
for _, key := range keysToRemove {
userVotes.Remove(key)
}

totalUsers--
invalidateCache()
}
Comment thread
gfanton marked this conversation as resolved.

// GetOwnable returns the ownable object for ownership management
func GetOwnable() *ownable.Ownable {
return owner
}
128 changes: 128 additions & 0 deletions packages/r/greg007/TrustFactor/cache.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package trustfactor

import (
"strings"
"time"
)

// =============================================================================
// PERFORMANCE OPTIMIZATION - CACHING & SORTING
// =============================================================================

// invalidateCache clears the sorted user cache when data changes
func invalidateCache() {
lastSortTime = 0
sortedUsers = make([]address, 0)
}

// GetSortedUsers returns users sorted by composite score with caching
func GetSortedUsers() []address {
now := time.Now().Unix()

// Check if cache is still valid
if now-lastSortTime < SORT_CACHE_TTL && len(sortedUsers) == totalUsers {
return sortedUsers
}

// Rebuild cache if expired or invalid
return rebuildSortCache()
}

// rebuildSortCache rebuilds the sorted user cache using optimized quicksort
func rebuildSortCache() []address {
// Convert AVL tree to slice for sorting
users := make([]address, 0, trustScores.Size())
trustScores.Iterate("", "", func(key string, value interface{}) bool {
users = append(users, address(key))
return false
})

// Quicksort by composite score (descending)
quickSortByScore(users, 0, len(users)-1)

// Update cache
sortedUsers = users
lastSortTime = time.Now().Unix()
totalUsers = len(users)

return sortedUsers
}

// quickSortByScore implements quicksort algorithm for user addresses by composite score
func quickSortByScore(users []address, low, high int) {
if low < high {
pi := partitionByScore(users, low, high)
quickSortByScore(users, low, pi-1)
quickSortByScore(users, pi+1, high)
}
}

// partitionByScore partitions the array for quicksort (descending order)
func partitionByScore(users []address, low, high int) int {
pivotValue, _ := trustScores.Get(string(users[high]))
pivotScore := calculateCompositeScore(*pivotValue.(*TrustScore))
i := low - 1

for j := low; j < high; j++ {
currentValue, _ := trustScores.Get(string(users[j]))
currentScore := calculateCompositeScore(*currentValue.(*TrustScore))
if currentScore > pivotScore {
i++
users[i], users[j] = users[j], users[i]
}
}

users[i+1], users[high] = users[high], users[i+1]
return i + 1
}

// =============================================================================
// SEARCH AND FILTERING
// =============================================================================

// FilterUsers returns paginated and filtered user results
func FilterUsers(searchTerm string, limit int, offset int) ([]address, int) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check if you can use p/nt/avl/pager for paging in this case instead of writing your own pager

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't, because I'm currently using an array and not a tree. If I switch to a tree, I think it will unnecessarily complicate the code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not only about paging. In Gno, every global variable is persisted to storage after each transaction. If you maintain a sorted slice, any update requires reallocating and rewriting the entire slice to storage. With 100k users, that's 100k storage writes per update.
Instead, use an AVL tree keyed by score. AVL trees maintain lexicographic order automatically.

See gno.land/r/leon/hor for a pattern using padded score keys for numeric sorting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if searchTerm == "" {
sorted := GetSortedUsers()
total := len(sorted)

end := offset + limit
if end > total {
end = total
}
if offset > total {
return []address{}, total
}

return sorted[offset:end], total
}

var filtered []address
for _, addr := range GetSortedUsers() {
addrStr := string(addr)
if containsSubstring(addrStr, searchTerm) {
filtered = append(filtered, addr)
}
}

total := len(filtered)
end := offset + limit
if end > total {
end = total
}
if offset > total {
return []address{}, total
}

return filtered[offset:end], total
}

// containsSubstring checks if a string contains a substring (case-sensitive)
func containsSubstring(str, substr string) bool {
Comment thread
gfanton marked this conversation as resolved.
return strings.Index(str, substr) >= 0
}

// GetPerformanceStats returns cache and performance statistics
func GetPerformanceStats() (int, int, int64) {
return totalUsers, len(sortedUsers), lastSortTime
}
84 changes: 84 additions & 0 deletions packages/r/greg007/TrustFactor/comments.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package trustfactor

import (
"chain/runtime"
"time"

"gno.land/p/nt/avl"
)

// =============================================================================
// COMMENT FUNCTIONS
// =============================================================================

// AddComment adds a comment to a user's profile
func AddComment(cur realm, target address, text string) {
caller := runtime.OriginCaller()

// Verify caller is registered
if !trustScores.Has(string(caller)) {
panic("commenter must be registered")
}

// Verify target exists
if !trustScores.Has(string(target)) {
panic("target user not registered")
}

// Validate comment text
if len(text) == 0 {
panic("comment text cannot be empty")
}
if len(text) > 500 {
panic("comment text too long (max 500 characters)")
}

// Initialize AVL tree for target if not exists
var targetTree *avl.Tree
if treeValue, exists := userComments.Get(string(target)); exists {
targetTree = treeValue.(*avl.Tree)
} else {
targetTree = avl.NewTree()
userComments.Set(string(target), targetTree)
}

// Create comment
comment := Comment{
Author: caller,
Text: text,
Timestamp: time.Now().Unix(),
}

// Use timestamp + author as unique key
key := string(comment.Timestamp) + "_" + string(caller)
targetTree.Set(key, comment)
}

// GetComments returns all comments for a user's profile
func GetComments(target address) []Comment {
treeValue, exists := userComments.Get(string(target))
if !exists {
return []Comment{}
}
tree := treeValue.(*avl.Tree)

comments := make([]Comment, 0)
tree.ReverseIterate("", "", func(key string, value any) bool {
if comment, ok := value.(Comment); ok {
comments = append(comments, comment)
}
return false
})

return comments
}

// GetCommentCount returns the number of comments for a user
func GetCommentCount(target address) int {
treeValue, exists := userComments.Get(string(target))
if !exists {
return 0
}
tree := treeValue.(*avl.Tree)
return tree.Size()
}
51 changes: 51 additions & 0 deletions packages/r/greg007/TrustFactor/getters.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package trustfactor

// =============================================================================
// BASIC GETTER FUNCTIONS
// =============================================================================

// GetTrustScore returns the raw trust score for a user
func GetTrustScore(addr address) (float64, bool) {
value, exists := trustScores.Get(string(addr))
if !exists {
return 0.0, false
}
trust := value.(*TrustScore)
return trust.Score, true
}

// GetCompositeScore returns the calculated composite score (with time decay and confidence)
func GetCompositeScore(addr address) (float64, bool) {
value, exists := trustScores.Get(string(addr))
if !exists {
return 0.0, false
}
trust := value.(*TrustScore)
return calculateCompositeScore(*trust), true
}

// GetTrustDetails returns the complete TrustScore struct for a user
func GetTrustDetails(addr address) TrustScore {
value, exists := trustScores.Get(string(addr))
if !exists {
return TrustScore{Score: 0, LastUpdate: 0, Evaluator: ""}
}
return *value.(*TrustScore)
}

// =============================================================================
// DATA ACCESS FUNCTIONS
// =============================================================================

// GetTotalUsers returns the total number of registered users
func GetTotalUsers() int {
return totalUsers
}

// GetVoteHistory returns the voting history for a specific user
func GetVoteHistory(addr address) []VoteHistory {
if historyValue, exists := voteHistory.Get(string(addr)); exists {
return historyValue.([]VoteHistory)
}
return []VoteHistory{}
}
2 changes: 2 additions & 0 deletions packages/r/greg007/TrustFactor/gnomod.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module = "gno.land/r/greg007/trustfactor"
gno = "0.9"
81 changes: 81 additions & 0 deletions packages/r/greg007/TrustFactor/helpers.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package trustfactor

import (
"strconv"
"time"

"gno.land/r/sys/users"
)

// formatTimeAgo converts a Unix timestamp into a human-readable relative time string.
// The format varies based on the time elapsed: "Today", "N days ago", "N week(s) ago", etc.
func formatTimeAgo(timestamp int64) string {
now := time.Now().Unix()
daysSince := (now - timestamp) / 86400

if daysSince == 0 {
return "Today"
}
if daysSince == 1 {
return "1 day ago"
}
if daysSince < 7 {
return strconv.FormatInt(daysSince, 10) + " days ago"
}
if daysSince < 30 {
weeks := daysSince / 7
return strconv.FormatInt(weeks, 10) + " week(s) ago"
}
if daysSince < 365 {
months := daysSince / 30
return strconv.FormatInt(months, 10) + " month(s) ago"
}

years := daysSince / 365
return strconv.FormatInt(years, 10) + " year(s) ago"
}

// getDisplayName returns the registered username from sys/users, or a shortened
// address if no username is registered for the given address.
func getDisplayName(addr address) string {
userData := users.ResolveAddress(addr)

if userData != nil {
return userData.Name()
}

return shortenAddress(string(addr), 12, 4)
}

// shortenAddress truncates a long address string to show only the prefix and suffix,
// separated by "..." for display purposes.
func shortenAddress(addr string, prefixLen, suffixLen int) string {
if len(addr) <= prefixLen+suffixLen {
return addr
}
return addr[:prefixLen] + "..." + addr[len(addr)-suffixLen:]
}

// getConfidenceIcon returns an emoji icon representing the confidence level:
// 🟢 for high (≥0.8), 🟡 for medium (≥0.5), 🔴 for low (<0.5).
func getConfidenceIcon(confidence float64) string {
if confidence >= 0.8 {
return "🟢"
}
if confidence >= 0.5 {
return "🟡"
}
return "🔴"
}

// getConfidenceColor returns a hex color code based on confidence level for SVG rendering:
// black for high confidence, dark gray for medium, light gray for low.
func getConfidenceColor(confidence float64) string {
if confidence >= 0.8 {
return "#000000"
}
if confidence >= 0.5 {
return "#666666"
}
return "#CCCCCC"
}
Loading
Loading