Compare commits

..

No commits in common. "master" and "africastalking-endpoint" have entirely different histories.

109 changed files with 440 additions and 1650 deletions

View File

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

View File

@ -2,9 +2,6 @@
PORT=7123
HOST=127.0.0.1
#AfricasTalking USSD POST endpoint
AT_ENDPOINT=/ussd/africastalking
#PostgreSQL
DB_HOST=localhost
DB_USER=postgres
@ -15,6 +12,10 @@ DB_SSLMODE=disable
DB_TIMEZONE=Africa/Nairobi
#External API Calls
CUSTODIAL_URL_BASE=http://localhost:5003
BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
DATA_URL_BASE=http://localhost:5006
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
#AfricasTalking USSD POST endpoint
AT_ENDPOINT=/ussd/africastalking

View File

@ -1,56 +0,0 @@
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 }}

View File

@ -1,41 +0,0 @@
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

@ -1,91 +1,8 @@
# URDT USSD service
# ussd
This is a USSD service built using the [go-vise](https://github.com/nolash/go-vise) engine.
> USSD
## 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.
USSD service.
## License

View File

@ -20,7 +20,6 @@ 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"
@ -30,14 +29,28 @@ import (
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
build = "dev"
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
InfoLogger *log.Logger
ErrorLogger *log.Logger
)
func init() {
initializers.LoadEnvVariables()
logFile := "urdt-ussd-africastalking.log"
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
InfoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
// Inject into remote package
remote.InfoLogger = InfoLogger
remote.ErrorLogger = ErrorLogger
}
type atRequestParser struct{}
@ -45,14 +58,14 @@ 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)
ErrorLogger.Println("got an invalid request:", rq)
return "", handlers.ErrInvalidRequest
}
// Capture body (if any) for logging
body, err := io.ReadAll(rqv.Body)
if err != nil {
log.Printf("failed to read request body:", err)
ErrorLogger.Println("failed to read request body:", err)
return "", fmt.Errorf("failed to read request body: %v", err)
}
// Reset the body for further reading
@ -62,13 +75,13 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
bodyLog := map[string]string{"body": string(body)}
logBytes, err := json.Marshal(bodyLog)
if err != nil {
log.Printf("failed to marshal request body:", err)
ErrorLogger.Println("failed to marshal request body:", err)
} else {
log.Printf("Received request:", string(logBytes))
InfoLogger.Println("Received request:", string(logBytes))
}
if err := rqv.ParseForm(); err != nil {
log.Printf("failed to parse form data: %v", err)
ErrorLogger.Println("failed to parse form data: %v", err)
return "", fmt.Errorf("failed to parse form data: %v", err)
}
@ -77,13 +90,7 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
return "", fmt.Errorf("no phone number found")
}
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
if err != nil {
fmt.Printf("Error: %v\n", err)
return "", fmt.Errorf("failed to format number")
}
return formattedNumber, nil
return phoneNumber, nil
}
func (arp *atRequestParser) GetInput(rq any) ([]byte, error) {
@ -124,7 +131,7 @@ func main() {
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.Parse()
logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database)
@ -166,10 +173,6 @@ 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 {

View File

@ -4,6 +4,7 @@ import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"path"
@ -23,12 +24,27 @@ import (
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
InfoLogger *log.Logger
ErrorLogger *log.Logger
)
func init() {
initializers.LoadEnvVariables()
}
logFile := "urdt-ussd-async.log"
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
InfoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
// Inject into remote package
remote.InfoLogger = InfoLogger
remote.ErrorLogger = ErrorLogger
}
type asyncRequestParser struct {
sessionId string
input []byte

View File

@ -4,6 +4,7 @@ import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
@ -26,10 +27,26 @@ import (
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
InfoLogger *log.Logger
ErrorLogger *log.Logger
)
func init() {
initializers.LoadEnvVariables()
logFile := "urdt-ussd-http.log"
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
InfoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
// Inject into remote package
remote.InfoLogger = InfoLogger
remote.ErrorLogger = ErrorLogger
}
func main() {

View File

@ -4,6 +4,7 @@ import (
"context"
"flag"
"fmt"
"log"
"os"
"path"
@ -20,10 +21,26 @@ import (
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
InfoLogger *log.Logger
ErrorLogger *log.Logger
)
func init() {
initializers.LoadEnvVariables()
logFile := "urdt-ussd-cli.log"
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
InfoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
// Inject into remote package
remote.InfoLogger = InfoLogger
remote.ErrorLogger = ErrorLogger
}
func main() {

View File

@ -2,7 +2,6 @@ package common
import (
"encoding/binary"
"errors"
"git.defalsify.org/vise.git/logging"
)
@ -49,23 +48,3 @@ 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")
}
}

View File

@ -1,73 +0,0 @@
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

@ -1,81 +0,0 @@
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
}

View File

@ -1,129 +0,0 @@
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)
}

View File

@ -1,119 +0,0 @@
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

@ -3,7 +3,6 @@ package common
import (
"context"
"fmt"
"math/big"
"strings"
"git.grassecon.net/urdt/ussd/internal/storage"
@ -25,11 +24,7 @@ func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
for i, h := range holdings {
symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol))
// Scale down the balance
scaledBalance := ScaleDownBalance(h.Balance, h.TokenDecimals)
balances = append(balances, fmt.Sprintf("%d:%s", i+1, scaledBalance))
balances = append(balances, fmt.Sprintf("%d:%s", i+1, h.Balance))
decimals = append(decimals, fmt.Sprintf("%d:%s", i+1, h.TokenDecimals))
addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress))
}
@ -42,25 +37,14 @@ func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
return data
}
func ScaleDownBalance(balance, decimals string) string {
// Convert balance and decimals to big.Float
bal := new(big.Float)
bal.SetString(balance)
dec, ok := new(big.Int).SetString(decimals, 10)
if !ok {
dec = big.NewInt(0) // Default to 0 decimals in case of conversion failure
}
divisor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), dec, nil))
scaledBalance := new(big.Float).Quo(bal, divisor)
// Return the scaled balance without trailing decimals if it's an integer
if scaledBalance.IsInt() {
return scaledBalance.Text('f', 0)
}
return scaledBalance.Text('f', -1)
}
//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)
// }
//}
// GetVoucherData retrieves and matches voucher data
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
@ -100,7 +84,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)
@ -151,7 +135,7 @@ func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId str
return data, nil
}
// UpdateVoucherData sets the active voucher data in the DataStore.
// UpdateVoucherData sets the active voucher data and clears the temporary 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

View File

@ -59,13 +59,13 @@ func TestMatchVoucher(t *testing.T) {
func TestProcessVouchers(t *testing.T) {
holdings := []dataserviceapi.TokenHoldings{
{ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100000000"},
{ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200000000"},
{ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100"},
{ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200"},
}
expectedResult := VoucherMetadata{
Symbols: "1:SRF\n2:MILO",
Balances: "1:100\n2:20000",
Balances: "1:100\n2:200",
Decimals: "1:6\n2:4",
Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa",
}

View File

@ -7,33 +7,30 @@ import (
)
const (
createAccountPath = "/api/v2/account/create"
trackStatusPath = "/api/track"
balancePathPrefix = "/api/account"
trackPath = "/api/v2/account/status"
tokenTransferPrefix = "/api/v2/token/transfer"
voucherHoldingsPathPrefix = "/api/v1/holdings"
createAccountPath = "/api/v2/account/create"
trackStatusPath = "/api/track"
balancePathPrefix = "/api/account"
trackPath = "/api/v2/account/status"
voucherHoldingsPathPrefix = "/api/v1/holdings"
voucherTransfersPathPrefix = "/api/v1/transfers/last10"
voucherDataPathPrefix = "/api/v1/token"
AliasPrefix = "api/v1/alias"
voucherDataPathPrefix = "/api/v1/token"
)
var (
custodialURLBase string
dataURLBase string
BearerToken string
dataURLBase string
CustodialAPIKey string
DataAPIKey string
)
var (
CreateAccountURL string
TrackStatusURL string
BalanceURL string
TrackURL string
TokenTransferURL string
VoucherHoldingsURL string
VoucherTransfersURL string
VoucherDataURL string
CheckAliasURL string
CreateAccountURL string
TrackStatusURL string
BalanceURL string
TrackURL string
VoucherHoldingsURL string
VoucherTransfersURL string
VoucherDataURL string
)
func setBase() error {
@ -41,7 +38,8 @@ func setBase() error {
custodialURLBase = initializers.GetEnv("CUSTODIAL_URL_BASE", "http://localhost:5003")
dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
CustodialAPIKey = initializers.GetEnv("CUSTODIAL_API_KEY", "xd")
DataAPIKey = initializers.GetEnv("DATA_API_KEY", "xd")
_, err = url.JoinPath(custodialURLBase, "/foo")
if err != nil {
@ -60,15 +58,13 @@ 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
}

View File

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

View File

@ -1,21 +0,0 @@
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

View File

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

34
go.mod
View File

@ -2,38 +2,44 @@ module git.grassecon.net/urdt/ussd
go 1.23.0
toolchain go1.23.2
require (
git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b
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/ussd-data-service v1.2.0-beta
github.com/joho/godotenv v1.5.1
github.com/peteole/testdata-loader v0.3.0
github.com/stretchr/testify v1.9.0
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/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.18.0 // indirect
)
require (
github.com/alecthomas/participle/v2 v2.0.0 // indirect
github.com/alecthomas/repr v0.2.0 // indirect
github.com/barbashov/iso639-3 v0.0.0-20211020172741-1f4ffb2d8d1c // indirect
github.com/davecgh/go-spew v1.1.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
View File

@ -1,5 +1,5 @@
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=
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b h1:dxBplsIlzJHV+5EH+gzB+w08Blt7IJbb2jeRe1OEjLU=
git.defalsify.org/vise.git v0.2.1-0.20241017112704-307fa6fcdc6b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
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 v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk=
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0=
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a h1:q/YH7nE2j8epNmFnTu0tU1vwtCxtQ6nH+d7hRVV5krU=
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a/go.mod h1:hdKaKwqiW6/kphK4j/BhmuRlZDLo1+DYo3gYw5O0siw=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=

View File

@ -80,7 +80,6 @@ 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)
@ -103,13 +102,12 @@ 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_community_balance", ussdHandlers.FetchCommunityBalance)
ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances)
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)
@ -117,10 +115,6 @@ 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

View File

@ -2,6 +2,7 @@ package ussd
import (
"context"
"encoding/json"
"fmt"
"log"
"path"
@ -585,9 +586,9 @@ func TestGetRecipient(t *testing.T) {
ctx, store := InitializeTestStore(t)
ctx = context.WithValue(ctx, "SessionId", sessionId)
recepient := "0712345678"
recepient := "0xcasgatweksalw1018221"
err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(recepient))
err := store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(recepient))
if err != nil {
t.Fatal(err)
}
@ -1225,53 +1226,28 @@ func TestInitiateTransaction(t *testing.T) {
}
tests := []struct {
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 string
input []byte
Recipient []byte
Amount []byte
ActiveSym []byte
status string
expectedResult resource.Result
}{
{
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",
},
name: "Test initiate transaction",
Amount: []byte("0.002"),
ActiveSym: []byte("SRF"),
Recipient: []byte("0x12415ass27192"),
expectedResult: resource.Result{
FlagReset: []uint32{account_authorized_flag},
Content: "Your request has been sent. 0711223344 will receive 1.00 SRF from 254712345678.",
Content: "Your request has been sent. 0x12415ass27192 will receive 0.002 SRF from 254712345678.",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(tt.TemporaryValue))
if err != nil {
t.Fatal(err)
}
err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_SYM, []byte(tt.ActiveSym))
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))
err := store.WriteEntry(ctx, sessionId, common.DATA_AMOUNT, []byte(tt.Amount))
if err != nil {
t.Fatal(err)
}
@ -1279,19 +1255,13 @@ func TestInitiateTransaction(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_DECIMAL, []byte(tt.ActiveDecimal))
err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_SYM, []byte(tt.ActiveSym))
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", []byte(""))
res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", tt.input)
// Assert that no errors occurred
assert.NoError(t, err)
@ -1483,12 +1453,10 @@ 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 {
@ -1498,33 +1466,19 @@ func TestValidateRecipient(t *testing.T) {
}{
{
name: "Test with invalid recepient",
input: []byte("9234adf5"),
input: []byte("000"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_recipient},
Content: "9234adf5",
Content: "000",
},
},
{
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"),
name: "Test with valid recepient",
input: []byte("0705X2"),
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
@ -1772,43 +1726,58 @@ func TestConfirmPin(t *testing.T) {
}
}
func TestFetchCommunityBalance(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")
// 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
languageCode string
expectedResult resource.Result
name string
balanceResponse *models.BalanceResult
expectedResult resource.Result
}{
{
name: "Test community balance content when language is english",
expectedResult: resource.Result{
Content: "Community Balance: 0.00",
name: "Test when fetch custodial balances is not a success",
balanceResponse: &models.BalanceResult{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
expectedResult: resource.Result{
FlagReset: []uint32{flag_api_error},
},
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,
}
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Language", lang.Language{
Code: tt.languageCode,
})
// Set up the expected behavior of the mock
mockAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResponse, nil)
// Call the method
res, _ := h.FetchCommunityBalance(ctx, "fetch_community_balance", []byte(""))
res, _ := h.FetchCustodialBalances(ctx, "fetch_custodial_balances", []byte(""))
//Assert that the result set to content is what was expected
assert.Equal(t, res, tt.expectedResult, "Result should match expected result")
@ -2018,7 +1987,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)

View File

@ -4,8 +4,8 @@ import (
"context"
"git.defalsify.org/vise.git/db"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"git.defalsify.org/vise.git/lang"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
)
var (
@ -114,8 +114,3 @@ func(tdb *ThreadGdbmDb) Close() error {
tdb.db = nil
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,13 +41,10 @@ func buildConnStr() string {
dbName := initializers.GetEnv("DB_NAME", "")
port := initializers.GetEnv("DB_PORT", "5432")
connString := fmt.Sprintf(
return 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 {

View File

@ -28,6 +28,7 @@ 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)
@ -38,17 +39,7 @@ 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)
}

View File

@ -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,14 +27,14 @@ 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",
@ -47,16 +47,6 @@ func (tas *TestAccountService) FetchTransactions(ctx context.Context, publicKey
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
}

View File

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

View File

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

View File

@ -53,7 +53,7 @@
]
},
{
"name": "send_with_invite",
"name": "send_with_invalid_inputs",
"steps": [
{
"input": "",
@ -61,23 +61,43 @@
},
{
"input": "1",
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
"expectedContent": "Enter recipient's phone number:\n0:Back"
},
{
"input": "000",
"expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit"
"expectedContent": "000 is not registered or invalid, please try again:\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
"expectedContent": "Enter recipient's phone number:\n0:Back"
},
{
"input": "0712345678",
"expectedContent": "0712345678 is not registered, please try again:\n1:Retry\n2:Invite to Sarafu Network\n9:Quit"
"input": "065656",
"expectedContent": "{max_amount}\nEnter amount:\n0:Back"
},
{
"input": "2",
"expectedContent": "Your invite request for 0712345678 to Sarafu Network failed. Please try again later."
"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}."
}
]
},

View File

@ -1,6 +1,6 @@
package models
type AccountResult struct {
PublicKey string `json:"publicKey"`
PublicKey string `json:"publicKey"`
TrackingId string `json:"trackingId"`
}

View File

@ -2,6 +2,7 @@ package models
import "encoding/json"
type BalanceResult struct {
Balance string `json:"balance"`
Nonce json.Number `json:"nonce"`

View File

@ -1,5 +0,0 @@
package models
type TokenTransferResponse struct {
TrackingId string `json:"trackingId"`
}

18
models/tokenresponse.go Normal file
View File

@ -0,0 +1,18 @@
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"`
}

View File

@ -14,5 +14,5 @@ type Transaction struct {
}
type TrackStatusResult struct {
Active bool `json:"active"`
Active bool `json:"active"`
}

View File

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

View File

@ -0,0 +1,21 @@
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"`
}

View File

@ -16,6 +16,11 @@ import (
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
var (
InfoLogger *log.Logger
ErrorLogger *log.Logger
)
type AccountServiceInterface interface {
CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error)
CreateAccount(ctx context.Context) (*models.AccountResult, error)
@ -23,8 +28,6 @@ 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 {
@ -52,7 +55,7 @@ func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey stri
return nil, err
}
_, err = doRequest(ctx, req, &r)
_, err = doCustodialRequest(ctx, req, &r)
if err != nil {
return nil, err
}
@ -76,7 +79,7 @@ func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*
return nil, err
}
_, err = doRequest(ctx, req, &balanceResult)
_, err = doCustodialRequest(ctx, req, &balanceResult)
return &balanceResult, err
}
@ -93,8 +96,9 @@ func (as *AccountService) CreateAccount(ctx context.Context) (*models.AccountRes
if err != nil {
return nil, err
}
_, err = doRequest(ctx, req, &r)
_, err = doCustodialRequest(ctx, req, &r)
if err != nil {
log.Printf("Failed to make custodial %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error())
return nil, err
}
@ -105,9 +109,7 @@ 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 struct {
Holdings []dataserviceapi.TokenHoldings `json:"holdings"`
}
var r []dataserviceapi.TokenHoldings
ep, err := url.JoinPath(config.VoucherHoldingsURL, publicKey)
if err != nil {
@ -119,21 +121,19 @@ func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) (
return nil, err
}
_, err = doRequest(ctx, req, &r)
_, err = doDataRequest(ctx, req, r)
if err != nil {
return nil, err
}
return r.Holdings, nil
return r, nil
}
// FetchTransactions retrieves the last 10 transactions for a given public key from the data indexer API endpoint
// Parameters:
// - publicKey: The public key associated with the account.
func (as *AccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
var r struct {
Transfers []dataserviceapi.Last10TxResponse `json:"transfers"`
}
var r []dataserviceapi.Last10TxResponse
ep, err := url.JoinPath(config.VoucherTransfersURL, publicKey)
if err != nil {
@ -145,21 +145,19 @@ func (as *AccountService) FetchTransactions(ctx context.Context, publicKey strin
return nil, err
}
_, err = doRequest(ctx, req, &r)
_, err = doDataRequest(ctx, req, r)
if err != nil {
return nil, err
}
return r.Transfers, nil
return r, nil
}
// VoucherData retrieves voucher metadata from the data indexer API endpoint.
// Parameters:
// - address: The voucher address.
func (as *AccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
var r struct {
TokenDetails models.VoucherDataResult `json:"tokenDetails"`
}
var voucherDataResult models.VoucherDataResult
ep, err := url.JoinPath(config.VoucherDataURL, address)
if err != nil {
@ -171,83 +169,22 @@ func (as *AccountService) VoucherData(ctx context.Context, address string) (*mod
return nil, err
}
_, err = doRequest(ctx, req, &r)
return &r.TokenDetails, err
}
// TokenTransfer creates a new token transfer in the custodial system.
// Returns:
// - *models.TokenTransferResponse: A pointer to an TokenTransferResponse struct containing the trackingId.
// If there is an error during the request or processing, this will be nil.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil.
func (as *AccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
var r models.TokenTransferResponse
// Create request payload
payload := map[string]string{
"amount": amount,
"from": from,
"to": to,
"tokenAddress": tokenAddress,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return nil, err
}
// Create a new request
req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes))
if err != nil {
return nil, err
}
_, err = doRequest(ctx, req, &r)
if err != nil {
return nil, err
}
return &r, nil
}
// 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
_, err = doCustodialRequest(ctx, req, &voucherDataResult)
return &voucherDataResult, err
}
func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
var okResponse api.OKResponse
var errResponse api.ErrResponse
req.Header.Set("Authorization", "Bearer "+config.BearerToken)
req.Header.Set("Content-Type", "application/json")
logRequestDetails(req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("Failed to make %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error())
errResponse.Description = err.Error()
return nil, err
}
defer resp.Body.Close()
log.Printf("Received response for %s: Status Code: %d | Content-Type: %s", req.URL, resp.StatusCode, resp.Header.Get("Content-Type"))
InfoLogger.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
@ -276,13 +213,25 @@ 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)
logRequestDetails(req)
return doRequest(ctx, req, rcpt)
}
func doDataRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
req.Header.Set("X-GE-KEY", config.DataAPIKey)
logRequestDetails(req)
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)
ErrorLogger.Printf("Error reading request body: %s", err)
return
}
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
@ -290,5 +239,5 @@ func logRequestDetails(req *http.Request) {
bodyBytes = []byte("-")
}
log.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes))
InfoLogger.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes))
}

View File

@ -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 0
LOAD get_recipient 12
LOAD get_sender 64
LOAD get_amount 32
INCMP transaction_pin *

View File

@ -1,5 +1,5 @@
MOUT retry 1
MOUT retry 0
MOUT quit 9
HALT
INCMP _ 1
INCMP _ 0
INCMP quit 9

View File

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

View File

@ -1,12 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
Current family name: {{.get_current_profile_info}}
Enter family name:

View File

@ -1,2 +0,0 @@
Jina la familia la sasa: {{.get_current_profile_info}}
Weka jina la familia

View File

@ -1,2 +0,0 @@
Current name: {{.get_current_profile_info}}
Enter your first names:

View File

@ -1,2 +0,0 @@
Jina la kwanza la sasa {{.get_current_profile_info}}
Weka majina yako ya kwanza:

View File

@ -1,2 +0,0 @@
Current location: {{.get_current_profile_info}}
Enter your location:

View File

@ -1,2 +0,0 @@
Eneo la sasa {{.get_current_profile_info}}
Weka eneo:

View File

@ -1,2 +0,0 @@
Current offerings: {{.get_current_profile_info}}
Enter the services or goods you offer:

View File

@ -1,2 +0,0 @@
Unachouza kwa sasa: {{.get_current_profile_info}}
Weka unachouza

View File

@ -2,8 +2,8 @@ LOAD reset_account_authorized 16
RELOAD reset_account_authorized
LOAD reset_allow_update 0
RELOAD reset_allow_update
MOUT edit_first_name 1
MOUT edit_family_name 2
MOUT edit_name 1
MOUT edit_familyname 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 edit_first_name 1
INCMP edit_family_name 2
INCMP enter_name 1
INCMP enter_familyname 2
INCMP select_gender 3
INCMP edit_yob 4
INCMP edit_location 5
INCMP edit_offerings 6
INCMP enter_yob 4
INCMP enter_location 5
INCMP enter_offerings 6
INCMP view_profile 7

View File

@ -1,2 +0,0 @@
Current year of birth: {{.get_current_profile_info}}
Enter your year of birth

View File

@ -1,2 +0,0 @@
Mwaka wa sasa wa kuzaliwa {{.get_current_profile_info}}
Weka mwaka wa kuzaliwa

View File

@ -0,0 +1 @@
Enter family name:

View File

@ -1,7 +1,5 @@
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

View File

@ -0,0 +1 @@
Weka jina la familia

View File

@ -0,0 +1 @@
Enter your location:

View File

@ -1,7 +1,5 @@
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

View File

@ -0,0 +1 @@
Weka eneo:

View File

@ -0,0 +1 @@
Enter your first names:

View File

@ -1,8 +1,5 @@
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

View File

@ -0,0 +1 @@
Weka majina yako ya kwanza:

View File

@ -0,0 +1 @@
Enter the services or goods you offer:

View File

@ -1,7 +1,5 @@
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

View File

@ -0,0 +1 @@
Weka unachouza

View File

@ -0,0 +1 @@
Enter your year of birth

View File

@ -1,12 +1,8 @@
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 6
RELOAD verify_yob
LOAD verify_yob 0
CATCH incorrect_date_format flag_incorrect_date_format 1
LOAD save_yob 0
RELOAD save_yob

View File

@ -0,0 +1 @@
Weka mwaka wa kuzaliwa

View File

@ -2,5 +2,5 @@ LOAD reset_incorrect_date_format 8
MOUT retry 1
MOUT quit 9
HALT
INCMP _ 1
INCMP enter_yob 1
INCMP quit 9

View File

@ -1 +1 @@
{{.validate_recipient}} is invalid, please try again:
{{.validate_recipient}} is not registered or invalid, please try again:

View File

@ -1 +1 @@
{{.validate_recipient}} sio sahihi, tafadhali weka tena:
{{.validate_recipient}} haijasajiliwa au sio sahihi, tafadhali weka tena:

View File

@ -1 +0,0 @@
Invite to Sarafu Network

View File

@ -1 +0,0 @@
Karibisha kwa matandao wa Sarafu

View File

@ -1 +0,0 @@
{{.validate_recipient}} is not registered, please try again:

View File

@ -1,8 +0,0 @@
MAP validate_recipient
MOUT retry 1
MOUT invite 2
MOUT quit 9
HALT
INCMP _ 1
INCMP invite_result 2
INCMP quit 9

View File

@ -1 +0,0 @@
{{.validate_recipient}} haijasajiliwa, tafadhali weka tena:

View File

@ -1,2 +0,0 @@
LOAD invite_valid_recipient 0
HALT

View File

@ -12,15 +12,3 @@ 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"

View File

@ -11,6 +11,5 @@ 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

View File

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

View File

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

View File

@ -6,4 +6,3 @@ MOUT back 0
HALT
INCMP _ 0
INCMP select_voucher 1
INCMP voucher_details 2

View File

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

View File

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

View File

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

View File

@ -19,5 +19,3 @@ 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_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_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
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

View File

@ -1,2 +1 @@
Current gender: {{.get_current_profile_info}}
Select gender:

View File

@ -1,7 +1,5 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
LOAD get_current_profile_info 0
RELOAD get_current_profile_info
MOUT male 1
MOUT female 2
MOUT unspecified 3
@ -11,3 +9,7 @@ INCMP _ 0
INCMP set_male 1
INCMP set_female 2
INCMP set_unspecified 3

View File

@ -1,2 +1 @@
Jinsia ya sasa {{.get_current_profile_info}}
Chagua jinsia

View File

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

Some files were not shown because too many files have changed in this diff Show More