Merge branch 'master' into lash/store-dumper
This commit is contained in:
		
						commit
						3d2ca606ca
					
				
							
								
								
									
										13
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| /** | ||||
| !/cmd/africastalking | ||||
| !/common | ||||
| !/config | ||||
| !/initializers | ||||
| !/internal | ||||
| !/models | ||||
| !/remote | ||||
| !/services | ||||
| !/LICENSE | ||||
| !/README.md | ||||
| !/go.* | ||||
| !/.env.example | ||||
							
								
								
									
										10
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.env.example
									
									
									
									
									
								
							| @ -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,7 +15,6 @@ DB_SSLMODE=disable | ||||
| DB_TIMEZONE=Africa/Nairobi | ||||
| 
 | ||||
| #External API Calls | ||||
| CREATE_ACCOUNT_URL=http://localhost:5003/api/v2/account/create | ||||
| TRACK_STATUS_URL=https://custodial.sarafu.africa/api/track/ | ||||
| BALANCE_URL=https://custodial.sarafu.africa/api/account/status/ | ||||
| TRACK_URL=http://localhost:5003/api/v2/account/status | ||||
| CUSTODIAL_URL_BASE=http://localhost:5003 | ||||
| BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr | ||||
| DATA_URL_BASE=http://localhost:5006 | ||||
|  | ||||
							
								
								
									
										56
									
								
								.github/workflows/docker.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								.github/workflows/docker.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| name: release | ||||
| 
 | ||||
| on: | ||||
|   push: | ||||
|     tags: | ||||
|       - "v*" | ||||
| 
 | ||||
| jobs: | ||||
|   docker: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
| 
 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
| 
 | ||||
|       - name: Check out repo | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
| 
 | ||||
|       - name: Cache Docker layers | ||||
|         uses: actions/cache@v3 | ||||
|         with: | ||||
|           path: /tmp/.buildx-cache | ||||
|           key: ${{ runner.os }}-buildx-${{ github.sha }} | ||||
|           restore-keys: | | ||||
|             ${{ runner.os }}-buildx- | ||||
| 
 | ||||
|       - name: Login to GHCR Docker registry | ||||
|         uses: docker/login-action@v1 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.repository_owner }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
| 
 | ||||
|       - name: Set outputs | ||||
|         run: | | ||||
|           echo "RELEASE_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV \ | ||||
|           && echo "RELEASE_SHORT_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV | ||||
| 
 | ||||
|       - name: Build and push image | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           context: ./ | ||||
|           file: ./Dockerfile | ||||
|           platforms: linux/amd64 | ||||
|           push: true | ||||
|           build-args: | | ||||
|             BUILD=${{ env.RELEASE_SHORT_COMMIT }} | ||||
|           cache-from: type=local,src=/tmp/.buildx-cache | ||||
|           cache-to: type=local,dest=/tmp/.buildx-cache | ||||
|           tags: | | ||||
|             ghcr.io/grassrootseconomics/urdt-ussd:latest | ||||
|             ghcr.io/grassrootseconomics/urdt-ussd:${{ env.RELEASE_TAG }} | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -6,3 +6,4 @@ go.work* | ||||
| cmd/.state/ | ||||
| id_* | ||||
| *.gdbm | ||||
| *.log | ||||
|  | ||||
							
								
								
									
										41
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| FROM golang:1.23.0-bookworm AS build | ||||
| 
 | ||||
| ENV CGO_ENABLED=1 | ||||
| 
 | ||||
| ARG BUILDPLATFORM | ||||
| ARG TARGETPLATFORM | ||||
| ARG BUILD=dev | ||||
| 
 | ||||
| WORKDIR /build | ||||
| COPY . . | ||||
| RUN apt update && apt install libgdbm-dev | ||||
| RUN git clone https://git.defalsify.org/vise.git go-vise | ||||
| 
 | ||||
| WORKDIR /build/services/registration | ||||
| RUN echo "Compiling go-vise files" | ||||
| RUN make VISE_PATH=/build/go-vise -B | ||||
| 
 | ||||
| WORKDIR /build | ||||
| RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM" | ||||
| RUN go mod download | ||||
| RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go | ||||
| 
 | ||||
| FROM debian:bookworm-slim | ||||
| 
 | ||||
| ENV DEBIAN_FRONTEND=noninteractive | ||||
| 
 | ||||
| RUN apt update && apt install libgdbm-dev ca-certificates -y | ||||
| RUN apt-get clean && rm -rf /var/lib/apt/lists/* | ||||
| 
 | ||||
| WORKDIR /service | ||||
| 
 | ||||
| COPY --from=build /build/ussd-africastalking . | ||||
| COPY --from=build /build/LICENSE . | ||||
| COPY --from=build /build/README.md . | ||||
| COPY --from=build /build/services ./services | ||||
| COPY --from=build /build/.env.example . | ||||
| RUN mv .env.example .env | ||||
| 
 | ||||
| EXPOSE 7123 | ||||
| 
 | ||||
| CMD ["./ussd-africastalking"] | ||||
							
								
								
									
										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" | ||||
| @ -16,6 +20,7 @@ import ( | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| 	"git.defalsify.org/vise.git/resource" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/common" | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/initializers" | ||||
| 	"git.grassecon.net/urdt/ussd/internal/handlers" | ||||
| @ -27,6 +32,8 @@ import ( | ||||
| var ( | ||||
| 	logg      = logging.NewVanilla() | ||||
| 	scriptDir = path.Join("services", "registration") | ||||
| 
 | ||||
| 	build = "dev" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| @ -38,9 +45,30 @@ type atRequestParser struct{} | ||||
| func (arp *atRequestParser) GetSessionId(rq any) (string, error) { | ||||
| 	rqv, ok := rq.(*http.Request) | ||||
| 	if !ok { | ||||
| 		log.Printf("got an invalid request:", rq) | ||||
| 		return "", handlers.ErrInvalidRequest | ||||
| 	} | ||||
| 
 | ||||
| 	// Capture body (if any) for logging
 | ||||
| 	body, err := io.ReadAll(rqv.Body) | ||||
| 	if err != nil { | ||||
| 		log.Printf("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.Printf("failed to marshal request body:", err) | ||||
| 	} else { | ||||
| 		log.Printf("Received request:", string(logBytes)) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := rqv.ParseForm(); err != nil { | ||||
| 		log.Printf("failed to parse form data: %v", err) | ||||
| 		return "", fmt.Errorf("failed to parse form data: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| @ -49,7 +77,13 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) { | ||||
| 		return "", fmt.Errorf("no phone number found") | ||||
| 	} | ||||
| 
 | ||||
| 	return phoneNumber, nil | ||||
| 	formattedNumber, err := common.FormatPhoneNumber(phoneNumber) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error: %v\n", err) | ||||
| 		return "", fmt.Errorf("failed to format number") | ||||
| 	} | ||||
| 
 | ||||
| 	return formattedNumber, nil | ||||
| } | ||||
| 
 | ||||
| func (arp *atRequestParser) GetInput(rq any) ([]byte, error) { | ||||
| @ -90,7 +124,7 @@ func main() { | ||||
| 	flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") | ||||
| 	flag.Parse() | ||||
| 
 | ||||
| 	logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) | ||||
| 	logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) | ||||
| 
 | ||||
| 	ctx := context.Background() | ||||
| 	ctx = context.WithValue(ctx, "Database", database) | ||||
| @ -132,6 +166,10 @@ func main() { | ||||
| 	} | ||||
| 
 | ||||
| 	lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, err.Error()) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	lhs.SetDataStore(&userdataStore) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| @ -156,9 +194,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) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										21
									
								
								common/db.go
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								common/db.go
									
									
									
									
									
								
							| @ -2,6 +2,7 @@ package common | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/binary" | ||||
| 	"errors" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/logging" | ||||
| ) | ||||
| @ -48,3 +49,23 @@ 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") | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										73
									
								
								common/recipient.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								common/recipient.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // Define the regex patterns as constants
 | ||||
| const ( | ||||
| 	phoneRegex   = `^(?:\+254|254|0)?((?:7[0-9]{8})|(?:1[01][0-9]{7}))$` | ||||
| 	addressRegex = `^0x[a-fA-F0-9]{40}$` | ||||
| 	aliasRegex   = `^[a-zA-Z0-9]+$` | ||||
| ) | ||||
| 
 | ||||
| // IsValidPhoneNumber checks if the given number is a valid phone number
 | ||||
| func IsValidPhoneNumber(phonenumber string) bool { | ||||
| 	match, _ := regexp.MatchString(phoneRegex, phonenumber) | ||||
| 	return match | ||||
| } | ||||
| 
 | ||||
| // IsValidAddress checks if the given address is a valid Ethereum address
 | ||||
| func IsValidAddress(address string) bool { | ||||
| 	match, _ := regexp.MatchString(addressRegex, address) | ||||
| 	return match | ||||
| } | ||||
| 
 | ||||
| // IsValidAlias checks if the alias is a valid alias format
 | ||||
| func IsValidAlias(alias string) bool { | ||||
| 	match, _ := regexp.MatchString(aliasRegex, alias) | ||||
| 	return match | ||||
| } | ||||
| 
 | ||||
| // CheckRecipient validates the recipient format based on the criteria
 | ||||
| func CheckRecipient(recipient string) (string, error) { | ||||
| 	if IsValidPhoneNumber(recipient) { | ||||
| 		return "phone number", nil | ||||
| 	} | ||||
| 
 | ||||
| 	if IsValidAddress(recipient) { | ||||
| 		return "address", nil | ||||
| 	} | ||||
| 
 | ||||
| 	if IsValidAlias(recipient) { | ||||
| 		return "alias", nil | ||||
| 	} | ||||
| 
 | ||||
| 	return "", fmt.Errorf("invalid recipient: must be a phone number, address or alias") | ||||
| } | ||||
| 
 | ||||
| // FormatPhoneNumber formats a Kenyan phone number to "+254xxxxxxxx".
 | ||||
| func FormatPhoneNumber(phone string) (string, error) { | ||||
| 	if !IsValidPhoneNumber(phone) { | ||||
| 		return "", errors.New("invalid phone number") | ||||
| 	} | ||||
| 
 | ||||
| 	// Remove any leading "+" and spaces
 | ||||
| 	phone = strings.TrimPrefix(phone, "+") | ||||
| 	phone = strings.ReplaceAll(phone, " ", "") | ||||
| 
 | ||||
| 	// Replace leading "0" with "254" if present
 | ||||
| 	if strings.HasPrefix(phone, "0") { | ||||
| 		phone = "254" + phone[1:] | ||||
| 	} | ||||
| 
 | ||||
| 	// Add "+" if not already present
 | ||||
| 	if !strings.HasPrefix(phone, "254") { | ||||
| 		return "", errors.New("unexpected format") | ||||
| 	} | ||||
| 
 | ||||
| 	return "+" + phone, nil | ||||
| } | ||||
							
								
								
									
										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) | ||||
| } | ||||
							
								
								
									
										119
									
								
								common/transfer_statements.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								common/transfer_statements.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,119 @@ | ||||
| package common | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||
| ) | ||||
| 
 | ||||
| // TransferMetadata helps organize data fields
 | ||||
| type TransferMetadata struct { | ||||
| 	Senders        string | ||||
| 	Recipients     string | ||||
| 	TransferValues string | ||||
| 	Addresses      string | ||||
| 	TxHashes       string | ||||
| 	Dates          string | ||||
| 	Symbols        string | ||||
| 	Decimals       string | ||||
| } | ||||
| 
 | ||||
| // ProcessTransfers converts transfers into formatted strings
 | ||||
| func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetadata { | ||||
| 	var data TransferMetadata | ||||
| 	var senders, recipients, transferValues, addresses, txHashes, dates, symbols, decimals []string | ||||
| 
 | ||||
| 	for _, t := range transfers { | ||||
| 		senders = append(senders, t.Sender) | ||||
| 		recipients = append(recipients, t.Recipient) | ||||
| 
 | ||||
| 		// Scale down the amount
 | ||||
| 		scaledBalance := ScaleDownBalance(t.TransferValue, t.TokenDecimals) | ||||
| 		transferValues = append(transferValues, scaledBalance) | ||||
| 
 | ||||
| 		addresses = append(addresses, t.ContractAddress) | ||||
| 		txHashes = append(txHashes, t.TxHash) | ||||
| 		dates = append(dates, fmt.Sprintf("%s", t.DateBlock)) | ||||
| 		symbols = append(symbols, t.TokenSymbol) | ||||
| 		decimals = append(decimals, t.TokenDecimals) | ||||
| 	} | ||||
| 
 | ||||
| 	data.Senders = strings.Join(senders, "\n") | ||||
| 	data.Recipients = strings.Join(recipients, "\n") | ||||
| 	data.TransferValues = strings.Join(transferValues, "\n") | ||||
| 	data.Addresses = strings.Join(addresses, "\n") | ||||
| 	data.TxHashes = strings.Join(txHashes, "\n") | ||||
| 	data.Dates = strings.Join(dates, "\n") | ||||
| 	data.Symbols = strings.Join(symbols, "\n") | ||||
| 	data.Decimals = strings.Join(decimals, "\n") | ||||
| 
 | ||||
| 	return data | ||||
| } | ||||
| 
 | ||||
| // GetTransferData retrieves and matches transfer data
 | ||||
| // returns a formatted string of the full transaction/statement
 | ||||
| func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) { | ||||
| 	keys := []string{"txfrom", "txto", "txval", "txaddr", "txhash", "txdate", "txsym"} | ||||
| 	data := make(map[string]string) | ||||
| 
 | ||||
| 	for _, key := range keys { | ||||
| 		value, err := db.Get(ctx, []byte(key)) | ||||
| 		if err != nil { | ||||
| 			return "", fmt.Errorf("failed to get %s: %v", key, err) | ||||
| 		} | ||||
| 		data[key] = string(value) | ||||
| 	} | ||||
| 
 | ||||
| 	// Split the data
 | ||||
| 	senders := strings.Split(string(data["txfrom"]), "\n") | ||||
| 	recipients := strings.Split(string(data["txto"]), "\n") | ||||
| 	values := strings.Split(string(data["txval"]), "\n") | ||||
| 	addresses := strings.Split(string(data["txaddr"]), "\n") | ||||
| 	hashes := strings.Split(string(data["txhash"]), "\n") | ||||
| 	dates := strings.Split(string(data["txdate"]), "\n") | ||||
| 	syms := strings.Split(string(data["txsym"]), "\n") | ||||
| 
 | ||||
| 	// Check if index is within range
 | ||||
| 	if index < 1 || index > len(senders) { | ||||
| 		return "", fmt.Errorf("transaction not found: index %d out of range", index) | ||||
| 	} | ||||
| 
 | ||||
| 	// Adjust for 0-based indexing
 | ||||
| 	i := index - 1 | ||||
| 	transactionType := "received" | ||||
| 	party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i])) | ||||
| 	if strings.TrimSpace(senders[i]) == publicKey { | ||||
| 		transactionType = "sent" | ||||
| 		party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i])) | ||||
| 	} | ||||
| 
 | ||||
| 	formattedDate := formatDate(strings.TrimSpace(dates[i])) | ||||
| 
 | ||||
| 	// Build the full transaction detail
 | ||||
| 	detail := fmt.Sprintf( | ||||
| 		"%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s", | ||||
| 		transactionType, | ||||
| 		strings.TrimSpace(values[i]), | ||||
| 		strings.TrimSpace(syms[i]), | ||||
| 		party, | ||||
| 		strings.TrimSpace(addresses[i]), | ||||
| 		strings.TrimSpace(hashes[i]), | ||||
| 		formattedDate, | ||||
| 	) | ||||
| 
 | ||||
| 	return detail, nil | ||||
| } | ||||
| 
 | ||||
| // Helper function to format date in desired output
 | ||||
| func formatDate(dateStr string) string { | ||||
| 	parsedDate, err := time.Parse("2006-01-02 15:04:05 -0700 MST", dateStr) | ||||
| 	if err != nil { | ||||
| 		fmt.Println("Error parsing date:", err) | ||||
| 		return "" | ||||
| 	} | ||||
| 	return parsedDate.Format("2006-01-02 03:04:05 PM") | ||||
| } | ||||
| @ -3,6 +3,7 @@ package common | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"math/big" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"git.grassecon.net/urdt/ussd/internal/storage" | ||||
| @ -24,7 +25,11 @@ func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata { | ||||
| 
 | ||||
| 	for i, h := range holdings { | ||||
| 		symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol)) | ||||
| 		balances = append(balances, fmt.Sprintf("%d:%s", i+1, h.Balance)) | ||||
| 
 | ||||
| 		// 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)) | ||||
| 	} | ||||
| @ -37,14 +42,25 @@ func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata { | ||||
| 	return data | ||||
| } | ||||
| 
 | ||||
| //func StoreVouchers(db storage.PrefixDb, data VoucherMetadata) {
 | ||||
| //		value, err := db.Put(ctx, []byte(key))
 | ||||
| //		if err != nil {
 | ||||
| //			return nil, fmt.Errorf("failed to get %s: %v", key, err)
 | ||||
| //		}
 | ||||
| //		data[key] = string(value)
 | ||||
| //	}
 | ||||
| //}
 | ||||
| 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) { | ||||
| @ -84,7 +100,7 @@ func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, | ||||
| 	decList := strings.Split(decimals, "\n") | ||||
| 	addrList := strings.Split(addresses, "\n") | ||||
| 
 | ||||
| 	logg.Tracef("found" , "symlist", symList, "syms", symbols, "input", input) | ||||
| 	logg.Tracef("found", "symlist", symList, "syms", symbols, "input", input) | ||||
| 	for i, sym := range symList { | ||||
| 		parts := strings.SplitN(sym, ":", 2) | ||||
| 
 | ||||
| @ -135,7 +151,7 @@ func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId str | ||||
| 	return data, nil | ||||
| } | ||||
| 
 | ||||
| // UpdateVoucherData sets the active voucher data and clears the temporary voucher data in the DataStore.
 | ||||
| // 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
 | ||||
|  | ||||
| @ -59,13 +59,13 @@ func TestMatchVoucher(t *testing.T) { | ||||
| 
 | ||||
| func TestProcessVouchers(t *testing.T) { | ||||
| 	holdings := []dataserviceapi.TokenHoldings{ | ||||
| 		{ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100"}, | ||||
| 		{ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200"}, | ||||
| 		{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:200", | ||||
| 		Balances:  "1:100\n2:20000", | ||||
| 		Decimals:  "1:6\n2:4", | ||||
| 		Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa", | ||||
| 	} | ||||
|  | ||||
| @ -7,30 +7,33 @@ import ( | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	createAccountPath = "/api/v2/account/create" | ||||
| 	trackStatusPath = "/api/track" | ||||
| 	balancePathPrefix = "/api/account" | ||||
| 	trackPath = "/api/v2/account/status" | ||||
| 	voucherHoldingsPathPrefix = "/api/v1/holdings" | ||||
| 	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" | ||||
| 	voucherDataPathPrefix      = "/api/v1/token" | ||||
| 	AliasPrefix                = "api/v1/alias" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	custodialURLBase string | ||||
| 	dataURLBase string | ||||
| 	CustodialAPIKey string | ||||
| 	DataAPIKey string | ||||
| 	dataURLBase      string | ||||
| 	BearerToken      string | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	CreateAccountURL string | ||||
| 	TrackStatusURL   string | ||||
| 	BalanceURL	string | ||||
| 	TrackURL         string | ||||
| 	VoucherHoldingsURL	string | ||||
| 	VoucherTransfersURL	string | ||||
| 	VoucherDataURL	string | ||||
| 	CreateAccountURL    string | ||||
| 	TrackStatusURL      string | ||||
| 	BalanceURL          string | ||||
| 	TrackURL            string | ||||
| 	TokenTransferURL    string | ||||
| 	VoucherHoldingsURL  string | ||||
| 	VoucherTransfersURL string | ||||
| 	VoucherDataURL      string | ||||
| 	CheckAliasURL       string | ||||
| ) | ||||
| 
 | ||||
| func setBase() error { | ||||
| @ -38,8 +41,7 @@ func setBase() error { | ||||
| 
 | ||||
| 	custodialURLBase = initializers.GetEnv("CUSTODIAL_URL_BASE", "http://localhost:5003") | ||||
| 	dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006") | ||||
| 	CustodialAPIKey = initializers.GetEnv("CUSTODIAL_API_KEY", "xd") | ||||
| 	DataAPIKey = initializers.GetEnv("DATA_API_KEY", "xd") | ||||
| 	BearerToken = initializers.GetEnv("BEARER_TOKEN", "") | ||||
| 
 | ||||
| 	_, err = url.JoinPath(custodialURLBase, "/foo") | ||||
| 	if err != nil { | ||||
| @ -58,13 +60,15 @@ func LoadConfig() error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	CreateAccountURL, _  = url.JoinPath(custodialURLBase, createAccountPath) | ||||
| 	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) | ||||
| 	CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
							
								
								
									
										3
									
								
								dev/dialoguss/sample_user.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								dev/dialoguss/sample_user.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| url: http://localhost:7123 | ||||
| dial: "*384*96#" | ||||
| phoneNumber: +254722123456 | ||||
							
								
								
									
										21
									
								
								dev/docker-compose.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								dev/docker-compose.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| services: | ||||
|   ussd-pg-store: | ||||
|     image: postgres:17-alpine | ||||
|     restart: unless-stopped | ||||
|     user: postgres | ||||
|     environment: | ||||
|       - POSTGRES_PASSWORD=postgres | ||||
|       - POSTGRES_USER=postgres | ||||
|     volumes: | ||||
|       - ./init_db.sql:/docker-entrypoint-initdb.d/init_db.sql | ||||
|       - ussd-pg:/var/lib/postgresql/data | ||||
|     ports: | ||||
|       - "127.0.0.1:5432:5432" | ||||
|     healthcheck: | ||||
|       test: ["CMD-SHELL", "pg_isready"] | ||||
|       interval: 10s | ||||
|       timeout: 5s | ||||
|       retries: 5 | ||||
| volumes: | ||||
|   ussd-pg: | ||||
|     driver: local | ||||
							
								
								
									
										1
									
								
								dev/init_db.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								dev/init_db.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| CREATE DATABASE urdt_ussd; | ||||
							
								
								
									
										36
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								go.mod
									
									
									
									
									
								
							| @ -2,29 +2,16 @@ module git.grassecon.net/urdt/ussd | ||||
| 
 | ||||
| go 1.23.0 | ||||
| 
 | ||||
| toolchain go1.23.2 | ||||
| 
 | ||||
| require ( | ||||
| 	git.defalsify.org/vise.git v0.2.1-0.20241102200556-5348e1988045 | ||||
| 	git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a | ||||
| 	github.com/alecthomas/assert/v2 v2.2.2 | ||||
| 	github.com/gofrs/uuid v4.4.0+incompatible | ||||
| 	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/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a | ||||
| 
 | ||||
| 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/grassrootseconomics/ussd-data-service v1.2.0-beta | ||||
| 	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 | ||||
| 	github.com/peteole/testdata-loader v0.3.0 | ||||
| 	github.com/stretchr/testify v1.9.0 | ||||
| 	gopkg.in/leonelquinteros/gotext.v1 v1.3.1 | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| @ -33,13 +20,20 @@ require ( | ||||
| 	github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // 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/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/kr/text v0.2.0 // indirect | ||||
| 	github.com/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | ||||
| 	github.com/rogpeppe/go-internal v1.13.1 // indirect | ||||
| 	github.com/stretchr/objx v0.5.2 // indirect | ||||
| 	github.com/stretchr/testify v1.9.0 | ||||
| 	github.com/x448/float16 v0.8.4 // 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 | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|  | ||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | ||||
| git.defalsify.org/vise.git v0.2.1-0.20241102200556-5348e1988045 h1:uFFVEymnsJGVcuI2r//oNAqDD8l0HOkkEvqpOJHdJiY= | ||||
| git.defalsify.org/vise.git v0.2.1-0.20241102200556-5348e1988045/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= | ||||
| git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a h1:LvGKktk0kUnuRN3nF9r15D8OoV0sFaMmQr52kGq2gtE= | ||||
| git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= | ||||
| github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= | ||||
| github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= | ||||
| github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= | ||||
| @ -18,8 +18,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1 | ||||
| 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/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk= | ||||
| github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0= | ||||
| 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= | ||||
|  | ||||
| @ -80,6 +80,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.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) | ||||
| @ -102,12 +103,13 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn | ||||
| 	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) | ||||
| @ -115,6 +117,10 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn | ||||
| 	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("check_transactions", ussdHandlers.CheckTransactions) | ||||
| 	ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList) | ||||
| 	ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement) | ||||
| 
 | ||||
| 	return ussdHandlers, nil | ||||
| } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -2,7 +2,6 @@ package ussd | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"path" | ||||
| @ -586,9 +585,9 @@ func TestGetRecipient(t *testing.T) { | ||||
| 	ctx, store := InitializeTestStore(t) | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 
 | ||||
| 	recepient := "0xcasgatweksalw1018221" | ||||
| 	recepient := "0712345678" | ||||
| 
 | ||||
| 	err := store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(recepient)) | ||||
| 	err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(recepient)) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| @ -1226,32 +1225,41 @@ func TestInitiateTransaction(t *testing.T) { | ||||
| 	} | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		input          []byte | ||||
| 		Recipient      []byte | ||||
| 		Amount         []byte | ||||
| 		ActiveSym      []byte | ||||
| 		status         string | ||||
| 		expectedResult resource.Result | ||||
| 		name             string | ||||
| 		TemporaryValue   []byte | ||||
| 		ActiveSym        []byte | ||||
| 		StoredAmount     []byte | ||||
| 		TransferAmount   string | ||||
| 		PublicKey        []byte | ||||
| 		Recipient        []byte | ||||
| 		ActiveDecimal    []byte | ||||
| 		ActiveAddress    []byte | ||||
| 		TransferResponse *models.TokenTransferResponse | ||||
| 		expectedResult   resource.Result | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:      "Test initiate transaction", | ||||
| 			Amount:    []byte("0.002"), | ||||
| 			ActiveSym: []byte("SRF"), | ||||
| 			Recipient: []byte("0x12415ass27192"), | ||||
| 			name:           "Test initiate transaction", | ||||
| 			TemporaryValue: []byte("0711223344"), | ||||
| 			ActiveSym:      []byte("SRF"), | ||||
| 			StoredAmount:   []byte("1.00"), | ||||
| 			TransferAmount: "1000000", | ||||
| 			PublicKey:      []byte("0X13242618721"), | ||||
| 			Recipient:      []byte("0x12415ass27192"), | ||||
| 			ActiveDecimal:  []byte("6"), | ||||
| 			ActiveAddress:  []byte("0xd4c288865Ce"), | ||||
| 			TransferResponse: &models.TokenTransferResponse{ | ||||
| 				TrackingId: "1234567890", | ||||
| 			}, | ||||
| 			expectedResult: resource.Result{ | ||||
| 				FlagReset: []uint32{account_authorized_flag}, | ||||
| 				Content:   "Your request has been sent. 0x12415ass27192 will receive 0.002 SRF from 254712345678.", | ||||
| 				Content:   "Your request has been sent. 0711223344 will receive 1.00 SRF from 254712345678.", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			err := store.WriteEntry(ctx, sessionId, common.DATA_AMOUNT, []byte(tt.Amount)) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(tt.Recipient)) | ||||
| 			err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(tt.TemporaryValue)) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| @ -1259,9 +1267,31 @@ func TestInitiateTransaction(t *testing.T) { | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			err = store.WriteEntry(ctx, sessionId, common.DATA_AMOUNT, []byte(tt.StoredAmount)) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			err = store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(tt.PublicKey)) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(tt.Recipient)) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_DECIMAL, []byte(tt.ActiveDecimal)) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_ADDRESS, []byte(tt.ActiveAddress)) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 
 | ||||
| 			mockAccountService.On("TokenTransfer").Return(tt.TransferResponse, nil) | ||||
| 
 | ||||
| 			// Call the method under test
 | ||||
| 			res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", tt.input) | ||||
| 			res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", []byte("")) | ||||
| 
 | ||||
| 			// Assert that no errors occurred
 | ||||
| 			assert.NoError(t, err) | ||||
| @ -1453,10 +1483,12 @@ func TestValidateRecipient(t *testing.T) { | ||||
| 	} | ||||
| 
 | ||||
| 	sessionId := "session123" | ||||
| 	publicKey := "0X13242618721" | ||||
| 	ctx, store := InitializeTestStore(t) | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 
 | ||||
| 	flag_invalid_recipient, _ := fm.parser.GetFlag("flag_invalid_recipient") | ||||
| 	flag_invalid_recipient_with_invite, _ := fm.parser.GetFlag("flag_invalid_recipient_with_invite") | ||||
| 
 | ||||
| 	// Define test cases
 | ||||
| 	tests := []struct { | ||||
| @ -1466,19 +1498,33 @@ func TestValidateRecipient(t *testing.T) { | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:  "Test with invalid recepient", | ||||
| 			input: []byte("000"), | ||||
| 			input: []byte("9234adf5"), | ||||
| 			expectedResult: resource.Result{ | ||||
| 				FlagSet: []uint32{flag_invalid_recipient}, | ||||
| 				Content: "000", | ||||
| 				Content: "9234adf5", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Test with valid recepient", | ||||
| 			input:          []byte("0705X2"), | ||||
| 			name:  "Test with valid unregistered recepient", | ||||
| 			input: []byte("0712345678"), | ||||
| 			expectedResult: resource.Result{ | ||||
| 				FlagSet: []uint32{flag_invalid_recipient_with_invite}, | ||||
| 				Content: "0712345678", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Test with valid registered recepient", | ||||
| 			input:          []byte("0711223344"), | ||||
| 			expectedResult: resource.Result{}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	// store a public key for the valid recipient
 | ||||
| 	err = store.WriteEntry(ctx, "0711223344", common.DATA_PUBLIC_KEY, []byte(publicKey)) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			// Create the Handlers instance
 | ||||
| @ -1726,58 +1772,43 @@ func TestConfirmPin(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestFetchCustodialBalances(t *testing.T) { | ||||
| 	fm, err := NewFlagManager(flagsPath) | ||||
| 	if err != nil { | ||||
| 		t.Logf(err.Error()) | ||||
| 	} | ||||
| 	flag_api_error, _ := fm.GetFlag("flag_api_call_error") | ||||
| func TestFetchCommunityBalance(t *testing.T) { | ||||
| 
 | ||||
| 	// Define test data
 | ||||
| 	sessionId := "session123" | ||||
| 	publicKey := "0X13242618721" | ||||
| 
 | ||||
| 	ctx, store := InitializeTestStore(t) | ||||
| 	ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 
 | ||||
| 	err = store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(publicKey)) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		name            string | ||||
| 		balanceResponse *models.BalanceResult | ||||
| 		expectedResult  resource.Result | ||||
| 		name           string | ||||
| 		languageCode   string | ||||
| 		expectedResult resource.Result | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "Test when fetch custodial balances is not a success", | ||||
| 			balanceResponse: &models.BalanceResult{ | ||||
| 				Balance: "0.003 CELO", | ||||
| 				Nonce:   json.Number("0"), | ||||
| 			}, | ||||
| 			name: "Test community balance content when language is english", | ||||
| 			expectedResult: resource.Result{ | ||||
| 				FlagReset: []uint32{flag_api_error}, | ||||
| 				Content: "Community Balance: 0.00", | ||||
| 			}, | ||||
| 			languageCode: "eng", | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 
 | ||||
| 			mockAccountService := new(mocks.MockAccountService) | ||||
| 			mockState := state.NewState(16) | ||||
| 
 | ||||
| 			h := &Handlers{ | ||||
| 				userdataStore:  store, | ||||
| 				flagManager:    fm.parser, | ||||
| 				st:             mockState, | ||||
| 				accountService: mockAccountService, | ||||
| 			} | ||||
| 
 | ||||
| 			// Set up the expected behavior of the mock
 | ||||
| 			mockAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResponse, nil) | ||||
| 			ctx = context.WithValue(ctx, "SessionId", sessionId) | ||||
| 			ctx = context.WithValue(ctx, "Language", lang.Language{ | ||||
| 				Code: tt.languageCode, | ||||
| 			}) | ||||
| 
 | ||||
| 			// Call the method
 | ||||
| 			res, _ := h.FetchCustodialBalances(ctx, "fetch_custodial_balances", []byte("")) | ||||
| 			res, _ := h.FetchCommunityBalance(ctx, "fetch_community_balance", []byte("")) | ||||
| 
 | ||||
| 			//Assert that the result set to content is what was expected
 | ||||
| 			assert.Equal(t, res, tt.expectedResult, "Result should match expected result") | ||||
| @ -1987,7 +2018,7 @@ func TestSetVoucher(t *testing.T) { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	res, err := h.SetVoucher(ctx, "set_voucher", []byte{}) | ||||
| 	res, err := h.SetVoucher(ctx, "set_voucher", []byte("")) | ||||
| 
 | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
|  | ||||
| @ -4,8 +4,8 @@ import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"git.defalsify.org/vise.git/db" | ||||
| 	"git.defalsify.org/vise.git/lang" | ||||
| 	gdbmdb "git.defalsify.org/vise.git/db/gdbm" | ||||
| 	"git.defalsify.org/vise.git/lang" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
|  | ||||
| @ -41,10 +41,13 @@ func buildConnStr() string { | ||||
| 	dbName := initializers.GetEnv("DB_NAME", "") | ||||
| 	port := initializers.GetEnv("DB_PORT", "5432") | ||||
| 
 | ||||
| 	return fmt.Sprintf( | ||||
| 	connString := fmt.Sprintf( | ||||
| 		"postgres://%s:%s@%s:%s/%s", | ||||
| 		user, password, host, port, dbName, | ||||
| 	) | ||||
| 	logg.Debugf("pg conn string", "conn", connString) | ||||
| 
 | ||||
| 	return connString | ||||
| } | ||||
| 
 | ||||
| func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService { | ||||
|  | ||||
| @ -28,7 +28,6 @@ func (m *MockAccountService) TrackAccountStatus(ctx context.Context, 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) | ||||
| @ -39,7 +38,17 @@ func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey st | ||||
| 	return args.Get(0).([]dataserviceapi.Last10TxResponse), args.Error(1) | ||||
| } | ||||
| 
 | ||||
| func(m MockAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) { | ||||
| 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) | ||||
| } | ||||
| 
 | ||||
| func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { | ||||
| 	args := m.Called() | ||||
| 	return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1) | ||||
| } | ||||
|  | ||||
| @ -12,14 +12,14 @@ type TestAccountService struct { | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { | ||||
| 	return &models.AccountResult { | ||||
| 	return &models.AccountResult{ | ||||
| 		TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d", | ||||
| 		PublicKey: "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD", | ||||
| 		PublicKey:  "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD", | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { | ||||
| 	balanceResponse := &models.BalanceResult { | ||||
| 	balanceResponse := &models.BalanceResult{ | ||||
| 		Balance: "0.003 CELO", | ||||
| 		Nonce:   json.Number("0"), | ||||
| 	} | ||||
| @ -27,26 +27,36 @@ func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey strin | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) { | ||||
| 	return &models.TrackStatusResult { | ||||
| 	return &models.TrackStatusResult{ | ||||
| 		Active: true, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { | ||||
| 	return []dataserviceapi.TokenHoldings { | ||||
| 		dataserviceapi.TokenHoldings { | ||||
| 	return []dataserviceapi.TokenHoldings{ | ||||
| 		dataserviceapi.TokenHoldings{ | ||||
| 			ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", | ||||
| 			TokenSymbol:     "SRF", | ||||
| 			TokenDecimals:   "6", | ||||
| 			Balance:         "2745987", | ||||
| 		}, | ||||
| 	}, nil  | ||||
| 	}, 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) { | ||||
| 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 | ||||
| } | ||||
| 
 | ||||
| func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { | ||||
| 	return &dataserviceapi.AliasAddress{}, nil | ||||
| } | ||||
|  | ||||
| @ -2,8 +2,8 @@ | ||||
| 
 | ||||
| package testtag | ||||
| 
 | ||||
| import "git.grassecon.net/urdt/ussd/internal/handlers/server" | ||||
| import "git.grassecon.net/urdt/ussd/remote" | ||||
| 
 | ||||
| var ( | ||||
| 	AccountService server.AccountServiceInterface | ||||
| 	AccountService remote.AccountServiceInterface | ||||
| ) | ||||
|  | ||||
| @ -54,7 +54,7 @@ | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1235", | ||||
|                     "expectedContent": "Incorrect pin\n1:retry\n9:Quit" | ||||
|                     "expectedContent": "Incorrect pin\n1:Retry\n9:Quit" | ||||
|                 }, | ||||
|                 { | ||||
|                     "input": "1", | ||||
| @ -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", | ||||
| @ -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", | ||||
|  | ||||
| @ -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,7 +53,7 @@ | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 "name": "send_with_invalid_inputs", | ||||
|                 "name": "send_with_invite", | ||||
|                 "steps": [ | ||||
|                     { | ||||
|                         "input": "", | ||||
| @ -61,43 +61,23 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1", | ||||
|                         "expectedContent": "Enter recipient's phone number:\n0:Back" | ||||
|                         "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" | ||||
|                     }, | ||||
|                     { | ||||
|                         "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" | ||||
|                         "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "065656", | ||||
|                         "expectedContent": "{max_amount}\nEnter amount:\n0:Back" | ||||
|                         "input": "0712345678", | ||||
|                         "expectedContent": "0712345678 is not registered, please try again:\n1:Retry\n2:Invite to Sarafu Network\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "10000000", | ||||
|                         "expectedContent": "Amount 10000000 is invalid, please try again:\n1:retry\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1", | ||||
|                         "expectedContent": "{max_amount}\nEnter amount:\n0:Back" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1.00", | ||||
|                         "expectedContent": "065656 will receive {send_amount} from {session_id}\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 {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "1234", | ||||
|                         "expectedContent": "Your request has been sent. 065656 will receive {send_amount} from {session_id}." | ||||
|                         "input": "2", | ||||
|                         "expectedContent": "Your invite request for 0712345678 to Sarafu Network failed. Please try again later." | ||||
|                     } | ||||
|                 ] | ||||
|             }, | ||||
| @ -140,7 +120,7 @@ | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "6", | ||||
|                         "expectedContent": "Address: {public_key}\n9:Quit" | ||||
|                         "expectedContent": "Address: {public_key}\n0:Back\n9:Quit" | ||||
|                     }, | ||||
|                     { | ||||
|                         "input": "9", | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| package models | ||||
| 
 | ||||
| type AccountResult struct { | ||||
| 	PublicKey string `json:"publicKey"` | ||||
| 	PublicKey  string `json:"publicKey"` | ||||
| 	TrackingId string `json:"trackingId"` | ||||
| } | ||||
|  | ||||
| @ -2,7 +2,6 @@ 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"` | ||||
| } | ||||
| @ -1,18 +0,0 @@ | ||||
| package models | ||||
| 
 | ||||
| type ApiResponse struct { | ||||
| 	OK          bool   `json:"ok"` | ||||
| 	Description string `json:"description"` | ||||
| 	Result      Result `json:"result"` | ||||
| } | ||||
| 
 | ||||
| type Result struct { | ||||
| 	Holdings []Holding `json:"holdings"` | ||||
| } | ||||
| 
 | ||||
| type Holding struct { | ||||
| 	ContractAddress string `json:"contractAddress"` | ||||
| 	TokenSymbol     string `json:"tokenSymbol"` | ||||
| 	TokenDecimals   string `json:"tokenDecimals"` | ||||
| 	Balance         string `json:"balance"` | ||||
| } | ||||
| @ -14,5 +14,5 @@ type Transaction struct { | ||||
| } | ||||
| 
 | ||||
| type TrackStatusResult struct { | ||||
| 	Active	bool `json:"active"` | ||||
| 	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"` | ||||
| } | ||||
| @ -1,21 +0,0 @@ | ||||
| package models | ||||
| 
 | ||||
| import dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||
| 
 | ||||
| //type VoucherHoldingResponse struct {
 | ||||
| //	Ok          bool          `json:"ok"`
 | ||||
| //	Description string        `json:"description"`
 | ||||
| //	Result      VoucherResult `json:"result"`
 | ||||
| //}
 | ||||
| 
 | ||||
| // VoucherResult holds the list of token holdings
 | ||||
| type VoucherResult struct { | ||||
| 	Holdings []dataserviceapi.TokenHoldings `json:"holdings"` | ||||
| } | ||||
| 
 | ||||
| type VoucherDataResult struct { | ||||
| 	TokenName	string `json:"tokenName"` | ||||
| 	TokenSymbol     string `json:"tokenSymbol"` | ||||
| 	TokenDecimals   string `json:"tokenDecimals"` | ||||
| 	SinkAddress	string `json:"sinkAddress"` | ||||
| } | ||||
| @ -1,20 +1,19 @@ | ||||
| package remote | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 
 | ||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||
| 	"github.com/grassrootseconomics/eth-custodial/pkg/api" | ||||
| 	"git.grassecon.net/urdt/ussd/config" | ||||
| 	"git.grassecon.net/urdt/ussd/models" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	"github.com/grassrootseconomics/eth-custodial/pkg/api" | ||||
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" | ||||
| ) | ||||
| 
 | ||||
| type AccountServiceInterface interface { | ||||
| @ -24,6 +23,8 @@ type AccountServiceInterface interface { | ||||
| 	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) | ||||
| 	CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) | ||||
| } | ||||
| 
 | ||||
| type AccountService struct { | ||||
| @ -51,7 +52,7 @@ func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey stri | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err =  doCustodialRequest(ctx, req, &r) | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @ -75,11 +76,10 @@ func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (* | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doCustodialRequest(ctx, req, &balanceResult) | ||||
| 	_, 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.
 | ||||
| @ -93,8 +93,7 @@ func (as *AccountService) CreateAccount(ctx context.Context) (*models.AccountRes | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err =  doCustodialRequest(ctx, req, &r) | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @ -106,7 +105,9 @@ func (as *AccountService) CreateAccount(ctx context.Context) (*models.AccountRes | ||||
| // Parameters:
 | ||||
| //   - publicKey: The public key associated with the account.
 | ||||
| func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { | ||||
| 	var r []dataserviceapi.TokenHoldings | ||||
| 	var r struct { | ||||
| 		Holdings []dataserviceapi.TokenHoldings `json:"holdings"` | ||||
| 	} | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.VoucherHoldingsURL, publicKey) | ||||
| 	if err != nil { | ||||
| @ -118,20 +119,21 @@ func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) ( | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err =  doDataRequest(ctx, req, r) | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return r, nil | ||||
| 	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 []dataserviceapi.Last10TxResponse | ||||
| 	var r struct { | ||||
| 		Transfers []dataserviceapi.Last10TxResponse `json:"transfers"` | ||||
| 	} | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.VoucherTransfersURL, publicKey) | ||||
| 	if err != nil { | ||||
| @ -143,20 +145,21 @@ func (as *AccountService) FetchTransactions(ctx context.Context, publicKey strin | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err =  doDataRequest(ctx, req, r) | ||||
| 	_, err = doRequest(ctx, req, &r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return r, nil | ||||
| 	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 voucherDataResult models.VoucherDataResult | ||||
| 	var r struct { | ||||
| 		TokenDetails models.VoucherDataResult `json:"tokenDetails"` | ||||
| 	} | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.VoucherDataURL, address) | ||||
| 	if err != nil { | ||||
| @ -168,22 +171,83 @@ func (as *AccountService) VoucherData(ctx context.Context, address string) (*mod | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = doCustodialRequest(ctx, req, &voucherDataResult) | ||||
| 	return &voucherDataResult, 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 | ||||
| } | ||||
| 
 | ||||
| // CheckAliasAddress retrieves the address of an alias from the API endpoint.
 | ||||
| // Parameters:
 | ||||
| //   - alias: The alias of the user.
 | ||||
| func (as *AccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) { | ||||
| 	var r dataserviceapi.AliasAddress | ||||
| 
 | ||||
| 	ep, err := url.JoinPath(config.CheckAliasURL, alias) | ||||
| 	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, err | ||||
| } | ||||
| 
 | ||||
| func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { | ||||
| 	var okResponse  api.OKResponse | ||||
| 	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 | ||||
| @ -212,12 +276,19 @@ func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKRespons | ||||
| 	return &okResponse, err | ||||
| } | ||||
| 
 | ||||
| func doCustodialRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { | ||||
| 	req.Header.Set("X-GE-KEY", config.CustodialAPIKey) | ||||
| 	return doRequest(ctx, req, rcpt) | ||||
| } | ||||
| 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("-") | ||||
| 	} | ||||
| 
 | ||||
| func doDataRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) { | ||||
| 	req.Header.Set("X-GE-KEY", config.DataAPIKey) | ||||
| 	return doRequest(ctx, req, rcpt) | ||||
| 	log.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes)) | ||||
| } | ||||
|  | ||||
| @ -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}} | ||||
| @ -6,10 +6,10 @@ 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 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
									
								
								services/registration/check_statement
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/check_statement
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Please enter your PIN to view statement: | ||||
							
								
								
									
										12
									
								
								services/registration/check_statement.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								services/registration/check_statement.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| LOAD check_transactions 0 | ||||
| RELOAD check_transactions | ||||
| CATCH no_transfers flag_no_transfers 1 | ||||
| LOAD authorize_account 6 | ||||
| MOUT back 0 | ||||
| MOUT quit 9 | ||||
| HALT | ||||
| RELOAD authorize_account | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| INCMP _ 0 | ||||
| INCMP quit 9 | ||||
| INCMP transactions * | ||||
							
								
								
									
										1
									
								
								services/registration/check_statement_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/check_statement_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Tafadhali weka PIN yako kuona taarifa ya matumizi: | ||||
| @ -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
									
								
								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}} | ||||
							
								
								
									
										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,5 +1,7 @@ | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| 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 | ||||
							
								
								
									
										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: | ||||
| @ -1,5 +1,8 @@ | ||||
| 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 | ||||
							
								
								
									
										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,5 +1,7 @@ | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| 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 | ||||
							
								
								
									
										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 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,8 +1,12 @@ | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| 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 | ||||
							
								
								
									
										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 +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_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/enter_other_number_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Weka nambari ya simu ili kutuma ombi la kubadilisha nambari ya siri: | ||||
							
								
								
									
										1
									
								
								services/registration/enter_others_new_pin_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/enter_others_new_pin_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Tafadhali weka PIN mpya ya: {{.retrieve_blocked_number}} | ||||
| @ -1 +0,0 @@ | ||||
| Enter your year of birth | ||||
| @ -1 +0,0 @@ | ||||
| Weka mwaka wa kuzaliwa | ||||
| @ -2,5 +2,5 @@ LOAD reset_incorrect_date_format 8 | ||||
| MOUT retry 1 | ||||
| MOUT quit 9 | ||||
| HALT | ||||
| INCMP enter_yob 1 | ||||
| INCMP _ 1 | ||||
| INCMP quit 9 | ||||
|  | ||||
| @ -1 +1 @@ | ||||
| {{.validate_recipient}} is not registered or invalid, please try again: | ||||
| {{.validate_recipient}} is invalid, please try again: | ||||
| @ -1 +1 @@ | ||||
| {{.validate_recipient}} haijasajiliwa au sio sahihi, tafadhali weka tena: | ||||
| {{.validate_recipient}} sio sahihi, tafadhali weka tena: | ||||
							
								
								
									
										1
									
								
								services/registration/invite_menu
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/invite_menu
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Invite to Sarafu Network | ||||
							
								
								
									
										1
									
								
								services/registration/invite_menu_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/invite_menu_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Karibisha kwa matandao wa Sarafu | ||||
							
								
								
									
										1
									
								
								services/registration/invite_recipient
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/invite_recipient
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| {{.validate_recipient}} is not registered, please try again: | ||||
							
								
								
									
										8
									
								
								services/registration/invite_recipient.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								services/registration/invite_recipient.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| MAP validate_recipient | ||||
| MOUT retry 1 | ||||
| MOUT invite 2 | ||||
| MOUT quit 9 | ||||
| HALT | ||||
| INCMP _ 1 | ||||
| INCMP invite_result 2 | ||||
| INCMP quit 9 | ||||
							
								
								
									
										1
									
								
								services/registration/invite_recipient_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/invite_recipient_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| {{.validate_recipient}} haijasajiliwa, tafadhali weka tena: | ||||
							
								
								
									
										2
									
								
								services/registration/invite_result.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								services/registration/invite_result.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| LOAD invite_valid_recipient 0 | ||||
| HALT | ||||
| @ -12,3 +12,15 @@ msgstr "Kwa usaidizi zaidi,piga: 0757628885" | ||||
| 
 | ||||
| msgid "Balance: %s\n" | ||||
| msgstr "Salio: %s\n" | ||||
| 
 | ||||
| msid "Your invite request for %s to Sarafu Network failed. Please try again later." | ||||
| msgstr "Ombi lako la kumwalika %s kwa matandao wa Sarafu halikufaulu. Tafadhali jaribu tena baadaye." | ||||
| 
 | ||||
| msgid "Your invitation to %s to join Sarafu Network has been sent." | ||||
| msgstr "Ombi lako la kumwalika %s kwa matandao wa Sarafu limetumwa." | ||||
| 
 | ||||
| msgid "Your request failed. Please try again later." | ||||
| msgstr "Ombi lako halikufaulu. Tafadhali jaribu tena baadaye." | ||||
| 
 | ||||
| msgid "Community Balance: 0.00" | ||||
| msgid "Salio la Kikundi: 0.00" | ||||
|  | ||||
| @ -11,5 +11,6 @@ INCMP main 0 | ||||
| INCMP edit_profile 1 | ||||
| INCMP change_language 2 | ||||
| INCMP balances 3 | ||||
| INCMP check_statement 4 | ||||
| INCMP pin_management 5 | ||||
| INCMP address 6 | ||||
|  | ||||
| @ -1 +1 @@ | ||||
| {{.fetch_custodial_balances}} | ||||
| {{.check_balance}} | ||||
| @ -1,7 +1,7 @@ | ||||
| LOAD reset_incorrect 6 | ||||
| LOAD fetch_custodial_balances 0 | ||||
| LOAD check_balance 0 | ||||
| CATCH api_failure  flag_api_call_error  1 | ||||
| MAP fetch_custodial_balances | ||||
| MAP check_balance | ||||
| CATCH incorrect_pin flag_incorrect_pin 1 | ||||
| CATCH pin_entry flag_account_authorized 0 | ||||
| MOUT back 0 | ||||
|  | ||||
| @ -6,3 +6,4 @@ MOUT back 0 | ||||
| HALT | ||||
| INCMP _ 0 | ||||
| INCMP select_voucher 1 | ||||
| INCMP voucher_details 2 | ||||
|  | ||||
							
								
								
									
										1
									
								
								services/registration/no_admin_privilege_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/no_admin_privilege_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Huna mapendeleo ya kufanya kitendo hiki | ||||
							
								
								
									
										1
									
								
								services/registration/no_transfers
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/no_transfers
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| No transfers history | ||||
							
								
								
									
										5
									
								
								services/registration/no_transfers.vis
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								services/registration/no_transfers.vis
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| MOUT back 0 | ||||
| MOUT quit 9 | ||||
| HALT | ||||
| INCMP ^ 0 | ||||
| INCMP quit 9 | ||||
							
								
								
									
										1
									
								
								services/registration/no_transfers_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/no_transfers_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| Hakuna historia kwa akaunti yako | ||||
							
								
								
									
										1
									
								
								services/registration/others_pin_mismatch_swa
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/registration/others_pin_mismatch_swa
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| PIN uliyoweka hailingani.Jaribu tena. | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user