diff --git a/cmd/africastalking/main.go b/cmd/africastalking/main.go index 8aaacde..4705b1a 100644 --- a/cmd/africastalking/main.go +++ b/cmd/africastalking/main.go @@ -16,8 +16,8 @@ import ( "git.defalsify.org/vise.git/lang" "git.defalsify.org/vise.git/resource" - "git.grassecon.net/grassrootseconomics/visedriver/config" - "git.grassecon.net/grassrootseconomics/visedriver/initializers" + "git.grassecon.net/grassrootseconomics/sarafu-vise/config" + "git.grassecon.net/grassrootseconomics/common/env" "git.grassecon.net/grassrootseconomics/visedriver/storage" "git.grassecon.net/grassrootseconomics/visedriver/session" @@ -36,7 +36,7 @@ var ( ) func init() { - initializers.LoadEnvVariables() + env.LoadEnvVariables() } func main() { @@ -58,8 +58,8 @@ func main() { flag.StringVar(&connStr, "c", "", "connection string") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") - flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") - flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") + flag.StringVar(&host, "h", env.GetEnv("HOST", "127.0.0.1"), "http host") + flag.UintVar(&port, "p", env.GetEnvUint("PORT", 7123), "http port") flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") flag.Var(&langs, "language", "add symbol resolution for language") flag.Parse() @@ -147,7 +147,7 @@ func main() { sh := at.NewATSessionHandler(bsh) mux := http.NewServeMux() - mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh) + mux.Handle(env.GetEnv("AT_ENDPOINT", "/"), sh) s := &http.Server{ Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), diff --git a/cmd/async/main.go b/cmd/async/main.go index 3b06692..f024d5d 100644 --- a/cmd/async/main.go +++ b/cmd/async/main.go @@ -14,8 +14,8 @@ import ( "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" - "git.grassecon.net/grassrootseconomics/visedriver/config" - "git.grassecon.net/grassrootseconomics/visedriver/initializers" + "git.grassecon.net/grassrootseconomics/sarafu-vise/config" + "git.grassecon.net/grassrootseconomics/common/env" "git.grassecon.net/grassrootseconomics/visedriver/storage" "git.grassecon.net/grassrootseconomics/visedriver/session" "git.grassecon.net/grassrootseconomics/visedriver/request" @@ -31,7 +31,7 @@ var ( ) func init() { - initializers.LoadEnvVariables() + env.LoadEnvVariables() } type asyncRequestParser struct { @@ -66,8 +66,8 @@ func main() { flag.StringVar(&connStr, "c", "", "connection string") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") - flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") - flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") + flag.StringVar(&host, "h", env.GetEnv("HOST", "127.0.0.1"), "http host") + flag.UintVar(&port, "p", env.GetEnvUint("PORT", 7123), "http port") flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") flag.Var(&langs, "language", "add symbol resolution for language") flag.Parse() diff --git a/cmd/http/main.go b/cmd/http/main.go index bcd454f..e2339a6 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -16,8 +16,8 @@ import ( "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" - "git.grassecon.net/grassrootseconomics/visedriver/config" - "git.grassecon.net/grassrootseconomics/visedriver/initializers" + "git.grassecon.net/grassrootseconomics/sarafu-vise/config" + "git.grassecon.net/grassrootseconomics/common/env" httpsession "git.grassecon.net/grassrootseconomics/visedriver/session/http" "git.grassecon.net/grassrootseconomics/visedriver/storage" "git.grassecon.net/grassrootseconomics/visedriver/session" @@ -34,7 +34,7 @@ var ( ) func init() { - initializers.LoadEnvVariables() + env.LoadEnvVariables() } func main() { @@ -54,8 +54,8 @@ func main() { flag.StringVar(&connStr, "c", "", "connection string") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") - flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") - flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") + flag.StringVar(&host, "h", env.GetEnv("HOST", "127.0.0.1"), "http host") + flag.UintVar(&port, "p", env.GetEnvUint("PORT", 7123), "http port") flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") flag.Var(&langs, "language", "add symbol resolution for language") flag.Parse() diff --git a/cmd/main.go b/cmd/main.go index 1a7d218..8963fcb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -11,8 +11,8 @@ import ( "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/lang" - "git.grassecon.net/grassrootseconomics/visedriver/config" - "git.grassecon.net/grassrootseconomics/visedriver/initializers" + "git.grassecon.net/grassrootseconomics/sarafu-vise/config" + "git.grassecon.net/grassrootseconomics/common/env" "git.grassecon.net/grassrootseconomics/visedriver/storage" httpremote "git.grassecon.net/grassrootseconomics/sarafu-api/remote/http" "git.grassecon.net/grassrootseconomics/sarafu-vise/args" @@ -26,7 +26,7 @@ var ( ) func init() { - initializers.LoadEnvVariables() + env.LoadEnvVariables() } // TODO: external script automatically generate language handler list from select language vise code OR consider dynamic menu generation script possibility diff --git a/cmd/ssh/main.go b/cmd/ssh/main.go index 82e5bbf..0444ceb 100644 --- a/cmd/ssh/main.go +++ b/cmd/ssh/main.go @@ -14,8 +14,8 @@ import ( "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/logging" - "git.grassecon.net/grassrootseconomics/visedriver/config" - "git.grassecon.net/grassrootseconomics/visedriver/initializers" + "git.grassecon.net/grassrootseconomics/sarafu-vise/config" + "git.grassecon.net/grassrootseconomics/common/env" "git.grassecon.net/grassrootseconomics/sarafu-vise/ssh" "git.grassecon.net/grassrootseconomics/visedriver/storage" ) @@ -30,7 +30,7 @@ var ( ) func init() { - initializers.LoadEnvVariables() + env.LoadEnvVariables() } func main() { diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..53b44ab --- /dev/null +++ b/config/config.go @@ -0,0 +1,66 @@ +package config + +import ( + "strings" + + "git.grassecon.net/grassrootseconomics/common/env" + apiconfig "git.grassecon.net/grassrootseconomics/sarafu-api/config" +) + + +var ( + defaultLanguage = "eng" + languages []string +) + +var ( + DbConn string + DefaultLanguage string + Languages []string +) + +func setLanguage() error { + defaultLanguage = env.GetEnv("DEFAULT_LANGUAGE", defaultLanguage) + languages = strings.Split(env.GetEnv("LANGUAGES", defaultLanguage), ",") + haveDefaultLanguage := false + for i, v := range(languages) { + languages[i] = strings.ReplaceAll(v, " ", "") + if languages[i] == defaultLanguage { + haveDefaultLanguage = true + } + } + + if !haveDefaultLanguage { + languages = append([]string{defaultLanguage}, languages...) + } + + return nil +} + + + +func setConn() error { + DbConn = env.GetEnv("DB_CONN", "") + return nil +} + +// LoadConfig initializes the configuration values after environment variables are loaded. +func LoadConfig() error { + //err := apiconfig.SetBase() + err := apiconfig.LoadConfig() + if err != nil { + return err + } + err = setConn() + if err != nil { + return err + } + err = setLanguage() + if err != nil { + return err + } + DefaultLanguage = defaultLanguage + Languages = languages + + return nil +} diff --git a/debug/cap.go b/debug/cap.go new file mode 100644 index 0000000..458bb48 --- /dev/null +++ b/debug/cap.go @@ -0,0 +1,5 @@ +package debug + +var ( + DebugCap uint32 +) diff --git a/debug/db.go b/debug/db.go new file mode 100644 index 0000000..ba01abe --- /dev/null +++ b/debug/db.go @@ -0,0 +1,84 @@ +package debug + +import ( + "fmt" + "encoding/binary" + + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" + visedb "git.defalsify.org/vise.git/db" +) + +var ( + dbTypStr map[storedb.DataTyp]string = make(map[storedb.DataTyp]string) +) + +type KeyInfo struct { + SessionId string + Typ uint8 + SubTyp storedb.DataTyp + Label string + Description string +} + +func (k KeyInfo) String() string { + v := uint16(k.SubTyp) + s := subTypToString(k.SubTyp) + if s == "" { + v = uint16(k.Typ) + s = typToString(k.Typ) + } + return fmt.Sprintf("Session Id: %s\nTyp: %s (%d)\n", k.SessionId, s, v) +} + +func ToKeyInfo(k []byte, sessionId string) (KeyInfo, error) { + o := KeyInfo{} + b := []byte(sessionId) + + if len(k) <= len(b) { + return o, fmt.Errorf("storage key missing") + } + + o.SessionId = sessionId + + o.Typ = uint8(k[0]) + k = k[1:] + o.SessionId = string(k[:len(b)]) + k = k[len(b):] + + if o.Typ == visedb.DATATYPE_USERDATA { + if len(k) == 0 { + return o, fmt.Errorf("missing subtype key") + } + v := binary.BigEndian.Uint16(k[:2]) + o.SubTyp = storedb.DataTyp(v) + o.Label = subTypToString(o.SubTyp) + k = k[2:] + } else { + o.Label = typToString(o.Typ) + } + + if len(k) != 0 { + return o, fmt.Errorf("excess key information") + } + + return o, nil +} + +func FromKey(k []byte) (KeyInfo, error) { + o := KeyInfo{} + + if len(k) < 4 { + return o, fmt.Errorf("insufficient key length") + } + + sessionIdBytes := k[1:len(k)-2] + return ToKeyInfo(k, string(sessionIdBytes)) +} + +func subTypToString(v storedb.DataTyp) string { + return dbTypStr[v + visedb.DATATYPE_USERDATA + 1] +} + +func typToString(v uint8) string { + return dbTypStr[storedb.DataTyp(uint16(v))] +} diff --git a/debug/db_debug.go b/debug/db_debug.go new file mode 100644 index 0000000..13de8ac --- /dev/null +++ b/debug/db_debug.go @@ -0,0 +1,44 @@ +// +build debugdb + +package debug + +import ( + "git.defalsify.org/vise.git/db" + + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" +) + +func init() { + DebugCap |= 1 + dbTypStr[db.DATATYPE_STATE] = "internal state" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_TRACKING_ID] = "tracking id" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_PUBLIC_KEY] = "public key" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_ACCOUNT_PIN] = "account pin" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_FIRST_NAME] = "first name" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_FAMILY_NAME] = "family name" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_YOB] = "year of birth" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_LOCATION] = "location" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_GENDER] = "gender" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_OFFERINGS] = "offerings" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_RECIPIENT] = "recipient" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_AMOUNT] = "amount" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_TEMPORARY_VALUE] = "temporary value" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_ACTIVE_SYM] = "active sym" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_ACTIVE_BAL] = "active bal" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_BLOCKED_NUMBER] = "blocked number" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_PUBLIC_KEY_REVERSE] = "public_key_reverse" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_ACTIVE_DECIMAL] = "active decimal" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_ACTIVE_ADDRESS] = "active address" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_VOUCHER_SYMBOLS] = "voucher symbols" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_VOUCHER_BALANCES] = "voucher balances" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_VOUCHER_DECIMALS] = "voucher decimals" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_VOUCHER_ADDRESSES] = "voucher addresses" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_TX_SENDERS] = "tx senders" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_TX_RECIPIENTS] = "tx recipients" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_TX_VALUES] = "tx values" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_TX_ADDRESSES] = "tx addresses" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_TX_HASHES] = "tx hashes" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_TX_DATES] = "tx dates" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_TX_SYMBOLS] = "tx symbols" + dbTypStr[db.DATATYPE_USERDATA + 1 + storedb.DATA_TX_DECIMALS] = "tx decimals" +} diff --git a/debug/db_test.go b/debug/db_test.go new file mode 100644 index 0000000..c8c4d30 --- /dev/null +++ b/debug/db_test.go @@ -0,0 +1,78 @@ +package debug + +import ( + "testing" + + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" + visedb "git.defalsify.org/vise.git/db" +) + +func TestDebugDbSubKeyInfo(t *testing.T) { + s := "foo" + b := []byte{0x20} + b = append(b, []byte(s)...) + b = append(b, []byte{0x00, 0x02}...) + r, err := ToKeyInfo(b, s) + if err != nil { + t.Fatal(err) + } + if r.SessionId != s { + t.Fatalf("expected %s, got %s", s, r.SessionId) + } + if r.Typ != 32 { + t.Fatalf("expected 64, got %d", r.Typ) + } + if r.SubTyp != 2 { + t.Fatalf("expected 2, got %d", r.SubTyp) + } + if DebugCap & 1 > 0 { + if r.Label != "tracking id" { + t.Fatalf("expected 'tracking id', got '%s'", r.Label) + } + } +} + +func TestDebugDbKeyInfo(t *testing.T) { + s := "bar" + b := []byte{0x10} + b = append(b, []byte(s)...) + r, err := ToKeyInfo(b, s) + if err != nil { + t.Fatal(err) + } + if r.SessionId != s { + t.Fatalf("expected %s, got %s", s, r.SessionId) + } + if r.Typ != 16 { + t.Fatalf("expected 16, got %d", r.Typ) + } + if DebugCap & 1 > 0 { + if r.Label != "internal state" { + t.Fatalf("expected 'internal_state', got '%s'", r.Label) + } + } +} + +func TestDebugDbKeyInfoRestore(t *testing.T) { + s := "bar" + b := []byte{visedb.DATATYPE_USERDATA} + b = append(b, []byte(s)...) + k := storedb.ToBytes(storedb.DATA_ACTIVE_SYM) + b = append(b, k...) + + r, err := ToKeyInfo(b, s) + if err != nil { + t.Fatal(err) + } + if r.SessionId != s { + t.Fatalf("expected %s, got %s", s, r.SessionId) + } + if r.Typ != 32 { + t.Fatalf("expected 32, got %d", r.Typ) + } + if DebugCap & 1 > 0 { + if r.Label != "active sym" { + t.Fatalf("expected 'active sym', got '%s'", r.Label) + } + } +} diff --git a/devtools/lang/main.go b/devtools/lang/main.go new file mode 100644 index 0000000..2cbf2a3 --- /dev/null +++ b/devtools/lang/main.go @@ -0,0 +1,126 @@ +// create language files from environment +package main + +import ( + "flag" + "fmt" + "os" + "path" + "strings" + + "git.defalsify.org/vise.git/logging" + "git.defalsify.org/vise.git/lang" + "git.grassecon.net/grassrootseconomics/sarafu-vise/config" + "git.grassecon.net/grassrootseconomics/common/env" +) + +const ( + + changeHeadSrc = `LOAD reset_account_authorized 0 +LOAD reset_incorrect 0 +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH pin_entry flag_account_authorized 0 +` + + selectSrc = `LOAD set_language 6 +RELOAD set_language +CATCH terms flag_account_created 0 +MOVE language_changed +` +) + +var ( + logg = logging.NewVanilla() + mouts string + incmps string +) + +func init() { + env.LoadEnvVariables() +} + +func toLanguageLabel(ln lang.Language) string { + s := ln.Name + v := strings.Split(s, " (") + if len(v) > 1 { + s = v[0] + } + return s +} + +func toLanguageKey(ln lang.Language) string { + s := toLanguageLabel(ln) + return strings.ToLower(s) +} + +func main() { + var srcDir string + + flag.StringVar(&srcDir, "o", ".", "resource dir write to") + flag.Parse() + + logg.Infof("start command", "dir", srcDir) + + err := config.LoadConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "config load error: %v", err) + os.Exit(1) + } + logg.Tracef("using languages", "lang", config.Languages) + + for i, v := range(config.Languages) { + ln, err := lang.LanguageFromCode(v) + if err != nil { + fmt.Fprintf(os.Stderr, "error parsing language: %s\n", v) + os.Exit(1) + } + n := i + 1 + s := toLanguageKey(ln) + mouts += fmt.Sprintf("MOUT %s %v\n", s, n) + v = "set_" + ln.Code + incmps += fmt.Sprintf("INCMP %s %v\n", v, n) + + p := path.Join(srcDir, v) + w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) + if err != nil { + fmt.Fprintf(os.Stderr, "failed open language set template output: %v\n", err) + os.Exit(1) + } + s = toLanguageLabel(ln) + defer w.Close() + _, err = w.Write([]byte(s)) + if err != nil { + fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) + os.Exit(1) + } + } + src := mouts + "HALT\n" + incmps + src += "INCMP . *\n" + + p := path.Join(srcDir, "select_language.vis") + w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) + if err != nil { + fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err) + os.Exit(1) + } + defer w.Close() + _, err = w.Write([]byte(src)) + if err != nil { + fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) + os.Exit(1) + } + + src = changeHeadSrc + src + p = path.Join(srcDir, "change_language.vis") + w, err = os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) + if err != nil { + fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err) + os.Exit(1) + } + defer w.Close() + _, err = w.Write([]byte(src)) + if err != nil { + fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) + os.Exit(1) + } +} diff --git a/devtools/store/dump/main.go b/devtools/store/dump/main.go index 005e3b1..826576b 100644 --- a/devtools/store/dump/main.go +++ b/devtools/store/dump/main.go @@ -7,8 +7,8 @@ import ( "os" "path" - "git.grassecon.net/grassrootseconomics/visedriver/config" - "git.grassecon.net/grassrootseconomics/visedriver/initializers" + "git.grassecon.net/grassrootseconomics/sarafu-vise/config" + "git.grassecon.net/grassrootseconomics/common/env" "git.grassecon.net/grassrootseconomics/visedriver/storage" "git.grassecon.net/grassrootseconomics/sarafu-vise/debug" "git.defalsify.org/vise.git/db" @@ -21,7 +21,7 @@ var ( ) func init() { - initializers.LoadEnvVariables() + env.LoadEnvVariables() } diff --git a/devtools/store/generate/main.go b/devtools/store/generate/main.go index c055717..3daa435 100644 --- a/devtools/store/generate/main.go +++ b/devtools/store/generate/main.go @@ -10,9 +10,9 @@ import ( testdataloader "github.com/peteole/testdata-loader" "git.defalsify.org/vise.git/logging" - "git.grassecon.net/grassrootseconomics/visedriver/config" + "git.grassecon.net/grassrootseconomics/sarafu-vise/config" "git.grassecon.net/grassrootseconomics/visedriver/storage" - "git.grassecon.net/grassrootseconomics/visedriver/initializers" + "git.grassecon.net/grassrootseconomics/common/env" "git.grassecon.net/grassrootseconomics/sarafu-vise/store" storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" ) @@ -24,7 +24,7 @@ var ( ) func init() { - initializers.LoadEnvVariables() + env.LoadEnvVariables() } func main() { diff --git a/go.mod b/go.mod index a4d8e1c..d23f254 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.23.4 require ( git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d - git.grassecon.net/grassrootseconomics/common v0.0.0-20250112090451-d33e94fda029 - git.grassecon.net/grassrootseconomics/sarafu-api v0.0.0-20250111211303-3ea726a0302c - git.grassecon.net/grassrootseconomics/visedriver v0.8.0-beta.10.0.20250112075903-5b312f9569f0 + git.grassecon.net/grassrootseconomics/common v0.0.0-20250112094202-96345daf4d75 + git.grassecon.net/grassrootseconomics/sarafu-api v0.0.0-20250112095506-c877bccff604 + git.grassecon.net/grassrootseconomics/visedriver v0.8.0-beta.10.0.20250112093740-ec4ad6e44b3f git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250112092227-e0892ac0be76 github.com/alecthomas/assert/v2 v2.2.2 github.com/gofrs/uuid v4.4.0+incompatible diff --git a/go.sum b/go.sum index 4a31d94..3a0bd11 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw= git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= -git.grassecon.net/grassrootseconomics/common v0.0.0-20250112090451-d33e94fda029 h1:UKi91cAsxexbVKVuD1YBU/VF5+50ryyohfeJ4heSkVs= -git.grassecon.net/grassrootseconomics/common v0.0.0-20250112090451-d33e94fda029/go.mod h1:WcWUIkf9vKW4Vy+hc1QJORFZgMLtVQiTWoObue5EQ+8= -git.grassecon.net/grassrootseconomics/sarafu-api v0.0.0-20250111211303-3ea726a0302c h1:5h1nsczPXBhOfe5Wbyccp3ontooztKUVAtDw8aoT8BI= -git.grassecon.net/grassrootseconomics/sarafu-api v0.0.0-20250111211303-3ea726a0302c/go.mod h1:CXdVutRsCkdWWCJ9hELi/72z3FDKkhLksxCXBSnjuKI= -git.grassecon.net/grassrootseconomics/visedriver v0.8.0-beta.10.0.20250112075903-5b312f9569f0 h1:gICHTIEhiBv3SJ7kdcogUIGtfmoaWvZGalp2sBRj2c0= -git.grassecon.net/grassrootseconomics/visedriver v0.8.0-beta.10.0.20250112075903-5b312f9569f0/go.mod h1:E6W7ZOa7ZvVr0Bc5ot0LNSwpSPYq4hXlAIvEPy3AJ7U= +git.grassecon.net/grassrootseconomics/common v0.0.0-20250112094202-96345daf4d75 h1:g/dZdu1teBggAMFCwm7BpV44bz394+nhAduULjX6Kk4= +git.grassecon.net/grassrootseconomics/common v0.0.0-20250112094202-96345daf4d75/go.mod h1:wgQJZGIS6QuNLHqDhcsvehsbn5PvgV7aziRebMnJi60= +git.grassecon.net/grassrootseconomics/sarafu-api v0.0.0-20250112095506-c877bccff604 h1:5s+vedXGY9Glt0uhbWgxH5O2rjOqDqgwvUBdZktu+PA= +git.grassecon.net/grassrootseconomics/sarafu-api v0.0.0-20250112095506-c877bccff604/go.mod h1:8qm6dwqOzUdAjxdeB/f0kiBwYvI4Mr1uYVaJpP2w3c4= +git.grassecon.net/grassrootseconomics/visedriver v0.8.0-beta.10.0.20250112093740-ec4ad6e44b3f h1:Ev39tqya9KxU5ABXFOv5TXl5s5rZnuT4B6BYZ1aIzns= +git.grassecon.net/grassrootseconomics/visedriver v0.8.0-beta.10.0.20250112093740-ec4ad6e44b3f/go.mod h1:E6W7ZOa7ZvVr0Bc5ot0LNSwpSPYq4hXlAIvEPy3AJ7U= git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250112092227-e0892ac0be76 h1:3v0Q/baP/8FEe4kl2DC6pirMgFOAbn69O2f5ddOjcoI= git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250112092227-e0892ac0be76/go.mod h1:JEfOHnOCCwH8s7eevu4ImTdO8oQwRD/bqYtmfT/pwzQ= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= diff --git a/store/db/db.go b/store/db/db.go new file mode 100644 index 0000000..91341d3 --- /dev/null +++ b/store/db/db.go @@ -0,0 +1,134 @@ +package db + +import ( + "encoding/binary" + "errors" + + "git.defalsify.org/vise.git/logging" +) + +// DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA. +// +// All keys are used only within the context of a single account. Unless otherwise specified, the user context is the session id. +// +// * The first byte is vise/db.DATATYPE_USERDATA +// * The last 2 bytes are the DataTyp value, big-endian. +// * The intermediate bytes are the id of the user context. +// +// All values are strings +type DataTyp uint16 + +const ( + // API Tracking id to follow status of account creation + DATA_TRACKING_ID = iota + // EVM address returned from API on account creation + DATA_PUBLIC_KEY + // Currently active PIN used to authenticate ussd state change requests + DATA_ACCOUNT_PIN + // The first name of the user + DATA_FIRST_NAME + // The last name of the user + DATA_FAMILY_NAME + // The year-of-birth of the user + DATA_YOB + // The location of the user + DATA_LOCATION + // The gender of the user + DATA_GENDER + // The offerings description of the user + DATA_OFFERINGS + // The ethereum address of the recipient of an ongoing send request + DATA_RECIPIENT + // The voucher value amount of an ongoing send request + DATA_AMOUNT + // A general swap field for temporary values + DATA_TEMPORARY_VALUE + // Currently active voucher symbol of user + DATA_ACTIVE_SYM + // Voucher balance of user's currently active voucher + DATA_ACTIVE_BAL + // String boolean indicating whether use of PIN is blocked + DATA_BLOCKED_NUMBER + // Reverse mapping of a user's evm address to a session id. + DATA_PUBLIC_KEY_REVERSE + // Decimal count of the currently active voucher + DATA_ACTIVE_DECIMAL + // EVM address of the currently active voucher + DATA_ACTIVE_ADDRESS + //Holds count of the number of incorrect PIN attempts + DATA_INCORRECT_PIN_ATTEMPTS + //ISO 639 code for the selected language. + DATA_SELECTED_LANGUAGE_CODE +) + +const ( + // List of valid voucher symbols in the user context. + DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota + // List of voucher balances for vouchers valid in the user context. + DATA_VOUCHER_BALANCES + // List of voucher decimal counts for vouchers valid in the user context. + DATA_VOUCHER_DECIMALS + // List of voucher EVM addresses for vouchers valid in the user context. + DATA_VOUCHER_ADDRESSES + // List of senders for valid transactions in the user context. +) + +const ( + DATA_TX_SENDERS = 512 + iota + // List of recipients for valid transactions in the user context. + DATA_TX_RECIPIENTS + // List of voucher values for valid transactions in the user context. + DATA_TX_VALUES + // List of voucher EVM addresses for valid transactions in the user context. + DATA_TX_ADDRESSES + // List of valid transaction hashes in the user context. + DATA_TX_HASHES + // List of transaction dates for valid transactions in the user context. + DATA_TX_DATES + // List of voucher symbols for valid transactions in the user context. + DATA_TX_SYMBOLS + // List of voucher decimal counts for valid transactions in the user context. + DATA_TX_DECIMALS +) + +var ( + logg = logging.NewVanilla().WithDomain("urdt-common") +) + +func typToBytes(typ DataTyp) []byte { + var b [2]byte + binary.BigEndian.PutUint16(b[:], uint16(typ)) + return b[:] +} + +func PackKey(typ DataTyp, data []byte) []byte { + v := typToBytes(typ) + return append(v, data...) +} + +func StringToDataTyp(str string) (DataTyp, error) { + switch str { + case "DATA_FIRST_NAME": + return DATA_FIRST_NAME, nil + case "DATA_FAMILY_NAME": + return DATA_FAMILY_NAME, nil + case "DATA_YOB": + return DATA_YOB, nil + case "DATA_LOCATION": + return DATA_LOCATION, nil + case "DATA_GENDER": + return DATA_GENDER, nil + case "DATA_OFFERINGS": + return DATA_OFFERINGS, nil + + default: + return 0, errors.New("invalid DataTyp string") + } +} + +// ToBytes converts DataTyp or int to a byte slice +func ToBytes[T ~uint16 | int](value T) []byte { + bytes := make([]byte, 2) + binary.BigEndian.PutUint16(bytes, uint16(value)) + return bytes +} diff --git a/store/db/sub_prefix_db.go b/store/db/sub_prefix_db.go new file mode 100644 index 0000000..ae59171 --- /dev/null +++ b/store/db/sub_prefix_db.go @@ -0,0 +1,43 @@ +package db + +import ( + "context" + + "git.defalsify.org/vise.git/db" +) + +// PrefixDb interface abstracts the database operations. +type PrefixDb interface { + Get(ctx context.Context, key []byte) ([]byte, error) + Put(ctx context.Context, key []byte, val []byte) error +} + +var _ PrefixDb = (*SubPrefixDb)(nil) + +type SubPrefixDb struct { + store db.Db + pfx []byte +} + +func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb { + return &SubPrefixDb{ + store: store, + pfx: pfx, + } +} + +func (s *SubPrefixDb) toKey(k []byte) []byte { + return append(s.pfx, k...) +} + +func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { + s.store.SetPrefix(db.DATATYPE_USERDATA) + key = s.toKey(key) + return s.store.Get(ctx, key) +} + +func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { + s.store.SetPrefix(db.DATATYPE_USERDATA) + key = s.toKey(key) + return s.store.Put(ctx, key, val) +} diff --git a/store/db/sub_prefix_db_test.go b/store/db/sub_prefix_db_test.go new file mode 100644 index 0000000..d5d25fd --- /dev/null +++ b/store/db/sub_prefix_db_test.go @@ -0,0 +1,54 @@ +package db + +import ( + "bytes" + "context" + "testing" + + memdb "git.defalsify.org/vise.git/db/mem" +) + +func TestSubPrefix(t *testing.T) { + ctx := context.Background() + db := memdb.NewMemDb() + err := db.Connect(ctx, "") + if err != nil { + t.Fatal(err) + } + sdba := NewSubPrefixDb(db, []byte("tinkywinky")) + err = sdba.Put(ctx, []byte("foo"), []byte("dipsy")) + if err != nil { + t.Fatal(err) + } + + r, err := sdba.Get(ctx, []byte("foo")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(r, []byte("dipsy")) { + t.Fatalf("expected 'dipsy', got %s", r) + } + + sdbb := NewSubPrefixDb(db, []byte("lala")) + r, err = sdbb.Get(ctx, []byte("foo")) + if err == nil { + t.Fatal("expected not found") + } + + err = sdbb.Put(ctx, []byte("foo"), []byte("pu")) + if err != nil { + t.Fatal(err) + } + r, err = sdbb.Get(ctx, []byte("foo")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(r, []byte("pu")) { + t.Fatalf("expected 'pu', got %s", r) + } + + r, err = sdba.Get(ctx, []byte("foo")) + if !bytes.Equal(r, []byte("dipsy")) { + t.Fatalf("expected 'dipsy', got %s", r) + } +} diff --git a/store/tokens.go b/store/tokens.go new file mode 100644 index 0000000..7c3ad0c --- /dev/null +++ b/store/tokens.go @@ -0,0 +1,83 @@ +package store + +import ( + "context" + "errors" + "math/big" + "reflect" + "strconv" + + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" +) + +type TransactionData struct { + TemporaryValue string + ActiveSym string + Amount string + PublicKey string + Recipient string + ActiveDecimal string + ActiveAddress string +} + +func ParseAndScaleAmount(storedAmount, activeDecimal string) (string, error) { + // Parse token decimal + tokenDecimal, err := strconv.Atoi(activeDecimal) + if err != nil { + + return "", err + } + + // Parse amount + amount, _, err := big.ParseFloat(storedAmount, 10, 0, big.ToZero) + if err != nil { + return "", err + } + + // Scale the amount + multiplier := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(tokenDecimal)), nil)) + finalAmount := new(big.Float).Mul(amount, multiplier) + + // Convert finalAmount to a string + finalAmountStr := new(big.Int) + finalAmount.Int(finalAmountStr) + + return finalAmountStr.String(), nil +} + +func ReadTransactionData(ctx context.Context, store DataStore, sessionId string) (TransactionData, error) { + data := TransactionData{} + fieldToKey := map[string]storedb.DataTyp{ + "TemporaryValue": storedb.DATA_TEMPORARY_VALUE, + "ActiveSym": storedb.DATA_ACTIVE_SYM, + "Amount": storedb.DATA_AMOUNT, + "PublicKey": storedb.DATA_PUBLIC_KEY, + "Recipient": storedb.DATA_RECIPIENT, + "ActiveDecimal": storedb.DATA_ACTIVE_DECIMAL, + "ActiveAddress": storedb.DATA_ACTIVE_ADDRESS, + } + + v := reflect.ValueOf(&data).Elem() + for fieldName, key := range fieldToKey { + field := v.FieldByName(fieldName) + if !field.IsValid() || !field.CanSet() { + return data, errors.New("invalid struct field: " + fieldName) + } + + value, err := readStringEntry(ctx, store, sessionId, key) + if err != nil { + return data, err + } + field.SetString(value) + } + + return data, nil +} + +func readStringEntry(ctx context.Context, store DataStore, sessionId string, key storedb.DataTyp) (string, error) { + entry, err := store.ReadEntry(ctx, sessionId, key) + if err != nil { + return "", err + } + return string(entry), nil +} diff --git a/store/tokens_test.go b/store/tokens_test.go new file mode 100644 index 0000000..ca7ca47 --- /dev/null +++ b/store/tokens_test.go @@ -0,0 +1,130 @@ +package store + +import ( + "testing" + + "github.com/alecthomas/assert/v2" + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" +) + +func TestParseAndScaleAmount(t *testing.T) { + tests := []struct { + name string + amount string + decimals string + want string + expectError bool + }{ + { + name: "whole number", + amount: "123", + decimals: "2", + want: "12300", + expectError: false, + }, + { + name: "decimal number", + amount: "123.45", + decimals: "2", + want: "12345", + expectError: false, + }, + { + name: "zero decimals", + amount: "123.45", + decimals: "0", + want: "123", + expectError: false, + }, + { + name: "large number", + amount: "1000000.01", + decimals: "6", + want: "1000000010000", + expectError: false, + }, + { + name: "invalid amount", + amount: "abc", + decimals: "2", + want: "", + expectError: true, + }, + { + name: "invalid decimals", + amount: "123.45", + decimals: "abc", + want: "", + expectError: true, + }, + { + name: "zero amount", + amount: "0", + decimals: "2", + want: "0", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseAndScaleAmount(tt.amount, tt.decimals) + + // Check error cases + if tt.expectError { + if err == nil { + t.Errorf("ParseAndScaleAmount(%q, %q) expected error, got nil", tt.amount, tt.decimals) + } + return + } + + if err != nil { + t.Errorf("ParseAndScaleAmount(%q, %q) unexpected error: %v", tt.amount, tt.decimals, err) + return + } + + if got != tt.want { + t.Errorf("ParseAndScaleAmount(%q, %q) = %v, want %v", tt.amount, tt.decimals, got, tt.want) + } + }) + } +} + +func TestReadTransactionData(t *testing.T) { + sessionId := "session123" + publicKey := "0X13242618721" + ctx, store := InitializeTestDb(t) + + // Test transaction data + transactionData := map[storedb.DataTyp]string{ + storedb.DATA_TEMPORARY_VALUE: "0712345678", + storedb.DATA_ACTIVE_SYM: "SRF", + storedb.DATA_AMOUNT: "1000000", + storedb.DATA_PUBLIC_KEY: publicKey, + storedb.DATA_RECIPIENT: "0x41c188d63Qa", + storedb.DATA_ACTIVE_DECIMAL: "6", + storedb.DATA_ACTIVE_ADDRESS: "0xd4c288865Ce", + } + + // Store the data + for key, value := range transactionData { + if err := store.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil { + t.Fatal(err) + } + } + + expectedResult := TransactionData{ + TemporaryValue: "0712345678", + ActiveSym: "SRF", + Amount: "1000000", + PublicKey: publicKey, + Recipient: "0x41c188d63Qa", + ActiveDecimal: "6", + ActiveAddress: "0xd4c288865Ce", + } + + data, err := ReadTransactionData(ctx, store, sessionId) + + assert.NoError(t, err) + assert.Equal(t, expectedResult, data) +} diff --git a/store/transfer_statements.go b/store/transfer_statements.go new file mode 100644 index 0000000..0061885 --- /dev/null +++ b/store/transfer_statements.go @@ -0,0 +1,127 @@ +package store + +import ( + "context" + "fmt" + "strings" + "time" + + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +// TransferMetadata helps organize data fields +type TransferMetadata struct { + Senders string + Recipients string + TransferValues string + Addresses string + TxHashes string + Dates string + Symbols string + Decimals string +} + +// ProcessTransfers converts transfers into formatted strings +func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetadata { + var data TransferMetadata + var senders, recipients, transferValues, addresses, txHashes, dates, symbols, decimals []string + + for _, t := range transfers { + senders = append(senders, t.Sender) + recipients = append(recipients, t.Recipient) + + // Scale down the amount + scaledBalance := ScaleDownBalance(t.TransferValue, t.TokenDecimals) + transferValues = append(transferValues, scaledBalance) + + addresses = append(addresses, t.ContractAddress) + txHashes = append(txHashes, t.TxHash) + dates = append(dates, fmt.Sprintf("%s", t.DateBlock)) + symbols = append(symbols, t.TokenSymbol) + decimals = append(decimals, t.TokenDecimals) + } + + data.Senders = strings.Join(senders, "\n") + data.Recipients = strings.Join(recipients, "\n") + data.TransferValues = strings.Join(transferValues, "\n") + data.Addresses = strings.Join(addresses, "\n") + data.TxHashes = strings.Join(txHashes, "\n") + data.Dates = strings.Join(dates, "\n") + data.Symbols = strings.Join(symbols, "\n") + data.Decimals = strings.Join(decimals, "\n") + + return data +} + +// GetTransferData retrieves and matches transfer data +// returns a formatted string of the full transaction/statement +func GetTransferData(ctx context.Context, db storedb.PrefixDb, publicKey string, index int) (string, error) { + keys := []storedb.DataTyp{ + storedb.DATA_TX_SENDERS, + storedb.DATA_TX_RECIPIENTS, + storedb.DATA_TX_VALUES, + storedb.DATA_TX_ADDRESSES, + storedb.DATA_TX_HASHES, + storedb.DATA_TX_DATES, + storedb.DATA_TX_SYMBOLS, + } + data := make(map[storedb.DataTyp]string) + + for _, key := range keys { + value, err := db.Get(ctx, storedb.ToBytes(key)) + if err != nil { + return "", fmt.Errorf("failed to get %s: %v", storedb.ToBytes(key), err) + } + data[key] = string(value) + } + + // Split the data + senders := strings.Split(string(data[storedb.DATA_TX_SENDERS]), "\n") + recipients := strings.Split(string(data[storedb.DATA_TX_RECIPIENTS]), "\n") + values := strings.Split(string(data[storedb.DATA_TX_VALUES]), "\n") + addresses := strings.Split(string(data[storedb.DATA_TX_ADDRESSES]), "\n") + hashes := strings.Split(string(data[storedb.DATA_TX_HASHES]), "\n") + dates := strings.Split(string(data[storedb.DATA_TX_DATES]), "\n") + syms := strings.Split(string(data[storedb.DATA_TX_SYMBOLS]), "\n") + + // Check if index is within range + if index < 1 || index > len(senders) { + return "", fmt.Errorf("transaction not found: index %d out of range", index) + } + + // Adjust for 0-based indexing + i := index - 1 + transactionType := "Received" + party := fmt.Sprintf("From: %s", strings.TrimSpace(senders[i])) + if strings.TrimSpace(senders[i]) == publicKey { + transactionType = "Sent" + party = fmt.Sprintf("To: %s", strings.TrimSpace(recipients[i])) + } + + formattedDate := formatDate(strings.TrimSpace(dates[i])) + + // Build the full transaction detail + detail := fmt.Sprintf( + "%s %s %s\n%s\nContract address: %s\nTxhash: %s\nDate: %s", + transactionType, + strings.TrimSpace(values[i]), + strings.TrimSpace(syms[i]), + party, + strings.TrimSpace(addresses[i]), + strings.TrimSpace(hashes[i]), + formattedDate, + ) + + return detail, nil +} + +// Helper function to format date in desired output +func formatDate(dateStr string) string { + parsedDate, err := time.Parse("2006-01-02 15:04:05 -0700 MST", dateStr) + if err != nil { + fmt.Println("Error parsing date:", err) + return "" + } + return parsedDate.Format("2006-01-02 03:04:05 PM") +} diff --git a/store/vouchers.go b/store/vouchers.go new file mode 100644 index 0000000..37381a4 --- /dev/null +++ b/store/vouchers.go @@ -0,0 +1,179 @@ +package store + +import ( + "context" + "fmt" + "math/big" + "strings" + + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +// VoucherMetadata helps organize data fields +type VoucherMetadata struct { + Symbols string + Balances string + Decimals string + Addresses string +} + +// ProcessVouchers converts holdings into formatted strings +func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata { + var data VoucherMetadata + var symbols, balances, decimals, addresses []string + + for i, h := range holdings { + symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol)) + + // Scale down the balance + scaledBalance := ScaleDownBalance(h.Balance, h.TokenDecimals) + + balances = append(balances, fmt.Sprintf("%d:%s", i+1, scaledBalance)) + decimals = append(decimals, fmt.Sprintf("%d:%s", i+1, h.TokenDecimals)) + addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress)) + } + + data.Symbols = strings.Join(symbols, "\n") + data.Balances = strings.Join(balances, "\n") + data.Decimals = strings.Join(decimals, "\n") + data.Addresses = strings.Join(addresses, "\n") + + return data +} + +func ScaleDownBalance(balance, decimals string) string { + // Convert balance and decimals to big.Float + bal := new(big.Float) + bal.SetString(balance) + + dec, ok := new(big.Int).SetString(decimals, 10) + if !ok { + dec = big.NewInt(0) // Default to 0 decimals in case of conversion failure + } + + divisor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), dec, nil)) + scaledBalance := new(big.Float).Quo(bal, divisor) + + // Return the scaled balance without trailing decimals if it's an integer + if scaledBalance.IsInt() { + return scaledBalance.Text('f', 0) + } + return scaledBalance.Text('f', -1) +} + +// GetVoucherData retrieves and matches voucher data +func GetVoucherData(ctx context.Context, db storedb.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) { + keys := []storedb.DataTyp{ + storedb.DATA_VOUCHER_SYMBOLS, + storedb.DATA_VOUCHER_BALANCES, + storedb.DATA_VOUCHER_DECIMALS, + storedb.DATA_VOUCHER_ADDRESSES, + } + data := make(map[storedb.DataTyp]string) + + for _, key := range keys { + value, err := db.Get(ctx, storedb.ToBytes(key)) + if err != nil { + return nil, fmt.Errorf("failed to get %s: %v", storedb.ToBytes(key), err) + } + data[key] = string(value) + } + + symbol, balance, decimal, address := MatchVoucher(input, + data[storedb.DATA_VOUCHER_SYMBOLS], + data[storedb.DATA_VOUCHER_BALANCES], + data[storedb.DATA_VOUCHER_DECIMALS], + data[storedb.DATA_VOUCHER_ADDRESSES], + ) + + if symbol == "" { + return nil, nil + } + + return &dataserviceapi.TokenHoldings{ + TokenSymbol: string(symbol), + Balance: string(balance), + TokenDecimals: string(decimal), + ContractAddress: string(address), + }, nil +} + +// MatchVoucher finds the matching voucher symbol, balance, decimals and contract address based on the input. +func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, balance, decimal, address string) { + symList := strings.Split(symbols, "\n") + balList := strings.Split(balances, "\n") + decList := strings.Split(decimals, "\n") + addrList := strings.Split(addresses, "\n") + + logg.Tracef("found", "symlist", symList, "syms", symbols, "input", input) + for i, sym := range symList { + parts := strings.SplitN(sym, ":", 2) + + if input == parts[0] || strings.EqualFold(input, parts[1]) { + symbol = parts[1] + if i < len(balList) { + balance = strings.SplitN(balList[i], ":", 2)[1] + } + if i < len(decList) { + decimal = strings.SplitN(decList[i], ":", 2)[1] + } + if i < len(addrList) { + address = strings.SplitN(addrList[i], ":", 2)[1] + } + break + } + } + return +} + +// StoreTemporaryVoucher saves voucher metadata as temporary entries in the DataStore. +func StoreTemporaryVoucher(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { + tempData := fmt.Sprintf("%s,%s,%s,%s", data.TokenSymbol, data.Balance, data.TokenDecimals, data.ContractAddress) + + if err := store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(tempData)); err != nil { + return err + } + + return nil +} + +// GetTemporaryVoucherData retrieves temporary voucher metadata from the DataStore. +func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) { + temp_data, err := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE) + if err != nil { + return nil, err + } + + values := strings.SplitN(string(temp_data), ",", 4) + + data := &dataserviceapi.TokenHoldings{} + + data.TokenSymbol = values[0] + data.Balance = values[1] + data.TokenDecimals = values[2] + data.ContractAddress = values[3] + + return data, nil +} + +// UpdateVoucherData updates the active voucher data in the DataStore. +func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { + logg.TraceCtxf(ctx, "dtal", "data", data) + // Active voucher data entries + activeEntries := map[storedb.DataTyp][]byte{ + storedb.DATA_ACTIVE_SYM: []byte(data.TokenSymbol), + storedb.DATA_ACTIVE_BAL: []byte(data.Balance), + storedb.DATA_ACTIVE_DECIMAL: []byte(data.TokenDecimals), + storedb.DATA_ACTIVE_ADDRESS: []byte(data.ContractAddress), + } + + // Write active data + for key, value := range activeEntries { + if err := store.WriteEntry(ctx, sessionId, key, value); err != nil { + return err + } + } + + return nil +} diff --git a/store/vouchers_test.go b/store/vouchers_test.go new file mode 100644 index 0000000..5e3a99b --- /dev/null +++ b/store/vouchers_test.go @@ -0,0 +1,200 @@ +package store + +import ( + "context" + "fmt" + "testing" + + "github.com/alecthomas/assert/v2" + "github.com/stretchr/testify/require" + + visedb "git.defalsify.org/vise.git/db" + memdb "git.defalsify.org/vise.git/db/mem" + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +// InitializeTestDb sets up and returns an in-memory database and store. +func InitializeTestDb(t *testing.T) (context.Context, *UserDataStore) { + ctx := context.Background() + + // Initialize memDb + db := memdb.NewMemDb() + err := db.Connect(ctx, "") + require.NoError(t, err, "Failed to connect to memDb") + + // Create UserDataStore with memDb + store := &UserDataStore{Db: db} + + t.Cleanup(func() { + db.Close() // Ensure the DB is closed after each test + }) + + return ctx, store +} + +func TestMatchVoucher(t *testing.T) { + symbols := "1:SRF\n2:MILO" + balances := "1:100\n2:200" + decimals := "1:6\n2:4" + addresses := "1:0xd4c288865Ce\n2:0x41c188d63Qa" + + // Test for valid voucher + symbol, balance, decimal, address := MatchVoucher("2", symbols, balances, decimals, addresses) + + // Assertions for valid voucher + assert.Equal(t, "MILO", symbol) + assert.Equal(t, "200", balance) + assert.Equal(t, "4", decimal) + assert.Equal(t, "0x41c188d63Qa", address) + + // Test for non-existent voucher + symbol, balance, decimal, address = MatchVoucher("3", symbols, balances, decimals, addresses) + + // Assertions for non-match + assert.Equal(t, "", symbol) + assert.Equal(t, "", balance) + assert.Equal(t, "", decimal) + assert.Equal(t, "", address) +} + +func TestProcessVouchers(t *testing.T) { + holdings := []dataserviceapi.TokenHoldings{ + {ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100000000"}, + {ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200000000"}, + } + + expectedResult := VoucherMetadata{ + Symbols: "1:SRF\n2:MILO", + Balances: "1:100\n2:20000", + Decimals: "1:6\n2:4", + Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa", + } + + result := ProcessVouchers(holdings) + + assert.Equal(t, expectedResult, result) +} + +func TestGetVoucherData(t *testing.T) { + ctx := context.Background() + + db := memdb.NewMemDb() + err := db.Connect(ctx, "") + if err != nil { + t.Fatal(err) + } + + prefix := storedb.ToBytes(visedb.DATATYPE_USERDATA) + spdb := storedb.NewSubPrefixDb(db, prefix) + + // Test voucher data + mockData := map[storedb.DataTyp][]byte{ + storedb.DATA_VOUCHER_SYMBOLS: []byte("1:SRF\n2:MILO"), + storedb.DATA_VOUCHER_BALANCES: []byte("1:100\n2:200"), + storedb.DATA_VOUCHER_DECIMALS: []byte("1:6\n2:4"), + storedb.DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), + } + + // Put the data + for key, value := range mockData { + err = spdb.Put(ctx, []byte(storedb.ToBytes(key)), []byte(value)) + if err != nil { + t.Fatal(err) + } + } + + result, err := GetVoucherData(ctx, spdb, "1") + + assert.NoError(t, err) + assert.Equal(t, "SRF", result.TokenSymbol) + assert.Equal(t, "100", result.Balance) + assert.Equal(t, "6", result.TokenDecimals) + assert.Equal(t, "0xd4c288865Ce", result.ContractAddress) +} + +func TestStoreTemporaryVoucher(t *testing.T) { + ctx, store := InitializeTestDb(t) + sessionId := "session123" + + // Test data + voucherData := &dataserviceapi.TokenHoldings{ + TokenSymbol: "SRF", + Balance: "200", + TokenDecimals: "6", + ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", + } + + // Execute the function being tested + err := StoreTemporaryVoucher(ctx, store, sessionId, voucherData) + require.NoError(t, err) + + // Verify stored data + expectedData := fmt.Sprintf("%s,%s,%s,%s", "SRF", "200", "6", "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9") + + storedValue, err := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE) + require.NoError(t, err) + require.Equal(t, expectedData, string(storedValue), "Mismatch for key %v", storedb.DATA_TEMPORARY_VALUE) +} + +func TestGetTemporaryVoucherData(t *testing.T) { + ctx, store := InitializeTestDb(t) + sessionId := "session123" + + // Test voucher data + tempData := &dataserviceapi.TokenHoldings{ + TokenSymbol: "SRF", + Balance: "200", + TokenDecimals: "6", + ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", + } + + // Store the data + err := StoreTemporaryVoucher(ctx, store, sessionId, tempData) + require.NoError(t, err) + + // Execute the function being tested + data, err := GetTemporaryVoucherData(ctx, store, sessionId) + require.NoError(t, err) + require.Equal(t, tempData, data) +} + +func TestUpdateVoucherData(t *testing.T) { + ctx, store := InitializeTestDb(t) + sessionId := "session123" + + // New voucher data + newData := &dataserviceapi.TokenHoldings{ + TokenSymbol: "SRF", + Balance: "200", + TokenDecimals: "6", + ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", + } + + // Old temporary data + tempData := &dataserviceapi.TokenHoldings{ + TokenSymbol: "OLD", + Balance: "100", + TokenDecimals: "8", + ContractAddress: "0xold", + } + require.NoError(t, StoreTemporaryVoucher(ctx, store, sessionId, tempData)) + + // Execute update + err := UpdateVoucherData(ctx, store, sessionId, newData) + require.NoError(t, err) + + // Verify active data was stored correctly + activeEntries := map[storedb.DataTyp][]byte{ + storedb.DATA_ACTIVE_SYM: []byte(newData.TokenSymbol), + storedb.DATA_ACTIVE_BAL: []byte(newData.Balance), + storedb.DATA_ACTIVE_DECIMAL: []byte(newData.TokenDecimals), + storedb.DATA_ACTIVE_ADDRESS: []byte(newData.ContractAddress), + } + + for key, expectedValue := range activeEntries { + storedValue, err := store.ReadEntry(ctx, sessionId, key) + require.NoError(t, err) + require.Equal(t, expectedValue, storedValue, "Active data mismatch for key %v", key) + } +} diff --git a/testutil/engine.go b/testutil/engine.go index 063350e..4331476 100644 --- a/testutil/engine.go +++ b/testutil/engine.go @@ -15,8 +15,8 @@ import ( "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" - "git.grassecon.net/grassrootseconomics/visedriver/initializers" - "git.grassecon.net/grassrootseconomics/visedriver/config" + "git.grassecon.net/grassrootseconomics/common/env" + "git.grassecon.net/grassrootseconomics/sarafu-vise/config" "git.grassecon.net/grassrootseconomics/sarafu-vise/handlers" "git.grassecon.net/grassrootseconomics/visedriver/storage" "git.grassecon.net/grassrootseconomics/sarafu-api/testutil/testservice" @@ -35,7 +35,7 @@ var ( ) func init() { - initializers.LoadEnvVariablesPath(baseDir) + env.LoadEnvVariablesPath(baseDir) config.LoadConfig() }