228 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			228 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package store
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"math/big"
 | |
| 	"strings"
 | |
| 
 | |
| 	"git.defalsify.org/vise.git/logging"
 | |
| 	storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
 | |
| 	dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	logg = logging.NewVanilla().WithDomain("vouchers").WithContextKey("SessionId")
 | |
| )
 | |
| 
 | |
| // symbolReplacements holds mappings of invalid symbols → valid ones
 | |
| var symbolReplacements = map[string]string{
 | |
| 	"USD₮": "USDT",
 | |
| }
 | |
| 
 | |
| // VoucherMetadata helps organize data fields
 | |
| type VoucherMetadata struct {
 | |
| 	Symbols   string
 | |
| 	Balances  string
 | |
| 	Decimals  string
 | |
| 	Addresses string
 | |
| }
 | |
| 
 | |
| // sanitizeSymbol replaces known invalid token symbols with normalized ones
 | |
| func sanitizeSymbol(symbol string) string {
 | |
| 	if replacement, ok := symbolReplacements[symbol]; ok {
 | |
| 		return replacement
 | |
| 	}
 | |
| 	return symbol
 | |
| }
 | |
| 
 | |
| // 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 {
 | |
| 		// normalize token symbol before use
 | |
| 		cleanSymbol := sanitizeSymbol(h.TokenSymbol)
 | |
| 
 | |
| 		symbols = append(symbols, fmt.Sprintf("%d:%s", i+1, cleanSymbol))
 | |
| 
 | |
| 		// 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.TokenAddress))
 | |
| 	}
 | |
| 
 | |
| 	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, store DataStore, sessionId string, input string) (*dataserviceapi.TokenHoldings, error) {
 | |
| 	keys := []storedb.DataTyp{
 | |
| 		storedb.DATA_VOUCHER_SYMBOLS,
 | |
| 		storedb.DATA_VOUCHER_BALANCES,
 | |
| 		storedb.DATA_VOUCHER_DECIMALS,
 | |
| 		storedb.DATA_VOUCHER_ADDRESSES,
 | |
| 	}
 | |
| 	data := make(map[storedb.DataTyp]string)
 | |
| 
 | |
| 	for _, key := range keys {
 | |
| 		value, err := store.ReadEntry(ctx, sessionId, key)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to get data key %x: %v", key, err)
 | |
| 		}
 | |
| 		data[key] = string(value)
 | |
| 	}
 | |
| 
 | |
| 	symbol, balance, decimal, address := MatchVoucher(input,
 | |
| 		data[storedb.DATA_VOUCHER_SYMBOLS],
 | |
| 		data[storedb.DATA_VOUCHER_BALANCES],
 | |
| 		data[storedb.DATA_VOUCHER_DECIMALS],
 | |
| 		data[storedb.DATA_VOUCHER_ADDRESSES],
 | |
| 	)
 | |
| 
 | |
| 	if symbol == "" {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	return &dataserviceapi.TokenHoldings{
 | |
| 		TokenSymbol:   string(symbol),
 | |
| 		Balance:       string(balance),
 | |
| 		TokenDecimals: string(decimal),
 | |
| 		TokenAddress:  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.TokenAddress)
 | |
| 
 | |
| 	if err := store.WriteEntry(ctx, sessionId, storedb.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, storedb.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.TokenAddress = 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[storedb.DataTyp][]byte{
 | |
| 		storedb.DATA_ACTIVE_SYM:     []byte(data.TokenSymbol),
 | |
| 		storedb.DATA_ACTIVE_BAL:     []byte(data.Balance),
 | |
| 		storedb.DATA_ACTIVE_DECIMAL: []byte(data.TokenDecimals),
 | |
| 		storedb.DATA_ACTIVE_ADDRESS: []byte(data.TokenAddress),
 | |
| 	}
 | |
| 
 | |
| 	// Write active data
 | |
| 	for key, value := range activeEntries {
 | |
| 		if err := store.WriteEntry(ctx, sessionId, key, value); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // FormatVoucherList combines the voucher symbols with their balances (SRF 0.11)
 | |
| func FormatVoucherList(ctx context.Context, symbolsData, balancesData string) []string {
 | |
| 	symbols := strings.Split(symbolsData, "\n")
 | |
| 	balances := strings.Split(balancesData, "\n")
 | |
| 
 | |
| 	var combined []string
 | |
| 	for i := 0; i < len(symbols) && i < len(balances); i++ {
 | |
| 		symbolParts := strings.SplitN(symbols[i], ":", 2)
 | |
| 		balanceParts := strings.SplitN(balances[i], ":", 2)
 | |
| 
 | |
| 		if len(symbolParts) == 2 && len(balanceParts) == 2 {
 | |
| 			index := strings.TrimSpace(symbolParts[0])
 | |
| 			symbol := strings.TrimSpace(symbolParts[1])
 | |
| 			rawBalance := strings.TrimSpace(balanceParts[1])
 | |
| 
 | |
| 			formattedBalance, err := TruncateDecimalString(rawBalance, 2)
 | |
| 			if err != nil {
 | |
| 				logg.ErrorCtxf(ctx, "failed to format balance", "balance", rawBalance, "error", err)
 | |
| 				formattedBalance = rawBalance
 | |
| 			}
 | |
| 
 | |
| 			combined = append(combined, fmt.Sprintf("%s: %s %s", index, symbol, formattedBalance))
 | |
| 		}
 | |
| 	}
 | |
| 	return combined
 | |
| }
 |