Compare commits

..

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

109 changed files with 440 additions and 1650 deletions

View File

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

View File

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

View File

@ -1,56 +0,0 @@
name: release
on:
push:
tags:
- "v*"
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Check out repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to GHCR Docker registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set outputs
run: |
echo "RELEASE_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV \
&& echo "RELEASE_SHORT_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Build and push image
uses: docker/build-push-action@v2
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64
push: true
build-args: |
BUILD=${{ env.RELEASE_SHORT_COMMIT }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
tags: |
ghcr.io/grassrootseconomics/urdt-ussd:latest
ghcr.io/grassrootseconomics/urdt-ussd:${{ env.RELEASE_TAG }}

View File

@ -1,41 +0,0 @@
FROM golang:1.23.0-bookworm AS build
ENV CGO_ENABLED=1
ARG BUILDPLATFORM
ARG TARGETPLATFORM
ARG BUILD=dev
WORKDIR /build
COPY . .
RUN apt update && apt install libgdbm-dev
RUN git clone https://git.defalsify.org/vise.git go-vise
WORKDIR /build/services/registration
RUN echo "Compiling go-vise files"
RUN make VISE_PATH=/build/go-vise -B
WORKDIR /build
RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM"
RUN go mod download
RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update && apt install libgdbm-dev ca-certificates -y
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /service
COPY --from=build /build/ussd-africastalking .
COPY --from=build /build/LICENSE .
COPY --from=build /build/README.md .
COPY --from=build /build/services ./services
COPY --from=build /build/.env.example .
RUN mv .env.example .env
EXPOSE 7123
CMD ["./ussd-africastalking"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ package common
import ( import (
"encoding/binary" "encoding/binary"
"errors"
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
) )
@ -49,23 +48,3 @@ func PackKey(typ DataTyp, data []byte) []byte {
v := typToBytes(typ) v := typToBytes(typ)
return append(v, data...) return append(v, data...)
} }
func StringToDataTyp(str string) (DataTyp, error) {
switch str {
case "DATA_FIRST_NAME":
return DATA_FIRST_NAME, nil
case "DATA_FAMILY_NAME":
return DATA_FAMILY_NAME, nil
case "DATA_YOB":
return DATA_YOB, nil
case "DATA_LOCATION":
return DATA_LOCATION, nil
case "DATA_GENDER":
return DATA_GENDER, nil
case "DATA_OFFERINGS":
return DATA_OFFERINGS, nil
default:
return 0, errors.New("invalid DataTyp string")
}
}

View File

@ -1,73 +0,0 @@
package common
import (
"errors"
"fmt"
"regexp"
"strings"
)
// Define the regex patterns as constants
const (
phoneRegex = `^(?:\+254|254|0)?((?:7[0-9]{8})|(?:1[01][0-9]{7}))$`
addressRegex = `^0x[a-fA-F0-9]{40}$`
aliasRegex = `^[a-zA-Z0-9]+$`
)
// IsValidPhoneNumber checks if the given number is a valid phone number
func IsValidPhoneNumber(phonenumber string) bool {
match, _ := regexp.MatchString(phoneRegex, phonenumber)
return match
}
// IsValidAddress checks if the given address is a valid Ethereum address
func IsValidAddress(address string) bool {
match, _ := regexp.MatchString(addressRegex, address)
return match
}
// IsValidAlias checks if the alias is a valid alias format
func IsValidAlias(alias string) bool {
match, _ := regexp.MatchString(aliasRegex, alias)
return match
}
// CheckRecipient validates the recipient format based on the criteria
func CheckRecipient(recipient string) (string, error) {
if IsValidPhoneNumber(recipient) {
return "phone number", nil
}
if IsValidAddress(recipient) {
return "address", nil
}
if IsValidAlias(recipient) {
return "alias", nil
}
return "", fmt.Errorf("invalid recipient: must be a phone number, address or alias")
}
// FormatPhoneNumber formats a Kenyan phone number to "+254xxxxxxxx".
func FormatPhoneNumber(phone string) (string, error) {
if !IsValidPhoneNumber(phone) {
return "", errors.New("invalid phone number")
}
// Remove any leading "+" and spaces
phone = strings.TrimPrefix(phone, "+")
phone = strings.ReplaceAll(phone, " ", "")
// Replace leading "0" with "254" if present
if strings.HasPrefix(phone, "0") {
phone = "254" + phone[1:]
}
// Add "+" if not already present
if !strings.HasPrefix(phone, "254") {
return "", errors.New("unexpected format")
}
return "+" + phone, nil
}

View File

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

View File

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

View File

@ -1,119 +0,0 @@
package common
import (
"context"
"fmt"
"strings"
"time"
"git.grassecon.net/urdt/ussd/internal/storage"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
// TransferMetadata helps organize data fields
type TransferMetadata struct {
Senders string
Recipients string
TransferValues string
Addresses string
TxHashes string
Dates string
Symbols string
Decimals string
}
// ProcessTransfers converts transfers into formatted strings
func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetadata {
var data TransferMetadata
var senders, recipients, transferValues, addresses, txHashes, dates, symbols, decimals []string
for _, t := range transfers {
senders = append(senders, t.Sender)
recipients = append(recipients, t.Recipient)
// Scale down the amount
scaledBalance := ScaleDownBalance(t.TransferValue, t.TokenDecimals)
transferValues = append(transferValues, scaledBalance)
addresses = append(addresses, t.ContractAddress)
txHashes = append(txHashes, t.TxHash)
dates = append(dates, fmt.Sprintf("%s", t.DateBlock))
symbols = append(symbols, t.TokenSymbol)
decimals = append(decimals, t.TokenDecimals)
}
data.Senders = strings.Join(senders, "\n")
data.Recipients = strings.Join(recipients, "\n")
data.TransferValues = strings.Join(transferValues, "\n")
data.Addresses = strings.Join(addresses, "\n")
data.TxHashes = strings.Join(txHashes, "\n")
data.Dates = strings.Join(dates, "\n")
data.Symbols = strings.Join(symbols, "\n")
data.Decimals = strings.Join(decimals, "\n")
return data
}
// GetTransferData retrieves and matches transfer data
// returns a formatted string of the full transaction/statement
func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) {
keys := []string{"txfrom", "txto", "txval", "txaddr", "txhash", "txdate", "txsym"}
data := make(map[string]string)
for _, key := range keys {
value, err := db.Get(ctx, []byte(key))
if err != nil {
return "", fmt.Errorf("failed to get %s: %v", key, err)
}
data[key] = string(value)
}
// Split the data
senders := strings.Split(string(data["txfrom"]), "\n")
recipients := strings.Split(string(data["txto"]), "\n")
values := strings.Split(string(data["txval"]), "\n")
addresses := strings.Split(string(data["txaddr"]), "\n")
hashes := strings.Split(string(data["txhash"]), "\n")
dates := strings.Split(string(data["txdate"]), "\n")
syms := strings.Split(string(data["txsym"]), "\n")
// Check if index is within range
if index < 1 || index > len(senders) {
return "", fmt.Errorf("transaction not found: index %d out of range", index)
}
// Adjust for 0-based indexing
i := index - 1
transactionType := "received"
party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i]))
if strings.TrimSpace(senders[i]) == publicKey {
transactionType = "sent"
party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i]))
}
formattedDate := formatDate(strings.TrimSpace(dates[i]))
// Build the full transaction detail
detail := fmt.Sprintf(
"%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s",
transactionType,
strings.TrimSpace(values[i]),
strings.TrimSpace(syms[i]),
party,
strings.TrimSpace(addresses[i]),
strings.TrimSpace(hashes[i]),
formattedDate,
)
return detail, nil
}
// Helper function to format date in desired output
func formatDate(dateStr string) string {
parsedDate, err := time.Parse("2006-01-02 15:04:05 -0700 MST", dateStr)
if err != nil {
fmt.Println("Error parsing date:", err)
return ""
}
return parsedDate.Format("2006-01-02 03:04:05 PM")
}

View File

@ -3,7 +3,6 @@ package common
import ( import (
"context" "context"
"fmt" "fmt"
"math/big"
"strings" "strings"
"git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/storage"
@ -25,11 +24,7 @@ func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
for i, h := range holdings { for i, h := range holdings {
symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol)) 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)) decimals = append(decimals, fmt.Sprintf("%d:%s", i+1, h.TokenDecimals))
addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress)) addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress))
} }
@ -42,25 +37,14 @@ func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
return data return data
} }
func ScaleDownBalance(balance, decimals string) string { //func StoreVouchers(db storage.PrefixDb, data VoucherMetadata) {
// Convert balance and decimals to big.Float // value, err := db.Put(ctx, []byte(key))
bal := new(big.Float) // if err != nil {
bal.SetString(balance) // return nil, fmt.Errorf("failed to get %s: %v", key, err)
// }
dec, ok := new(big.Int).SetString(decimals, 10) // data[key] = string(value)
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 // GetVoucherData retrieves and matches voucher data
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) { func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
@ -100,7 +84,7 @@ func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol,
decList := strings.Split(decimals, "\n") decList := strings.Split(decimals, "\n")
addrList := strings.Split(addresses, "\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 { for i, sym := range symList {
parts := strings.SplitN(sym, ":", 2) parts := strings.SplitN(sym, ":", 2)
@ -151,7 +135,7 @@ func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId str
return data, nil return data, nil
} }
// UpdateVoucherData sets the active voucher data in the DataStore. // UpdateVoucherData sets the active voucher data and clears the temporary voucher data in the DataStore.
func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { func UpdateVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
logg.TraceCtxf(ctx, "dtal", "data", data) logg.TraceCtxf(ctx, "dtal", "data", data)
// Active voucher data entries // Active voucher data entries

View File

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

View File

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

View File

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

View File

@ -1,21 +0,0 @@
services:
ussd-pg-store:
image: postgres:17-alpine
restart: unless-stopped
user: postgres
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_USER=postgres
volumes:
- ./init_db.sql:/docker-entrypoint-initdb.d/init_db.sql
- ussd-pg:/var/lib/postgresql/data
ports:
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
volumes:
ussd-pg:
driver: local

View File

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

34
go.mod
View File

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

8
go.sum
View File

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

View File

@ -80,7 +80,6 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance)
ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient)
ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) 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("max_amount", ussdHandlers.MaxAmount)
ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount)
ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount)
@ -103,13 +102,12 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin)
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
ls.DbRs.AddLocalFunc("fetch_community_balance", ussdHandlers.FetchCommunityBalance) ls.DbRs.AddLocalFunc("fetch_custodial_balances", ussdHandlers.FetchCustodialBalances)
ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher) ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher)
ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers) ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList) ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher) ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher) 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("reset_valid_pin", ussdHandlers.ResetValidPin)
ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckPinMisMatch) ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckPinMisMatch)
ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber) ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber)
@ -117,10 +115,6 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn
ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber) ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber)
ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin) ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin)
ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin) ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin)
ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo)
ls.DbRs.AddLocalFunc("check_transactions", ussdHandlers.CheckTransactions)
ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList)
ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement)
return ussdHandlers, nil return ussdHandlers, nil
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@ func (m *MockAccountService) TrackAccountStatus(ctx context.Context, trackingId
return args.Get(0).(*models.TrackStatusResult), args.Error(1) return args.Get(0).(*models.TrackStatusResult), args.Error(1)
} }
func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { func (m *MockAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
args := m.Called(publicKey) args := m.Called(publicKey)
return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1) return args.Get(0).([]dataserviceapi.TokenHoldings), args.Error(1)
@ -38,17 +39,7 @@ func (m *MockAccountService) FetchTransactions(ctx context.Context, publicKey st
return args.Get(0).([]dataserviceapi.Last10TxResponse), args.Error(1) 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) args := m.Called(address)
return args.Get(0).(*models.VoucherDataResult), args.Error(1) return args.Get(0).(*models.VoucherDataResult), args.Error(1)
} }
func (m *MockAccountService) TokenTransfer(ctx context.Context, amount, from, to, tokenAddress string) (*models.TokenTransferResponse, error) {
args := m.Called()
return args.Get(0).(*models.TokenTransferResponse), args.Error(1)
}
func (m *MockAccountService) CheckAliasAddress(ctx context.Context, alias string) (*dataserviceapi.AliasAddress, error) {
args := m.Called()
return args.Get(0).(*dataserviceapi.AliasAddress), args.Error(1)
}

View File

@ -12,14 +12,14 @@ type TestAccountService struct {
} }
func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) { func (tas *TestAccountService) CreateAccount(ctx context.Context) (*models.AccountResult, error) {
return &models.AccountResult{ return &models.AccountResult {
TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d", TrackingId: "075ccc86-f6ef-4d33-97d5-e91cfb37aa0d",
PublicKey: "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD", PublicKey: "0x623EFAFa8868df4B934dd12a8B26CB3Dd75A7AdD",
}, nil }, nil
} }
func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) { func (tas *TestAccountService) CheckBalance(ctx context.Context, publicKey string) (*models.BalanceResult, error) {
balanceResponse := &models.BalanceResult{ balanceResponse := &models.BalanceResult {
Balance: "0.003 CELO", Balance: "0.003 CELO",
Nonce: json.Number("0"), 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) { func (tas *TestAccountService) TrackAccountStatus(ctx context.Context, publicKey string) (*models.TrackStatusResult, error) {
return &models.TrackStatusResult{ return &models.TrackStatusResult {
Active: true, Active: true,
}, nil }, nil
} }
func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) { func (tas *TestAccountService) FetchVouchers(ctx context.Context, publicKey string) ([]dataserviceapi.TokenHoldings, error) {
return []dataserviceapi.TokenHoldings{ return []dataserviceapi.TokenHoldings {
dataserviceapi.TokenHoldings{ dataserviceapi.TokenHoldings {
ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", ContractAddress: "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee",
TokenSymbol: "SRF", TokenSymbol: "SRF",
TokenDecimals: "6", TokenDecimals: "6",
@ -47,16 +47,6 @@ func (tas *TestAccountService) FetchTransactions(ctx context.Context, publicKey
return []dataserviceapi.Last10TxResponse{}, nil 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 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 package testtag
import "git.grassecon.net/urdt/ussd/remote" import "git.grassecon.net/urdt/ussd/internal/handlers/server"
var ( var (
AccountService remote.AccountServiceInterface AccountService server.AccountServiceInterface
) )

View File

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

View File

@ -53,7 +53,7 @@
] ]
}, },
{ {
"name": "send_with_invite", "name": "send_with_invalid_inputs",
"steps": [ "steps": [
{ {
"input": "", "input": "",
@ -61,23 +61,43 @@
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" "expectedContent": "Enter recipient's phone number:\n0:Back"
}, },
{ {
"input": "000", "input": "000",
"expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit" "expectedContent": "000 is not registered or invalid, please try again:\n1:Retry\n9:Quit"
}, },
{ {
"input": "1", "input": "1",
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back" "expectedContent": "Enter recipient's phone number:\n0:Back"
}, },
{ {
"input": "0712345678", "input": "065656",
"expectedContent": "0712345678 is not registered, please try again:\n1:Retry\n2:Invite to Sarafu Network\n9:Quit" "expectedContent": "{max_amount}\nEnter amount:\n0:Back"
}, },
{ {
"input": "2", "input": "10000000",
"expectedContent": "Your invite request for 0712345678 to Sarafu Network failed. Please try again later." "expectedContent": "Amount 10000000 is invalid, please try again:\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "{max_amount}\nEnter amount:\n0:Back"
},
{
"input": "1.00",
"expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1222",
"expectedContent": "Incorrect pin\n1:Retry\n9:Quit"
},
{
"input": "1",
"expectedContent": "065656 will receive {send_amount} from {session_id}\nPlease enter your PIN to confirm:\n0:Back\n9:Quit"
},
{
"input": "1234",
"expectedContent": "Your request has been sent. 065656 will receive {send_amount} from {session_id}."
} }
] ]
}, },

View File

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

View File

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

View File

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

18
models/tokenresponse.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,10 +6,10 @@ MOUT back 0
HALT HALT
LOAD validate_amount 64 LOAD validate_amount 64
RELOAD validate_amount 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 CATCH invalid_amount flag_invalid_amount 1
INCMP _ 0 INCMP _ 0
LOAD get_recipient 0 LOAD get_recipient 12
LOAD get_sender 64 LOAD get_sender 64
LOAD get_amount 32 LOAD get_amount 32
INCMP transaction_pin * INCMP transaction_pin *

View File

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

View File

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

View File

@ -1,12 +0,0 @@
LOAD check_transactions 0
RELOAD check_transactions
CATCH no_transfers flag_no_transfers 1
LOAD authorize_account 6
MOUT back 0
MOUT quit 9
HALT
RELOAD authorize_account
CATCH incorrect_pin flag_incorrect_pin 1
INCMP _ 0
INCMP quit 9
INCMP transactions *

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,8 @@ LOAD reset_account_authorized 16
RELOAD reset_account_authorized RELOAD reset_account_authorized
LOAD reset_allow_update 0 LOAD reset_allow_update 0
RELOAD reset_allow_update RELOAD reset_allow_update
MOUT edit_first_name 1 MOUT edit_name 1
MOUT edit_family_name 2 MOUT edit_familyname 2
MOUT edit_gender 3 MOUT edit_gender 3
MOUT edit_yob 4 MOUT edit_yob 4
MOUT edit_location 5 MOUT edit_location 5
@ -12,10 +12,10 @@ MOUT view 7
MOUT back 0 MOUT back 0
HALT HALT
INCMP my_account 0 INCMP my_account 0
INCMP edit_first_name 1 INCMP enter_name 1
INCMP edit_family_name 2 INCMP enter_familyname 2
INCMP select_gender 3 INCMP select_gender 3
INCMP edit_yob 4 INCMP enter_yob 4
INCMP edit_location 5 INCMP enter_location 5
INCMP edit_offerings 6 INCMP enter_offerings 6
INCMP view_profile 7 INCMP view_profile 7

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_familyname flag_allow_update 1 CATCH update_familyname flag_allow_update 1
LOAD get_current_profile_info 0
RELOAD get_current_profile_info
MOUT back 0 MOUT back 0
HALT HALT
LOAD save_familyname 0 LOAD save_familyname 0

View File

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

View File

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

View File

@ -1,7 +1,5 @@
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_location flag_allow_update 1 CATCH update_location flag_allow_update 1
LOAD get_current_profile_info 0
RELOAD get_current_profile_info
MOUT back 0 MOUT back 0
HALT HALT
LOAD save_location 0 LOAD save_location 0

View File

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

View File

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

View File

@ -1,8 +1,5 @@
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_firstname flag_allow_update 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 MOUT back 0
HALT HALT
LOAD save_firstname 0 LOAD save_firstname 0

View File

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

View File

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

View File

@ -1,7 +1,5 @@
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_offerings flag_allow_update 1 CATCH update_offerings flag_allow_update 1
LOAD get_current_profile_info 0
RELOAD get_current_profile_info
LOAD save_offerings 0 LOAD save_offerings 0
MOUT back 0 MOUT back 0
HALT HALT

View File

@ -0,0 +1 @@
Weka unachouza

View File

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

View File

@ -1,12 +1,8 @@
CATCH incorrect_pin flag_incorrect_pin 1 CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_yob flag_allow_update 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 MOUT back 0
HALT HALT
LOAD verify_yob 6 LOAD verify_yob 0
RELOAD verify_yob
CATCH incorrect_date_format flag_incorrect_date_format 1 CATCH incorrect_date_format flag_incorrect_date_format 1
LOAD save_yob 0 LOAD save_yob 0
RELOAD save_yob RELOAD save_yob

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,15 +12,3 @@ msgstr "Kwa usaidizi zaidi,piga: 0757628885"
msgid "Balance: %s\n" msgid "Balance: %s\n"
msgstr "Salio: %s\n" msgstr "Salio: %s\n"
msid "Your invite request for %s to Sarafu Network failed. Please try again later."
msgstr "Ombi lako la kumwalika %s kwa matandao wa Sarafu halikufaulu. Tafadhali jaribu tena baadaye."
msgid "Your invitation to %s to join Sarafu Network has been sent."
msgstr "Ombi lako la kumwalika %s kwa matandao wa Sarafu limetumwa."
msgid "Your request failed. Please try again later."
msgstr "Ombi lako halikufaulu. Tafadhali jaribu tena baadaye."
msgid "Community Balance: 0.00"
msgid "Salio la Kikundi: 0.00"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1 flag flag_language_set 8 checks whether the user has set their prefered language
19 flag flag_no_active_voucher 26 this is set when a user does not have an active voucher
20 flag flag_admin_privilege 27 this is set when a user has admin privileges.
21 flag flag_unregistered_number 28 this is set when an unregistered phonenumber tries to perform an action
flag flag_no_transfers 29 this is set when a user does not have any transactions
flag flag_incorrect_statement 30 this is set when the selected statement is invalid

View File

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

View File

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

View File

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

View File

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

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