Merge branch 'master' into lash/ssh-4

This commit is contained in:
lash 2024-12-18 07:46:13 +01:00
commit 9758fd4941
175 changed files with 3174 additions and 586 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

View File

@ -2,6 +2,9 @@
PORT=7123
HOST=127.0.0.1
#AfricasTalking USSD POST endpoint
AT_ENDPOINT=/ussd/africastalking
#PostgreSQL
DB_HOST=localhost
DB_USER=postgres
@ -12,7 +15,6 @@ DB_SSLMODE=disable
DB_TIMEZONE=Africa/Nairobi
#External API Calls
CREATE_ACCOUNT_URL=http://localhost:5003/api/v2/account/create
TRACK_STATUS_URL=https://custodial.sarafu.africa/api/track/
BALANCE_URL=https://custodial.sarafu.africa/api/account/status/
TRACK_URL=http://localhost:5003/api/v2/account/status
CUSTODIAL_URL_BASE=http://localhost:5003
BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
DATA_URL_BASE=http://localhost:5006

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

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ go.work*
cmd/.state/
id_*
*.gdbm
*.log

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

@ -1,8 +1,91 @@
# ussd
# URDT USSD service
> USSD
This is a USSD service built using the [go-vise](https://github.com/nolash/go-vise) engine.
USSD service.
## Prerequisites
### 1. [go-vise](https://github.com/nolash/go-vise)
Set up `go-vise` by cloning the repository into a separate directory. The main upstream repository is hosted at: `https://git.defalsify.org/vise.git`
```
git clone https://git.defalsify.org/vise.git
```
## Setup
1. Clone the ussd repo in its own directory
```
git clone https://git.grassecon.net/urdt/ussd.git
```
2. Navigate to the project directory.
3. Enter the `services/registration` subfolder:
```
cd services/registration
```
4. make the .bin files from the .vis files
```
make VISE_PATH=/var/path/to/your/go-vise -B
```
5. Return to the project root (`cd ../..`)
6. Run the USSD menu
```
go run cmd/main.go -session-id=0712345678
```
## Running the different binaries
1. ### CLI:
```
go run cmd/main.go -session-id=0712345678
```
2. ### Africastalking:
```
go run cmd/africastalking/main.go
```
3. ### Async:
```
go run cmd/async/main.go
```
4. ### Http:
```
go run cmd/http/main.go
```
## Flags
Below are the supported flags:
1. `-session-id`:
Specifies the session ID. (CLI only).
Default: `075xx2123`.
Example:
```
go run cmd/main.go -session-id=0712345678
```
2. `-d`:
Enables engine debug output.
Default: `false`.
Example:
```
go run cmd/main.go -session-id=0712345678 -d
```
3. `-db`:
Specifies the database type.
Default: `gdbm`.
Example:
```
go run cmd/main.go -session-id=0712345678 -d -db=postgres
```
>Note: If using `-db=postgres`, ensure PostgreSQL is running with the connection details specified in your `.env` file.
## License

View File

@ -1,9 +1,12 @@
package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"os/signal"
@ -16,6 +19,7 @@ import (
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/urdt/ussd/common"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/handlers"
@ -27,6 +31,8 @@ import (
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
build = "dev"
)
func init() {
@ -38,9 +44,30 @@ type atRequestParser struct{}
func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
rqv, ok := rq.(*http.Request)
if !ok {
logg.Warnf("got an invalid request", "req", rq)
return "", handlers.ErrInvalidRequest
}
// Capture body (if any) for logging
body, err := io.ReadAll(rqv.Body)
if err != nil {
logg.Warnf("failed to read request body", "err", err)
return "", fmt.Errorf("failed to read request body: %v", err)
}
// Reset the body for further reading
rqv.Body = io.NopCloser(bytes.NewReader(body))
// Log the body as JSON
bodyLog := map[string]string{"body": string(body)}
logBytes, err := json.Marshal(bodyLog)
if err != nil {
logg.Warnf("failed to marshal request body", "err", err)
} else {
logg.Debugf("received request", "bytes", logBytes)
}
if err := rqv.ParseForm(); err != nil {
logg.Warnf("failed to parse form data", "err", err)
return "", fmt.Errorf("failed to parse form data: %v", err)
}
@ -49,7 +76,13 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
return "", fmt.Errorf("no phone number found")
}
return phoneNumber, nil
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
if err != nil {
logg.Warnf("failed to format phone number", "err", err)
return "", fmt.Errorf("failed to format number")
}
return formattedNumber, nil
}
func (arp *atRequestParser) GetInput(rq any) ([]byte, error) {
@ -90,7 +123,7 @@ func main() {
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
flag.Parse()
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
ctx := context.Background()
ctx = context.WithValue(ctx, "Database", database)
@ -132,6 +165,10 @@ func main() {
}
lhs, err := handlers.NewLocalHandlerService(ctx, pfp, true, dbResource, cfg, rs)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
lhs.SetDataStore(&userdataStore)
if err != nil {
@ -156,9 +193,13 @@ func main() {
rp := &atRequestParser{}
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
sh := httpserver.NewATSessionHandler(bsh)
mux := http.NewServeMux()
mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh)
s := &http.Server{
Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))),
Handler: sh,
Handler: mux,
}
s.RegisterOnShutdown(sh.Shutdown)

View File

@ -2,36 +2,89 @@ package common
import (
"encoding/binary"
"errors"
"git.defalsify.org/vise.git/logging"
)
// DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA.
//
// All keys are used only within the context of a single account. Unless otherwise specified, the user context is the session id.
//
// * The first byte is vise/db.DATATYPE_USERDATA
// * The last 2 bytes are the DataTyp value, big-endian.
// * The intermediate bytes are the id of the user context.
//
// All values are strings
type DataTyp uint16
const (
DATA_ACCOUNT DataTyp = iota
DATA_ACCOUNT_CREATED
DATA_TRACKING_ID
// API Tracking id to follow status of account creation
DATA_TRACKING_ID = iota
// EVM address returned from API on account creation
DATA_PUBLIC_KEY
DATA_CUSTODIAL_ID
// Currently active PIN used to authenticate ussd state change requests
DATA_ACCOUNT_PIN
DATA_ACCOUNT_STATUS
// The first name of the user
DATA_FIRST_NAME
// The last name of the user
DATA_FAMILY_NAME
// The year-of-birth of the user
DATA_YOB
// The location of the user
DATA_LOCATION
// The gender of the user
DATA_GENDER
// The offerings description of the user
DATA_OFFERINGS
// The ethereum address of the recipient of an ongoing send request
DATA_RECIPIENT
// The voucher value amount of an ongoing send request
DATA_AMOUNT
// A general swap field for temporary values
DATA_TEMPORARY_VALUE
// Currently active voucher symbol of user
DATA_ACTIVE_SYM
// Voucher balance of user's currently active voucher
DATA_ACTIVE_BAL
// String boolean indicating whether use of PIN is blocked
DATA_BLOCKED_NUMBER
// Reverse mapping of a user's evm address to a session id.
DATA_PUBLIC_KEY_REVERSE
// Decimal count of the currently active voucher
DATA_ACTIVE_DECIMAL
// EVM address of the currently active voucher
DATA_ACTIVE_ADDRESS
DATA_TRANSACTIONS
)
const (
// List of valid voucher symbols in the user context.
DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota
// List of voucher balances for vouchers valid in the user context.
DATA_VOUCHER_BALANCES
// List of voucher decimal counts for vouchers valid in the user context.
DATA_VOUCHER_DECIMALS
// List of voucher EVM addresses for vouchers valid in the user context.
DATA_VOUCHER_ADDRESSES
// List of senders for valid transactions in the user context.
)
const (
DATA_TX_SENDERS = 512 + iota
// List of recipients for valid transactions in the user context.
DATA_TX_RECIPIENTS
// List of voucher values for valid transactions in the user context.
DATA_TX_VALUES
// List of voucher EVM addresses for valid transactions in the user context.
DATA_TX_ADDRESSES
// List of valid transaction hashes in the user context.
DATA_TX_HASHES
// List of transaction dates for valid transactions in the user context.
DATA_TX_DATES
// List of voucher symbols for valid transactions in the user context.
DATA_TX_SYMBOLS
// List of voucher decimal counts for valid transactions in the user context.
DATA_TX_DECIMALS
)
var (
@ -48,3 +101,30 @@ func PackKey(typ DataTyp, data []byte) []byte {
v := typToBytes(typ)
return append(v, data...)
}
func StringToDataTyp(str string) (DataTyp, error) {
switch str {
case "DATA_FIRST_NAME":
return DATA_FIRST_NAME, nil
case "DATA_FAMILY_NAME":
return DATA_FAMILY_NAME, nil
case "DATA_YOB":
return DATA_YOB, nil
case "DATA_LOCATION":
return DATA_LOCATION, nil
case "DATA_GENDER":
return DATA_GENDER, nil
case "DATA_OFFERINGS":
return DATA_OFFERINGS, nil
default:
return 0, errors.New("invalid DataTyp string")
}
}
// ToBytes converts DataTyp or int to a byte slice
func ToBytes[T ~uint16 | int](value T) []byte {
bytes := make([]byte, 2)
binary.BigEndian.PutUint16(bytes, uint16(value))
return bytes
}

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
}

81
common/tokens.go Normal file
View File

@ -0,0 +1,81 @@
package common
import (
"context"
"errors"
"math/big"
"reflect"
"strconv"
)
type TransactionData struct {
TemporaryValue string
ActiveSym string
Amount string
PublicKey string
Recipient string
ActiveDecimal string
ActiveAddress string
}
func ParseAndScaleAmount(storedAmount, activeDecimal string) (string, error) {
// Parse token decimal
tokenDecimal, err := strconv.Atoi(activeDecimal)
if err != nil {
return "", err
}
// Parse amount
amount, _, err := big.ParseFloat(storedAmount, 10, 0, big.ToZero)
if err != nil {
return "", err
}
// Scale the amount
multiplier := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(tokenDecimal)), nil))
finalAmount := new(big.Float).Mul(amount, multiplier)
// Convert finalAmount to a string
finalAmountStr := new(big.Int)
finalAmount.Int(finalAmountStr)
return finalAmountStr.String(), nil
}
func ReadTransactionData(ctx context.Context, store DataStore, sessionId string) (TransactionData, error) {
data := TransactionData{}
fieldToKey := map[string]DataTyp{
"TemporaryValue": DATA_TEMPORARY_VALUE,
"ActiveSym": DATA_ACTIVE_SYM,
"Amount": DATA_AMOUNT,
"PublicKey": DATA_PUBLIC_KEY,
"Recipient": DATA_RECIPIENT,
"ActiveDecimal": DATA_ACTIVE_DECIMAL,
"ActiveAddress": DATA_ACTIVE_ADDRESS,
}
v := reflect.ValueOf(&data).Elem()
for fieldName, key := range fieldToKey {
field := v.FieldByName(fieldName)
if !field.IsValid() || !field.CanSet() {
return data, errors.New("invalid struct field: " + fieldName)
}
value, err := readStringEntry(ctx, store, sessionId, key)
if err != nil {
return data, err
}
field.SetString(value)
}
return data, nil
}
func readStringEntry(ctx context.Context, store DataStore, sessionId string, key DataTyp) (string, error) {
entry, err := store.ReadEntry(ctx, sessionId, key)
if err != nil {
return "", err
}
return string(entry), nil
}

129
common/tokens_test.go Normal file
View File

@ -0,0 +1,129 @@
package common
import (
"testing"
"github.com/alecthomas/assert/v2"
)
func TestParseAndScaleAmount(t *testing.T) {
tests := []struct {
name string
amount string
decimals string
want string
expectError bool
}{
{
name: "whole number",
amount: "123",
decimals: "2",
want: "12300",
expectError: false,
},
{
name: "decimal number",
amount: "123.45",
decimals: "2",
want: "12345",
expectError: false,
},
{
name: "zero decimals",
amount: "123.45",
decimals: "0",
want: "123",
expectError: false,
},
{
name: "large number",
amount: "1000000.01",
decimals: "6",
want: "1000000010000",
expectError: false,
},
{
name: "invalid amount",
amount: "abc",
decimals: "2",
want: "",
expectError: true,
},
{
name: "invalid decimals",
amount: "123.45",
decimals: "abc",
want: "",
expectError: true,
},
{
name: "zero amount",
amount: "0",
decimals: "2",
want: "0",
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseAndScaleAmount(tt.amount, tt.decimals)
// Check error cases
if tt.expectError {
if err == nil {
t.Errorf("ParseAndScaleAmount(%q, %q) expected error, got nil", tt.amount, tt.decimals)
}
return
}
if err != nil {
t.Errorf("ParseAndScaleAmount(%q, %q) unexpected error: %v", tt.amount, tt.decimals, err)
return
}
if got != tt.want {
t.Errorf("ParseAndScaleAmount(%q, %q) = %v, want %v", tt.amount, tt.decimals, got, tt.want)
}
})
}
}
func TestReadTransactionData(t *testing.T) {
sessionId := "session123"
publicKey := "0X13242618721"
ctx, store := InitializeTestDb(t)
// Test transaction data
transactionData := map[DataTyp]string{
DATA_TEMPORARY_VALUE: "0712345678",
DATA_ACTIVE_SYM: "SRF",
DATA_AMOUNT: "1000000",
DATA_PUBLIC_KEY: publicKey,
DATA_RECIPIENT: "0x41c188d63Qa",
DATA_ACTIVE_DECIMAL: "6",
DATA_ACTIVE_ADDRESS: "0xd4c288865Ce",
}
// Store the data
for key, value := range transactionData {
if err := store.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil {
t.Fatal(err)
}
}
expectedResult := TransactionData{
TemporaryValue: "0712345678",
ActiveSym: "SRF",
Amount: "1000000",
PublicKey: publicKey,
Recipient: "0x41c188d63Qa",
ActiveDecimal: "6",
ActiveAddress: "0xd4c288865Ce",
}
data, err := ReadTransactionData(ctx, store, sessionId)
assert.NoError(t, err)
assert.Equal(t, expectedResult, data)
}

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 := []DataTyp{DATA_TX_SENDERS, DATA_TX_RECIPIENTS, DATA_TX_VALUES, DATA_TX_ADDRESSES, DATA_TX_HASHES, DATA_TX_DATES, DATA_TX_SYMBOLS}
data := make(map[DataTyp]string)
for _, key := range keys {
value, err := db.Get(ctx, ToBytes(key))
if err != nil {
return "", fmt.Errorf("failed to get %s: %v", ToBytes(key), err)
}
data[key] = string(value)
}
// Split the data
senders := strings.Split(string(data[DATA_TX_SENDERS]), "\n")
recipients := strings.Split(string(data[DATA_TX_RECIPIENTS]), "\n")
values := strings.Split(string(data[DATA_TX_VALUES]), "\n")
addresses := strings.Split(string(data[DATA_TX_ADDRESSES]), "\n")
hashes := strings.Split(string(data[DATA_TX_HASHES]), "\n")
dates := strings.Split(string(data[DATA_TX_DATES]), "\n")
syms := strings.Split(string(data[DATA_TX_SYMBOLS]), "\n")
// Check if index is within range
if index < 1 || index > len(senders) {
return "", fmt.Errorf("transaction not found: index %d out of range", index)
}
// Adjust for 0-based indexing
i := index - 1
transactionType := "received"
party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i]))
if strings.TrimSpace(senders[i]) == publicKey {
transactionType = "sent"
party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i]))
}
formattedDate := formatDate(strings.TrimSpace(dates[i]))
// Build the full transaction detail
detail := fmt.Sprintf(
"%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s",
transactionType,
strings.TrimSpace(values[i]),
strings.TrimSpace(syms[i]),
party,
strings.TrimSpace(addresses[i]),
strings.TrimSpace(hashes[i]),
formattedDate,
)
return detail, nil
}
// Helper function to format date in desired output
func formatDate(dateStr string) string {
parsedDate, err := time.Parse("2006-01-02 15:04:05 -0700 MST", dateStr)
if err != nil {
fmt.Println("Error parsing date:", err)
return ""
}
return parsedDate.Format("2006-01-02 03:04:05 PM")
}

View File

@ -20,7 +20,7 @@ type UserDataStore struct {
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId)
k := PackKey(typ, []byte(sessionId))
k := ToBytes(typ)
return store.Get(ctx, k)
}
@ -29,6 +29,6 @@ func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId)
k := PackKey(typ, []byte(sessionId))
k := ToBytes(typ)
return store.Put(ctx, k, value)
}

View File

@ -3,6 +3,7 @@ package common
import (
"context"
"fmt"
"math/big"
"strings"
"git.grassecon.net/urdt/ussd/internal/storage"
@ -24,7 +25,11 @@ func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
for i, h := range holdings {
symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol))
balances = append(balances, fmt.Sprintf("%d:%s", i+1, h.Balance))
// Scale down the balance
scaledBalance := ScaleDownBalance(h.Balance, h.TokenDecimals)
balances = append(balances, fmt.Sprintf("%d:%s", i+1, scaledBalance))
decimals = append(decimals, fmt.Sprintf("%d:%s", i+1, h.TokenDecimals))
addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress))
}
@ -37,33 +42,45 @@ func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
return data
}
//func StoreVouchers(db storage.PrefixDb, data VoucherMetadata) {
// value, err := db.Put(ctx, []byte(key))
// if err != nil {
// return nil, fmt.Errorf("failed to get %s: %v", key, err)
// }
// data[key] = string(value)
// }
//}
func ScaleDownBalance(balance, decimals string) string {
// Convert balance and decimals to big.Float
bal := new(big.Float)
bal.SetString(balance)
dec, ok := new(big.Int).SetString(decimals, 10)
if !ok {
dec = big.NewInt(0) // Default to 0 decimals in case of conversion failure
}
divisor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), dec, nil))
scaledBalance := new(big.Float).Quo(bal, divisor)
// Return the scaled balance without trailing decimals if it's an integer
if scaledBalance.IsInt() {
return scaledBalance.Text('f', 0)
}
return scaledBalance.Text('f', -1)
}
// GetVoucherData retrieves and matches voucher data
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
keys := []string{"sym", "bal", "deci", "addr"}
data := make(map[string]string)
keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES}
data := make(map[DataTyp]string)
for _, key := range keys {
value, err := db.Get(ctx, []byte(key))
value, err := db.Get(ctx, ToBytes(key))
if err != nil {
return nil, fmt.Errorf("failed to get %s: %v", key, err)
return nil, fmt.Errorf("failed to get %s: %v", ToBytes(key), err)
}
data[key] = string(value)
}
symbol, balance, decimal, address := MatchVoucher(input,
data["sym"],
data["bal"],
data["deci"],
data["addr"])
data[DATA_VOUCHER_SYMBOLS],
data[DATA_VOUCHER_BALANCES],
data[DATA_VOUCHER_DECIMALS],
data[DATA_VOUCHER_ADDRESSES],
)
if symbol == "" {
return nil, nil
@ -84,7 +101,7 @@ func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol,
decList := strings.Split(decimals, "\n")
addrList := strings.Split(addresses, "\n")
logg.Tracef("found" , "symlist", symList, "syms", symbols, "input", input)
logg.Tracef("found", "symlist", symList, "syms", symbols, "input", input)
for i, sym := range symList {
parts := strings.SplitN(sym, ":", 2)
@ -135,7 +152,7 @@ func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId str
return data, nil
}
// UpdateVoucherData sets the active voucher data and clears the temporary voucher data in the DataStore.
// UpdateVoucherData updates the active voucher data in the DataStore.
func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
logg.TraceCtxf(ctx, "dtal", "data", data)
// Active voucher data entries

View File

@ -8,8 +8,9 @@ import (
"github.com/alecthomas/assert/v2"
"github.com/stretchr/testify/require"
"git.grassecon.net/urdt/ussd/internal/storage"
visedb "git.defalsify.org/vise.git/db"
memdb "git.defalsify.org/vise.git/db/mem"
"git.grassecon.net/urdt/ussd/internal/storage"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
@ -59,13 +60,13 @@ func TestMatchVoucher(t *testing.T) {
func TestProcessVouchers(t *testing.T) {
holdings := []dataserviceapi.TokenHoldings{
{ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100"},
{ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200"},
{ContractAddress: "0xd4c288865Ce", TokenSymbol: "SRF", TokenDecimals: "6", Balance: "100000000"},
{ContractAddress: "0x41c188d63Qa", TokenSymbol: "MILO", TokenDecimals: "4", Balance: "200000000"},
}
expectedResult := VoucherMetadata{
Symbols: "1:SRF\n2:MILO",
Balances: "1:100\n2:200",
Balances: "1:100\n2:20000",
Decimals: "1:6\n2:4",
Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa",
}
@ -83,19 +84,21 @@ func TestGetVoucherData(t *testing.T) {
if err != nil {
t.Fatal(err)
}
spdb := storage.NewSubPrefixDb(db, []byte("vouchers"))
prefix := ToBytes(visedb.DATATYPE_USERDATA)
spdb := storage.NewSubPrefixDb(db, prefix)
// Test voucher data
mockData := map[string][]byte{
"sym": []byte("1:SRF\n2:MILO"),
"bal": []byte("1:100\n2:200"),
"deci": []byte("1:6\n2:4"),
"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
mockData := map[DataTyp][]byte{
DATA_VOUCHER_SYMBOLS: []byte("1:SRF\n2:MILO"),
DATA_VOUCHER_BALANCES: []byte("1:100\n2:200"),
DATA_VOUCHER_DECIMALS: []byte("1:6\n2:4"),
DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
}
// Put the data
for key, value := range mockData {
err = spdb.Put(ctx, []byte(key), []byte(value))
err = spdb.Put(ctx, []byte(ToBytes(key)), []byte(value))
if err != nil {
t.Fatal(err)
}

View File

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

5
debug/cap.go Normal file
View File

@ -0,0 +1,5 @@
package debug
var (
DebugCap uint32
)

84
debug/db.go Normal file
View File

@ -0,0 +1,84 @@
package debug
import (
"fmt"
"encoding/binary"
"git.grassecon.net/urdt/ussd/common"
"git.defalsify.org/vise.git/db"
)
var (
dbTypStr map[common.DataTyp]string = make(map[common.DataTyp]string)
)
type KeyInfo struct {
SessionId string
Typ uint8
SubTyp common.DataTyp
Label string
Description string
}
func (k KeyInfo) String() string {
v := uint16(k.SubTyp)
s := subTypToString(k.SubTyp)
if s == "" {
v = uint16(k.Typ)
s = typToString(k.Typ)
}
return fmt.Sprintf("Session Id: %s\nTyp: %s (%d)\n", k.SessionId, s, v)
}
func ToKeyInfo(k []byte, sessionId string) (KeyInfo, error) {
o := KeyInfo{}
b := []byte(sessionId)
if len(k) <= len(b) {
return o, fmt.Errorf("storage key missing")
}
o.SessionId = sessionId
o.Typ = uint8(k[0])
k = k[1:]
o.SessionId = string(k[:len(b)])
k = k[len(b):]
if o.Typ == db.DATATYPE_USERDATA {
if len(k) == 0 {
return o, fmt.Errorf("missing subtype key")
}
v := binary.BigEndian.Uint16(k[:2])
o.SubTyp = common.DataTyp(v)
o.Label = subTypToString(o.SubTyp)
k = k[2:]
} else {
o.Label = typToString(o.Typ)
}
if len(k) != 0 {
return o, fmt.Errorf("excess key information")
}
return o, nil
}
func FromKey(k []byte) (KeyInfo, error) {
o := KeyInfo{}
if len(k) < 4 {
return o, fmt.Errorf("insufficient key length")
}
sessionIdBytes := k[1:len(k)-2]
return ToKeyInfo(k, string(sessionIdBytes))
}
func subTypToString(v common.DataTyp) string {
return dbTypStr[v + db.DATATYPE_USERDATA + 1]
}
func typToString(v uint8) string {
return dbTypStr[common.DataTyp(uint16(v))]
}

48
debug/db_debug.go Normal file
View File

@ -0,0 +1,48 @@
// +build debugdb
package debug
import (
"git.defalsify.org/vise.git/db"
"git.grassecon.net/urdt/ussd/common"
)
func init() {
DebugCap |= 1
dbTypStr[db.DATATYPE_STATE] = "internal state"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT] = "account"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_CREATED] = "account created"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_CUSTODIAL_ID] = "custodial id"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_STATUS] = "account status"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_LOCATION] = "location"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_GENDER] = "gender"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_OFFERINGS] = "offerings"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_RECIPIENT] = "recipient"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_AMOUNT] = "amount"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TEMPORARY_VALUE] = "temporary value"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_SYM] = "active sym"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_BAL] = "active bal"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_BLOCKED_NUMBER] = "blocked number"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY_REVERSE] = "public_key_reverse"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_DECIMAL] = "active decimal"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_ADDRESS] = "active address"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_SYMBOLS] = "voucher symbols"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_BALANCES] = "voucher balances"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_DECIMALS] = "voucher decimals"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_ADDRESSES] = "voucher addresses"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SENDERS] = "tx senders"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_RECIPIENTS] = "tx recipients"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_VALUES] = "tx values"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_ADDRESSES] = "tx addresses"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_HASHES] = "tx hashes"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DATES] = "tx dates"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SYMBOLS] = "tx symbols"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DECIMALS] = "tx decimals"
}

78
debug/db_test.go Normal file
View File

@ -0,0 +1,78 @@
package debug
import (
"testing"
"git.grassecon.net/urdt/ussd/common"
"git.defalsify.org/vise.git/db"
)
func TestDebugDbSubKeyInfo(t *testing.T) {
s := "foo"
b := []byte{0x20}
b = append(b, []byte(s)...)
b = append(b, []byte{0x00, 0x02}...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 32 {
t.Fatalf("expected 64, got %d", r.Typ)
}
if r.SubTyp != 2 {
t.Fatalf("expected 2, got %d", r.SubTyp)
}
if DebugCap & 1 > 0 {
if r.Label != "tracking id" {
t.Fatalf("expected 'tracking id', got '%s'", r.Label)
}
}
}
func TestDebugDbKeyInfo(t *testing.T) {
s := "bar"
b := []byte{0x10}
b = append(b, []byte(s)...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 16 {
t.Fatalf("expected 16, got %d", r.Typ)
}
if DebugCap & 1 > 0 {
if r.Label != "internal state" {
t.Fatalf("expected 'internal_state', got '%s'", r.Label)
}
}
}
func TestDebugDbKeyInfoRestore(t *testing.T) {
s := "bar"
b := []byte{db.DATATYPE_USERDATA}
b = append(b, []byte(s)...)
k := common.ToBytes(common.DATA_ACTIVE_SYM)
b = append(b, k...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 32 {
t.Fatalf("expected 32, got %d", r.Typ)
}
if DebugCap & 1 > 0 {
if r.Label != "active sym" {
t.Fatalf("expected 'active sym', got '%s'", r.Label)
}
}
}

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;

79
devtools/gen/main.go Normal file
View File

@ -0,0 +1,79 @@
package main
import (
"context"
"crypto/sha1"
"flag"
"fmt"
"os"
"path"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/common"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
)
func init() {
initializers.LoadEnvVariables()
}
func main() {
config.LoadConfig()
var dbDir string
var sessionId string
var database string
var engineDebug bool
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.Parse()
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database)
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
store, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userStore := common.UserDataStore{store}
h := sha1.New()
h.Write([]byte(sessionId))
address := h.Sum(nil)
addressString := fmt.Sprintf("%x", address)
err = userStore.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(addressString))
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = userStore.WriteEntry(ctx, addressString, common.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId))
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = store.Close()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
}

78
devtools/store/main.go Normal file
View File

@ -0,0 +1,78 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"path"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/debug"
"git.defalsify.org/vise.git/logging"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
)
func init() {
initializers.LoadEnvVariables()
}
func main() {
config.LoadConfig()
var dbDir string
var sessionId string
var database string
var engineDebug bool
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.Parse()
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database)
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
store, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
d, err := store.Dump(ctx, []byte(sessionId))
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
for true {
k, v := d.Next(ctx)
if k == nil {
break
}
o, err := debug.FromKey(k)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Printf("%vValue: %v\n\n", o, v)
}
err = store.Close()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
}

28
doc/data.md Normal file
View File

@ -0,0 +1,28 @@
# Internals
## Version
This document describes component versions:
* `urdt-ussd` `v0.5.0-beta`
* `go-vise` `v0.2.2`
## User profile data
All user profile items are stored under keys matching the user's session id, prefixed with the 8-bit value `git.defalsify.org/vise.git/db.DATATYPE_USERDATA` (32), and followed with a 16-big big-endian value subprefix.
For example, given the sessionId `+254123` and the key `git.grassecon.net/urdt-ussd/common.DATA_PUBLIC_KEY` (2) will be stored under the key:
```
0x322b3235343132330002
prefix sessionid subprefix
32 2b323534313233 0002
```
### Sub-prefixes
All sub-prefixes are defined as constants in the `git.grassecon.net/urdt-ussd/common` package. The constant names have the prefix `DATA_`
Please refer to inline godoc documentation for the `git.grassecon.net/urdt-ussd/common` package for details on each data item.

36
go.mod
View File

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

8
go.sum
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.20241017112704-307fa6fcdc6b/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80 h1:GYUVXRUtMpA40T4COeAduoay6CIgXjD5cfDYZOTFIKw=
git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
@ -18,8 +18,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQxMP/6OST1BByrNDj+rqXDmU=
github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo=
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a h1:q/YH7nE2j8epNmFnTu0tU1vwtCxtQ6nH+d7hRVV5krU=
github.com/grassrootseconomics/ussd-data-service v0.0.0-20241003123429-4904b4438a3a/go.mod h1:hdKaKwqiW6/kphK4j/BhmuRlZDLo1+DYo3gYw5O0siw=
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk=
github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo=
github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=

View File

@ -55,6 +55,9 @@ func(f *BaseSessionHandler) Process(rqs RequestSession) (RequestSession, error)
}
f.hn = f.hn.WithPersister(rqs.Storage.Persister)
defer func() {
f.hn.Exit()
}()
eni := f.GetEngine(rqs.Config, f.rs, rqs.Storage.Persister)
en, ok := eni.(*engine.DefaultEngine)
if !ok {

View File

@ -80,6 +80,7 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset)
ls.DbRs.AddLocalFunc("invite_valid_recipient", ussdHandlers.InviteValidRecipient)
ls.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount)
ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
@ -102,12 +103,13 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances)
ls.DbRs.AddLocalFunc("fetch_community_balance", ussdHandlers.FetchCommunityBalance)
ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher)
ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails)
ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin)
ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckPinMisMatch)
ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber)
@ -115,6 +117,12 @@ 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)
ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems)
ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack)
return ussdHandlers, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@ package ussd
import (
"context"
"encoding/json"
"fmt"
"log"
"path"
@ -23,6 +22,7 @@ import (
testdataloader "github.com/peteole/testdata-loader"
"github.com/stretchr/testify/require"
visedb "git.defalsify.org/vise.git/db"
memdb "git.defalsify.org/vise.git/db/mem"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
@ -57,7 +57,8 @@ func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *storage.SubPr
if err != nil {
t.Fatal(err)
}
spdb := storage.NewSubPrefixDb(db, []byte("vouchers"))
prefix := common.ToBytes(visedb.DATATYPE_USERDATA)
spdb := storage.NewSubPrefixDb(db, prefix)
return spdb
}
@ -180,11 +181,14 @@ func TestSaveFirstname(t *testing.T) {
fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_firstname_set, _ := fm.GetFlag("flag_firstname_set")
// Set the flag in the State
mockState := state.NewState(16)
mockState := state.NewState(128)
mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data
firstName := "John"
@ -192,6 +196,8 @@ func TestSaveFirstname(t *testing.T) {
t.Fatal(err)
}
expectedResult.FlagSet = []uint32{flag_firstname_set}
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: store,
@ -204,7 +210,7 @@ func TestSaveFirstname(t *testing.T) {
// Assert results
assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res)
assert.Equal(t, expectedResult, res)
// Verify that the DATA_FIRST_NAME entry has been updated with the temporary value
storedFirstName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FIRST_NAME)
@ -219,11 +225,16 @@ func TestSaveFamilyname(t *testing.T) {
fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_firstname_set, _ := fm.GetFlag("flag_familyname_set")
// Set the flag in the State
mockState := state.NewState(16)
mockState := state.NewState(128)
mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
expectedResult.FlagSet = []uint32{flag_firstname_set}
// Define test data
familyName := "Doeee"
@ -243,7 +254,7 @@ func TestSaveFamilyname(t *testing.T) {
// Assert results
assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res)
assert.Equal(t, expectedResult, res)
// Verify that the DATA_FAMILY_NAME entry has been updated with the temporary value
storedFamilyName, _ := store.ReadEntry(ctx, sessionId, common.DATA_FAMILY_NAME)
@ -258,11 +269,14 @@ func TestSaveYoB(t *testing.T) {
fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_yob_set, _ := fm.GetFlag("flag_yob_set")
// Set the flag in the State
mockState := state.NewState(16)
mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data
yob := "1980"
@ -270,6 +284,8 @@ func TestSaveYoB(t *testing.T) {
t.Fatal(err)
}
expectedResult.FlagSet = []uint32{flag_yob_set}
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: store,
@ -282,7 +298,7 @@ func TestSaveYoB(t *testing.T) {
// Assert results
assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res)
assert.Equal(t, expectedResult, res)
// Verify that the DATA_YOB entry has been updated with the temporary value
storedYob, _ := store.ReadEntry(ctx, sessionId, common.DATA_YOB)
@ -297,11 +313,14 @@ func TestSaveLocation(t *testing.T) {
fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_location_set, _ := fm.GetFlag("flag_location_set")
// Set the flag in the State
mockState := state.NewState(16)
mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data
location := "Kilifi"
@ -309,6 +328,8 @@ func TestSaveLocation(t *testing.T) {
t.Fatal(err)
}
expectedResult.FlagSet = []uint32{flag_location_set}
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: store,
@ -321,7 +342,7 @@ func TestSaveLocation(t *testing.T) {
// Assert results
assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res)
assert.Equal(t, expectedResult, res)
// Verify that the DATA_LOCATION entry has been updated with the temporary value
storedLocation, _ := store.ReadEntry(ctx, sessionId, common.DATA_LOCATION)
@ -336,11 +357,14 @@ func TestSaveOfferings(t *testing.T) {
fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_offerings_set, _ := fm.GetFlag("flag_offerings_set")
// Set the flag in the State
mockState := state.NewState(16)
mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update)
expectedResult := resource.Result{}
// Define test data
offerings := "Bananas"
@ -348,6 +372,8 @@ func TestSaveOfferings(t *testing.T) {
t.Fatal(err)
}
expectedResult.FlagSet = []uint32{flag_offerings_set}
// Create the Handlers instance with the mock store
h := &Handlers{
userdataStore: store,
@ -360,7 +386,7 @@ func TestSaveOfferings(t *testing.T) {
// Assert results
assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res)
assert.Equal(t, expectedResult, res)
// Verify that the DATA_OFFERINGS entry has been updated with the temporary value
storedOfferings, _ := store.ReadEntry(ctx, sessionId, common.DATA_OFFERINGS)
@ -375,9 +401,10 @@ func TestSaveGender(t *testing.T) {
fm, _ := NewFlagManager(flagsPath)
flag_allow_update, _ := fm.GetFlag("flag_allow_update")
flag_gender_set, _ := fm.GetFlag("flag_gender_set")
// Set the flag in the State
mockState := state.NewState(16)
mockState := state.NewState(108)
mockState.SetFlag(flag_allow_update)
// Define test cases
@ -421,12 +448,16 @@ func TestSaveGender(t *testing.T) {
flagManager: fm.parser,
}
expectedResult := resource.Result{}
// Call the method
res, err := h.SaveGender(ctx, "save_gender", tt.input)
expectedResult.FlagSet = []uint32{flag_gender_set}
// Assert results
assert.NoError(t, err)
assert.Equal(t, resource.Result{}, res)
assert.Equal(t, expectedResult, res)
// Verify that the DATA_GENDER entry has been updated with the temporary value
storedGender, _ := store.ReadEntry(ctx, sessionId, common.DATA_GENDER)
@ -586,9 +617,9 @@ func TestGetRecipient(t *testing.T) {
ctx, store := InitializeTestStore(t)
ctx = context.WithValue(ctx, "SessionId", sessionId)
recepient := "0xcasgatweksalw1018221"
recepient := "0712345678"
err := store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(recepient))
err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(recepient))
if err != nil {
t.Fatal(err)
}
@ -1226,32 +1257,41 @@ func TestInitiateTransaction(t *testing.T) {
}
tests := []struct {
name string
input []byte
Recipient []byte
Amount []byte
ActiveSym []byte
status string
expectedResult resource.Result
name string
TemporaryValue []byte
ActiveSym []byte
StoredAmount []byte
TransferAmount string
PublicKey []byte
Recipient []byte
ActiveDecimal []byte
ActiveAddress []byte
TransferResponse *models.TokenTransferResponse
expectedResult resource.Result
}{
{
name: "Test initiate transaction",
Amount: []byte("0.002"),
ActiveSym: []byte("SRF"),
Recipient: []byte("0x12415ass27192"),
name: "Test initiate transaction",
TemporaryValue: []byte("0711223344"),
ActiveSym: []byte("SRF"),
StoredAmount: []byte("1.00"),
TransferAmount: "1000000",
PublicKey: []byte("0X13242618721"),
Recipient: []byte("0x12415ass27192"),
ActiveDecimal: []byte("6"),
ActiveAddress: []byte("0xd4c288865Ce"),
TransferResponse: &models.TokenTransferResponse{
TrackingId: "1234567890",
},
expectedResult: resource.Result{
FlagReset: []uint32{account_authorized_flag},
Content: "Your request has been sent. 0x12415ass27192 will receive 0.002 SRF from 254712345678.",
Content: "Your request has been sent. 0711223344 will receive 1.00 SRF from 254712345678.",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := store.WriteEntry(ctx, sessionId, common.DATA_AMOUNT, []byte(tt.Amount))
if err != nil {
t.Fatal(err)
}
err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(tt.Recipient))
err := store.WriteEntry(ctx, sessionId, common.DATA_TEMPORARY_VALUE, []byte(tt.TemporaryValue))
if err != nil {
t.Fatal(err)
}
@ -1259,9 +1299,31 @@ func TestInitiateTransaction(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = store.WriteEntry(ctx, sessionId, common.DATA_AMOUNT, []byte(tt.StoredAmount))
if err != nil {
t.Fatal(err)
}
err = store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(tt.PublicKey))
if err != nil {
t.Fatal(err)
}
err = store.WriteEntry(ctx, sessionId, common.DATA_RECIPIENT, []byte(tt.Recipient))
if err != nil {
t.Fatal(err)
}
err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_DECIMAL, []byte(tt.ActiveDecimal))
if err != nil {
t.Fatal(err)
}
err = store.WriteEntry(ctx, sessionId, common.DATA_ACTIVE_ADDRESS, []byte(tt.ActiveAddress))
if err != nil {
t.Fatal(err)
}
mockAccountService.On("TokenTransfer").Return(tt.TransferResponse, nil)
// Call the method under test
res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", tt.input)
res, _ := h.InitiateTransaction(ctx, "transaction_reset_amount", []byte(""))
// Assert that no errors occurred
assert.NoError(t, err)
@ -1453,10 +1515,12 @@ func TestValidateRecipient(t *testing.T) {
}
sessionId := "session123"
publicKey := "0X13242618721"
ctx, store := InitializeTestStore(t)
ctx = context.WithValue(ctx, "SessionId", sessionId)
flag_invalid_recipient, _ := fm.parser.GetFlag("flag_invalid_recipient")
flag_invalid_recipient_with_invite, _ := fm.parser.GetFlag("flag_invalid_recipient_with_invite")
// Define test cases
tests := []struct {
@ -1466,27 +1530,59 @@ func TestValidateRecipient(t *testing.T) {
}{
{
name: "Test with invalid recepient",
input: []byte("000"),
input: []byte("7?1234"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_recipient},
Content: "000",
Content: "7?1234",
},
},
{
name: "Test with valid recepient",
input: []byte("0705X2"),
name: "Test with valid unregistered recepient",
input: []byte("0712345678"),
expectedResult: resource.Result{
FlagSet: []uint32{flag_invalid_recipient_with_invite},
Content: "0712345678",
},
},
{
name: "Test with valid registered recepient",
input: []byte("0711223344"),
expectedResult: resource.Result{},
},
{
name: "Test with address",
input: []byte("0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9"),
expectedResult: resource.Result{},
},
{
name: "Test with alias recepient",
input: []byte("alias123"),
expectedResult: resource.Result{},
},
}
// store a public key for the valid recipient
err = store.WriteEntry(ctx, "+254711223344", common.DATA_PUBLIC_KEY, []byte(publicKey))
if err != nil {
t.Fatal(err)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockAccountService := new(mocks.MockAccountService)
// Create the Handlers instance
h := &Handlers{
flagManager: fm.parser,
userdataStore: store,
flagManager: fm.parser,
userdataStore: store,
accountService: mockAccountService,
}
aliasResponse := &dataserviceapi.AliasAddress{
Address: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
mockAccountService.On("CheckAliasAddress", string(tt.input)).Return(aliasResponse, nil)
// Call the method
res, err := h.ValidateRecipient(ctx, "validate_recepient", tt.input)
@ -1518,7 +1614,7 @@ func TestCheckBalance(t *testing.T) {
publicKey: "0X98765432109",
activeSym: "ETH",
activeBal: "1.5",
expectedResult: resource.Result{Content: "Balance: 1.5 ETH\n"},
expectedResult: resource.Result{Content: "Balance: 1.50 ETH\n"},
expectError: false,
},
}
@ -1726,58 +1822,43 @@ func TestConfirmPin(t *testing.T) {
}
}
func TestFetchCustodialBalances(t *testing.T) {
fm, err := NewFlagManager(flagsPath)
if err != nil {
t.Logf(err.Error())
}
flag_api_error, _ := fm.GetFlag("flag_api_call_error")
func TestFetchCommunityBalance(t *testing.T) {
// Define test data
sessionId := "session123"
publicKey := "0X13242618721"
ctx, store := InitializeTestStore(t)
ctx = context.WithValue(ctx, "SessionId", sessionId)
err = store.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(publicKey))
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
balanceResponse *models.BalanceResult
expectedResult resource.Result
name string
languageCode string
expectedResult resource.Result
}{
{
name: "Test when fetch custodial balances is not a success",
balanceResponse: &models.BalanceResult{
Balance: "0.003 CELO",
Nonce: json.Number("0"),
},
name: "Test community balance content when language is english",
expectedResult: resource.Result{
FlagReset: []uint32{flag_api_error},
Content: "Community Balance: 0.00",
},
languageCode: "eng",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockAccountService := new(mocks.MockAccountService)
mockState := state.NewState(16)
h := &Handlers{
userdataStore: store,
flagManager: fm.parser,
st: mockState,
accountService: mockAccountService,
}
// Set up the expected behavior of the mock
mockAccountService.On("CheckBalance", string(publicKey)).Return(tt.balanceResponse, nil)
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Language", lang.Language{
Code: tt.languageCode,
})
// Call the method
res, _ := h.FetchCustodialBalances(ctx, "fetch_custodial_balances", []byte(""))
res, _ := h.FetchCommunityBalance(ctx, "fetch_community_balance", []byte(""))
//Assert that the result set to content is what was expected
assert.Equal(t, res, tt.expectedResult, "Result should match expected result")
@ -1888,7 +1969,7 @@ func TestCheckVouchers(t *testing.T) {
assert.NoError(t, err)
// Read voucher sym data from the store
voucherData, err := spdb.Get(ctx, []byte("sym"))
voucherData, err := spdb.Get(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS))
if err != nil {
t.Fatal(err)
}
@ -1912,7 +1993,7 @@ func TestGetVoucherList(t *testing.T) {
expectedSym := []byte("1:SRF\n2:MILO")
// Put voucher sym data from the store
err := spdb.Put(ctx, []byte("sym"), expectedSym)
err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), expectedSym)
if err != nil {
t.Fatal(err)
}
@ -1942,16 +2023,16 @@ func TestViewVoucher(t *testing.T) {
}
// Define mock voucher data
mockData := map[string][]byte{
"sym": []byte("1:SRF\n2:MILO"),
"bal": []byte("1:100\n2:200"),
"deci": []byte("1:6\n2:4"),
"addr": []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
mockData := map[common.DataTyp][]byte{
common.DATA_VOUCHER_SYMBOLS: []byte("1:SRF\n2:MILO"),
common.DATA_VOUCHER_BALANCES: []byte("1:100\n2:200"),
common.DATA_VOUCHER_DECIMALS: []byte("1:6\n2:4"),
common.DATA_VOUCHER_ADDRESSES: []byte("1:0xd4c288865Ce\n2:0x41c188d63Qa"),
}
// Put the data
for key, value := range mockData {
err = spdb.Put(ctx, []byte(key), []byte(value))
err = spdb.Put(ctx, []byte(common.ToBytes(key)), []byte(value))
if err != nil {
t.Fatal(err)
}
@ -1959,7 +2040,7 @@ func TestViewVoucher(t *testing.T) {
res, err := h.ViewVoucher(ctx, "view_voucher", []byte("1"))
assert.NoError(t, err)
assert.Equal(t, res.Content, "SRF\n100")
assert.Equal(t, res.Content, "Symbol: SRF\nBalance: 100")
}
func TestSetVoucher(t *testing.T) {
@ -1987,9 +2068,48 @@ 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)
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"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/lang"
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
"git.defalsify.org/vise.git/lang"
)
var (
@ -114,3 +114,9 @@ func(tdb *ThreadGdbmDb) Close() error {
tdb.db = nil
return err
}
func(tdb *ThreadGdbmDb) Dump(ctx context.Context, key []byte) (*db.Dumper, error) {
tdb.reserve()
defer tdb.release()
return tdb.db.Dump(ctx, key)
}

View File

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

View File

@ -6,10 +6,6 @@ import (
"git.defalsify.org/vise.git/db"
)
const (
DATATYPE_USERSUB = 64
)
// PrefixDb interface abstracts the database operations.
type PrefixDb interface {
Get(ctx context.Context, key []byte) ([]byte, error)
@ -35,13 +31,13 @@ func (s *SubPrefixDb) toKey(k []byte) []byte {
}
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
s.store.SetPrefix(DATATYPE_USERSUB)
s.store.SetPrefix(db.DATATYPE_USERDATA)
key = s.toKey(key)
return s.store.Get(ctx, key)
}
func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error {
s.store.SetPrefix(DATATYPE_USERSUB)
s.store.SetPrefix(db.DATATYPE_USERDATA)
key = s.toKey(key)
return s.store.Put(ctx, key, val)
}

View File

@ -28,7 +28,6 @@ func (m *MockAccountService) TrackAccountStatus(ctx context.Context, trackingId
return args.Get(0).(*models.TrackStatusResult), args.Error(1)
}
func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
args := m.Called(publicKey)
return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1)
@ -39,7 +38,17 @@ func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey st
return args.Get(0).([]dataserviceapi.Last10TxResponse), args.Error(1)
}
func(m MockAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
func (m *MockAccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
args := m.Called(address)
return args.Get(0).(*models.VoucherDataResult), args.Error(1)
}
func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
args := m.Called()
return args.Get(0).(*models.TokenTransferResponse), args.Error(1)
}
func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
args := m.Called(alias)
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,6 +47,16 @@ 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/internal/handlers/server"
import "git.grassecon.net/urdt/ussd/remote"
var (
AccountService server.AccountServiceInterface
AccountService remote.AccountServiceInterface
)

View File

@ -1,6 +1,9 @@
package utils
import "time"
import (
"strconv"
"time"
)
// CalculateAge calculates the age based on a given birthdate and the current date in the format dd/mm/yy
// It adjusts for cases where the current date is before the birthday in the current year.
@ -25,11 +28,29 @@ func CalculateAge(birthdate, today time.Time) int {
// It subtracts the YOB from the current year to determine the age.
//
// Parameters:
// yob: The year of birth as an integer.
//
// yob: The year of birth as an integer.
//
// Returns:
// The calculated age as an integer.
//
// The calculated age as an integer.
func CalculateAgeWithYOB(yob int) int {
currentYear := time.Now().Year()
return currentYear - yob
currentYear := time.Now().Year()
return currentYear - yob
}
//IsValidYob checks if the provided yob can be considered valid
func IsValidYOb(yob string) bool {
currentYear := time.Now().Year()
yearOfBirth, err := strconv.ParseInt(yob, 10, 64)
if err != nil {
return false
}
if yearOfBirth >= 1900 && int(yearOfBirth) <= currentYear {
return true
} else {
return false
}
}

17
internal/utils/name.go Normal file
View File

@ -0,0 +1,17 @@
package utils
func ConstructName(firstName, familyName, defaultValue string) string {
name := defaultValue
if familyName != defaultValue {
if firstName != defaultValue {
name = firstName + " " + familyName
} else {
name = familyName
}
} else {
if firstName != defaultValue {
name = firstName
}
}
return name
}

View File

@ -54,7 +54,7 @@
},
{
"input": "1235",
"expectedContent": "Incorrect pin\n1:retry\n9:Quit"
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
},
{
"input": "1",
@ -62,7 +62,7 @@
},
{
"input": "1234",
"expectedContent": "Select language:\n0:english\n1:kiswahili"
"expectedContent": "Select language:\n0:English\n1:Kiswahili"
},
{
"input": "0",
@ -95,7 +95,7 @@
},
{
"input": "1235",
"expectedContent": "Incorrect pin\n1:retry\n9:Quit"
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
},
{
"input": "1",
@ -103,7 +103,7 @@
},
{
"input": "1234",
"expectedContent": "Your balance is 0.003 CELO\n0:Back\n9:Quit"
"expectedContent": "Balance: {balance}\n\n0:Back\n9:Quit"
},
{
"input": "0",
@ -141,7 +141,7 @@
},
{
"input": "1235",
"expectedContent": "Incorrect pin\n1:retry\n9:Quit"
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
},
{
"input": "1",
@ -149,7 +149,7 @@
},
{
"input": "1234",
"expectedContent": "Your community balance is 0.003 CELO\n0:Back\n9:Quit"
"expectedContent": "{balance}\n0:Back\n9:Quit"
},
{
"input": "0",
@ -167,7 +167,7 @@
]
},
{
"name": "menu_my_account_edit_firstname",
"name": "menu_my_account_edit_all_account_details_starting_from_firstname",
"steps": [
{
"input": "",
@ -187,6 +187,26 @@
},
{
"input": "foo",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "bar",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
@ -197,10 +217,6 @@
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@ -208,7 +224,7 @@
]
},
{
"name": "menu_my_account_edit_familyname",
"name": "menu_my_account_edit_familyname_when_all_account__details_have_been_set",
"steps": [
{
"input": "",
@ -238,10 +254,6 @@
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@ -250,7 +262,7 @@
]
},
{
"name": "menu_my_account_edit_gender",
"name": "menu_my_account_edit_gender_when_all_account__details_have_been_set",
"steps": [
{
"input": "",
@ -280,10 +292,6 @@
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@ -291,7 +299,7 @@
]
},
{
"name": "menu_my_account_edit_yob",
"name": "menu_my_account_edit_yob_when_all_account__details_have_been_set",
"steps": [
{
"input": "",
@ -321,10 +329,6 @@
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@ -332,7 +336,7 @@
]
},
{
"name": "menu_my_account_edit_location",
"name": "menu_my_account_edit_location_when_all_account_details_have_been_set",
"steps": [
{
"input": "",
@ -362,10 +366,6 @@
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@ -373,7 +373,7 @@
]
},
{
"name": "menu_my_account_edit_offerings",
"name": "menu_my_account_edit_offerings_when_all_account__details_have_been_set",
"steps": [
{
"input": "",
@ -403,10 +403,6 @@
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
@ -434,16 +430,12 @@
},
{
"input": "1234",
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 79\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back"
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 84\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"

View File

@ -3,6 +3,7 @@ package menutraversaltest
import (
"bytes"
"context"
"flag"
"log"
"math/rand"
"os"
@ -15,14 +16,15 @@ import (
)
var (
testData = driver.ReadData()
testStore = ".test_state"
groupTestFile = "group_test.json"
sessionID string
src = rand.NewSource(42)
g = rand.New(src)
testData = driver.ReadData()
testStore = ".test_state"
sessionID string
src = rand.NewSource(42)
g = rand.New(src)
)
var groupTestFile = flag.String("test-file", "group_test.json", "The test file to use for running the group tests")
func GenerateSessionId() string {
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
v, err := uu.NewV4()
@ -337,7 +339,7 @@ func TestMainMenuSend(t *testing.T) {
}
func TestGroups(t *testing.T) {
groups, err := driver.LoadTestGroups(groupTestFile)
groups, err := driver.LoadTestGroups(*groupTestFile)
if err != nil {
log.Fatalf("Failed to load test groups: %v", err)
}

View File

@ -0,0 +1,68 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_family_name",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "2",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "bar",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -0,0 +1,61 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_firstname",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your first names:\n0:Back"
},
{
"input": "foo",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "bar",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -0,0 +1,55 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_gender",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "3",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -0,0 +1,46 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_location",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "5",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -0,0 +1,42 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_offerings",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "6",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -0,0 +1,50 @@
{
"groups": [
{
"name": "menu_my_account_edit_all_account_details_starting_from_yob",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "4",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -0,0 +1,70 @@
{
"groups": [
{
"name": "menu_my_account_edit_familyname_when_adjacent_profile_information_set",
"steps": [
{
"input": "",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
},
{
"input": "3",
"expectedContent": "My Account\n1:Profile\n2:Change language\n3:Check balances\n4:Check statement\n5:PIN options\n6:My Address\n0:Back"
},
{
"input": "1",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "3",
"expectedContent": "Select gender: \n1:Male\n2:Female\n3:Unspecified\n0:Back"
},
{
"input": "1",
"expectedContent": "Enter your year of birth\n0:Back"
},
{
"input": "1940",
"expectedContent": "Enter your location:\n0:Back"
},
{
"input": "Kilifi",
"expectedContent": "Enter the services or goods you offer: \n0:Back"
},
{
"input": "Bananas",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "2",
"expectedContent": "Enter family name:\n0:Back"
},
{
"input": "foo2",
"expectedContent": "Please enter your PIN:"
},
{
"input": "1234",
"expectedContent": "Profile updated successfully\n\n0:Back\n9:Quit"
},
{
"input": "0",
"expectedContent": "My profile\n1:Edit name\n2:Edit family name\n3:Edit gender\n4:Edit year of birth\n5:Edit location\n6:Edit offerings\n7:View profile\n0:Back"
},
{
"input": "0",
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
}
]
}
]
}

View File

@ -7,11 +7,11 @@
"steps": [
{
"input": "",
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili"
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili"
},
{
"input": "0",
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no"
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n0:Yes\n1:No"
},
{
"input": "0",
@ -23,7 +23,7 @@
},
{
"input": "1111",
"expectedContent": "The PIN is not a match. Try again\n1:retry\n9:Quit"
"expectedContent": "The PIN is not a match. Try again\n1:Retry\n9:Quit"
},
{
"input": "1",
@ -40,11 +40,11 @@
"steps": [
{
"input": "",
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:english\n1:kiswahili"
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili"
},
{
"input": "0",
"expectedContent": "Do you agree to terms and conditions?\n0:yes\n1:no"
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n0:Yes\n1:No"
},
{
"input": "1",
@ -53,7 +53,7 @@
]
},
{
"name": "send_with_invalid_inputs",
"name": "send_with_invite",
"steps": [
{
"input": "",
@ -61,43 +61,23 @@
},
{
"input": "1",
"expectedContent": "Enter recipient's phone number:\n0:Back"
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
},
{
"input": "000",
"expectedContent": "000 is not registered or invalid, please try again:\n1:retry\n9:Quit"
"expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "Enter recipient's phone number:\n0:Back"
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
},
{
"input": "065656",
"expectedContent": "{max_amount}\nEnter amount:\n0:Back"
"input": "0712345678",
"expectedContent": "0712345678 is not registered, please try again:\n1:Retry\n2:Invite to Sarafu Network\n9:Quit"
},
{
"input": "10000000",
"expectedContent": "Amount 10000000 is invalid, please try again:\n1:retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "{max_amount}\nEnter amount:\n0:Back"
},
{
"input": "1.00",
"expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1222",
"expectedContent": "Incorrect pin\n1:retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1234",
"expectedContent": "Your request has been sent. 065656 will receive {send_amount} from {session_id}."
"input": "2",
"expectedContent": "Your invite request for 0712345678 to Sarafu Network failed. Please try again later."
}
]
},
@ -140,7 +120,7 @@
},
{
"input": "6",
"expectedContent": "Address: {public_key}\n9:Quit"
"expectedContent": "Address: {public_key}\n0:Back\n9:Quit"
},
{
"input": "9",

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

18
models/profile.go Normal file
View File

@ -0,0 +1,18 @@
package models
type Profile struct {
ProfileItems []string
Max int
}
func (p *Profile) InsertOrShift(index int, value string) {
if index < len(p.ProfileItems) {
p.ProfileItems = append(p.ProfileItems[:index], value)
} else {
for len(p.ProfileItems) < index {
p.ProfileItems = append(p.ProfileItems, "0")
}
p.ProfileItems = append(p.ProfileItems, "0")
p.ProfileItems[index] = value
}
}

View File

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

View File

@ -1,18 +0,0 @@
package models
type ApiResponse struct {
OK bool `json:"ok"`
Description string `json:"description"`
Result Result `json:"result"`
}
type Result struct {
Holdings []Holding `json:"holdings"`
}
type Holding struct {
ContractAddress string `json:"contractAddress"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
Balance string `json:"balance"`
}

View File

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

View File

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

View File

@ -1,21 +0,0 @@
package models
import dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
//type VoucherHoldingResponse struct {
// Ok bool `json:"ok"`
// Description string `json:"description"`
// Result VoucherResult `json:"result"`
//}
// VoucherResult holds the list of token holdings
type VoucherResult struct {
Holdings []dataserviceapi.TokenHoldings `json:"holdings"`
}
type VoucherDataResult struct {
TokenName string `json:"tokenName"`
TokenSymbol string `json:"tokenSymbol"`
TokenDecimals string `json:"tokenDecimals"`
SinkAddress string `json:"sinkAddress"`
}

View File

@ -1,20 +1,19 @@
package remote
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"log"
"net/http"
"net/url"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
"github.com/grassrootseconomics/eth-custodial/pkg/api"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/models"
)
var (
"github.com/grassrootseconomics/eth-custodial/pkg/api"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
type AccountServiceInterface interface {
@ -24,6 +23,8 @@ type AccountServiceInterface interface {
FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error)
FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error)
VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error)
TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error)
CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error)
}
type AccountService struct {
@ -51,7 +52,7 @@ func (as *AccountService) TrackAccountStatus(ctx context.Context, publicKey stri
return nil, err
}
_, err = doCustodialRequest(ctx, req, &r)
_, err = doRequest(ctx, req, &r)
if err != nil {
return nil, err
}
@ -75,11 +76,10 @@ func (as *AccountService) CheckBalance(ctx context.Context, publicKey string) (*
return nil, err
}
_, err = doCustodialRequest(ctx, req, &balanceResult)
_, err = doRequest(ctx, req, &balanceResult)
return &balanceResult, err
}
// CreateAccount creates a new account in the custodial system.
// Returns:
// - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account.
@ -93,8 +93,7 @@ func (as *AccountService) CreateAccount(ctx context.Context) (*models.AccountRes
if err != nil {
return nil, err
}
_, err = doCustodialRequest(ctx, req, &r)
_, err = doRequest(ctx, req, &r)
if err != nil {
return nil, err
}
@ -106,7 +105,9 @@ func (as *AccountService) CreateAccount(ctx context.Context) (*models.AccountRes
// Parameters:
// - publicKey: The public key associated with the account.
func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
var r []dataserviceapi.TokenHoldings
var r struct {
Holdings []dataserviceapi.TokenHoldings `json:"holdings"`
}
ep, err := url.JoinPath(config.VoucherHoldingsURL, publicKey)
if err != nil {
@ -118,20 +119,21 @@ func (as *AccountService) FetchVouchers(ctx context.Context, publicKey string) (
return nil, err
}
_, err = doDataRequest(ctx, req, r)
_, err = doRequest(ctx, req, &r)
if err != nil {
return nil, err
}
return r, nil
return r.Holdings, nil
}
// FetchTransactions retrieves the last 10 transactions for a given public key from the data indexer API endpoint
// Parameters:
// - publicKey: The public key associated with the account.
func (as *AccountService) FetchTransactions(ctx context.Context, publicKey string) ([]dataserviceapi.Last10TxResponse, error) {
var r []dataserviceapi.Last10TxResponse
var r struct {
Transfers []dataserviceapi.Last10TxResponse `json:"transfers"`
}
ep, err := url.JoinPath(config.VoucherTransfersURL, publicKey)
if err != nil {
@ -143,20 +145,21 @@ func (as *AccountService) FetchTransactions(ctx context.Context, publicKey strin
return nil, err
}
_, err = doDataRequest(ctx, req, r)
_, err = doRequest(ctx, req, &r)
if err != nil {
return nil, err
}
return r, nil
return r.Transfers, nil
}
// VoucherData retrieves voucher metadata from the data indexer API endpoint.
// Parameters:
// - address: The voucher address.
func (as *AccountService) VoucherData(ctx context.Context, address string) (*models.VoucherDataResult, error) {
var voucherDataResult models.VoucherDataResult
var r struct {
TokenDetails models.VoucherDataResult `json:"tokenDetails"`
}
ep, err := url.JoinPath(config.VoucherDataURL, address)
if err != nil {
@ -168,22 +171,83 @@ func (as *AccountService) VoucherData(ctx context.Context, address string) (*mod
return nil, err
}
_, err = doCustodialRequest(ctx, req, &voucherDataResult)
return &voucherDataResult, err
_, err = doRequest(ctx, req, &r)
return &r.TokenDetails, err
}
// TokenTransfer creates a new token transfer in the custodial system.
// Returns:
// - *models.TokenTransferResponse: A pointer to an TokenTransferResponse struct containing the trackingId.
// If there is an error during the request or processing, this will be nil.
// - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data.
// If no error occurs, this will be nil.
func (as *AccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
var r models.TokenTransferResponse
// Create request payload
payload := map[string]string{
"amount": amount,
"from": from,
"to": to,
"tokenAddress": tokenAddress,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return nil, err
}
// Create a new request
req, err := http.NewRequest("POST", config.TokenTransferURL, bytes.NewBuffer(payloadBytes))
if err != nil {
return nil, err
}
_, err = doRequest(ctx, req, &r)
if err != nil {
return nil, err
}
return &r, nil
}
// CheckAliasAddress retrieves the address of an alias from the API endpoint.
// Parameters:
// - alias: The alias of the user.
func (as *AccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
var r dataserviceapi.AliasAddress
ep, err := url.JoinPath(config.CheckAliasURL, alias)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", ep, nil)
if err != nil {
return nil, err
}
_, err = doRequest(ctx, req, &r)
return &r, err
}
func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
var okResponse api.OKResponse
var okResponse api.OKResponse
var errResponse api.ErrResponse
req.Header.Set("Authorization", "Bearer "+config.BearerToken)
req.Header.Set("Content-Type", "application/json")
logRequestDetails(req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("Failed to make %s request to endpoint: %s with reason: %s", req.Method, req.URL, err.Error())
errResponse.Description = err.Error()
return nil, err
}
defer resp.Body.Close()
log.Printf("Received response for %s: Status Code: %d | Content-Type: %s", req.URL, resp.StatusCode, resp.Header.Get("Content-Type"))
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
@ -202,7 +266,6 @@ func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKRespons
if len(okResponse.Result) == 0 {
return nil, errors.New("Empty api result")
}
return &okResponse, nil
v, err := json.Marshal(okResponse.Result)
if err != nil {
@ -213,12 +276,19 @@ func doRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKRespons
return &okResponse, err
}
func doCustodialRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
req.Header.Set("X-GE-KEY", config.CustodialAPIKey)
return doRequest(ctx, req, rcpt)
}
func logRequestDetails(req *http.Request) {
var bodyBytes []byte
contentType := req.Header.Get("Content-Type")
if req.Body != nil {
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
log.Printf("Error reading request body: %s", err)
return
}
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
} else {
bodyBytes = []byte("-")
}
func doDataRequest(ctx context.Context, req *http.Request, rcpt any) (*api.OKResponse, error) {
req.Header.Set("X-GE-KEY", config.DataAPIKey)
return doRequest(ctx, req, rcpt)
log.Printf("URL: %s | Content-Type: %s | Method: %s| Request Body: %s", req.URL, contentType, req.Method, string(bodyBytes))
}

View File

@ -0,0 +1 @@
Tatizo la kimtambo limetokea,tafadhali jaribu tena baadaye.

View File

@ -1,6 +1,8 @@
LOAD check_identifier 0
RELOAD check_identifier
MAP check_identifier
MOUT back 0
MOUT quit 9
HALT
INCMP _ 0
INCMP quit 9

View File

@ -0,0 +1 @@
Anwani:{{.check_identifier}}

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

View File

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

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 fetch_custodial_balances 0
LOAD fetch_community_balance 0
CATCH api_failure flag_api_call_error 1
MAP fetch_custodial_balances
MAP fetch_community_balance
CATCH incorrect_pin flag_incorrect_pin 1
CATCH pin_entry flag_account_authorized 0
MOUT back 0

View File

@ -0,0 +1 @@
Tafadhali thibitisha PIN mpya ya: {{.retrieve_blocked_number}}

View File

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

View File

@ -0,0 +1,18 @@
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
MAP get_current_profile_info
MOUT back 0
HALT
RELOAD set_back
CATCH _ flag_back_set 1
LOAD save_familyname 64
RELOAD save_familyname
CATCH pin_entry flag_familyname_set 1
CATCH select_gender flag_gender_set 0
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_familyname_set 0
INCMP select_gender *

View File

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

View File

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

View File

@ -0,0 +1,18 @@
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
RELOAD set_back
CATCH _ flag_back_set 1
LOAD save_firstname 128
RELOAD save_firstname
CATCH pin_entry flag_firstname_set 1
CATCH edit_family_name flag_familyname_set 0
CATCH edit_gender flag_gender_set 0
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_firstname_set 0

View File

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

View File

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

View File

@ -0,0 +1,15 @@
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
LOAD save_location 16
MOUT back 0
HALT
RELOAD set_back
CATCH _ flag_back_set 1
RELOAD save_location
INCMP _ 0
CATCH pin_entry flag_location_set 1
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_location_set 0
INCMP edit_offerings *

View File

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

View File

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

View File

@ -0,0 +1,14 @@
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 8
MOUT back 0
HALT
RELOAD set_back
CATCH _ flag_back_set 1
RELOAD save_offerings
INCMP _ 0
CATCH pin_entry flag_offerings_set 1
CATCH pin_entry flag_offerings_set 0
INCMP update_profile_items *

View File

@ -0,0 +1,2 @@
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_name 1
MOUT edit_familyname 2
MOUT edit_first_name 1
MOUT edit_family_name 2
MOUT edit_gender 3
MOUT edit_yob 4
MOUT edit_location 5
@ -11,11 +11,12 @@ MOUT edit_offerings 6
MOUT view 7
MOUT back 0
HALT
INCMP my_account 0
INCMP enter_name 1
INCMP enter_familyname 2
LOAD set_back 6
INCMP ^ 0
INCMP edit_first_name 1
INCMP edit_family_name 2
INCMP select_gender 3
INCMP enter_yob 4
INCMP enter_location 5
INCMP enter_offerings 6
INCMP edit_yob 4
INCMP edit_location 5
INCMP edit_offerings 6
INCMP view_profile 7

View File

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

View File

@ -0,0 +1,19 @@
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
RELOAD set_back
CATCH _ flag_back_set 1
LOAD verify_yob 6
RELOAD verify_yob
CATCH incorrect_date_format flag_incorrect_date_format 1
LOAD save_yob 32
RELOAD save_yob
CATCH pin_entry flag_yob_set 1
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_yob_set 0
INCMP edit_location *

View File

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

View File

@ -0,0 +1 @@
English

View File

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

View File

@ -1,8 +0,0 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_familyname flag_allow_update 1
MOUT back 0
HALT
LOAD save_familyname 0
RELOAD save_familyname
INCMP _ 0
INCMP pin_entry *

View File

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

View File

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

View File

@ -1,8 +0,0 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_location flag_allow_update 1
MOUT back 0
HALT
LOAD save_location 0
RELOAD save_location
INCMP _ 0
INCMP pin_entry *

View File

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

View File

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

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