-
Notifications
You must be signed in to change notification settings - Fork 7
feat: add Trust Factor app #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 17 commits
31a7f31
41b4c40
bcf2234
725b171
478298e
7d8f21e
4a9fa29
76a631c
81ae662
fc0bba1
5731942
391b45d
b699819
0c9d450
cb20391
30a25b4
40542fb
07fcf45
0a34fc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
| // ============================================================================= | ||
|
|
||
| // 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() | ||
| } | ||
|
gfanton marked this conversation as resolved.
|
||
|
|
||
| // GetOwnable returns the ownable object for ownership management | ||
| func GetOwnable() *ownable.Ownable { | ||
| return owner | ||
| } | ||
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. See
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
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 | ||
| } | ||
| 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() | ||
| } |
| 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{} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| module = "gno.land/r/greg007/trustfactor" | ||
| gno = "0.9" |
| 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" | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.