Compare commits

..

47 Commits

Author SHA1 Message Date
b615c27cf6 Merge pull request 'trigger-balance-reload' (#193) from trigger-balance-reload into master
Some checks failed
release / docker (push) Has been cancelled
Reviewed-on: #193
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-12-02 16:36:26 +01:00
22e870b3e5 Merge branch 'master' into trigger-balance-reload 2024-12-02 16:36:08 +01:00
da462346f9 Merge pull request 'Always reset persister in handler' (#194) from lash/no-persister-deadlock into master
Reviewed-on: #194
2024-12-02 16:17:36 +01:00
lash
406bd84875 Always reset persister in handler 2024-12-02 14:53:18 +00:00
7976e237ca Merge pull request 'voucher-details' (#179) from voucher-details into master
Reviewed-on: #179
Reviewed-by: Alfred Kamanda <alfredkamandamw@gmail.com>
2024-12-02 11:43:30 +01:00
alfred-mk
19ec8f0817 Merge branch 'master' into voucher-details 2024-12-02 13:32:32 +03:00
alfred-mk
ef3a3d6717 updated the TestGetVoucherDetails 2024-12-02 13:30:33 +03:00
alfred-mk
c2019267d1 capitalize the voucher descriptions 2024-12-02 13:26:46 +03:00
alfred-mk
aa7497573e removed unused code 2024-11-30 15:29:28 +03:00
alfred-mk
54c1fe51ef update the active voucher data when checking the current vouchers 2024-11-30 15:28:21 +03:00
alfred-mk
7a86b2ad3b updated the UpdateVoucherData description 2024-11-30 15:26:13 +03:00
alfred-mk
6b23c284e5 check vouchers before checking the balance 2024-11-30 15:24:14 +03:00
alfred-mk
08ff1056d7 Validate aliases, addresses and phone numbers in the send menu (#176)
Some checks failed
release / docker (push) Has been cancelled
- Update the phone number regex
- Check whether the recipient is a valid phone number, alias or address

Reviewed-on: #176
Co-authored-by: alfred-mk <alfredmwaik@gmail.com>
Co-committed-by: alfred-mk <alfredmwaik@gmail.com>
2024-11-26 07:24:57 +01:00
0f4a7e900f Merge pull request 'feat: upgrade go-vise dep, minor cleanups to go.mod' (#182) from sohail/upgrade-deps into master
Reviewed-on: #182
Reviewed-by: lash <accounts-grassrootseconomics@holbrook.no>
2024-11-26 07:02:07 +01:00
2d6c434bde feat: upgrade go-vise dep, minor cleanups to go.mod
* added stub for Dump()
2024-11-25 09:56:24 +03:00
f5d2644031 Merge pull request 'account-statement' (#126) from account-statement into master
Reviewed-on: #126
2024-11-22 19:24:08 +01:00
09b4fa2860 Merge branch 'master' into account-statement 2024-11-22 19:23:59 +01:00
a748c1b6b2 chore: fix old reference 2024-11-22 11:53:13 +03:00
Carlosokumu
1174500e3f add test for voucher details 2024-11-21 15:19:36 +03:00
Carlosokumu
b8d938d3aa add voucher details 2024-11-21 13:04:19 +03:00
Carlosokumu
d1e9340ea9 add voucher details 2024-11-21 13:03:43 +03:00
alfred-mk
51bad64a51 Merge branch 'master' into account-statement 2024-11-20 19:00:14 +03:00
212cd48249 debug: postgres conn string
Some checks failed
release / docker (push) Has been cancelled
2024-11-19 17:53:06 +03:00
7adc0c9c08 fix: Dockerfile to include .env
Some checks failed
release / docker (push) Has been cancelled
2024-11-19 17:25:20 +03:00
0a19a6c48b fix: github ci spec
Some checks failed
release / docker (push) Has been cancelled
2024-11-19 17:18:13 +03:00
66b5843b0d feat: dockerfile and github based CI build for Africastalking variant (#174)
## Summary

* fixed missing error handler in main
* add injectable build string in main
* add (dynamically linked) docker build
* add github CI workflow
* add extra but useful dev files in dev folder

* closes #93

## Notes

* We'll move to a self-hosted CI runner once it is installed on Gitea.

Reviewed-on: #174
Co-authored-by: Mohammed Sohail <sohailsameja@gmail.com>
Co-committed-by: Mohammed Sohail <sohailsameja@gmail.com>
2024-11-19 15:15:24 +01:00
alfred-mk
df89fe69e1 return to the main node 2024-11-19 17:09:59 +03:00
alfred-mk
9498242901 removed debug statement 2024-11-19 16:27:19 +03:00
alfred-mk
eab6dbd74c Check if index is within range 2024-11-19 16:23:35 +03:00
alfred-mk
6159686a8e update function comment 2024-11-19 16:21:55 +03:00
alfred-mk
1d82bc2261 added transactions functions 2024-11-19 16:07:28 +03:00
alfred-mk
bfcdd79f33 view single statement 2024-11-19 16:04:46 +03:00
alfred-mk
d49b68597c check transactions and catch when none exist 2024-11-19 16:03:54 +03:00
alfred-mk
1d9f4fc13e added statement related flags 2024-11-19 15:50:19 +03:00
alfred-mk
6fa0d8e2ff Merge branch 'master' into account-statement 2024-11-19 10:33:09 +03:00
carlos
109a2a2020 Merge pull request 'menu-balances' (#173) from menu-balances into master
Reviewed-on: #173
Reviewed-by: Alfred Kamanda <alfredkamandamw@gmail.com>
2024-11-19 08:18:43 +01:00
alfred-mk
bc6d8098f3 updated the failed test 2024-11-19 10:15:34 +03:00
Carlosokumu
01e75e9217 update test 2024-11-18 17:30:34 +03:00
Carlosokumu
f2b17880ba check community and my balance separately 2024-11-18 17:30:09 +03:00
Carlosokumu
1d4f116079 community balance str 2024-11-18 17:23:59 +03:00
Carlosokumu
44c52b6ed7 rename fetch_custodial_balances -> fetch_community_balance 2024-11-18 17:23:36 +03:00
Carlosokumu
454f67b317 show balance based on current voucher 2024-11-18 17:21:04 +03:00
Carlosokumu
e1506a3dcf show a placehlolder community balance 2024-11-18 17:20:12 +03:00
carlos
b22a4adec1 Merge pull request 'updated send node' (#172) from send-node into master
Reviewed-on: #172
2024-11-18 14:19:42 +01:00
alfred-mk
fc0043e3f6 Adde the transactions node 2024-10-23 14:52:15 +03:00
alfred-mk
d41ba79ae4 Adde the check_statement node 2024-10-23 14:51:59 +03:00
alfred-mk
f36847d966 Added a placeholder function to get transactions 2024-10-23 14:51:17 +03:00
46 changed files with 830 additions and 168 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
/**
!/cmd/africastalking
!/common
!/config
!/initializers
!/internal
!/models
!/remote
!/services
!/LICENSE
!/README.md
!/go.*
!/.env.example

56
.github/workflows/docker.yaml vendored Normal file
View 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 }}

41
Dockerfile Normal file
View 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"]

View File

@@ -20,6 +20,7 @@ import (
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/common"
"git.grassecon.net/urdt/ussd/config" "git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/handlers"
@@ -29,8 +30,10 @@ import (
) )
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
build = "dev"
) )
func init() { func init() {
@@ -42,14 +45,14 @@ type atRequestParser struct{}
func (arp *atRequestParser) GetSessionId(rq any) (string, error) { func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
rqv, ok := rq.(*http.Request) rqv, ok := rq.(*http.Request)
if !ok { if !ok {
log.Println("got an invalid request:", rq) log.Printf("got an invalid request:", rq)
return "", handlers.ErrInvalidRequest return "", handlers.ErrInvalidRequest
} }
// Capture body (if any) for logging // Capture body (if any) for logging
body, err := io.ReadAll(rqv.Body) body, err := io.ReadAll(rqv.Body)
if err != nil { if err != nil {
log.Println("failed to read request body:", err) log.Printf("failed to read request body:", err)
return "", fmt.Errorf("failed to read request body: %v", err) return "", fmt.Errorf("failed to read request body: %v", err)
} }
// Reset the body for further reading // Reset the body for further reading
@@ -59,13 +62,13 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
bodyLog := map[string]string{"body": string(body)} bodyLog := map[string]string{"body": string(body)}
logBytes, err := json.Marshal(bodyLog) logBytes, err := json.Marshal(bodyLog)
if err != nil { if err != nil {
log.Println("failed to marshal request body:", err) log.Printf("failed to marshal request body:", err)
} else { } else {
log.Println("Received request:", string(logBytes)) log.Printf("Received request:", string(logBytes))
} }
if err := rqv.ParseForm(); err != nil { if err := rqv.ParseForm(); err != nil {
log.Println("failed to parse form data: %v", err) log.Printf("failed to parse form data: %v", err)
return "", fmt.Errorf("failed to parse form data: %v", err) return "", fmt.Errorf("failed to parse form data: %v", err)
} }
@@ -74,7 +77,13 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
return "", fmt.Errorf("no phone number found") 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) { func (arp *atRequestParser) GetInput(rq any) ([]byte, error) {
@@ -115,7 +124,7 @@ func main() {
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.Parse() 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.Background()
ctx = context.WithValue(ctx, "Database", database) ctx = context.WithValue(ctx, "Database", database)
@@ -157,6 +166,10 @@ func main() {
} }
lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs) 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) lhs.SetDataStore(&userdataStore)
if err != nil { if err != nil {

73
common/recipient.go Normal file
View 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
}

View 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")
}

View File

@@ -151,7 +151,7 @@ func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId str
return data, nil return data, nil
} }
// UpdateVoucherData sets the active voucher data in the DataStore. // UpdateVoucherData updates the active voucher data in the DataStore.
func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
logg.TraceCtxf(ctx, "dtal", "data", data) logg.TraceCtxf(ctx, "dtal", "data", data)
// Active voucher data entries // Active voucher data entries

View File

@@ -15,6 +15,7 @@ const (
voucherHoldingsPathPrefix = "/api/v1/holdings" voucherHoldingsPathPrefix = "/api/v1/holdings"
voucherTransfersPathPrefix = "/api/v1/transfers/last10" voucherTransfersPathPrefix = "/api/v1/transfers/last10"
voucherDataPathPrefix = "/api/v1/token" voucherDataPathPrefix = "/api/v1/token"
AliasPrefix = "api/v1/alias"
) )
var ( var (
@@ -32,6 +33,7 @@ var (
VoucherHoldingsURL string VoucherHoldingsURL string
VoucherTransfersURL string VoucherTransfersURL string
VoucherDataURL string VoucherDataURL string
CheckAliasURL string
) )
func setBase() error { func setBase() error {
@@ -66,6 +68,7 @@ func LoadConfig() error {
VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix) VoucherHoldingsURL, _ = url.JoinPath(dataURLBase, voucherHoldingsPathPrefix)
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix) VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix) VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix)
return nil return nil
} }

View File

@@ -0,0 +1,3 @@
url: http://localhost:7123
dial: "*384*96#"
phoneNumber: +254722123456

21
dev/docker-compose.yaml Normal file
View 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
View File

@@ -0,0 +1 @@
CREATE DATABASE urdt_ussd;

36
go.mod
View File

@@ -2,29 +2,16 @@ module git.grassecon.net/urdt/ussd
go 1.23.0 go 1.23.0
toolchain go1.23.2
require ( require (
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a
github.com/alecthomas/assert/v2 v2.2.2 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/grassrootseconomics/eth-custodial v1.3.0-beta
github.com/peteole/testdata-loader v0.3.0 github.com/grassrootseconomics/ussd-data-service v1.2.0-beta
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
)
require github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a // indirect
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/kr/text v0.2.0 // indirect github.com/peteole/testdata-loader v0.3.0
github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.27.0 // indirect gopkg.in/leonelquinteros/gotext.v1 v1.3.1
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.18.0 // indirect
) )
require ( require (
@@ -33,13 +20,20 @@ require (
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // 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/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 // indirect
github.com/hexops/gotextdiff v1.0.3 // 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/mattn/kinako v0.0.0-20170717041458-332c0a7e205a // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0
github.com/x448/float16 v0.8.4 // indirect 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 gopkg.in/yaml.v3 v3.0.1 // indirect
) )

8
go.sum
View File

@@ -1,5 +1,5 @@
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU= git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a h1:LvGKktk0kUnuRN3nF9r15D8OoV0sFaMmQr52kGq2gtE=
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= 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 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= 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/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 h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= 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 v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk=
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/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 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=

View File

@@ -103,7 +103,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) 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("set_default_voucher", ussdHandlers.SetDefaultVoucher)
ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers) ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList) ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
@@ -118,6 +118,9 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin) ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin)
ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin) ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin)
ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo) 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 return ussdHandlers, nil
} }

View File

@@ -10,7 +10,6 @@ import (
"strings" "strings"
"git.defalsify.org/vise.git/asm" "git.defalsify.org/vise.git/asm"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
"git.defalsify.org/vise.git/cache" "git.defalsify.org/vise.git/cache"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
@@ -25,22 +24,26 @@ import (
"gopkg.in/leonelquinteros/gotext.v1" "gopkg.in/leonelquinteros/gotext.v1"
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
) )
var ( var (
logg = logging.NewVanilla().WithDomain("ussdmenuhandler") logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
translationDir = path.Join(scriptDir, "locale") translationDir = path.Join(scriptDir, "locale")
okResponse *api.OKResponse
errResponse *api.ErrResponse
) )
// Define the regex patterns as constants // Define the regex patterns as constants
const ( const (
phoneRegex = `(\(\d{3}\)\s?|\d{3}[-.\s]?)?\d{3}[-.\s]?\d{4}`
pinPattern = `^\d{4}$` pinPattern = `^\d{4}$`
) )
// isValidPIN checks whether the given input is a 4 digit number
func isValidPIN(pin string) bool {
match, _ := regexp.MatchString(pinPattern, pin)
return match
}
// FlagManager handles centralized flag management // FlagManager handles centralized flag management
type FlagManager struct { type FlagManager struct {
parser *asm.FlagParser parser *asm.FlagParser
@@ -95,17 +98,6 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
return h, nil return h, nil
} }
// isValidPIN checks whether the given input is a 4 digit number
func isValidPIN(pin string) bool {
match, _ := regexp.MatchString(pinPattern, pin)
return match
}
func isValidPhoneNumber(phonenumber string) bool {
match, _ := regexp.MatchString(phoneRegex, phonenumber)
return match
}
func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers { func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers {
if h.pe != nil { if h.pe != nil {
panic("persister already set") panic("persister already set")
@@ -120,6 +112,9 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
logg.WarnCtxf(ctx, "handler init called before it is ready or more than once", "state", h.st, "cache", h.ca) logg.WarnCtxf(ctx, "handler init called before it is ready or more than once", "state", h.st, "cache", h.ca)
return r, nil return r, nil
} }
defer func() {
h.pe = nil
}()
h.st = h.pe.GetState() h.st = h.pe.GetState()
h.ca = h.pe.GetMemory() h.ca = h.pe.GetMemory()
@@ -139,7 +134,6 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
logg.ErrorCtxf(ctx, "perister fail in handler", "state", h.st, "cache", h.ca) logg.ErrorCtxf(ctx, "perister fail in handler", "state", h.st, "cache", h.ca)
return r, fmt.Errorf("cannot get state and memory for handler") return r, fmt.Errorf("cannot get state and memory for handler")
} }
h.pe = nil
logg.DebugCtxf(ctx, "handler has been initialized", "state", h.st, "cache", h.ca) logg.DebugCtxf(ctx, "handler has been initialized", "state", h.st, "cache", h.ca)
@@ -822,42 +816,14 @@ func (h *Handlers) CheckBalance(ctx context.Context, sym string, input []byte) (
return res, nil return res, nil
} }
func (h *Handlers) FetchCustodialBalances(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) FetchCommunityBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
code := codeFromCtx(ctx)
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
sessionId, ok := ctx.Value("SessionId").(string) //TODO:
if !ok { //Check if the address is a community account,if then,get the actual balance
return res, fmt.Errorf("missing session") res.Content = l.Get("Community Balance: 0.00")
}
symbol, _ := h.st.Where()
balanceType := strings.Split(symbol, "_")[0]
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err)
return res, err
}
balanceResponse, err := h.accountService.CheckBalance(ctx, string(publicKey))
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_api_error)
balance := balanceResponse.Balance
switch balanceType {
case "my":
res.Content = fmt.Sprintf("Your balance is %s", balance)
case "community":
res.Content = fmt.Sprintf("Your community balance is %s", balance)
default:
break
}
return res, nil return res, nil
} }
@@ -905,7 +871,7 @@ func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input
} }
blockedNumber := string(input) blockedNumber := string(input)
_, err = store.ReadEntry(ctx, blockedNumber, common.DATA_PUBLIC_KEY) _, err = store.ReadEntry(ctx, blockedNumber, common.DATA_PUBLIC_KEY)
if !isValidPhoneNumber(blockedNumber) { if !common.IsValidPhoneNumber(blockedNumber) {
res.FlagSet = append(res.FlagSet, flag_unregistered_number) res.FlagSet = append(res.FlagSet, flag_unregistered_number)
return res, nil return res, nil
} }
@@ -926,10 +892,9 @@ func (h *Handlers) ValidateBlockedNumber(ctx context.Context, sym string, input
return res, nil return res, nil
} }
// ValidateRecipient validates that the given input is a valid phone number. // ValidateRecipient validates that the given input is valid.
func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
var err error
store := h.userdataStore store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
@@ -937,13 +902,16 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
recipient := string(input)
flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient") flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient")
flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite") flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
recipient := string(input)
if recipient != "0" { if recipient != "0" {
if !isValidPhoneNumber(recipient) { recipientType, err := common.CheckRecipient(recipient)
if err != nil {
// Invalid recipient format (not a phone number, address, or valid alias format)
res.FlagSet = append(res.FlagSet, flag_invalid_recipient) res.FlagSet = append(res.FlagSet, flag_invalid_recipient)
res.Content = recipient res.Content = recipient
@@ -957,25 +925,61 @@ func (h *Handlers) ValidateRecipient(ctx context.Context, sym string, input []by
return res, err return res, err
} }
publicKey, err := store.ReadEntry(ctx, recipient, common.DATA_PUBLIC_KEY) switch recipientType {
if err != nil { case "phone number":
if db.IsNotFound(err) { // format the phone number
logg.InfoCtxf(ctx, "Unregistered number") formattedNumber, err := common.FormatPhoneNumber(recipient)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite) logg.ErrorCtxf(ctx, "Failed to format the phone number: %s", recipient, "error", err)
res.Content = recipient return res, err
return res, nil
} }
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) // Check if the phone number is registered
return res, err publicKey, err := store.ReadEntry(ctx, formattedNumber, common.DATA_PUBLIC_KEY)
} if err != nil {
if db.IsNotFound(err) {
logg.InfoCtxf(ctx, "Unregistered phone number: %s", recipient)
res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite)
res.Content = recipient
return res, nil
}
err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, publicKey) logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err)
if err != nil { return res, err
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", string(publicKey), "error", err) }
return res, nil
// Save the publicKey as the recipient
err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, publicKey)
if err != nil {
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", string(publicKey), "error", err)
return res, err
}
case "address":
// Save the valid Ethereum address as the recipient
err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(recipient))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", recipient, "error", err)
return res, err
}
case "alias":
// Call the API to validate and retrieve the address for the alias
r, aliasErr := h.accountService.CheckAliasAddress(ctx, recipient)
if aliasErr != nil {
res.FlagSet = append(res.FlagSet, flag_api_error)
res.Content = recipient
logg.ErrorCtxf(ctx, "failed on CheckAliasAddress", "error", aliasErr)
return res, err
}
// Alias validation succeeded, save the Ethereum address
err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(r.Address))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", common.DATA_RECIPIENT, "value", r.Address, "error", err)
return res, err
}
} }
} }
@@ -1530,6 +1534,38 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte)
return res, nil return res, nil
} }
// check the current active sym and update the data
activeSym, _ := store.ReadEntry(ctx, sessionId, common.DATA_ACTIVE_SYM)
if activeSym != nil {
activeSymStr := string(activeSym)
// Find the matching voucher data
var activeData *dataserviceapi.TokenHoldings
for _, voucher := range vouchersResp {
if voucher.TokenSymbol == activeSymStr {
activeData = &voucher
break
}
}
if activeData == nil {
logg.ErrorCtxf(ctx, "activeSym not found in vouchers", "activeSym", activeSymStr)
return res, fmt.Errorf("activeSym %s not found in vouchers", activeSymStr)
}
// Scale down the balance
scaledBalance := common.ScaleDownBalance(activeData.Balance, activeData.TokenDecimals)
// Update the balance field with the scaled value
activeData.Balance = scaledBalance
// Pass the matching voucher data to UpdateVoucherData
if err := common.UpdateVoucherData(ctx, h.userdataStore, sessionId, activeData); err != nil {
logg.ErrorCtxf(ctx, "failed on UpdateVoucherData", "error", err)
return res, err
}
}
data := common.ProcessVouchers(vouchersResp) data := common.ProcessVouchers(vouchersResp)
// Store all voucher data // Store all voucher data
@@ -1654,10 +1690,178 @@ func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []by
return res, nil return res, nil
} }
tokenSymbol := voucherData.TokenSymbol res.Content = fmt.Sprintf(
tokenName := voucherData.TokenName "Name: %s\nSymbol: %s\nCommodity: %s\nLocation: %s", voucherData.TokenName, voucherData.TokenSymbol, voucherData.TokenCommodity, voucherData.TokenLocation,
)
res.Content = fmt.Sprintf("%s %s", tokenSymbol, tokenName)
return res, nil
}
// CheckTransactions retrieves the transactions from the API using the "PublicKey" and stores to prefixDb
func (h *Handlers) CheckTransactions(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_no_transfers, _ := h.flagManager.GetFlag("flag_no_transfers")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_error")
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err)
return res, err
}
// Fetch transactions from the API using the public key
transactionsResp, err := h.accountService.FetchTransactions(ctx, string(publicKey))
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error)
logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err)
return res, err
}
// Return if there are no transactions
if len(transactionsResp) == 0 {
res.FlagSet = append(res.FlagSet, flag_no_transfers)
return res, nil
}
data := common.ProcessTransfers(transactionsResp)
// Store all transaction data
dataMap := map[string]string{
"txfrom": data.Senders,
"txto": data.Recipients,
"txval": data.TransferValues,
"txaddr": data.Addresses,
"txhash": data.TxHashes,
"txdate": data.Dates,
"txsym": data.Symbols,
"txdeci": data.Decimals,
}
for key, value := range dataMap {
if err := h.prefixDb.Put(ctx, []byte(key), []byte(value)); err != nil {
logg.ErrorCtxf(ctx, "failed to write to prefixDb", "error", err)
return res, err
}
}
res.FlagReset = append(res.FlagReset, flag_no_transfers)
return res, nil
}
// GetTransactionsList fetches the list of transactions and formats them
func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err)
return res, err
}
// Read transactions from the store and format them
TransactionSenders, err := h.prefixDb.Get(ctx, []byte("txfrom"))
if err != nil {
logg.ErrorCtxf(ctx, "Failed to read the TransactionSenders from prefixDb", "error", err)
return res, err
}
TransactionSyms, err := h.prefixDb.Get(ctx, []byte("txsym"))
if err != nil {
logg.ErrorCtxf(ctx, "Failed to read the TransactionSyms from prefixDb", "error", err)
return res, err
}
TransactionValues, err := h.prefixDb.Get(ctx, []byte("txval"))
if err != nil {
logg.ErrorCtxf(ctx, "Failed to read the TransactionValues from prefixDb", "error", err)
return res, err
}
TransactionDates, err := h.prefixDb.Get(ctx, []byte("txdate"))
if err != nil {
logg.ErrorCtxf(ctx, "Failed to read the TransactionDates from prefixDb", "error", err)
return res, err
}
// Parse the data
senders := strings.Split(string(TransactionSenders), "\n")
syms := strings.Split(string(TransactionSyms), "\n")
values := strings.Split(string(TransactionValues), "\n")
dates := strings.Split(string(TransactionDates), "\n")
var formattedTransactions []string
for i := 0; i < len(senders); i++ {
sender := strings.TrimSpace(senders[i])
sym := strings.TrimSpace(syms[i])
value := strings.TrimSpace(values[i])
date := strings.Split(strings.TrimSpace(dates[i]), " ")[0]
status := "received"
if sender == string(publicKey) {
status = "sent"
}
formattedTransactions = append(formattedTransactions, fmt.Sprintf("%d:%s %s %s %s", i+1, status, value, sym, date))
}
res.Content = strings.Join(formattedTransactions, "\n")
return res, nil
}
// ViewTransactionStatement retrieves the transaction statement
// and displays it to the user
func (h *Handlers) ViewTransactionStatement(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err)
return res, err
}
flag_incorrect_statement, _ := h.flagManager.GetFlag("flag_incorrect_statement")
inputStr := string(input)
if inputStr == "0" || inputStr == "99" || inputStr == "11" || inputStr == "22" {
res.FlagReset = append(res.FlagReset, flag_incorrect_statement)
return res, nil
}
// Convert input string to integer
index, err := strconv.Atoi(strings.TrimSpace(inputStr))
if err != nil {
return res, fmt.Errorf("invalid input: must be a number between 1 and 10")
}
if index < 1 || index > 10 {
return res, fmt.Errorf("invalid input: index must be between 1 and 10")
}
statement, err := common.GetTransferData(ctx, h.prefixDb, string(publicKey), index)
if err != nil {
return res, fmt.Errorf("failed to retrieve transfer data: %v", err)
}
if statement == "" {
res.FlagSet = append(res.FlagSet, flag_incorrect_statement)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_incorrect_statement)
res.Content = statement
return res, nil return res, nil
} }

View File

@@ -2,7 +2,6 @@ package ussd
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log" "log"
"path" "path"
@@ -1773,58 +1772,43 @@ func TestConfirmPin(t *testing.T) {
} }
} }
func TestFetchCustodialBalances(t *testing.T) { func TestFetchCommunityBalance(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
// Define test data // Define test data
sessionId := "session123" sessionId := "session123"
publicKey := "0X13242618721"
ctx, store := InitializeTestStore(t) 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 { tests := []struct {
name string name string
balanceResponse *models.BalanceResult languageCode string
expectedResult resource.Result expectedResult resource.Result
}{ }{
{ {
name: "Test when fetch custodial balances is not a success", name: "Test community balance content when language is english",
balanceResponse: &models.BalanceResult{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
expectedResult: resource.Result{ expectedResult: resource.Result{
FlagReset: []uint32{flag_api_error}, Content: "Community Balance: 0.00",
}, },
languageCode: "eng",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mockAccountService := new(mocks.MockAccountService) mockAccountService := new(mocks.MockAccountService)
mockState := state.NewState(16) mockState := state.NewState(16)
h := &Handlers{ h := &Handlers{
userdataStore: store, userdataStore: store,
flagManager: fm.parser,
st: mockState, st: mockState,
accountService: mockAccountService, accountService: mockAccountService,
} }
ctx = context.WithValue(ctx, "SessionId", sessionId)
// Set up the expected behavior of the mock ctx = context.WithValue(ctx, "Language", lang.Language{
mockAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResponse, nil) Code: tt.languageCode,
})
// Call the method // 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 that the result set to content is what was expected
assert.Equal(t, res, tt.expectedResult, "Result should match expected result") assert.Equal(t, res, tt.expectedResult, "Result should match expected result")
@@ -2040,3 +2024,42 @@ func TestSetVoucher(t *testing.T) {
assert.Equal(t, string(tempData.TokenSymbol), res.Content) assert.Equal(t, string(tempData.TokenSymbol), res.Content)
} }
func TestGetVoucherDetails(t *testing.T) {
ctx, store := InitializeTestStore(t)
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
mockAccountService := new(mocks.MockAccountService)
sessionId := "session123"
ctx = context.WithValue(ctx, "SessionId", sessionId)
expectedResult := resource.Result{}
tokA_AAddress := "0x0000000000000000000000000000000000000000"
h := &Handlers{
userdataStore: store,
flagManager: fm.parser,
accountService: mockAccountService,
}
err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_ADDRESS, []byte(tokA_AAddress))
if err != nil {
t.Fatal(err)
}
tokenDetails := &models.VoucherDataResult{
TokenName: "Token A",
TokenSymbol: "TOKA",
TokenLocation: "Kilifi,Kenya",
TokenCommodity: "Farming",
}
expectedResult.Content = fmt.Sprintf(
"Name: %s\nSymbol: %s\nCommodity: %s\nLocation: %s", tokenDetails.TokenName, tokenDetails.TokenSymbol, tokenDetails.TokenCommodity, tokenDetails.TokenLocation,
)
mockAccountService.On("VoucherData", string(tokA_AAddress)).Return(tokenDetails, nil)
res, err := h.GetVoucherDetails(ctx, "SessionId", []byte(""))
assert.NoError(t, err)
assert.Equal(t, expectedResult, res)
}

View File

@@ -4,8 +4,8 @@ import (
"context" "context"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/lang"
gdbmdb "git.defalsify.org/vise.git/db/gdbm" gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"git.defalsify.org/vise.git/lang"
) )
var ( var (
@@ -114,3 +114,8 @@ func(tdb *ThreadGdbmDb) Close() error {
tdb.db = nil tdb.db = nil
return err return err
} }
func(tdb *ThreadGdbmDb) Dump(_ context.Context, _ []byte) (*db.Dumper, error) {
logg.Warnf("method not implemented for thread gdbm db")
return nil, nil
}

View File

@@ -41,10 +41,13 @@ func buildConnStr() string {
dbName := initializers.GetEnv("DB_NAME", "") dbName := initializers.GetEnv("DB_NAME", "")
port := initializers.GetEnv("DB_PORT", "5432") port := initializers.GetEnv("DB_PORT", "5432")
return fmt.Sprintf( connString := fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s", "postgres://%s:%s@%s:%s/%s",
user, password, host, port, dbName, user, password, host, port, dbName,
) )
logg.Debugf("pg conn string", "conn", connString)
return connString
} }
func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService { func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService {

View File

@@ -47,3 +47,8 @@ func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to
args := m.Called() args := m.Called()
return args.Get(0).(*models.TokenTransferResponse), args.Error(1) 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)
}

View File

@@ -33,8 +33,8 @@ func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey
} }
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
return []dataserviceapi.TokenHoldings { return []dataserviceapi.TokenHoldings{
dataserviceapi.TokenHoldings { dataserviceapi.TokenHoldings{
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
TokenSymbol: "SRF", TokenSymbol: "SRF",
TokenDecimals: "6", TokenDecimals: "6",
@@ -56,3 +56,7 @@ func (tas *TestAccountService) TokenTransfer(ctx context.Context, amount, from,
TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af", TrackingId: "e034d147-747d-42ea-928d-b5a7cb3426af",
}, nil }, nil
} }
func (m TestAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
return &dataserviceapi.AliasAddress{}, nil
}

View File

@@ -2,8 +2,8 @@
package testtag package testtag
import "git.grassecon.net/urdt/ussd/internal/handlers/server" import "git.grassecon.net/urdt/ussd/remote"
var ( var (
AccountService server.AccountServiceInterface AccountService remote.AccountServiceInterface
) )

View File

@@ -103,7 +103,7 @@
}, },
{ {
"input": "1234", "input": "1234",
"expectedContent": "Your balance is 0.003 CELO\n0:Back\n9:Quit" "expectedContent": "Balance: {balance}\n\n0:Back\n9:Quit"
}, },
{ {
"input": "0", "input": "0",
@@ -149,7 +149,7 @@
}, },
{ {
"input": "1234", "input": "1234",
"expectedContent": "Your community balance is 0.003 CELO\n0:Back\n9:Quit" "expectedContent": "{balance}\n0:Back\n9:Quit"
}, },
{ {
"input": "0", "input": "0",

View File

@@ -61,7 +61,7 @@
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Enter recipient's phone number:\n0:Back" "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
}, },
{ {
"input": "000", "input": "000",
@@ -69,7 +69,7 @@
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Enter recipient's phone number:\n0:Back" "expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
}, },
{ {
"input": "0712345678", "input": "0712345678",

View File

@@ -1,8 +1,10 @@
package models package models
type VoucherDataResult struct { type VoucherDataResult struct {
TokenName string `json:"tokenName"` TokenName string `json:"tokenName"`
TokenSymbol string `json:"tokenSymbol"` TokenSymbol string `json:"tokenSymbol"`
TokenDecimals int `json:"tokenDecimals"` TokenDecimals int `json:"tokenDecimals"`
SinkAddress string `json:"sinkAddress"` SinkAddress string `json:"sinkAddress"`
TokenCommodity string `json:"tokenCommodity"`
TokenLocation string `json:"tokenLocation"`
} }

View File

@@ -24,6 +24,7 @@ type AccountServiceInterface interface {
FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error)
VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error)
TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, 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 { type AccountService struct {
@@ -209,6 +210,26 @@ func (as *AccountService) TokenTransfer(ctx context.Context, amount, from, to, t
return &r, nil 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) { 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 var errResponse api.ErrResponse

View File

@@ -0,0 +1 @@
Please enter your PIN to view statement:

View 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 *

View File

@@ -0,0 +1 @@
Tafadhali weka PIN yako kuona taarifa ya matumizi:

View File

@@ -1 +1 @@
Salio la kikundi {{.fetch_community_balance}}

View File

@@ -1 +1 @@
{{.fetch_custodial_balances}} {{.fetch_community_balance}}

View File

@@ -1,7 +1,7 @@
LOAD reset_incorrect 6 LOAD reset_incorrect 6
LOAD fetch_custodial_balances 0 LOAD fetch_community_balance 0
CATCH api_failure flag_api_call_error 1 CATCH api_failure flag_api_call_error 1
MAP fetch_custodial_balances MAP fetch_community_balance
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH pin_entry flag_account_authorized 0 CATCH pin_entry flag_account_authorized 0
MOUT back 0 MOUT back 0

View File

@@ -21,3 +21,6 @@ msgstr "Ombi lako la kumwalika %s kwa matandao wa Sarafu limetumwa."
msgid "Your request failed. Please try again later." msgid "Your request failed. Please try again later."
msgstr "Ombi lako halikufaulu. Tafadhali jaribu tena baadaye." msgstr "Ombi lako halikufaulu. Tafadhali jaribu tena baadaye."
msgid "Community Balance: 0.00"
msgid "Salio la Kikundi: 0.00"

View File

@@ -1,10 +1,10 @@
LOAD set_default_voucher 8 LOAD set_default_voucher 8
RELOAD set_default_voucher RELOAD set_default_voucher
LOAD check_balance 64
RELOAD check_balance
LOAD check_vouchers 10 LOAD check_vouchers 10
RELOAD check_vouchers RELOAD check_vouchers
CATCH api_failure flag_api_call_error 1 LOAD check_balance 64
RELOAD check_balance
CATCH api_failure flag_api_call_error 1
MAP check_balance MAP check_balance
MOUT send 1 MOUT send 1
MOUT vouchers 2 MOUT vouchers 2

View File

@@ -11,5 +11,6 @@ INCMP main 0
INCMP edit_profile 1 INCMP edit_profile 1
INCMP change_language 2 INCMP change_language 2
INCMP balances 3 INCMP balances 3
INCMP check_statement 4
INCMP pin_management 5 INCMP pin_management 5
INCMP address 6 INCMP address 6

View File

@@ -1 +1 @@
{{.fetch_custodial_balances}} {{.check_balance}}

View File

@@ -1,7 +1,7 @@
LOAD reset_incorrect 6 LOAD reset_incorrect 6
LOAD fetch_custodial_balances 0 LOAD check_balance 0
CATCH api_failure flag_api_call_error 1 CATCH api_failure flag_api_call_error 1
MAP fetch_custodial_balances MAP check_balance
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH pin_entry flag_account_authorized 0 CATCH pin_entry flag_account_authorized 0
MOUT back 0 MOUT back 0

View File

@@ -0,0 +1 @@
No transfers history

View File

@@ -0,0 +1,5 @@
MOUT back 0
MOUT quit 9
HALT
INCMP ^ 0
INCMP quit 9

View File

@@ -0,0 +1 @@
Hakuna historia kwa akaunti yako

View File

@@ -19,3 +19,5 @@ flag,flag_api_call_error,25,this is set when communication to an external servic
flag,flag_no_active_voucher,26,this is set when a user does not have an active voucher flag,flag_no_active_voucher,26,this is set when a user does not have an active voucher
flag,flag_admin_privilege,27,this is set when a user has admin privileges. flag,flag_admin_privilege,27,this is set when a user has admin privileges.
flag,flag_unregistered_number,28,this is set when an unregistered phonenumber tries to perform an action flag,flag_unregistered_number,28,this is set when an unregistered phonenumber tries to perform an action
flag,flag_no_transfers,29,this is set when a user does not have any transactions
flag,flag_incorrect_statement,30,this is set when the selected statement is invalid
1 flag flag_language_set 8 checks whether the user has set their prefered language
19 flag flag_no_active_voucher 26 this is set when a user does not have an active voucher
20 flag flag_admin_privilege 27 this is set when a user has admin privileges.
21 flag flag_unregistered_number 28 this is set when an unregistered phonenumber tries to perform an action
22 flag flag_no_transfers 29 this is set when a user does not have any transactions
23 flag flag_incorrect_statement 30 this is set when the selected statement is invalid

View File

@@ -1 +1 @@
Enter recipient's phone number: Enter recipient's phone number/address/alias:

View File

@@ -0,0 +1 @@
{{.get_transactions}}

View File

@@ -0,0 +1,15 @@
LOAD get_transactions 0
MAP get_transactions
MOUT back 0
MOUT quit 99
MNEXT next 11
MPREV prev 22
HALT
LOAD view_statement 0
RELOAD view_statement
CATCH . flag_incorrect_statement 1
INCMP ^ 0
INCMP quit 99
INCMP > 11
INCMP < 22
INCMP view_statement *

View File

@@ -0,0 +1 @@
{{.get_transactions}}

View File

@@ -0,0 +1 @@
{{.view_statement}}

View File

@@ -0,0 +1,10 @@
MAP view_statement
MOUT back 0
MOUT quit 9
MNEXT next 11
MPREV prev 22
HALT
INCMP _ 0
INCMP quit 9
INCMP > 11
INCMP < 22