Move out common, util, debug, storage to sarafu-vise

This commit is contained in:
lash 2025-01-12 07:59:03 +00:00
parent 2d89db3a37
commit 5b312f9569
Signed by untrusted user who does not match committer: lash
GPG Key ID: 21D2E7BB88C2A746
21 changed files with 0 additions and 1646 deletions

View File

@ -1,134 +0,0 @@
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 (
// API Tracking id to follow status of account creation
DATA_TRACKING_ID = iota
// EVM address returned from API on account creation
DATA_PUBLIC_KEY
// Currently active PIN used to authenticate ussd state change requests
DATA_ACCOUNT_PIN
// 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
//Holds count of the number of incorrect PIN attempts
DATA_INCORRECT_PIN_ATTEMPTS
//ISO 639 code for the selected language.
DATA_SELECTED_LANGUAGE_CODE
)
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 (
logg = logging.NewVanilla().WithDomain("urdt-common")
)
func typToBytes(typ DataTyp) []byte {
var b [2]byte
binary.BigEndian.PutUint16(b[:], uint16(typ))
return b[:]
}
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
}

View File

@ -1,31 +0,0 @@
package common
import (
"encoding/hex"
"strings"
)
func NormalizeHex(s string) (string, error) {
if len(s) >= 2 {
if s[:2] == "0x" {
s = s[2:]
}
}
r, err := hex.DecodeString(s)
if err != nil {
return "", err
}
return hex.EncodeToString(r), nil
}
func IsSameHex(left string, right string) bool {
bl, err := NormalizeHex(left)
if err != nil {
return false
}
br, err := NormalizeHex(left)
if err != nil {
return false
}
return strings.Compare(bl, br) == 0
}

View File

@ -1,37 +0,0 @@
package common
import (
"regexp"
"golang.org/x/crypto/bcrypt"
)
const (
// Define the regex pattern as a constant
pinPattern = `^\d{4}$`
//Allowed incorrect PIN attempts
AllowedPINAttempts = uint8(3)
)
// checks whether the given input is a 4 digit number
func IsValidPIN(pin string) bool {
match, _ := regexp.MatchString(pinPattern, pin)
return match
}
// HashPIN uses bcrypt with 8 salt rounds to hash the PIN
func HashPIN(pin string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pin), 8)
if err != nil {
return "", err
}
return string(hash), nil
}
// VerifyPIN compareS the hashed PIN with the plaintext PIN
func VerifyPIN(hashedPIN, pin string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(pin))
return err == nil
}

View File

@ -1,173 +0,0 @@
package common
import (
"testing"
"golang.org/x/crypto/bcrypt"
)
func TestIsValidPIN(t *testing.T) {
tests := []struct {
name string
pin string
expected bool
}{
{
name: "Valid PIN with 4 digits",
pin: "1234",
expected: true,
},
{
name: "Valid PIN with leading zeros",
pin: "0001",
expected: true,
},
{
name: "Invalid PIN with less than 4 digits",
pin: "123",
expected: false,
},
{
name: "Invalid PIN with more than 4 digits",
pin: "12345",
expected: false,
},
{
name: "Invalid PIN with letters",
pin: "abcd",
expected: false,
},
{
name: "Invalid PIN with special characters",
pin: "12@#",
expected: false,
},
{
name: "Empty PIN",
pin: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := IsValidPIN(tt.pin)
if actual != tt.expected {
t.Errorf("IsValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
}
})
}
}
func TestHashPIN(t *testing.T) {
tests := []struct {
name string
pin string
}{
{
name: "Valid PIN with 4 digits",
pin: "1234",
},
{
name: "Valid PIN with leading zeros",
pin: "0001",
},
{
name: "Empty PIN",
pin: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hashedPIN, err := HashPIN(tt.pin)
if err != nil {
t.Errorf("HashPIN(%q) returned an error: %v", tt.pin, err)
return
}
if hashedPIN == "" {
t.Errorf("HashPIN(%q) returned an empty hash", tt.pin)
}
// Ensure the hash can be verified with bcrypt
err = bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(tt.pin))
if tt.pin != "" && err != nil {
t.Errorf("HashPIN(%q) produced a hash that does not match: %v", tt.pin, err)
}
})
}
}
func TestVerifyMigratedHashPin(t *testing.T) {
tests := []struct {
pin string
hash string
}{
{
pin: "1234",
hash: "$2b$08$dTvIGxCCysJtdvrSnaLStuylPoOS/ZLYYkxvTeR5QmTFY3TSvPQC6",
},
}
for _, tt := range tests {
t.Run(tt.pin, func(t *testing.T) {
ok := VerifyPIN(tt.hash, tt.pin)
if !ok {
t.Errorf("VerifyPIN could not verify migrated PIN: %v", tt.pin)
}
})
}
}
func TestVerifyPIN(t *testing.T) {
tests := []struct {
name string
pin string
hashedPIN string
shouldPass bool
}{
{
name: "Valid PIN verification",
pin: "1234",
hashedPIN: hashPINHelper("1234"),
shouldPass: true,
},
{
name: "Invalid PIN verification with incorrect PIN",
pin: "5678",
hashedPIN: hashPINHelper("1234"),
shouldPass: false,
},
{
name: "Invalid PIN verification with empty PIN",
pin: "",
hashedPIN: hashPINHelper("1234"),
shouldPass: false,
},
{
name: "Invalid PIN verification with invalid hash",
pin: "1234",
hashedPIN: "invalidhash",
shouldPass: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := VerifyPIN(tt.hashedPIN, tt.pin)
if result != tt.shouldPass {
t.Errorf("VerifyPIN(%q, %q) = %v; expected %v", tt.hashedPIN, tt.pin, result, tt.shouldPass)
}
})
}
}
// Helper function to hash a PIN for testing purposes
func hashPINHelper(pin string) string {
hashedPIN, err := HashPIN(pin)
if err != nil {
panic("Failed to hash PIN for test setup: " + err.Error())
}
return hashedPIN
}

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,69 +0,0 @@
package common
import (
"context"
"errors"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/lang"
"git.grassecon.net/grassrootseconomics/visedriver/storage"
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
)
var (
ToConnData = storage.ToConnData
)
func StoreToDb(store *UserDataStore) db.Db {
return store.Db
}
func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb {
return dbstorage.NewSubPrefixDb(store.Db, pfx)
}
type StorageServices interface {
GetPersister(ctx context.Context) (*persist.Persister, error)
GetUserdataDb(ctx context.Context) (db.Db, error)
GetResource(ctx context.Context) (resource.Resource, error)
}
type StorageService struct {
svc *storage.MenuStorageService
}
func NewStorageService(conn storage.ConnData) (*StorageService, error) {
svc := &StorageService{
svc: storage.NewMenuStorageService(conn, ""),
}
return svc, nil
}
func (ss *StorageService) WithGettext(path string, lns []lang.Language) *StorageService {
ss.svc = ss.svc.WithGettext(path, lns)
return ss
}
// TODO: simplify enable poresource, conndata instead
func(ss *StorageService) SetResourceDir(resourceDir string) error {
ss.svc = ss.svc.WithResourceDir(resourceDir)
return nil
}
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
return ss.svc.GetPersister(ctx)
}
func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
return ss.svc.GetUserdataDb(ctx)
}
func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) {
return nil, errors.New("not implemented")
}
func(ss *StorageService) GetStateStore(ctx context.Context) (db.Db, error) {
return ss.svc.GetStateStore(ctx)
}

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"
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
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 dbstorage.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

@ -1,34 +0,0 @@
package common
import (
"context"
"git.defalsify.org/vise.git/db"
)
type DataStore interface {
db.Db
ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error)
WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error
}
type UserDataStore struct {
db.Db
}
// ReadEntry retrieves an entry to the userdata store.
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId)
k := ToBytes(typ)
return store.Get(ctx, k)
}
// WriteEntry adds an entry to the userdata store.
// BUG: this uses sessionId twice
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId)
k := ToBytes(typ)
return store.Put(ctx, k, value)
}

View File

@ -1,174 +0,0 @@
package common
import (
"context"
"fmt"
"math/big"
"strings"
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
// VoucherMetadata helps organize data fields
type VoucherMetadata struct {
Symbols string
Balances string
Decimals string
Addresses string
}
// ProcessVouchers converts holdings into formatted strings
func ProcessVouchers(holdings []dataserviceapi.TokenHoldings) VoucherMetadata {
var data VoucherMetadata
var symbols, balances, decimals, addresses []string
for i, h := range holdings {
symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, h.TokenSymbol))
// Scale down the balance
scaledBalance := ScaleDownBalance(h.Balance, h.TokenDecimals)
balances = append(balances, fmt.Sprintf("%d:%s", i+1, scaledBalance))
decimals = append(decimals, fmt.Sprintf("%d:%s", i+1, h.TokenDecimals))
addresses = append(addresses, fmt.Sprintf("%d:%s", i+1, h.ContractAddress))
}
data.Symbols = strings.Join(symbols, "\n")
data.Balances = strings.Join(balances, "\n")
data.Decimals = strings.Join(decimals, "\n")
data.Addresses = strings.Join(addresses, "\n")
return data
}
func ScaleDownBalance(balance, decimals string) string {
// Convert balance and decimals to big.Float
bal := new(big.Float)
bal.SetString(balance)
dec, ok := new(big.Int).SetString(decimals, 10)
if !ok {
dec = big.NewInt(0) // Default to 0 decimals in case of conversion failure
}
divisor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), dec, nil))
scaledBalance := new(big.Float).Quo(bal, divisor)
// Return the scaled balance without trailing decimals if it's an integer
if scaledBalance.IsInt() {
return scaledBalance.Text('f', 0)
}
return scaledBalance.Text('f', -1)
}
// GetVoucherData retrieves and matches voucher data
func GetVoucherData(ctx context.Context, db dbstorage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
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, ToBytes(key))
if err != nil {
return nil, fmt.Errorf("failed to get %s: %v", ToBytes(key), err)
}
data[key] = string(value)
}
symbol, balance, decimal, address := MatchVoucher(input,
data[DATA_VOUCHER_SYMBOLS],
data[DATA_VOUCHER_BALANCES],
data[DATA_VOUCHER_DECIMALS],
data[DATA_VOUCHER_ADDRESSES],
)
if symbol == "" {
return nil, nil
}
return &dataserviceapi.TokenHoldings{
TokenSymbol: string(symbol),
Balance: string(balance),
TokenDecimals: string(decimal),
ContractAddress: string(address),
}, nil
}
// MatchVoucher finds the matching voucher symbol, balance, decimals and contract address based on the input.
func MatchVoucher(input, symbols, balances, decimals, addresses string) (symbol, balance, decimal, address string) {
symList := strings.Split(symbols, "\n")
balList := strings.Split(balances, "\n")
decList := strings.Split(decimals, "\n")
addrList := strings.Split(addresses, "\n")
logg.Tracef("found", "symlist", symList, "syms", symbols, "input", input)
for i, sym := range symList {
parts := strings.SplitN(sym, ":", 2)
if input == parts[0] || strings.EqualFold(input, parts[1]) {
symbol = parts[1]
if i < len(balList) {
balance = strings.SplitN(balList[i], ":", 2)[1]
}
if i < len(decList) {
decimal = strings.SplitN(decList[i], ":", 2)[1]
}
if i < len(addrList) {
address = strings.SplitN(addrList[i], ":", 2)[1]
}
break
}
}
return
}
// StoreTemporaryVoucher saves voucher metadata as temporary entries in the DataStore.
func StoreTemporaryVoucher(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error {
tempData := fmt.Sprintf("%s,%s,%s,%s", data.TokenSymbol, data.Balance, data.TokenDecimals, data.ContractAddress)
if err := store.WriteEntry(ctx, sessionId, DATA_TEMPORARY_VALUE, []byte(tempData)); err != nil {
return err
}
return nil
}
// GetTemporaryVoucherData retrieves temporary voucher metadata from the DataStore.
func GetTemporaryVoucherData(ctx context.Context, store DataStore, sessionId string) (*dataserviceapi.TokenHoldings, error) {
temp_data, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE)
if err != nil {
return nil, err
}
values := strings.SplitN(string(temp_data), ",", 4)
data := &dataserviceapi.TokenHoldings{}
data.TokenSymbol = values[0]
data.Balance = values[1]
data.TokenDecimals = values[2]
data.ContractAddress = values[3]
return data, nil
}
// 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
activeEntries := map[DataTyp][]byte{
DATA_ACTIVE_SYM: []byte(data.TokenSymbol),
DATA_ACTIVE_BAL: []byte(data.Balance),
DATA_ACTIVE_DECIMAL: []byte(data.TokenDecimals),
DATA_ACTIVE_ADDRESS: []byte(data.ContractAddress),
}
// Write active data
for key, value := range activeEntries {
if err := store.WriteEntry(ctx, sessionId, key, value); err != nil {
return err
}
}
return nil
}

View File

@ -1,200 +0,0 @@
package common
import (
"context"
"fmt"
"testing"
"github.com/alecthomas/assert/v2"
"github.com/stretchr/testify/require"
visedb "git.defalsify.org/vise.git/db"
memdb "git.defalsify.org/vise.git/db/mem"
dbstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
)
// InitializeTestDb sets up and returns an in-memory database and store.
func InitializeTestDb(t *testing.T) (context.Context, *UserDataStore) {
ctx := context.Background()
// Initialize memDb
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
require.NoError(t, err, "Failed to connect to memDb")
// Create UserDataStore with memDb
store := &UserDataStore{Db: db}
t.Cleanup(func() {
db.Close() // Ensure the DB is closed after each test
})
return ctx, store
}
func TestMatchVoucher(t *testing.T) {
symbols := "1:SRF\n2:MILO"
balances := "1:100\n2:200"
decimals := "1:6\n2:4"
addresses := "1:0xd4c288865Ce\n2:0x41c188d63Qa"
// Test for valid voucher
symbol, balance, decimal, address := MatchVoucher("2", symbols, balances, decimals, addresses)
// Assertions for valid voucher
assert.Equal(t, "MILO", symbol)
assert.Equal(t, "200", balance)
assert.Equal(t, "4", decimal)
assert.Equal(t, "0x41c188d63Qa", address)
// Test for non-existent voucher
symbol, balance, decimal, address = MatchVoucher("3", symbols, balances, decimals, addresses)
// Assertions for non-match
assert.Equal(t, "", symbol)
assert.Equal(t, "", balance)
assert.Equal(t, "", decimal)
assert.Equal(t, "", address)
}
func TestProcessVouchers(t *testing.T) {
holdings := []dataserviceapi.TokenHoldings{
{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:20000",
Decimals: "1:6\n2:4",
Addresses: "1:0xd4c288865Ce\n2:0x41c188d63Qa",
}
result := ProcessVouchers(holdings)
assert.Equal(t, expectedResult, result)
}
func TestGetVoucherData(t *testing.T) {
ctx := context.Background()
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
if err != nil {
t.Fatal(err)
}
prefix := ToBytes(visedb.DATATYPE_USERDATA)
spdb := dbstorage.NewSubPrefixDb(db, prefix)
// Test voucher data
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(ToBytes(key)), []byte(value))
if err != nil {
t.Fatal(err)
}
}
result, err := GetVoucherData(ctx, spdb, "1")
assert.NoError(t, err)
assert.Equal(t, "SRF", result.TokenSymbol)
assert.Equal(t, "100", result.Balance)
assert.Equal(t, "6", result.TokenDecimals)
assert.Equal(t, "0xd4c288865Ce", result.ContractAddress)
}
func TestStoreTemporaryVoucher(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// Test data
voucherData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Execute the function being tested
err := StoreTemporaryVoucher(ctx, store, sessionId, voucherData)
require.NoError(t, err)
// Verify stored data
expectedData := fmt.Sprintf("%s,%s,%s,%s", "SRF", "200", "6", "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9")
storedValue, err := store.ReadEntry(ctx, sessionId, DATA_TEMPORARY_VALUE)
require.NoError(t, err)
require.Equal(t, expectedData, string(storedValue), "Mismatch for key %v", DATA_TEMPORARY_VALUE)
}
func TestGetTemporaryVoucherData(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// Test voucher data
tempData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Store the data
err := StoreTemporaryVoucher(ctx, store, sessionId, tempData)
require.NoError(t, err)
// Execute the function being tested
data, err := GetTemporaryVoucherData(ctx, store, sessionId)
require.NoError(t, err)
require.Equal(t, tempData, data)
}
func TestUpdateVoucherData(t *testing.T) {
ctx, store := InitializeTestDb(t)
sessionId := "session123"
// New voucher data
newData := &dataserviceapi.TokenHoldings{
TokenSymbol: "SRF",
Balance: "200",
TokenDecimals: "6",
ContractAddress: "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9",
}
// Old temporary data
tempData := &dataserviceapi.TokenHoldings{
TokenSymbol: "OLD",
Balance: "100",
TokenDecimals: "8",
ContractAddress: "0xold",
}
require.NoError(t, StoreTemporaryVoucher(ctx, store, sessionId, tempData))
// Execute update
err := UpdateVoucherData(ctx, store, sessionId, newData)
require.NoError(t, err)
// Verify active data was stored correctly
activeEntries := map[DataTyp][]byte{
DATA_ACTIVE_SYM: []byte(newData.TokenSymbol),
DATA_ACTIVE_BAL: []byte(newData.Balance),
DATA_ACTIVE_DECIMAL: []byte(newData.TokenDecimals),
DATA_ACTIVE_ADDRESS: []byte(newData.ContractAddress),
}
for key, expectedValue := range activeEntries {
storedValue, err := store.ReadEntry(ctx, sessionId, key)
require.NoError(t, err)
require.Equal(t, expectedValue, storedValue, "Active data mismatch for key %v", key)
}
}

View File

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

View File

@ -1,84 +0,0 @@
package debug
import (
"fmt"
"encoding/binary"
"git.grassecon.net/grassrootseconomics/visedriver/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))]
}

View File

@ -1,44 +0,0 @@
// +build debugdb
package debug
import (
"git.defalsify.org/vise.git/db"
"git.grassecon.net/grassrootseconomics/visedriver/common"
)
func init() {
DebugCap |= 1
dbTypStr[db.DATATYPE_STATE] = "internal state"
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_ACCOUNT_PIN] = "account pin"
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"
}

View File

@ -1,78 +0,0 @@
package debug
import (
"testing"
"git.grassecon.net/grassrootseconomics/visedriver/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

@ -1,43 +0,0 @@
package storage
import (
"context"
"git.defalsify.org/vise.git/db"
)
// PrefixDb interface abstracts the database operations.
type PrefixDb interface {
Get(ctx context.Context, key []byte) ([]byte, error)
Put(ctx context.Context, key []byte, val []byte) error
}
var _ PrefixDb = (*SubPrefixDb)(nil)
type SubPrefixDb struct {
store db.Db
pfx []byte
}
func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb {
return &SubPrefixDb{
store: store,
pfx: pfx,
}
}
func (s *SubPrefixDb) toKey(k []byte) []byte {
return append(s.pfx, k...)
}
func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) {
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(db.DATATYPE_USERDATA)
key = s.toKey(key)
return s.store.Put(ctx, key, val)
}

View File

@ -1,54 +0,0 @@
package storage
import (
"bytes"
"context"
"testing"
memdb "git.defalsify.org/vise.git/db/mem"
)
func TestSubPrefix(t *testing.T) {
ctx := context.Background()
db := memdb.NewMemDb()
err := db.Connect(ctx, "")
if err != nil {
t.Fatal(err)
}
sdba := NewSubPrefixDb(db, []byte("tinkywinky"))
err = sdba.Put(ctx, []byte("foo"), []byte("dipsy"))
if err != nil {
t.Fatal(err)
}
r, err := sdba.Get(ctx, []byte("foo"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(r, []byte("dipsy")) {
t.Fatalf("expected 'dipsy', got %s", r)
}
sdbb := NewSubPrefixDb(db, []byte("lala"))
r, err = sdbb.Get(ctx, []byte("foo"))
if err == nil {
t.Fatal("expected not found")
}
err = sdbb.Put(ctx, []byte("foo"), []byte("pu"))
if err != nil {
t.Fatal(err)
}
r, err = sdbb.Get(ctx, []byte("foo"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(r, []byte("pu")) {
t.Fatalf("expected 'pu', got %s", r)
}
r, err = sdba.Get(ctx, []byte("foo"))
if !bytes.Equal(r, []byte("dipsy")) {
t.Fatalf("expected 'dipsy', got %s", r)
}
}

View File

@ -1,56 +0,0 @@
package utils
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.
func CalculateAge(birthdate, today time.Time) int {
today = today.In(birthdate.Location())
ty, tm, td := today.Date()
today = time.Date(ty, tm, td, 0, 0, 0, 0, time.UTC)
by, bm, bd := birthdate.Date()
birthdate = time.Date(by, bm, bd, 0, 0, 0, 0, time.UTC)
if today.Before(birthdate) {
return 0
}
age := ty - by
anniversary := birthdate.AddDate(age, 0, 0)
if anniversary.After(today) {
age--
}
return age
}
// CalculateAgeWithYOB calculates the age based on the given year of birth (YOB).
// It subtracts the YOB from the current year to determine the age.
//
// Parameters:
//
// yob: The year of birth as an integer.
//
// Returns:
//
// The calculated age as an integer.
func CalculateAgeWithYOB(yob int) int {
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
}
}

View File

@ -1,11 +0,0 @@
package utils
var isoCodes = map[string]bool{
"eng": true, // English
"swa": true, // Swahili
"default": true, // Default language: English
}
func IsValidISO639(code string) bool {
return isoCodes[code]
}

View File

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