forked from urdt/ussd
		
	Merge branch 'master' into account-statement
This commit is contained in:
		
						commit
						6fa0d8e2ff
					
				| @ -2,6 +2,9 @@ | ||||
| PORT=7123 | ||||
| HOST=127.0.0.1 | ||||
| 
 | ||||
| #AfricasTalking USSD POST endpoint | ||||
| AT_ENDPOINT=/ussd/africastalking | ||||
| 
 | ||||
| #PostgreSQL | ||||
| DB_HOST=localhost | ||||
| DB_USER=postgres | ||||
| @ -12,6 +15,6 @@ DB_SSLMODE=disable | ||||
| DB_TIMEZONE=Africa/Nairobi | ||||
| 
 | ||||
| #External API Calls | ||||
| CREATE_ACCOUNT_URL=https://custodial.sarafu.africa/api/account/create | ||||
| TRACK_STATUS_URL=https://custodial.sarafu.africa/api/track/ | ||||
| BALANCE_URL=https://custodial.sarafu.africa/api/account/status/ | ||||
| CUSTODIAL_URL_BASE=http://localhost:5003 | ||||
| BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr | ||||
| DATA_URL_BASE=http://localhost:5006 | ||||
|  | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -6,3 +6,4 @@ go.work* | ||||
| cmd/.state/ | ||||
| id_* | ||||
| *.gdbm | ||||
| *.log | ||||
|  | ||||
							
								
								
									
										89
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										89
									
								
								README.md
									
									
									
									
									
								
							| @ -1,8 +1,91 @@ | ||||
| # ussd | ||||
| # URDT USSD service | ||||
| 
 | ||||
| > USSD | ||||
| This is a USSD service built using the [go-vise](https://github.com/nolash/go-vise) engine. | ||||
| 
 | ||||
| USSD service. | ||||
| ## Prerequisites | ||||
| ### 1. [go-vise](https://github.com/nolash/go-vise) | ||||
| 
 | ||||
| Set up `go-vise` by cloning the repository into a separate directory. The main upstream repository is hosted at: `https://git.defalsify.org/vise.git` | ||||
| ``` | ||||
| git clone https://git.defalsify.org/vise.git | ||||
| ``` | ||||
| 
 | ||||
| ## Setup | ||||
| 1. Clone the ussd repo in its own directory | ||||
| 
 | ||||
|     ``` | ||||
|     git clone https://git.grassecon.net/urdt/ussd.git | ||||
|     ``` | ||||
| 
 | ||||
| 2. Navigate to the project directory. | ||||
| 3. Enter the `services/registration` subfolder: | ||||
|     ``` | ||||
|     cd services/registration | ||||
|     ``` | ||||
| 4. make the .bin files from the .vis files | ||||
|     ``` | ||||
|     make VISE_PATH=/var/path/to/your/go-vise -B | ||||
|     ``` | ||||
| 5. Return to the project root (`cd ../..`) | ||||
| 6. Run the USSD menu  | ||||
|     ``` | ||||
|     go run cmd/main.go -session-id=0712345678 | ||||
|     ``` | ||||
| ## Running the different binaries | ||||
| 1. ### CLI:  | ||||
|     ``` | ||||
|     go run cmd/main.go -session-id=0712345678 | ||||
|     ``` | ||||
| 2. ### Africastalking:  | ||||
|     ``` | ||||
|     go run cmd/africastalking/main.go | ||||
|     ``` | ||||
| 3. ### Async:  | ||||
|     ``` | ||||
|     go run cmd/async/main.go | ||||
|     ``` | ||||
| 4. ### Http:  | ||||
|     ``` | ||||
|     go run cmd/http/main.go | ||||
|     ``` | ||||
|      | ||||
| ## Flags | ||||
| Below are the supported flags: | ||||
| 
 | ||||
| 1. `-session-id`:  | ||||
|      | ||||
|     Specifies the session ID. (CLI only).  | ||||
|      | ||||
|     Default: `075xx2123`. | ||||
| 
 | ||||
|     Example: | ||||
|     ``` | ||||
|     go run cmd/main.go -session-id=0712345678 | ||||
|     ``` | ||||
| 
 | ||||
| 2. `-d`:  | ||||
| 
 | ||||
|     Enables engine debug output.  | ||||
|      | ||||
|     Default: `false`. | ||||
| 
 | ||||
|     Example: | ||||
|     ``` | ||||
|     go run cmd/main.go -session-id=0712345678 -d | ||||
|     ``` | ||||
| 
 | ||||
| 3. `-db`:  | ||||
| 
 | ||||
|     Specifies the database type. | ||||
|      | ||||
|     Default: `gdbm`. | ||||
| 
 | ||||
|     Example: | ||||
|     ``` | ||||
|     go run cmd/main.go -session-id=0712345678 -d -db=postgres | ||||
|     ``` | ||||
| 
 | ||||
|     >Note: If using `-db=postgres`, ensure PostgreSQL is running with the connection details specified in your `.env` file. | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
|  | ||||
| @ -1,9 +1,13 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| @ -19,14 +23,14 @@ import ( | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/server" | ||||
| 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join("services", "registration") | ||||
| 	logg        = logging.NewVanilla() | ||||
| 	scriptDir   = path.Join("services", "registration") | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| @ -38,9 +42,30 @@ type atRequestParser struct{} | ||||
| func (arp *atRequestParser) GetSessionId(rq any) (string, error) { | ||||
| 	rqv, ok := rq.(*http.Request) | ||||
| 	if !ok { | ||||
| 		log.Println("got an invalid request:", rq) | ||||
| 		return "", handlers.ErrInvalidRequest | ||||
| 	} | ||||
| 
 | ||||
| 	// Capture body (if any) for logging
 | ||||
| 	body, err := io.ReadAll(rqv.Body) | ||||
| 	if err != nil { | ||||
| 		log.Println("failed to read request body:", err) | ||||
| 		return "", fmt.Errorf("failed to read request body: %v", err) | ||||
| 	} | ||||
| 	// Reset the body for further reading
 | ||||
| 	rqv.Body = io.NopCloser(bytes.NewReader(body)) | ||||
| 
 | ||||
| 	// Log the body as JSON
 | ||||
| 	bodyLog := map[string]string{"body": string(body)} | ||||
| 	logBytes, err := json.Marshal(bodyLog) | ||||
| 	if err != nil { | ||||
| 		log.Println("failed to marshal request body:", err) | ||||
| 	} else { | ||||
| 		log.Println("Received request:", string(logBytes)) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := rqv.ParseForm(); err != nil { | ||||
| 		log.Println("failed to parse form data: %v", err) | ||||
| 		return "", fmt.Errorf("failed to parse form data: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| @ -131,7 +156,7 @@ func main() { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) | ||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||
| 	lhs.SetDataStore(&userdataStore) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| @ -139,7 +164,7 @@ func main() { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	accountService := server.AccountService{} | ||||
| 	accountService := remote.AccountService{} | ||||
| 	hl, err := lhs.GetHandler(&accountService) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| @ -156,9 +181,13 @@ func main() { | ||||
| 	rp := &atRequestParser{} | ||||
| 	bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) | ||||
| 	sh := httpserver.NewATSessionHandler(bsh) | ||||
| 
 | ||||
| 	mux := http.NewServeMux() | ||||
| 	mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh) | ||||
| 
 | ||||
| 	s := &http.Server{ | ||||
| 		Addr:    fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), | ||||
| 		Handler: sh, | ||||
| 		Handler: mux, | ||||
| 	} | ||||
| 	s.RegisterOnShutdown(sh.Shutdown) | ||||
| 
 | ||||
|  | ||||
| @ -16,8 +16,8 @@ import ( | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/server" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| @ -104,9 +104,9 @@ func main() { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) | ||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||
| 	lhs.SetDataStore(&userdataStore) | ||||
| 	accountService := server.AccountService{} | ||||
| 	accountService := remote.AccountService{} | ||||
| 
 | ||||
| 	hl, err := lhs.GetHandler(&accountService) | ||||
| 	if err != nil { | ||||
|  | ||||
| @ -18,9 +18,9 @@ import ( | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/server" | ||||
| 	httpserver "git.grassecon.net/urdt/ussd/internal/http" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| @ -92,14 +92,15 @@ func main() { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) | ||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||
| 	lhs.SetDataStore(&userdataStore) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	accountService := server.AccountService{} | ||||
| 
 | ||||
| 	accountService := remote.AccountService{} | ||||
| 	hl, err := lhs.GetHandler(&accountService) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
|  | ||||
| @ -13,8 +13,8 @@ import ( | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/server" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| @ -88,7 +88,7 @@ func main() { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) | ||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||
| 	lhs.SetDataStore(&userdatastore) | ||||
| 	lhs.SetPersister(pe) | ||||
| 
 | ||||
| @ -97,7 +97,7 @@ func main() { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	accountService := server.AccountService{} | ||||
| 	accountService := remote.AccountService{} | ||||
| 	hl, err := lhs.GetHandler(&accountService) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
|  | ||||
							
								
								
									
										71
									
								
								common/db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								common/db.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/binary" | ||||
| 	"errors" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| ) | ||||
| 
 | ||||
| type DataTyp uint16 | ||||
| 
 | ||||
| const ( | ||||
| 	DATA_ACCOUNT DataTyp = iota | ||||
| 	DATA_ACCOUNT_CREATED | ||||
| 	DATA_TRACKING_ID | ||||
| 	DATA_PUBLIC_KEY | ||||
| 	DATA_CUSTODIAL_ID | ||||
| 	DATA_ACCOUNT_PIN | ||||
| 	DATA_ACCOUNT_STATUS | ||||
| 	DATA_FIRST_NAME | ||||
| 	DATA_FAMILY_NAME | ||||
| 	DATA_YOB | ||||
| 	DATA_LOCATION | ||||
| 	DATA_GENDER | ||||
| 	DATA_OFFERINGS | ||||
| 	DATA_RECIPIENT | ||||
| 	DATA_AMOUNT | ||||
| 	DATA_TEMPORARY_VALUE | ||||
| 	DATA_ACTIVE_SYM | ||||
| 	DATA_ACTIVE_BAL | ||||
| 	DATA_BLOCKED_NUMBER | ||||
| 	DATA_PUBLIC_KEY_REVERSE | ||||
| 	DATA_ACTIVE_DECIMAL | ||||
| 	DATA_ACTIVE_ADDRESS | ||||
| 	DATA_TRANSACTIONS | ||||
| ) | ||||
| 
 | ||||
| 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") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										31
									
								
								common/hex.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								common/hex.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/hex" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| func NormalizeHex(s string) (string, error) { | ||||
| 	if len(s) >= 2 { | ||||
| 		if s[:2] == "0x" { | ||||
| 			s = s[2:] | ||||
| 		} | ||||
| 	} | ||||
| 	r, err := hex.DecodeString(s) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return hex.EncodeToString(r), nil | ||||
| } | ||||
| 
 | ||||
| func IsSameHex(left string, right string) bool { | ||||
| 	bl, err := NormalizeHex(left) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	br, err := NormalizeHex(left) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	return strings.Compare(bl, br) == 0 | ||||
| } | ||||
							
								
								
									
										52
									
								
								common/storage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								common/storage.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/db" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 	"git.defalsify.org/vise.git/persist" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| ) | ||||
| 
 | ||||
| func StoreToDb(store *UserDataStore) db.Db { | ||||
| 	return store.Db | ||||
| } | ||||
| 
 | ||||
| func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb { | ||||
| 	return storage.NewSubPrefixDb(store.Db, pfx)	 | ||||
| } | ||||
| 
 | ||||
| type StorageServices interface { | ||||
| 	GetPersister(ctx context.Context) (*persist.Persister, error) | ||||
| 	GetUserdataDb(ctx context.Context) (db.Db, error) | ||||
| 	GetResource(ctx context.Context) (resource.Resource, error) | ||||
| 	EnsureDbDir() error | ||||
| } | ||||
| 
 | ||||
| type StorageService struct { | ||||
| 	svc *storage.MenuStorageService | ||||
| } | ||||
| 
 | ||||
| func NewStorageService(dbDir string) *StorageService { | ||||
| 	return &StorageService{ | ||||
| 		svc: storage.NewMenuStorageService(dbDir, ""), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) { | ||||
| 	return ss.svc.GetPersister(ctx) | ||||
| } | ||||
| 	 | ||||
| func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) { | ||||
| 	return ss.svc.GetUserdataDb(ctx) | ||||
| } | ||||
| 
 | ||||
| func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) { | ||||
| 	return nil, errors.New("not implemented") | ||||
| } | ||||
| 
 | ||||
| func(ss *StorageService) EnsureDbDir() error { | ||||
| 	return ss.svc.EnsureDbDir() | ||||
| } | ||||
							
								
								
									
										81
									
								
								common/tokens.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								common/tokens.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"math/big" | ||||
| 	"reflect" | ||||
| 	"strconv" | ||||
| ) | ||||
| 
 | ||||
| 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]DataTyp{ | ||||
| 		"TemporaryValue": DATA_TEMPORARY_VALUE, | ||||
| 		"ActiveSym":      DATA_ACTIVE_SYM, | ||||
| 		"Amount":         DATA_AMOUNT, | ||||
| 		"PublicKey":      DATA_PUBLIC_KEY, | ||||
| 		"Recipient":      DATA_RECIPIENT, | ||||
| 		"ActiveDecimal":  DATA_ACTIVE_DECIMAL, | ||||
| 		"ActiveAddress":  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 DataTyp) (string, error) { | ||||
| 	entry, err := store.ReadEntry(ctx, sessionId, key) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return string(entry), nil | ||||
| } | ||||
							
								
								
									
										129
									
								
								common/tokens_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								common/tokens_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,129 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/alecthomas/assert/v2" | ||||
| ) | ||||
| 
 | ||||
| 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[DataTyp]string{ | ||||
| 		DATA_TEMPORARY_VALUE: "0712345678", | ||||
| 		DATA_ACTIVE_SYM:      "SRF", | ||||
| 		DATA_AMOUNT:          "1000000", | ||||
| 		DATA_PUBLIC_KEY:      publicKey, | ||||
| 		DATA_RECIPIENT:       "0x41c188d63Qa", | ||||
| 		DATA_ACTIVE_DECIMAL:  "6", | ||||
| 		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) | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package utils | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| @ -16,7 +16,7 @@ type UserDataStore struct { | ||||
| 	db.Db | ||||
| } | ||||
| 
 | ||||
| // ReadEntry retrieves an entry from the store based on the provided parameters.
 | ||||
| // ReadEntry retrieves an entry to the userdata store.
 | ||||
| func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) { | ||||
| 	store.SetPrefix(db.DATATYPE_USERDATA) | ||||
| 	store.SetSession(sessionId) | ||||
| @ -24,6 +24,8 @@ func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ | ||||
| 	return store.Get(ctx, k) | ||||
| } | ||||
| 
 | ||||
| // WriteEntry adds an entry to the userdata store.
 | ||||
| // BUG: this uses sessionId twice
 | ||||
| func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error { | ||||
| 	store.SetPrefix(db.DATATYPE_USERDATA) | ||||
| 	store.SetSession(sessionId) | ||||
							
								
								
									
										173
									
								
								common/vouchers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								common/vouchers.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,173 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"math/big" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	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 storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) { | ||||
| 	keys := []string{"sym", "bal", "deci", "addr"} | ||||
| 	data := make(map[string]string) | ||||
| 
 | ||||
| 	for _, key := range keys { | ||||
| 		value, err := db.Get(ctx, []byte(key)) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed to get %s: %v", key, err) | ||||
| 		} | ||||
| 		data[key] = string(value) | ||||
| 	} | ||||
| 
 | ||||
| 	symbol, balance, decimal, address := MatchVoucher(input, | ||||
| 		data["sym"], | ||||
| 		data["bal"], | ||||
| 		data["deci"], | ||||
| 		data["addr"]) | ||||
| 
 | ||||
| 	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, 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, 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 sets 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[DataTyp][]byte{ | ||||
| 		DATA_ACTIVE_SYM:     []byte(data.TokenSymbol), | ||||
| 		DATA_ACTIVE_BAL:     []byte(data.Balance), | ||||
| 		DATA_ACTIVE_DECIMAL: []byte(data.TokenDecimals), | ||||
| 		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 | ||||
| } | ||||
							
								
								
									
										197
									
								
								common/vouchers_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								common/vouchers_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,197 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/alecthomas/assert/v2" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	memdb "git.defalsify.org/vise.git/db/mem" | ||||
| 	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) | ||||
| 	} | ||||
| 	spdb := storage.NewSubPrefixDb(db, []byte("vouchers")) | ||||
| 
 | ||||
| 	// Test voucher data
 | ||||
| 	mockData := map[string][]byte{ | ||||
| 		"sym":  []byte("1:SRF\n2:MILO"), | ||||
| 		"bal":  []byte("1:100\n2:200"), | ||||
| 		"deci": []byte("1:6\n2:4"), | ||||
| 		"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"), | ||||
| 	} | ||||
| 
 | ||||
| 	// Put the data
 | ||||
| 	for key, value := range mockData { | ||||
| 		err = spdb.Put(ctx, []byte(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, DATA_TEMPORARY_VALUE) | ||||
| 	require.NoError(t, err) | ||||
| 	require.Equal(t, expectedData, string(storedValue), "Mismatch for key %v", 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[DataTyp][]byte{ | ||||
| 		DATA_ACTIVE_SYM:     []byte(newData.TokenSymbol), | ||||
| 		DATA_ACTIVE_BAL:     []byte(newData.Balance), | ||||
| 		DATA_ACTIVE_DECIMAL: []byte(newData.TokenDecimals), | ||||
| 		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) | ||||
| 	} | ||||
| } | ||||
| @ -1,16 +1,71 @@ | ||||
| package config | ||||
| 
 | ||||
| import "git.grassecon.net/urdt/ussd/initializers" | ||||
| import ( | ||||
| 	"net/url" | ||||
| 
 | ||||
| var ( | ||||
| 	CreateAccountURL string | ||||
| 	TrackStatusURL   string | ||||
| 	BalanceURL       string | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| ) | ||||
| 
 | ||||
| // LoadConfig initializes the configuration values after environment variables are loaded.
 | ||||
| func LoadConfig() { | ||||
| 	CreateAccountURL = initializers.GetEnv("CREATE_ACCOUNT_URL", "https://custodial.sarafu.africa/api/account/create") | ||||
| 	TrackStatusURL = initializers.GetEnv("TRACK_STATUS_URL", "https://custodial.sarafu.africa/api/track/") | ||||
| 	BalanceURL = initializers.GetEnv("BALANCE_URL", "https://custodial.sarafu.africa/api/account/status/") | ||||
| const ( | ||||
| 	createAccountPath          = "/api/v2/account/create" | ||||
| 	trackStatusPath            = "/api/track" | ||||
| 	balancePathPrefix          = "/api/account" | ||||
| 	trackPath                  = "/api/v2/account/status" | ||||
| 	tokenTransferPrefix        = "/api/v2/token/transfer" | ||||
| 	voucherHoldingsPathPrefix  = "/api/v1/holdings" | ||||
| 	voucherTransfersPathPrefix = "/api/v1/transfers/last10" | ||||
| 	voucherDataPathPrefix      = "/api/v1/token" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	custodialURLBase string | ||||
| 	dataURLBase      string | ||||
| 	BearerToken      string | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	CreateAccountURL    string | ||||
| 	TrackStatusURL      string | ||||
| 	BalanceURL          string | ||||
| 	TrackURL            string | ||||
| 	TokenTransferURL    string | ||||
| 	VoucherHoldingsURL  string | ||||
| 	VoucherTransfersURL string | ||||
| 	VoucherDataURL      string | ||||
| ) | ||||
| 
 | ||||
| func setBase() error { | ||||
| 	var err error | ||||
| 
 | ||||
| 	custodialURLBase = initializers.GetEnv("CUSTODIAL_URL_BASE", "http://localhost:5003") | ||||
| 	dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006") | ||||
| 	BearerToken = initializers.GetEnv("BEARER_TOKEN", "") | ||||
| 
 | ||||
| 	_, err = url.JoinPath(custodialURLBase, "/foo") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	_, err = url.JoinPath(dataURLBase, "/bar") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // LoadConfig initializes the configuration values after environment variables are loaded.
 | ||||
| func LoadConfig() error { | ||||
| 	err := setBase() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath) | ||||
| 	TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath) | ||||
| 	BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix) | ||||
| 	TrackURL, _ = url.JoinPath(custodialURLBase, trackPath) | ||||
| 	TokenTransferURL, _ = url.JoinPath(custodialURLBase, tokenTransferPrefix) | ||||
| 	VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix) | ||||
| 	VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix) | ||||
| 	VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
							
								
								
									
										7
									
								
								devtools/admin/admin_numbers.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								devtools/admin/admin_numbers.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| { | ||||
|   "admins": [ | ||||
|     { | ||||
|       "phonenumber" : "<replace with any admin number to test with >" | ||||
|     } | ||||
|    ] | ||||
| } | ||||
							
								
								
									
										47
									
								
								devtools/admin/commands/seed.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								devtools/admin/commands/seed.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| package commands | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"os" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/utils" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg = logging.NewVanilla().WithDomain("adminstore") | ||||
| ) | ||||
| 
 | ||||
| type Admin struct { | ||||
| 	PhoneNumber string `json:"phonenumber"` | ||||
| } | ||||
| 
 | ||||
| type Config struct { | ||||
| 	Admins []Admin `json:"admins"` | ||||
| } | ||||
| 
 | ||||
| func Seed(ctx context.Context) error { | ||||
| 	var config Config | ||||
| 	adminstore, err := utils.NewAdminStore(ctx, "../admin_numbers") | ||||
| 	store := adminstore.FsStore | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer store.Close() | ||||
| 	data, err := os.ReadFile("admin_numbers.json") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := json.Unmarshal(data, &config); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, admin := range config.Admins { | ||||
| 		err := store.Put(ctx, []byte(admin.PhoneNumber), []byte("1")) | ||||
| 		if err != nil { | ||||
| 			logg.Printf(logging.LVL_DEBUG, "Failed to insert admin number", admin.PhoneNumber) | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										17
									
								
								devtools/admin/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								devtools/admin/main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"log" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/devtools/admin/commands" | ||||
| ) | ||||
| 
 | ||||
| func main() { | ||||
| 	ctx := context.Background() | ||||
| 	err := commands.Seed(ctx) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("Failed to initialize a list of admins with error %s", err) | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										24
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								go.mod
									
									
									
									
									
								
							| @ -1,27 +1,43 @@ | ||||
| module git.grassecon.net/urdt/ussd | ||||
| 
 | ||||
| go 1.22.6 | ||||
| go 1.23.0 | ||||
| 
 | ||||
| toolchain go1.23.2 | ||||
| 
 | ||||
| require ( | ||||
| 	git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b | ||||
| 	github.com/alecthomas/assert/v2 v2.2.2 | ||||
| 	github.com/grassrootseconomics/eth-custodial v1.3.0-beta | ||||
| 	github.com/peteole/testdata-loader v0.3.0 | ||||
| 	gopkg.in/leonelquinteros/gotext.v1 v1.3.1 | ||||
| ) | ||||
| 
 | ||||
| require github.com/joho/godotenv v1.5.1 // indirect | ||||
| require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a // indirect | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/jackc/pgpassfile v1.0.0 // indirect | ||||
| 	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect | ||||
| 	github.com/jackc/pgx/v5 v5.7.1 // indirect | ||||
| 	github.com/jackc/puddle/v2 v2.2.2 // indirect | ||||
| 	github.com/joho/godotenv v1.5.1 | ||||
| 	github.com/kr/text v0.2.0 // indirect | ||||
| 	github.com/rogpeppe/go-internal v1.13.1 // indirect | ||||
| 	golang.org/x/crypto v0.27.0 // indirect | ||||
| 	golang.org/x/sync v0.8.0 // indirect | ||||
| 	golang.org/x/text v0.18.0 // indirect | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/alecthomas/participle/v2 v2.0.0 // indirect | ||||
| 	github.com/alecthomas/repr v0.2.0 // indirect | ||||
| 	github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect | ||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||
| 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect | ||||
| 	github.com/fxamacker/cbor/v2 v2.4.0 // indirect | ||||
| 	github.com/gofrs/uuid v4.4.0+incompatible | ||||
| 	github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect | ||||
| 	github.com/hexops/gotextdiff v1.0.3 // indirect | ||||
| 	github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | ||||
| 	github.com/stretchr/objx v0.5.2 // indirect | ||||
| 	github.com/stretchr/testify v1.9.0 | ||||
| 	github.com/x448/float16 v0.8.4 // indirect | ||||
|  | ||||
							
								
								
									
										46
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								go.sum
									
									
									
									
									
								
							| @ -1,7 +1,3 @@ | ||||
| git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb h1:6P4kxihcwMjDKzvUFC6t2zGNb7MDW+l/ACGlSAN1N8Y= | ||||
| git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= | ||||
| git.defalsify.org/vise.git v0.2.0 h1:X2ZgiGRq4C+9qOlDMP0b/oE5QHjVQNT4aEFZB88ST0Q= | ||||
| git.defalsify.org/vise.git v0.2.0/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= | ||||
| git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU= | ||||
| git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= | ||||
| github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= | ||||
| @ -12,33 +8,67 @@ github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk | ||||
| github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= | ||||
| github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c h1:H9Nm+I7Cg/YVPpEV1RzU3Wq2pjamPc/UtHDgItcb7lE= | ||||
| github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c/go.mod h1:rGod7o6KPeJ+hyBpHfhi4v7blx9sf+QsHsA7KAsdN6U= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= | ||||
| github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= | ||||
| github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= | ||||
| github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= | ||||
| github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= | ||||
| github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU= | ||||
| github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= | ||||
| github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a h1:q/YH7nE2j8epNmFnTu0tU1vwtCxtQ6nH+d7hRVV5krU= | ||||
| github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a/go.mod h1:hdKaKwqiW6/kphK4j/BhmuRlZDLo1+DYo3gYw5O0siw= | ||||
| github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= | ||||
| github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= | ||||
| github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= | ||||
| github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= | ||||
| github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= | ||||
| github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= | ||||
| github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= | ||||
| github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= | ||||
| github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= | ||||
| github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= | ||||
| github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= | ||||
| github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= | ||||
| github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||
| github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= | ||||
| github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= | ||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||
| github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a h1:0Q3H0YXzMHiciXtRcM+j0jiCe8WKPQHoRgQiRTnfcLY= | ||||
| github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a/go.mod h1:CdTTBOYzS5E4mWS1N8NWP6AHI19MP0A2B18n3hLzRMk= | ||||
| github.com/pashagolub/pgxmock/v4 v4.3.0 h1:DqT7fk0OCK6H0GvqtcMsLpv8cIwWqdxWgfZNLeHCb/s= | ||||
| github.com/pashagolub/pgxmock/v4 v4.3.0/go.mod h1:9VoVHXwS3XR/yPtKGzwQvwZX1kzGB9sM8SviDcHDa3A= | ||||
| github.com/peteole/testdata-loader v0.3.0 h1:8jckE9KcyNHgyv/VPoaljvKZE0Rqr8+dPVYH6rfNr9I= | ||||
| github.com/peteole/testdata-loader v0.3.0/go.mod h1:Mt0ZbRtb56u8SLJpNP+BnQbENljMorYBpqlvt3cS83U= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= | ||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= | ||||
| github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= | ||||
| github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||||
| github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||
| github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= | ||||
| github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||
| golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= | ||||
| golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= | ||||
| golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= | ||||
| golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||
| golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= | ||||
| golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | ||||
| gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= | ||||
| gopkg.in/leonelquinteros/gotext.v1 v1.3.1/go.mod h1:X1WlGDeAFIYsW6GjgMm4VwUwZ2XjI7Zan2InxSUQWrU= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
|  | ||||
| @ -1,13 +1,17 @@ | ||||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/asm" | ||||
| 	"git.defalsify.org/vise.git/db" | ||||
| 	"git.defalsify.org/vise.git/engine" | ||||
| 	"git.defalsify.org/vise.git/persist" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/server" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/ussd" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/utils" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| type HandlerService interface { | ||||
| @ -28,20 +32,26 @@ type LocalHandlerService struct { | ||||
| 	DbRs          *resource.DbResource | ||||
| 	Pe            *persist.Persister | ||||
| 	UserdataStore *db.Db | ||||
| 	AdminStore    *utils.AdminStore | ||||
| 	Cfg           engine.Config | ||||
| 	Rs            resource.Resource | ||||
| } | ||||
| 
 | ||||
| func NewLocalHandlerService(fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) { | ||||
| func NewLocalHandlerService(ctx context.Context, fp string, debug bool, dbResource *resource.DbResource, cfg engine.Config, rs resource.Resource) (*LocalHandlerService, error) { | ||||
| 	parser, err := getParser(fp, debug) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	adminstore, err := utils.NewAdminStore(ctx, "admin_numbers") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &LocalHandlerService{ | ||||
| 		Parser: parser, | ||||
| 		DbRs:   dbResource, | ||||
| 		Cfg:    cfg, | ||||
| 		Rs:     rs, | ||||
| 		Parser:     parser, | ||||
| 		DbRs:       dbResource, | ||||
| 		AdminStore: adminstore, | ||||
| 		Cfg:        cfg, | ||||
| 		Rs:         rs, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| @ -53,16 +63,16 @@ func (ls *LocalHandlerService) SetDataStore(db *db.Db) { | ||||
| 	ls.UserdataStore = db | ||||
| } | ||||
| 
 | ||||
| func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceInterface) (*ussd.Handlers, error) { | ||||
| 	ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore,accountService) | ||||
| func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) { | ||||
| 	ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	ussdHandlers = ussdHandlers.WithPersister(ls.Pe) | ||||
| 	ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) | ||||
| 	ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) | ||||
| 	ls.DbRs.AddLocalFunc("save_pin", ussdHandlers.SavePin) | ||||
| 	ls.DbRs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) | ||||
| 	ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) | ||||
| 	ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin) | ||||
| 	ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) | ||||
| 	ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) | ||||
| 	ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) | ||||
| @ -70,6 +80,7 @@ func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceIn | ||||
| 	ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) | ||||
| 	ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) | ||||
| 	ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) | ||||
| 	ls.DbRs.AddLocalFunc("invite_valid_recipient", ussdHandlers.InviteValidRecipient) | ||||
| 	ls.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) | ||||
| 	ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) | ||||
| 	ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) | ||||
| @ -89,11 +100,24 @@ func (ls *LocalHandlerService) GetHandler(accountService server.AccountServiceIn | ||||
| 	ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) | ||||
| 	ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) | ||||
| 	ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) | ||||
| 	ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) | ||||
| 	ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) | ||||
| 	ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) | ||||
| 	ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) | ||||
| 	ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances) | ||||
| 	ls.DbRs.AddLocalFunc("fetch_community_balance", ussdHandlers.FetchCommunityBalance) | ||||
| 	ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher) | ||||
| 	ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers) | ||||
| 	ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList) | ||||
| 	ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher) | ||||
| 	ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher) | ||||
| 	ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails) | ||||
| 	ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin) | ||||
| 	ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckPinMisMatch) | ||||
| 	ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber) | ||||
| 	ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber) | ||||
| 	ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber) | ||||
| 	ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin) | ||||
| 	ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin) | ||||
| 	ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo) | ||||
| 	ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactions) | ||||
| 
 | ||||
| 	return ussdHandlers, nil | ||||
|  | ||||
| @ -1,153 +0,0 @@ | ||||
| package server | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/models" | ||||
| ) | ||||
| 
 | ||||
| type AccountServiceInterface interface { | ||||
| 	CheckBalance(publicKey string) (*models.BalanceResponse, error) | ||||
| 	CreateAccount() (*models.AccountResponse, error) | ||||
| 	CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) | ||||
| } | ||||
| 
 | ||||
| type AccountService struct { | ||||
| } | ||||
| 
 | ||||
| type TestAccountService struct { | ||||
| } | ||||
| 
 | ||||
| // CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID.
 | ||||
| //
 | ||||
| // Parameters:
 | ||||
| //   - trackingId: A unique identifier for the account.This should be obtained from a previous call to
 | ||||
| //     CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
 | ||||
| //     AccountResponse struct can be used here to check the account status during a transaction.
 | ||||
| //
 | ||||
| // Returns:
 | ||||
| //   - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
 | ||||
| //   - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
 | ||||
| //     If no error occurs, this will be nil.
 | ||||
| func (as *AccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) { | ||||
| 	resp, err := http.Get(config.TrackStatusURL + trackingId) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	body, err := io.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	var trackResp models.TrackStatusResponse | ||||
| 	err = json.Unmarshal(body, &trackResp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &trackResp, nil | ||||
| } | ||||
| 
 | ||||
| // CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
 | ||||
| // Parameters:
 | ||||
| //   - publicKey: The public key associated with the account whose balance needs to be checked.
 | ||||
| func (as *AccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) { | ||||
| 	resp, err := http.Get(config.BalanceURL + publicKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 	body, err := io.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	var balanceResp models.BalanceResponse | ||||
| 	err = json.Unmarshal(body, &balanceResp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &balanceResp, nil | ||||
| } | ||||
| 
 | ||||
| // CreateAccount creates a new account in the custodial system.
 | ||||
| // Returns:
 | ||||
| //   - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
 | ||||
| //     If there is an error during the request or processing, this will be nil.
 | ||||
| //   - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
 | ||||
| //     If no error occurs, this will be nil.
 | ||||
| func (as *AccountService) CreateAccount() (*models.AccountResponse, error) { | ||||
| 	resp, err := http.Post(config.CreateAccountURL, "application/json", nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 	body, err := io.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	var accountResp models.AccountResponse | ||||
| 	err = json.Unmarshal(body, &accountResp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &accountResp, nil | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) CreateAccount() (*models.AccountResponse, error) { | ||||
| 	return &models.AccountResponse{ | ||||
| 		Ok: true, | ||||
| 		Result: struct { | ||||
| 			CustodialId json.Number `json:"custodialId"` | ||||
| 			PublicKey   string      `json:"publicKey"` | ||||
| 			TrackingId  string      `json:"trackingId"` | ||||
| 		}{ | ||||
| 			CustodialId: json.Number("182"), | ||||
| 			PublicKey:   "0x48ADca309b5085852207FAaf2816eD72B52F527C", | ||||
| 			TrackingId:  "28ebe84d-b925-472c-87ae-bbdfa1fb97be", | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) { | ||||
| 
 | ||||
| 	balanceResponse := &models.BalanceResponse{ | ||||
| 		Ok: true, | ||||
| 		Result: struct { | ||||
| 			Balance string      `json:"balance"` | ||||
| 			Nonce   json.Number `json:"nonce"` | ||||
| 		}{ | ||||
| 			Balance: "0.003 CELO", | ||||
| 			Nonce:   json.Number("0"), | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	return balanceResponse, nil | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) { | ||||
| 	trackResponse := &models.TrackStatusResponse{ | ||||
| 		Ok: true, | ||||
| 		Result: struct { | ||||
| 			Transaction struct { | ||||
| 				CreatedAt     time.Time   "json:\"createdAt\"" | ||||
| 				Status        string      "json:\"status\"" | ||||
| 				TransferValue json.Number "json:\"transferValue\"" | ||||
| 				TxHash        string      "json:\"txHash\"" | ||||
| 				TxType        string      "json:\"txType\"" | ||||
| 			} | ||||
| 		}{ | ||||
| 			Transaction: models.Transaction{ | ||||
| 				CreatedAt:     time.Now(), | ||||
| 				Status:        "SUCCESS", | ||||
| 				TransferValue: json.Number("0.5"), | ||||
| 				TxHash:        "0x123abc456def", | ||||
| 				TxType:        "transfer", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	return trackResponse, nil | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -13,7 +13,7 @@ import ( | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/engine" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/mocks/httpmocks" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks" | ||||
| ) | ||||
| 
 | ||||
| // invalidRequestType is a custom type to test invalid request scenarios
 | ||||
|  | ||||
| @ -1,59 +0,0 @@ | ||||
| package mocks | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/lang" | ||||
| 	"github.com/stretchr/testify/mock" | ||||
| ) | ||||
| 
 | ||||
| type MockDb struct { | ||||
| 	mock.Mock | ||||
| } | ||||
| 
 | ||||
| func (m *MockDb) SetPrefix(prefix uint8) { | ||||
| 	m.Called(prefix) | ||||
| } | ||||
| 
 | ||||
| func (m *MockDb) Prefix() uint8 { | ||||
| 	args := m.Called() | ||||
| 	return args.Get(0).(uint8) | ||||
| } | ||||
| 
 | ||||
| func (m *MockDb) Safe() bool { | ||||
| 	args := m.Called() | ||||
| 	return args.Get(0).(bool) | ||||
| } | ||||
| 
 | ||||
| func (m *MockDb) SetLanguage(language *lang.Language) { | ||||
| 	m.Called(language) | ||||
| } | ||||
| 
 | ||||
| func (m *MockDb) SetLock(uint8, bool) error { | ||||
| 	args := m.Called() | ||||
| 	return args.Error(0) | ||||
| } | ||||
| 
 | ||||
| func (m *MockDb) Connect(ctx context.Context, connectionStr string) error { | ||||
| 	args := m.Called(ctx, connectionStr) | ||||
| 	return args.Error(0) | ||||
| } | ||||
| 
 | ||||
| func (m *MockDb) SetSession(sessionId string) { | ||||
| 	m.Called(sessionId) | ||||
| } | ||||
| 
 | ||||
| func (m *MockDb) Put(ctx context.Context, key, value []byte) error { | ||||
| 	args := m.Called(ctx, key, value) | ||||
| 	return args.Error(0) | ||||
| } | ||||
| 
 | ||||
| func (m *MockDb) Get(ctx context.Context, key []byte) ([]byte, error) { | ||||
| 	args := m.Called(ctx, key) | ||||
| 	return nil, args.Error(0) | ||||
| } | ||||
| 
 | ||||
| func (m *MockDb) Close() error { | ||||
| 	args := m.Called(nil) | ||||
| 	return args.Error(0) | ||||
| } | ||||
| @ -1,26 +0,0 @@ | ||||
| package mocks | ||||
| 
 | ||||
| import ( | ||||
| 	"git.grassecon.net/urdt/ussd/internal/models" | ||||
| 	"github.com/stretchr/testify/mock" | ||||
| ) | ||||
| 
 | ||||
| // MockAccountService implements AccountServiceInterface for testing
 | ||||
| type MockAccountService struct { | ||||
| 	mock.Mock | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) CreateAccount() (*models.AccountResponse, error) { | ||||
| 	args := m.Called() | ||||
| 	return args.Get(0).(*models.AccountResponse), args.Error(1) | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) CheckBalance(publicKey string) (*models.BalanceResponse, error) { | ||||
| 	args := m.Called(publicKey) | ||||
| 	return args.Get(0).(*models.BalanceResponse), args.Error(1) | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) CheckAccountStatus(trackingId string) (*models.TrackStatusResponse, error) { | ||||
| 	args := m.Called(trackingId) | ||||
| 	return args.Get(0).(*models.TrackStatusResponse), args.Error(1) | ||||
| } | ||||
| @ -1,24 +0,0 @@ | ||||
| package mocks | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/db" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/utils" | ||||
| 	"github.com/stretchr/testify/mock" | ||||
| ) | ||||
| 
 | ||||
| type MockUserDataStore struct { | ||||
| 	db.Db | ||||
| 	mock.Mock | ||||
| } | ||||
| 
 | ||||
| func (m *MockUserDataStore) ReadEntry(ctx context.Context, sessionId string, typ utils.DataTyp) ([]byte, error) { | ||||
| 	args := m.Called(ctx, sessionId, typ) | ||||
| 	return args.Get(0).([]byte), args.Error(1) | ||||
| } | ||||
| 
 | ||||
| func (m *MockUserDataStore) WriteEntry(ctx context.Context, sessionId string, typ utils.DataTyp, value []byte) error { | ||||
| 	args := m.Called(ctx, sessionId, typ, value) | ||||
| 	return args.Error(0) | ||||
| } | ||||
| @ -1,15 +0,0 @@ | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 
 | ||||
| ) | ||||
| 
 | ||||
| type AccountResponse struct { | ||||
| 	Ok     bool `json:"ok"` | ||||
| 	Result struct { | ||||
| 		CustodialId json.Number `json:"custodialId"` | ||||
| 		PublicKey   string      `json:"publicKey"` | ||||
| 		TrackingId  string      `json:"trackingId"` | ||||
| 	} `json:"result"` | ||||
| } | ||||
| @ -1,12 +0,0 @@ | ||||
| package models | ||||
| 
 | ||||
| import "encoding/json" | ||||
| 
 | ||||
| 
 | ||||
| type BalanceResponse struct { | ||||
| 	Ok     bool `json:"ok"` | ||||
| 	Result struct { | ||||
| 		Balance string      `json:"balance"` | ||||
| 		Nonce   json.Number `json:"nonce"` | ||||
| 	} `json:"result"` | ||||
| } | ||||
| @ -1,27 +0,0 @@ | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type Transaction struct { | ||||
| 	CreatedAt     time.Time   `json:"createdAt"` | ||||
| 	Status        string      `json:"status"` | ||||
| 	TransferValue json.Number `json:"transferValue"` | ||||
| 	TxHash        string      `json:"txHash"` | ||||
| 	TxType        string      `json:"txType"` | ||||
| } | ||||
| 
 | ||||
| type TrackStatusResponse struct { | ||||
| 	Ok     bool `json:"ok"` | ||||
| 	Result struct { | ||||
| 		Transaction struct { | ||||
| 			CreatedAt     time.Time   `json:"createdAt"` | ||||
| 			Status        string      `json:"status"` | ||||
| 			TransferValue json.Number `json:"transferValue"` | ||||
| 			TxHash        string      `json:"txHash"` | ||||
| 			TxType        string      `json:"txType"` | ||||
| 		} | ||||
| 	} `json:"result"` | ||||
| } | ||||
| @ -10,34 +10,38 @@ const ( | ||||
| 	DATATYPE_USERSUB = 64 | ||||
| ) | ||||
| 
 | ||||
| // 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 | ||||
| 	pfx   []byte | ||||
| } | ||||
| 
 | ||||
| func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb { | ||||
| 	return &SubPrefixDb{ | ||||
| 		store: store, | ||||
| 		pfx: pfx, | ||||
| 		pfx:   pfx, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func(s *SubPrefixDb) toKey(k []byte) []byte { | ||||
|         return append(s.pfx, k...) | ||||
| 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(DATATYPE_USERSUB) | ||||
| func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { | ||||
| 	s.store.SetPrefix(DATATYPE_USERSUB) | ||||
| 	key = s.toKey(key) | ||||
|         v, err := s.store.Get(ctx, key) | ||||
|         if err != nil { | ||||
|                 return nil, err | ||||
|         } | ||||
|         return v, nil | ||||
| 	return s.store.Get(ctx, key) | ||||
| } | ||||
| 
 | ||||
| func(s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { | ||||
|         s.store.SetPrefix(DATATYPE_USERSUB) | ||||
| func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { | ||||
| 	s.store.SetPrefix(DATATYPE_USERSUB) | ||||
| 	key = s.toKey(key) | ||||
|         return s.store.Put(ctx, key, val) | ||||
| 	return s.store.Put(ctx, key, val) | ||||
| } | ||||
|  | ||||
| @ -11,9 +11,11 @@ import ( | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/server" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/testservice" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/testtag" | ||||
| 	testdataloader "github.com/peteole/testdata-loader" | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| @ -71,7 +73,7 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	lhs, err := handlers.NewLocalHandlerService(pfp, true, dbResource, cfg, rs) | ||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||
| 	lhs.SetDataStore(&userDataStore) | ||||
| 	lhs.SetPersister(pe) | ||||
| 
 | ||||
| @ -80,16 +82,16 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 
 | ||||
| 	if AccountService == nil { | ||||
| 		AccountService = &server.AccountService{} | ||||
| 	if testtag.AccountService == nil { | ||||
| 		testtag.AccountService = &remote.AccountService{} | ||||
| 	} | ||||
| 
 | ||||
| 	switch AccountService.(type) { | ||||
| 	case *server.TestAccountService: | ||||
| 	switch testtag.AccountService.(type) { | ||||
| 	case *testservice.TestAccountService: | ||||
| 		go func() { | ||||
| 			eventChannel <- false | ||||
| 		}() | ||||
| 	case *server.AccountService: | ||||
| 	case *remote.AccountService: | ||||
| 		go func() { | ||||
| 			time.Sleep(5 * time.Second) // Wait for 5 seconds
 | ||||
| 			eventChannel <- true | ||||
| @ -98,7 +100,7 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) { | ||||
| 		panic("Unknown account service type") | ||||
| 	} | ||||
| 
 | ||||
| 	hl, err := lhs.GetHandler(AccountService) | ||||
| 	hl, err := lhs.GetHandler(testtag.AccountService) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
|  | ||||
							
								
								
									
										49
									
								
								internal/testutil/mocks/servicemock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								internal/testutil/mocks/servicemock.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| package mocks | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/models" | ||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||
| 	"github.com/stretchr/testify/mock" | ||||
| ) | ||||
| 
 | ||||
| // MockAccountService implements AccountServiceInterface for testing
 | ||||
| type MockAccountService struct { | ||||
| 	mock.Mock | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { | ||||
| 	args := m.Called() | ||||
| 	return args.Get(0).(*models.AccountResult), args.Error(1) | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { | ||||
| 	args := m.Called(publicKey) | ||||
| 	return args.Get(0).(*models.BalanceResult), args.Error(1) | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) TrackAccountStatus(ctx context.Context, trackingId string) (*models.TrackStatusResult, error) { | ||||
| 	args := m.Called(trackingId) | ||||
| 	return args.Get(0).(*models.TrackStatusResult), args.Error(1) | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { | ||||
| 	args := m.Called(publicKey) | ||||
| 	return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1) | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { | ||||
| 	args := m.Called(publicKey) | ||||
| 	return args.Get(0).([]dataserviceapi.Last10TxResponse), args.Error(1) | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { | ||||
| 	args := m.Called(address) | ||||
| 	return args.Get(0).(*models.VoucherDataResult), args.Error(1) | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { | ||||
| 	args := m.Called() | ||||
| 	return args.Get(0).(*models.TokenTransferResponse), args.Error(1) | ||||
| } | ||||
| @ -1,11 +0,0 @@ | ||||
| // +build !online
 | ||||
| 
 | ||||
| package testutil | ||||
| 
 | ||||
| import ( | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers/server" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	AccountService server.AccountServiceInterface = &server.TestAccountService{} | ||||
| ) | ||||
							
								
								
									
										58
									
								
								internal/testutil/testservice/TestAccountService.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								internal/testutil/testservice/TestAccountService.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| package testservice | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/models" | ||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||
| ) | ||||
| 
 | ||||
| type TestAccountService struct { | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { | ||||
| 	return &models.AccountResult{ | ||||
| 		TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d", | ||||
| 		PublicKey:  "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD", | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { | ||||
| 	balanceResponse := &models.BalanceResult{ | ||||
| 		Balance: "0.003 CELO", | ||||
| 		Nonce:   json.Number("0"), | ||||
| 	} | ||||
| 	return balanceResponse, nil | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) { | ||||
| 	return &models.TrackStatusResult{ | ||||
| 		Active: true, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { | ||||
| 	return []dataserviceapi.TokenHoldings { | ||||
| 		dataserviceapi.TokenHoldings { | ||||
| 			ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", | ||||
| 			TokenSymbol:     "SRF", | ||||
| 			TokenDecimals:   "6", | ||||
| 			Balance:         "2745987", | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { | ||||
| 	return []dataserviceapi.Last10TxResponse{}, nil | ||||
| } | ||||
| 
 | ||||
| func (m TestAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { | ||||
| 	return &models.VoucherDataResult{}, nil | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { | ||||
| 	return &models.TokenTransferResponse{ | ||||
| 		TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af", | ||||
| 	}, nil | ||||
| } | ||||
							
								
								
									
										12
									
								
								internal/testutil/testtag/offlinetest.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/testutil/testtag/offlinetest.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| // +build !online
 | ||||
| 
 | ||||
| package testtag | ||||
| 
 | ||||
| import ( | ||||
| 	"git.grassecon.net/urdt/ussd/remote" | ||||
| 	accountservice "git.grassecon.net/urdt/ussd/internal/testutil/testservice" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	AccountService remote.AccountServiceInterface = &accountservice.TestAccountService{} | ||||
| ) | ||||
| @ -1,6 +1,6 @@ | ||||
| // +build online
 | ||||
| 
 | ||||
| package testutil | ||||
| package testtag | ||||
| 
 | ||||
| import "git.grassecon.net/urdt/ussd/internal/handlers/server" | ||||
| 
 | ||||
							
								
								
									
										51
									
								
								internal/utils/adminstore.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								internal/utils/adminstore.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| package utils | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/db" | ||||
| 	fsdb "git.defalsify.org/vise.git/db/fs" | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	logg = logging.NewVanilla().WithDomain("adminstore") | ||||
| ) | ||||
| 
 | ||||
| type AdminStore struct { | ||||
| 	ctx     context.Context | ||||
| 	FsStore db.Db | ||||
| } | ||||
| 
 | ||||
| func NewAdminStore(ctx context.Context, fileName string) (*AdminStore, error) { | ||||
| 	fsStore, err := getFsStore(ctx, fileName) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &AdminStore{ctx: ctx, FsStore: fsStore}, nil | ||||
| } | ||||
| 
 | ||||
| func getFsStore(ctx context.Context, connectStr string) (db.Db, error) { | ||||
| 	fsStore := fsdb.NewFsDb() | ||||
| 	err := fsStore.Connect(ctx, connectStr) | ||||
| 	fsStore.SetPrefix(db.DATATYPE_USERDATA) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return fsStore, nil | ||||
| } | ||||
| 
 | ||||
| // Checks if the given sessionId is listed as an admin.
 | ||||
| func (as *AdminStore) IsAdmin(sessionId string) (bool, error) { | ||||
| 	_, err := as.FsStore.Get(as.ctx, []byte(sessionId)) | ||||
| 	if err != nil { | ||||
| 		if db.IsNotFound(err) { | ||||
| 			logg.Printf(logging.LVL_INFO, "Returning false because session id was not found") | ||||
| 			return false, nil | ||||
| 		} else { | ||||
| 			return false, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return true, nil | ||||
| } | ||||
| @ -1,37 +0,0 @@ | ||||
| package utils | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/binary" | ||||
| ) | ||||
| 
 | ||||
| type DataTyp uint16 | ||||
| 
 | ||||
| const ( | ||||
| 	DATA_ACCOUNT DataTyp = iota | ||||
| 	DATA_ACCOUNT_CREATED | ||||
| 	DATA_TRACKING_ID | ||||
| 	DATA_PUBLIC_KEY | ||||
| 	DATA_CUSTODIAL_ID | ||||
| 	DATA_ACCOUNT_PIN | ||||
| 	DATA_ACCOUNT_STATUS | ||||
| 	DATA_FIRST_NAME | ||||
| 	DATA_FAMILY_NAME | ||||
| 	DATA_YOB | ||||
| 	DATA_LOCATION | ||||
| 	DATA_GENDER | ||||
| 	DATA_OFFERINGS | ||||
| 	DATA_RECIPIENT | ||||
| 	DATA_AMOUNT | ||||
| 	DATA_TEMPORARY_PIN | ||||
| ) | ||||
| 
 | ||||
| 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...) | ||||
| } | ||||
| @ -5,7 +5,7 @@ | ||||
|             "steps": [ | ||||
|                 { | ||||
|                     "input": "", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "3", | ||||
| @ -13,7 +13,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "5", | ||||
|                     "expectedContent": "PIN Management\n1:Change PIN\n2:Reset other's PIN\n3:Guard my PIN\n0:Back" | ||||
|                     "expectedContent": "PIN Management\n1:Change PIN\n2:Reset other's PIN\n0:Back" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1", | ||||
| @ -33,7 +33,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
| @ -42,7 +42,7 @@ | ||||
|             "steps": [ | ||||
|                 { | ||||
|                     "input": "", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "3", | ||||
| @ -54,7 +54,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1235", | ||||
|                     "expectedContent": "Incorrect pin\n1:retry\n9:Quit" | ||||
|                     "expectedContent": "Incorrect pin\n1:Retry\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1", | ||||
| @ -70,7 +70,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
| @ -79,7 +79,7 @@ | ||||
|             "steps": [ | ||||
|                 { | ||||
|                     "input": "", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "3", | ||||
| @ -95,7 +95,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1235", | ||||
|                     "expectedContent": "Incorrect pin\n1:retry\n9:Quit" | ||||
|                     "expectedContent": "Incorrect pin\n1:Retry\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1", | ||||
| @ -103,7 +103,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1234", | ||||
|                     "expectedContent": "Your balance is 0.003 CELO\n0:Back\n9:Quit" | ||||
|                     "expectedContent": "Balance: {balance}\n\n0:Back\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
| @ -116,7 +116,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
| @ -125,7 +125,7 @@ | ||||
|             "steps": [ | ||||
|                 { | ||||
|                     "input": "", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "3", | ||||
| @ -141,7 +141,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1235", | ||||
|                     "expectedContent": "Incorrect pin\n1:retry\n9:Quit" | ||||
|                     "expectedContent": "Incorrect pin\n1:Retry\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1", | ||||
| @ -149,7 +149,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1234", | ||||
|                     "expectedContent": "Your community balance is 0.003 CELO\n0:Back\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n0:Back\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
| @ -162,7 +162,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
| @ -171,7 +171,7 @@ | ||||
|             "steps": [ | ||||
|                 { | ||||
|                     "input": "", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "3", | ||||
| @ -203,7 +203,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
| @ -212,7 +212,7 @@ | ||||
|             "steps": [ | ||||
|                 { | ||||
|                     "input": "", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "3", | ||||
| @ -244,7 +244,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|                 | ||||
|             ] | ||||
| @ -254,7 +254,7 @@ | ||||
|             "steps": [ | ||||
|                 { | ||||
|                     "input": "", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "3", | ||||
| @ -286,7 +286,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
| @ -295,7 +295,7 @@ | ||||
|             "steps": [ | ||||
|                 { | ||||
|                     "input": "", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "3", | ||||
| @ -327,7 +327,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
| @ -336,7 +336,7 @@ | ||||
|             "steps": [ | ||||
|                 { | ||||
|                     "input": "", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "3", | ||||
| @ -368,7 +368,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
| @ -377,7 +377,7 @@ | ||||
|             "steps": [ | ||||
|                 { | ||||
|                     "input": "", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "3", | ||||
| @ -409,7 +409,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
| @ -418,7 +418,7 @@ | ||||
|             "steps": [ | ||||
|                 { | ||||
|                     "input": "", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "3", | ||||
| @ -446,7 +446,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "0", | ||||
|                     "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|  | ||||
| @ -9,8 +9,8 @@ import ( | ||||
| 	"regexp" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/driver" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/testutil/driver" | ||||
| 	"github.com/gofrs/uuid" | ||||
| ) | ||||
| 
 | ||||
| @ -43,6 +43,39 @@ func extractPublicKey(response []byte) string { | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| // Extracts the balance value from the engine response.
 | ||||
| func extractBalance(response []byte) string { | ||||
| 	// Regex to match "Balance: <amount> <symbol>" followed by a newline
 | ||||
| 	re := regexp.MustCompile(`(?m)^Balance:\s+(\d+(\.\d+)?)\s+([A-Z]+)`) | ||||
| 	match := re.FindSubmatch(response) | ||||
| 	if match != nil { | ||||
| 		return string(match[1]) + " " + string(match[3]) // "<amount> <symbol>"
 | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| // Extracts the Maximum amount value from the engine response.
 | ||||
| func extractMaxAmount(response []byte) string { | ||||
| 	// Regex to match "Maximum amount: <amount>" followed by a newline
 | ||||
| 	re := regexp.MustCompile(`(?m)^Maximum amount:\s+(\d+(\.\d+)?)`) | ||||
| 	match := re.FindSubmatch(response) | ||||
| 	if match != nil { | ||||
| 		return string(match[1]) // "<amount>"
 | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| // Extracts the send amount value from the engine response.
 | ||||
| func extractSendAmount(response []byte) string { | ||||
| 	// Regex to match the pattern "will receive X.XX SYM from"
 | ||||
| 	re := regexp.MustCompile(`will receive (\d+\.\d{2}\s+[A-Z]+) from`) | ||||
| 	match := re.FindSubmatch(response) | ||||
| 	if match != nil { | ||||
| 		return string(match[1]) // Returns "X.XX SYM"
 | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| func TestMain(m *testing.M) { | ||||
| 	sessionID = GenerateSessionId() | ||||
| 	defer func() { | ||||
| @ -154,6 +187,12 @@ func TestMainMenuHelp(t *testing.T) { | ||||
| 				} | ||||
| 
 | ||||
| 				b := w.Bytes() | ||||
| 				balance := extractBalance(b) | ||||
| 
 | ||||
| 				expectedContent := []byte(step.ExpectedContent) | ||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) | ||||
| 
 | ||||
| 				step.ExpectedContent = string(expectedContent) | ||||
| 				match, err := step.MatchesExpectedContent(b) | ||||
| 				if err != nil { | ||||
| 					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) | ||||
| @ -189,6 +228,12 @@ func TestMainMenuQuit(t *testing.T) { | ||||
| 				} | ||||
| 
 | ||||
| 				b := w.Bytes() | ||||
| 				balance := extractBalance(b) | ||||
| 
 | ||||
| 				expectedContent := []byte(step.ExpectedContent) | ||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) | ||||
| 
 | ||||
| 				step.ExpectedContent = string(expectedContent) | ||||
| 				match, err := step.MatchesExpectedContent(b) | ||||
| 				if err != nil { | ||||
| 					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) | ||||
| @ -225,8 +270,13 @@ func TestMyAccount_MyAddress(t *testing.T) { | ||||
| 				} | ||||
| 				b := w.Bytes() | ||||
| 
 | ||||
| 				balance := extractBalance(b) | ||||
| 				publicKey := extractPublicKey(b) | ||||
| 				expectedContent := bytes.Replace([]byte(step.ExpectedContent), []byte("{public_key}"), []byte(publicKey), -1) | ||||
| 
 | ||||
| 				expectedContent := []byte(step.ExpectedContent) | ||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) | ||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{public_key}"), []byte(publicKey), -1) | ||||
| 
 | ||||
| 				step.ExpectedContent = string(expectedContent) | ||||
| 				match, err := step.MatchesExpectedContent(b) | ||||
| 				if err != nil { | ||||
| @ -240,6 +290,52 @@ func TestMyAccount_MyAddress(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestMainMenuSend(t *testing.T) { | ||||
| 	en, fn, _ := testutil.TestEngine(sessionID) | ||||
| 	defer fn() | ||||
| 	ctx := context.Background() | ||||
| 	sessions := testData | ||||
| 	for _, session := range sessions { | ||||
| 		groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs") | ||||
| 		for _, group := range groups { | ||||
| 			for _, step := range group.Steps { | ||||
| 				cont, err := en.Exec(ctx, []byte(step.Input)) | ||||
| 				if err != nil { | ||||
| 					t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err) | ||||
| 					return | ||||
| 				} | ||||
| 				if !cont { | ||||
| 					break | ||||
| 				} | ||||
| 				w := bytes.NewBuffer(nil) | ||||
| 				if _, err := en.Flush(ctx, w); err != nil { | ||||
| 					t.Fatalf("Test case '%s' failed during Flush: %v", group.Name, err) | ||||
| 				} | ||||
| 
 | ||||
| 				b := w.Bytes() | ||||
| 				balance := extractBalance(b) | ||||
| 				max_amount := extractMaxAmount(b) | ||||
| 				send_amount := extractSendAmount(b) | ||||
| 
 | ||||
| 				expectedContent := []byte(step.ExpectedContent) | ||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) | ||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{max_amount}"), []byte(max_amount), -1) | ||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{send_amount}"), []byte(send_amount), -1) | ||||
| 				expectedContent = bytes.Replace(expectedContent, []byte("{session_id}"), []byte(sessionID), -1) | ||||
| 
 | ||||
| 				step.ExpectedContent = string(expectedContent) | ||||
| 				match, err := step.MatchesExpectedContent(b) | ||||
| 				if err != nil { | ||||
| 					t.Fatalf("Error compiling regex for step '%s': %v", step.Input, err) | ||||
| 				} | ||||
| 				if !match { | ||||
| 					t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", step.ExpectedContent, b) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGroups(t *testing.T) { | ||||
| 	groups, err := driver.LoadTestGroups(groupTestFile) | ||||
| 	if err != nil { | ||||
| @ -265,6 +361,13 @@ func TestGroups(t *testing.T) { | ||||
| 				t.Errorf("Test case '%s' failed during Flush: %v", tt.Name, err) | ||||
| 			} | ||||
| 			b := w.Bytes() | ||||
| 			balance := extractBalance(b) | ||||
| 
 | ||||
| 			expectedContent := []byte(tt.ExpectedContent) | ||||
| 			expectedContent = bytes.Replace(expectedContent, []byte("{balance}"), []byte(balance), -1) | ||||
| 
 | ||||
| 			tt.ExpectedContent = string(expectedContent) | ||||
| 
 | ||||
| 			match, err := tt.MatchesExpectedContent(b) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("Error compiling regex for step '%s': %v", tt.Input, err) | ||||
| @ -272,7 +375,6 @@ func TestGroups(t *testing.T) { | ||||
| 			if !match { | ||||
| 				t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", tt.ExpectedContent, b) | ||||
| 			} | ||||
| 
 | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -23,7 +23,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1111", | ||||
|                         "expectedContent": "The PIN is not a match. Try again\n1:retry\n9:Quit" | ||||
|                         "expectedContent": "The PIN is not a match. Try again\n1:Retry\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1", | ||||
| @ -53,11 +53,11 @@ | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 "name": "send_with_invalid_inputs", | ||||
|                 "name": "send_with_invite", | ||||
|                 "steps": [ | ||||
|                     { | ||||
|                         "input": "", | ||||
|                         "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1", | ||||
| @ -65,39 +65,19 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "000", | ||||
|                         "expectedContent": "000 is not registered or invalid, please try again:\n1:retry\n9:Quit" | ||||
|                         "expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1", | ||||
|                         "expectedContent": "Enter recipient's phone number:\n0:Back" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "065656", | ||||
|                         "expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back" | ||||
|                         "input": "0712345678", | ||||
|                         "expectedContent": "0712345678 is not registered, please try again:\n1:Retry\n2:Invite to Sarafu Network\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "0.1", | ||||
|                         "expectedContent": "Amount 0.1 is invalid, please try again:\n1:retry\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1", | ||||
|                         "expectedContent": "Maximum amount: 0.003 CELO\nEnter amount:\n0:Back" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "0.001", | ||||
|                         "expectedContent": "065656 will receive 0.001 from {public_key}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1222", | ||||
|                         "expectedContent": "Incorrect pin\n1:retry\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1", | ||||
|                         "expectedContent": "065656 will receive 0.001 from {public_key}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1234", | ||||
|                         "expectedContent": "Your request has been sent. 065656 will receive 0.001 from {public_key}." | ||||
|                         "input": "2", | ||||
|                         "expectedContent": "Your invite request for 0712345678 to Sarafu Network failed. Please try again later." | ||||
|                     } | ||||
|                 ] | ||||
|             }, | ||||
| @ -106,7 +86,7 @@ | ||||
|                 "steps": [ | ||||
|                     { | ||||
|                         "input": "", | ||||
|                         "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "4", | ||||
| @ -119,7 +99,7 @@ | ||||
|                 "steps": [ | ||||
|                     { | ||||
|                         "input": "", | ||||
|                         "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "9", | ||||
| @ -132,7 +112,7 @@ | ||||
|                 "steps": [ | ||||
|                     { | ||||
|                         "input": "", | ||||
|                         "expectedContent": "Balance: 0.003 CELO\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                         "expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "3", | ||||
| @ -140,7 +120,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "6", | ||||
|                         "expectedContent": "Address: {public_key}\n9:Quit" | ||||
|                         "expectedContent": "Address: {public_key}\n0:Back\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "9", | ||||
|  | ||||
							
								
								
									
										6
									
								
								models/accountresponse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								models/accountresponse.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| package models | ||||
| 
 | ||||
| type AccountResult struct { | ||||
| 	PublicKey  string `json:"publicKey"` | ||||
| 	TrackingId string `json:"trackingId"` | ||||
| } | ||||
							
								
								
									
										8
									
								
								models/balanceresponse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								models/balanceresponse.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| package models | ||||
| 
 | ||||
| import "encoding/json" | ||||
| 
 | ||||
| type BalanceResult struct { | ||||
| 	Balance string      `json:"balance"` | ||||
| 	Nonce   json.Number `json:"nonce"` | ||||
| } | ||||
							
								
								
									
										5
									
								
								models/token_transfer_response.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								models/token_transfer_response.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| package models | ||||
| 
 | ||||
| type TokenTransferResponse struct { | ||||
| 	TrackingId string `json:"trackingId"` | ||||
| } | ||||
							
								
								
									
										18
									
								
								models/trackstatusresponse.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								models/trackstatusresponse.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type Transaction struct { | ||||
| 	CreatedAt     time.Time   `json:"createdAt"` | ||||
| 	Status        string      `json:"status"` | ||||
| 	TransferValue json.Number `json:"transferValue"` | ||||
| 	TxHash        string      `json:"txHash"` | ||||
| 	TxType        string      `json:"txType"` | ||||
| } | ||||
| 
 | ||||
| type TrackStatusResult struct { | ||||
| 	Active bool `json:"active"` | ||||
| } | ||||
							
								
								
									
										8
									
								
								models/voucher_data_result.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								models/voucher_data_result.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| package models | ||||
| 
 | ||||
| type VoucherDataResult struct { | ||||
| 	TokenName     string `json:"tokenName"` | ||||
| 	TokenSymbol   string `json:"tokenSymbol"` | ||||
| 	TokenDecimals int    `json:"tokenDecimals"` | ||||
| 	SinkAddress   string `json:"sinkAddress"` | ||||
| } | ||||
							
								
								
									
										273
									
								
								remote/accountservice.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								remote/accountservice.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,273 @@ | ||||
| package remote | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/models" | ||||
| 	"github.com/grassrootseconomics/eth-custodial/pkg/api" | ||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||
| ) | ||||
| 
 | ||||
| type AccountServiceInterface interface { | ||||
| 	CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) | ||||
| 	CreateAccount(ctx context.Context) (*models.AccountResult, error) | ||||
| 	TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) | ||||
| 	FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) | ||||
| 	FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) | ||||
| 	VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) | ||||
| 	TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) | ||||
| } | ||||
| 
 | ||||
| type AccountService struct { | ||||
| } | ||||
| 
 | ||||
| // Parameters:
 | ||||
| //   - trackingId: A unique identifier for the account.This should be obtained from a previous call to
 | ||||
| //     CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the
 | ||||
| //     AccountResponse struct can be used here to check the account status during a transaction.
 | ||||
| //
 | ||||
| // Returns:
 | ||||
| //   - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string.
 | ||||
| //   - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
 | ||||
| //     If no error occurs, this will be nil
 | ||||
| func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) { | ||||
| 	var r models.TrackStatusResult | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.TrackURL, publicKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &r, nil | ||||
| } | ||||
| 
 | ||||
| // CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint.
 | ||||
| // Parameters:
 | ||||
| //   - publicKey: The public key associated with the account whose balance needs to be checked.
 | ||||
| func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { | ||||
| 	var balanceResult models.BalanceResult | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.BalanceURL, publicKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &balanceResult) | ||||
| 	return &balanceResult, err | ||||
| } | ||||
| 
 | ||||
| // CreateAccount creates a new account in the custodial system.
 | ||||
| // Returns:
 | ||||
| //   - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
 | ||||
| //     If there is an error during the request or processing, this will be nil.
 | ||||
| //   - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
 | ||||
| //     If no error occurs, this will be nil.
 | ||||
| func (as *AccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { | ||||
| 	var r models.AccountResult | ||||
| 	// Create a new request
 | ||||
| 	req, err := http.NewRequest("POST", config.CreateAccountURL, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &r, nil | ||||
| } | ||||
| 
 | ||||
| // FetchVouchers retrieves the token holdings for a given public key from the data indexer API endpoint
 | ||||
| // Parameters:
 | ||||
| //   - publicKey: The public key associated with the account.
 | ||||
| func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { | ||||
| 	var r struct { | ||||
| 		Holdings []dataserviceapi.TokenHoldings `json:"holdings"` | ||||
| 	} | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.VoucherHoldingsURL, publicKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return r.Holdings, nil | ||||
| } | ||||
| 
 | ||||
| // FetchTransactions retrieves the last 10 transactions for a given public key from the data indexer API endpoint
 | ||||
| // Parameters:
 | ||||
| //   - publicKey: The public key associated with the account.
 | ||||
| func (as *AccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) { | ||||
| 	var r struct { | ||||
| 		Transfers []dataserviceapi.Last10TxResponse `json:"transfers"` | ||||
| 	} | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.VoucherTransfersURL, publicKey) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return r.Transfers, nil | ||||
| } | ||||
| 
 | ||||
| // VoucherData retrieves voucher metadata from the data indexer API endpoint.
 | ||||
| // Parameters:
 | ||||
| //   - address: The voucher address.
 | ||||
| func (as *AccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { | ||||
| 	var r struct { | ||||
| 		TokenDetails models.VoucherDataResult `json:"tokenDetails"` | ||||
| 	} | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.VoucherDataURL, address) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", ep, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	return &r.TokenDetails, err | ||||
| } | ||||
| 
 | ||||
| // TokenTransfer creates a new token transfer in the custodial system.
 | ||||
| // Returns:
 | ||||
| //   - *models.TokenTransferResponse: A pointer to an TokenTransferResponse struct containing the trackingId.
 | ||||
| //     If there is an error during the request or processing, this will be nil.
 | ||||
| //   - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
 | ||||
| //     If no error occurs, this will be nil.
 | ||||
| func (as *AccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) { | ||||
| 	var r models.TokenTransferResponse | ||||
| 
 | ||||
| 	// Create request payload
 | ||||
| 	payload := map[string]string{ | ||||
| 		"amount":       amount, | ||||
| 		"from":         from, | ||||
| 		"to":           to, | ||||
| 		"tokenAddress": tokenAddress, | ||||
| 	} | ||||
| 
 | ||||
| 	payloadBytes, err := json.Marshal(payload) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Create a new request
 | ||||
| 	req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &r, nil | ||||
| } | ||||
| 
 | ||||
| func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { | ||||
| 	var okResponse api.OKResponse | ||||
| 	var errResponse api.ErrResponse | ||||
| 
 | ||||
| 	req.Header.Set("Authorization", "Bearer "+config.BearerToken) | ||||
| 	req.Header.Set("Content-Type", "application/json") | ||||
| 
 | ||||
| 	logRequestDetails(req) | ||||
| 
 | ||||
| 	resp, err := http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Failed to make %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error()) | ||||
| 		errResponse.Description = err.Error() | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	log.Printf("Received response for %s: Status Code: %d | Content-Type: %s", req.URL, resp.StatusCode, resp.Header.Get("Content-Type")) | ||||
| 	body, err := io.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if resp.StatusCode >= http.StatusBadRequest { | ||||
| 		err := json.Unmarshal([]byte(body), &errResponse) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return nil, errors.New(errResponse.Description) | ||||
| 	} | ||||
| 	err = json.Unmarshal([]byte(body), &okResponse) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if len(okResponse.Result) == 0 { | ||||
| 		return nil, errors.New("Empty api result") | ||||
| 	} | ||||
| 
 | ||||
| 	v, err := json.Marshal(okResponse.Result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	err = json.Unmarshal(v, &rcpt) | ||||
| 	return &okResponse, err | ||||
| } | ||||
| 
 | ||||
| func logRequestDetails(req *http.Request) { | ||||
| 	var bodyBytes []byte | ||||
| 	contentType := req.Header.Get("Content-Type") | ||||
| 	if req.Body != nil { | ||||
| 		bodyBytes, err := io.ReadAll(req.Body) | ||||
| 		if err != nil { | ||||
| 			log.Printf("Error reading request body: %s", err) | ||||
| 			return | ||||
| 		} | ||||
| 		req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) | ||||
| 	} else { | ||||
| 		bodyBytes = []byte("-") | ||||
| 	} | ||||
| 
 | ||||
| 	log.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes)) | ||||
| } | ||||
							
								
								
									
										44
									
								
								sample_tokens.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								sample_tokens.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| { | ||||
|     "ok": true, | ||||
|     "description": "Token holdings with current balances", | ||||
|     "result": { | ||||
|       "holdings": [ | ||||
|         { | ||||
|           "contractAddress": "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", | ||||
|           "tokenSymbol": "FSPTST", | ||||
|           "tokenDecimals": "6", | ||||
|           "balance": "8869964242" | ||||
|         }, | ||||
|         { | ||||
|           "contractAddress": "0x724F2910D790B54A39a7638282a45B1D83564fFA", | ||||
|           "tokenSymbol": "GEO", | ||||
|           "tokenDecimals": "6", | ||||
|           "balance": "9884" | ||||
|         }, | ||||
|         { | ||||
|           "contractAddress": "0x2105a206B7bec31E2F90acF7385cc8F7F5f9D273", | ||||
|           "tokenSymbol": "MFNK", | ||||
|           "tokenDecimals": "6", | ||||
|           "balance": "19788697" | ||||
|         }, | ||||
|         { | ||||
|           "contractAddress": "0x63DE2Ac8D1008351Cc69Fb8aCb94Ba47728a7E83", | ||||
|           "tokenSymbol": "MILO", | ||||
|           "tokenDecimals": "6", | ||||
|           "balance": "75" | ||||
|         }, | ||||
|         { | ||||
|           "contractAddress": "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", | ||||
|           "tokenSymbol": "SOHAIL", | ||||
|           "tokenDecimals": "6", | ||||
|           "balance": "27874115" | ||||
|         }, | ||||
|         { | ||||
|           "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", | ||||
|           "tokenSymbol": "SRF", | ||||
|           "tokenDecimals": "6", | ||||
|           "balance": "2745987" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   } | ||||
| @ -1,4 +1,4 @@ | ||||
| RELOAD verify_pin | ||||
| RELOAD verify_create_pin | ||||
| CATCH create_pin_mismatch flag_pin_mismatch 1 | ||||
| LOAD quit 0 | ||||
| HALT | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| LOAD check_identifier 0 | ||||
| RELOAD check_identifier | ||||
| MAP check_identifier | ||||
| MOUT back 0 | ||||
| MOUT quit 9 | ||||
| HALT | ||||
| INCMP _ 0 | ||||
| INCMP quit 9 | ||||
|  | ||||
							
								
								
									
										1
									
								
								services/registration/address_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/address_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Anwani:{{.check_identifier}} | ||||
| @ -1,14 +1,15 @@ | ||||
| LOAD reset_transaction_amount 0 | ||||
| LOAD max_amount 10 | ||||
| RELOAD max_amount | ||||
| MAP max_amount | ||||
| MOUT back 0 | ||||
| HALT | ||||
| LOAD validate_amount 64 | ||||
| RELOAD validate_amount | ||||
| CATCH api_failure  flag_api_call_error  1 | ||||
| CATCH api_failure flag_api_call_error  1 | ||||
| CATCH invalid_amount flag_invalid_amount 1 | ||||
| INCMP _ 0 | ||||
| LOAD get_recipient 12 | ||||
| LOAD get_recipient 0 | ||||
| LOAD get_sender 64 | ||||
| LOAD get_amount 12 | ||||
| LOAD get_amount 32 | ||||
| INCMP transaction_pin * | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| MOUT retry 0 | ||||
| MOUT retry 1 | ||||
| MOUT quit 9 | ||||
| HALT | ||||
| INCMP _ 0 | ||||
| INCMP _ 1 | ||||
| INCMP quit 9 | ||||
|  | ||||
| @ -1 +1 @@ | ||||
| Salio la kikundi | ||||
| {{.fetch_community_balance}} | ||||
| @ -1 +1 @@ | ||||
| {{.fetch_custodial_balances}} | ||||
| {{.fetch_community_balance}} | ||||
| @ -1,7 +1,7 @@ | ||||
| LOAD reset_incorrect 6 | ||||
| LOAD fetch_custodial_balances 0 | ||||
| LOAD fetch_community_balance 0 | ||||
| CATCH api_failure  flag_api_call_error  1 | ||||
| MAP fetch_custodial_balances | ||||
| MAP fetch_community_balance | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| CATCH pin_entry flag_account_authorized 0 | ||||
| MOUT back 0 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| LOAD save_pin 0 | ||||
| LOAD save_temporary_pin 6 | ||||
| HALT | ||||
| LOAD verify_pin 8 | ||||
| LOAD verify_create_pin 8 | ||||
| INCMP account_creation * | ||||
|  | ||||
							
								
								
									
										1
									
								
								services/registration/confirm_others_new_pin
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/confirm_others_new_pin
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Please confirm new PIN  for:{{.retrieve_blocked_number}} | ||||
							
								
								
									
										14
									
								
								services/registration/confirm_others_new_pin.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								services/registration/confirm_others_new_pin.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| CATCH pin_entry flag_incorrect_pin 1 | ||||
| RELOAD retrieve_blocked_number | ||||
| MAP retrieve_blocked_number | ||||
| CATCH invalid_others_pin flag_valid_pin 0 | ||||
| CATCH pin_reset_result flag_account_authorized 1 | ||||
| LOAD save_others_temporary_pin 6 | ||||
| RELOAD save_others_temporary_pin | ||||
| MOUT back 0 | ||||
| HALT  | ||||
| INCMP _ 0 | ||||
| LOAD check_pin_mismatch 0 | ||||
| RELOAD check_pin_mismatch | ||||
| CATCH others_pin_mismatch flag_pin_mismatch 1 | ||||
| INCMP pin_entry * | ||||
							
								
								
									
										1
									
								
								services/registration/confirm_others_new_pin_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/confirm_others_new_pin_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Tafadhali thibitisha PIN mpya ya: {{.retrieve_blocked_number}} | ||||
| @ -3,5 +3,3 @@ MOUT back 0 | ||||
| HALT | ||||
| INCMP _ 0 | ||||
| INCMP * pin_reset_success | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -2,8 +2,8 @@ LOAD create_account 0 | ||||
| CATCH account_creation_failed flag_account_creation_failed 1 | ||||
| MOUT exit 0 | ||||
| HALT | ||||
| LOAD save_pin 0 | ||||
| RELOAD save_pin | ||||
| LOAD save_temporary_pin 6 | ||||
| RELOAD save_temporary_pin | ||||
| CATCH . flag_incorrect_pin 1 | ||||
| INCMP quit 0 | ||||
| INCMP confirm_create_pin * | ||||
|  | ||||
							
								
								
									
										2
									
								
								services/registration/edit_family_name
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/edit_family_name
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Current family name: {{.get_current_profile_info}} | ||||
| Enter family name: | ||||
| @ -1,9 +1,10 @@ | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| CATCH profile_update_success flag_allow_update 1 | ||||
| LOAD save_familyname 0 | ||||
| RELOAD save_familyname | ||||
| CATCH update_familyname flag_allow_update 1 | ||||
| LOAD get_current_profile_info 0 | ||||
| RELOAD get_current_profile_info | ||||
| MOUT back 0 | ||||
| HALT | ||||
| LOAD save_familyname 0 | ||||
| RELOAD save_familyname | ||||
| INCMP _ 0 | ||||
| INCMP pin_entry * | ||||
							
								
								
									
										2
									
								
								services/registration/edit_family_name_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/edit_family_name_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Jina la familia la sasa: {{.get_current_profile_info}} | ||||
| Weka jina la familia | ||||
							
								
								
									
										2
									
								
								services/registration/edit_first_name
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/edit_first_name
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Current name: {{.get_current_profile_info}} | ||||
| Enter your first names: | ||||
							
								
								
									
										11
									
								
								services/registration/edit_first_name.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								services/registration/edit_first_name.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| CATCH update_firstname flag_allow_update 1 | ||||
| LOAD get_current_profile_info 0 | ||||
| RELOAD get_current_profile_info | ||||
| MAP get_current_profile_info | ||||
| MOUT back 0 | ||||
| HALT | ||||
| LOAD save_firstname 0 | ||||
| RELOAD save_firstname | ||||
| INCMP _ 0 | ||||
| INCMP pin_entry * | ||||
							
								
								
									
										2
									
								
								services/registration/edit_first_name_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/edit_first_name_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Jina la kwanza la sasa {{.get_current_profile_info}} | ||||
| Weka majina yako ya kwanza: | ||||
							
								
								
									
										2
									
								
								services/registration/edit_location
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/edit_location
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Current location: {{.get_current_profile_info}} | ||||
| Enter your location: | ||||
| @ -1,8 +1,10 @@ | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| CATCH profile_update_success flag_allow_update 1 | ||||
| LOAD save_location 0 | ||||
| CATCH update_location flag_allow_update 1 | ||||
| LOAD get_current_profile_info 0 | ||||
| RELOAD get_current_profile_info | ||||
| MOUT back 0 | ||||
| HALT | ||||
| LOAD save_location 0 | ||||
| RELOAD save_location | ||||
| INCMP _ 0 | ||||
| INCMP pin_entry * | ||||
							
								
								
									
										2
									
								
								services/registration/edit_location_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/edit_location_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Eneo la sasa {{.get_current_profile_info}} | ||||
| Weka eneo: | ||||
							
								
								
									
										2
									
								
								services/registration/edit_offerings
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/edit_offerings
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Current offerings: {{.get_current_profile_info}} | ||||
| Enter the services or goods you offer:  | ||||
| @ -1,5 +1,7 @@ | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| CATCH profile_update_success flag_allow_update 1 | ||||
| CATCH update_offerings flag_allow_update 1 | ||||
| LOAD get_current_profile_info 0 | ||||
| RELOAD get_current_profile_info | ||||
| LOAD save_offerings 0 | ||||
| MOUT back 0 | ||||
| HALT | ||||
							
								
								
									
										2
									
								
								services/registration/edit_offerings_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/edit_offerings_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Unachouza kwa sasa: {{.get_current_profile_info}} | ||||
| Weka unachouza | ||||
| @ -2,8 +2,8 @@ LOAD reset_account_authorized 16 | ||||
| RELOAD reset_account_authorized | ||||
| LOAD reset_allow_update 0 | ||||
| RELOAD reset_allow_update | ||||
| MOUT edit_name 1 | ||||
| MOUT edit_familyname 2 | ||||
| MOUT edit_first_name 1 | ||||
| MOUT edit_family_name 2 | ||||
| MOUT edit_gender 3 | ||||
| MOUT edit_yob 4 | ||||
| MOUT edit_location 5 | ||||
| @ -12,10 +12,10 @@ MOUT view 7 | ||||
| MOUT back 0 | ||||
| HALT | ||||
| INCMP my_account 0 | ||||
| INCMP enter_name 1 | ||||
| INCMP enter_familyname 2 | ||||
| INCMP edit_first_name 1 | ||||
| INCMP edit_family_name 2 | ||||
| INCMP select_gender 3 | ||||
| INCMP enter_yob 4 | ||||
| INCMP enter_location 5 | ||||
| INCMP enter_offerings 6 | ||||
| INCMP edit_yob 4 | ||||
| INCMP edit_location 5 | ||||
| INCMP edit_offerings 6 | ||||
| INCMP view_profile 7 | ||||
|  | ||||
							
								
								
									
										2
									
								
								services/registration/edit_yob
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/edit_yob
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Current year of birth: {{.get_current_profile_info}} | ||||
| Enter your year of birth | ||||
| @ -1,10 +1,14 @@ | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| CATCH profile_update_success flag_allow_update 1 | ||||
| LOAD save_yob 0 | ||||
| CATCH update_yob flag_allow_update 1 | ||||
| LOAD get_current_profile_info 0 | ||||
| RELOAD get_current_profile_info | ||||
| MAP get_current_profile_info | ||||
| MOUT back 0 | ||||
| HALT | ||||
| LOAD verify_yob 0 | ||||
| LOAD verify_yob 6 | ||||
| RELOAD verify_yob | ||||
| CATCH incorrect_date_format flag_incorrect_date_format 1 | ||||
| LOAD save_yob 0 | ||||
| RELOAD save_yob | ||||
| INCMP _ 0 | ||||
| INCMP pin_entry * | ||||
							
								
								
									
										2
									
								
								services/registration/edit_yob_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/edit_yob_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| Mwaka wa sasa wa kuzaliwa {{.get_current_profile_info}} | ||||
| Weka mwaka wa kuzaliwa | ||||
| @ -1 +0,0 @@ | ||||
| Enter family name: | ||||
| @ -1 +0,0 @@ | ||||
| Weka jina la familia | ||||
| @ -1 +0,0 @@ | ||||
| Enter your location: | ||||
| @ -1 +0,0 @@ | ||||
| Weka eneo: | ||||
| @ -1 +0,0 @@ | ||||
| Enter your first names: | ||||
| @ -1,12 +0,0 @@ | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| CATCH profile_update_success flag_allow_update 1 | ||||
| LOAD save_firstname 0 | ||||
| RELOAD save_firstname  | ||||
| MOUT back 0 | ||||
| HALT | ||||
| RELOAD save_firstname | ||||
| INCMP _ 0 | ||||
| INCMP pin_entry * | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @ -1 +0,0 @@ | ||||
| Weka majina yako ya kwanza: | ||||
| @ -1 +0,0 @@ | ||||
| Enter the services or goods you offer:  | ||||
| @ -1 +0,0 @@ | ||||
| Weka unachouza | ||||
							
								
								
									
										1
									
								
								services/registration/enter_other_number
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/enter_other_number
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Enter other's phone number: | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user