Search Apps Documentation Source Content File Folder Download Copy Actions Download

core.gno

3.36 Kb · 105 lines
  1// Package memba_points_migration_core_v1 holds the pure, unit-testable logic for the one-time XP
  2// migration into memba_points_v1: parsing + validating the owner-submitted batch, and planning which
  3// entries to award (skipping already-migrated addresses, refusing once finalized). It imports no realm
  4// and holds no state — the realm (r/samcrew/memba_points_migration_v1) owns the state + the Award call.
  5// Splitting it out this way is what makes the migration's risky logic testable under bare `gno test`
  6// (the realm itself can't be, since it imports the unpublished points_v1).
  7package memba_points_migration_core_v1
  8
  9import "strings"
 10
 11// Entry is one validated (address, amount) grant.
 12type Entry struct {
 13	Addr   string
 14	Amount int64
 15}
 16
 17// PlanBatch parses a "addr:amount,addr:amount,..." batch and returns the entries to award:
 18//   - panics if finalized (migration permanently closed)
 19//   - panics on any malformed entry (bad format, non-address, amount outside 1..maxAward) — fail-fast,
 20//     so a botched export never half-applies
 21//   - caps the batch at maxEntries (per-tx gas bound)
 22//   - drops entries whose Addr already satisfies isMigrated (idempotent re-runs)
 23func PlanBatch(csv string, maxAward int64, maxEntries int, finalized bool, isMigrated func(string) bool) []Entry {
 24	if finalized {
 25		panic("migration finalized")
 26	}
 27	all := ParseBatch(csv, maxAward, maxEntries)
 28	out := make([]Entry, 0, len(all))
 29	for _, e := range all {
 30		if isMigrated(e.Addr) {
 31			continue
 32		}
 33		out = append(out, e)
 34	}
 35	return out
 36}
 37
 38// ParseBatch parses + validates the batch (no dedup against prior runs). Exposed for testing + reuse.
 39func ParseBatch(csv string, maxAward int64, maxEntries int) []Entry {
 40	if len(csv) == 0 {
 41		panic("empty batch")
 42	}
 43	parts := strings.Split(csv, ",")
 44	if len(parts) > maxEntries {
 45		panic("batch exceeds maxEntries")
 46	}
 47	out := make([]Entry, 0, len(parts))
 48	seen := make(map[string]bool)
 49	for _, p := range parts {
 50		kv := strings.Split(p, ":")
 51		if len(kv) != 2 {
 52			panic("entry must be addr:amount")
 53		}
 54		addr := kv[0]
 55		if !isValidAddress(addr) {
 56			panic("invalid address in batch")
 57		}
 58		if seen[addr] {
 59			panic("duplicate address in batch")
 60		}
 61		amt, ok := parseAmount(kv[1])
 62		if !ok || amt < 1 || amt > maxAward {
 63			panic("amount out of range (1..maxAward)")
 64		}
 65		seen[addr] = true
 66		out = append(out, Entry{Addr: addr, Amount: amt})
 67	}
 68	return out
 69}
 70
 71// isValidAddress does a cheap structural check for a gno bech32 address ("g1" + 38 lowercase-alnum
 72// chars). Deep checksum validation is unnecessary: the batch is owner-submitted from a trusted export,
 73// and memba_points_v1.Award is the ultimate authority on the address. Loose-accepts (never false-rejects
 74// a real address); a slightly-malformed one is an owner/export bug caught at the ceremony dry-run.
 75func isValidAddress(s string) bool {
 76	if len(s) != 40 || s[0] != 'g' || s[1] != '1' {
 77		return false
 78	}
 79	for i := 2; i < len(s); i++ {
 80		c := s[i]
 81		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) {
 82			return false
 83		}
 84	}
 85	return true
 86}
 87
 88// parseAmount parses a non-negative base-10 int64 (no strconv dep, overflow-guarded).
 89func parseAmount(s string) (int64, bool) {
 90	if len(s) == 0 || len(s) > 19 {
 91		return 0, false
 92	}
 93	var n int64
 94	for i := 0; i < len(s); i++ {
 95		c := s[i]
 96		if c < '0' || c > '9' {
 97			return 0, false
 98		}
 99		n = n*10 + int64(c-'0')
100		if n < 0 {
101			return 0, false
102		}
103	}
104	return n, true
105}