diff --git a/go.mod b/go.mod index a75e399..531e4b0 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,12 @@ go 1.23.4 require ( git.defalsify.org/vise.git v0.2.3-0.20250205173834-d1f6647211ac git.grassecon.net/grassrootseconomics/common v0.0.0-20250121134736-ba8cbbccea7d - git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250206112944-31eb30de0f69 + git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250313071411-ba43610ff00b git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.1.0.20250204132347-1eb0b1555244 git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694 github.com/alecthomas/assert/v2 v2.2.2 github.com/gofrs/uuid v4.4.0+incompatible - github.com/grassrootseconomics/ussd-data-service v1.2.0-beta + github.com/grassrootseconomics/ussd-data-service v1.4.0-beta github.com/jackc/pgx/v5 v5.7.1 github.com/peteole/testdata-loader v0.3.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 1cd58c7..b29d7a1 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,26 @@ git.grassecon.net/grassrootseconomics/common v0.0.0-20250121134736-ba8cbbccea7d git.grassecon.net/grassrootseconomics/common v0.0.0-20250121134736-ba8cbbccea7d/go.mod h1:wgQJZGIS6QuNLHqDhcsvehsbn5PvgV7aziRebMnJi60= git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250206112944-31eb30de0f69 h1:cbBpm9uNJak58MpFpNXJuvgCmz+A8kquXr9har4expg= git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250206112944-31eb30de0f69/go.mod h1:gOn89ipaDcDvmQXRMQYKUqcw/sJcwVOPVt2eC6Geip8= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250224114958-1f3ac220d126 h1:fXL9uXt8yiu5ImJnwiHoLsq8T2TJ6+8qa97pCt+gHxA= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250224114958-1f3ac220d126/go.mod h1:gOn89ipaDcDvmQXRMQYKUqcw/sJcwVOPVt2eC6Geip8= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250306151434-35f24b518375 h1:VilYLeBFXxOSgs+AD9B1+Mu+VJZHiSWACY5xF1x3xBE= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250306151434-35f24b518375/go.mod h1:K/TPgZ4OhPHBQq2X0ab3JZs4YjiexzSURZcfHLs9Pf4= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250307051649-4783e2dcb319 h1:Qg6ttFJ4zjF67PERvLcXJ4/bXUTdCaRJ96RFRmQZ4xc= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250307051649-4783e2dcb319/go.mod h1:K/TPgZ4OhPHBQq2X0ab3JZs4YjiexzSURZcfHLs9Pf4= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250310110330-a04f7ee66c35 h1:JeRpSf+/NTFTl3lhiSkHO54NIoK6KrXmyqawfiuKc0Y= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250310110330-a04f7ee66c35/go.mod h1:K/TPgZ4OhPHBQq2X0ab3JZs4YjiexzSURZcfHLs9Pf4= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250310111703-e3b6c25792c2 h1:QhiSpx3ndgQeR/CAynLdiOR+wNEJJDFWMKE2CHmp8n8= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250310111703-e3b6c25792c2/go.mod h1:K/TPgZ4OhPHBQq2X0ab3JZs4YjiexzSURZcfHLs9Pf4= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250310133937-ba8d2a19c2ed h1:r5w89jInk9wwJZ4kMmE8ENPV7chxORJWIfxsPmnAQwY= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250310133937-ba8d2a19c2ed/go.mod h1:K/TPgZ4OhPHBQq2X0ab3JZs4YjiexzSURZcfHLs9Pf4= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250310141205-3b39b86d0987 h1:HXFD3Pabi1H4tuKVZUjyqsYoI8JrhJrH0NXeVuB2buo= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250310141205-3b39b86d0987/go.mod h1:K/TPgZ4OhPHBQq2X0ab3JZs4YjiexzSURZcfHLs9Pf4= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250311124651-3244c717cbee h1:YONuCBfzDlpeXsQMWRiBrRlSC0YHWUNuC5Sit43Ncww= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250311124651-3244c717cbee/go.mod h1:K/TPgZ4OhPHBQq2X0ab3JZs4YjiexzSURZcfHLs9Pf4= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250313070549-3c2889ac72f0 h1:AMa2VmeRrmHz87+RtH9sJGTGnlYXqkhJG1tX/vgQAx0= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250313070549-3c2889ac72f0/go.mod h1:K/TPgZ4OhPHBQq2X0ab3JZs4YjiexzSURZcfHLs9Pf4= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250313071411-ba43610ff00b h1:+j02QfOVYvPteaIpL/3KQ6taQnoI7TFkcH23iBUhoZg= +git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20250313071411-ba43610ff00b/go.mod h1:K/TPgZ4OhPHBQq2X0ab3JZs4YjiexzSURZcfHLs9Pf4= git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.1.0.20250204132347-1eb0b1555244 h1:BXotWSKg04U97sf/xeWJuUTSVgKk2aEK+5BtBrnafXQ= git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.1.0.20250204132347-1eb0b1555244/go.mod h1:6B6ByxXOiRY0NR7K02Bf3fEu7z+2c/6q8PFVNjC5G8w= git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694 h1:DjJlBSz0S13acft5XZDWk7ZYnzElym0xLMYEVgyNJ+E= @@ -27,6 +47,8 @@ github.com/grassrootseconomics/eth-custodial v1.3.0-beta h1:twrMBhl89GqDUL9PlkzQ github.com/grassrootseconomics/eth-custodial v1.3.0-beta/go.mod h1:7uhRcdnJplX4t6GKCEFkbeDhhjlcaGJeJqevbcvGLZo= github.com/grassrootseconomics/ussd-data-service v1.2.0-beta h1:fn1gwbWIwHVEBtUC2zi5OqTlfI/5gU1SMk0fgGixIXk= github.com/grassrootseconomics/ussd-data-service v1.2.0-beta/go.mod h1:omfI0QtUwIdpu9gMcUqLMCG8O1XWjqJGBx1qUMiGWC0= +github.com/grassrootseconomics/ussd-data-service v1.4.0-beta h1:4fMd/3h2ZIhRg4GdHQmRw5FfD3MpJvFNNJQo+Q27f5M= +github.com/grassrootseconomics/ussd-data-service v1.4.0-beta/go.mod h1:9sGnorpKaK76FmOGXoh/xv7x5siSFNYdXxQo9BKW4DI= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4 h1:U4kkNYryi/qfbBF8gh7Vsbuz+cVmhf5kt6pE9bYYyLo= github.com/graygnuorg/go-gdbm v0.0.0-20220711140707-71387d66dce4/go.mod h1:zpZDgZFzeq9s0MIeB1P50NIEWDFFHSFBohI/NbaTD/Y= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= diff --git a/handlers/application/menuhandler.go b/handlers/application/menuhandler.go index b4cbd7b..8adeee7 100644 --- a/handlers/application/menuhandler.go +++ b/handlers/application/menuhandler.go @@ -2412,3 +2412,436 @@ func (h *MenuHandlers) ClearTemporaryValue(ctx context.Context, sym string, inpu } return res, nil } + +// GetPools fetches a list of 5 top pools +func (h *MenuHandlers) GetPools(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") + + // call the api to get a list of top 5 pools sorted by swaps + topPools, err := h.accountService.FetchTopPools(ctx) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) + return res, err + } + + // Return if there are no pools + if len(topPools) == 0 { + return res, nil + } + + data := store.ProcessPools(topPools) + + // Store all Pool data + dataMap := map[storedb.DataTyp]string{ + storedb.DATA_POOL_NAMES: data.PoolNames, + storedb.DATA_POOL_SYMBOLS: data.PoolSymbols, + storedb.DATA_POOL_ADDRESSES: data.PoolContractAdrresses, + } + + for key, value := range dataMap { + if err := h.prefixDb.Put(ctx, []byte(storedb.ToBytes(key)), []byte(value)); err != nil { + return res, nil + } + } + + res.Content = h.ReplaceSeparatorFunc(data.PoolSymbols) + + return res, nil +} + +// LoadSwapFromList gets all the possible tokens a user can select to swap from +func (h *MenuHandlers) LoadSwapFromList(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + + userStore := h.userdataStore + publicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", storedb.DATA_PUBLIC_KEY, "error", err) + return res, err + } + + flag_incorrect_pool, _ := h.flagManager.GetFlag("flag_incorrect_pool") + flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") + + inputStr := string(input) + if inputStr == "0" { + res.FlagReset = append(res.FlagReset, flag_incorrect_pool) + return res, nil + } + + poolData, err := store.GetPoolData(ctx, h.prefixDb, inputStr) + if err != nil { + return res, fmt.Errorf("failed to retrieve pool data: %v", err) + } + + if poolData == nil { + // no match found. Call the API + poolResp := dataserviceapi.PoolDetails{ + PoolName: "DevTest", + PoolSymbol: "DEVT", + PoolContractAdrress: "0x145F87d6198dEDD45C614FFD8b70E9a2fCCc5cc9", + LimiterAddress: "", + VoucherRegistry: "", + } + + if (poolResp == dataserviceapi.PoolDetails{}) { + // If the API does not return the data, set the flag + res.FlagSet = append(res.FlagSet, flag_incorrect_pool) + return res, nil + } + + poolData = &poolResp + } + + activePoolAddress := poolData.PoolContractAdrress + + // set the active pool contract address + err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_ADDRESS, []byte(activePoolAddress)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write active PoolContractAdrress entry with", "key", storedb.DATA_ACTIVE_POOL_ADDRESS, "value", activePoolAddress, "error", err) + return res, err + } + + res.FlagReset = append(res.FlagReset, flag_incorrect_pool) + + // call the api using the activePoolAddress and publicKey to get a list of SwapfromSymbolsData + swapFromList, err := h.accountService.GetPoolSwappableFromVouchers(ctx, activePoolAddress, string(publicKey)) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) + return res, err + } + + // Return if there are no vouchers + if len(swapFromList) == 0 { + res.FlagSet = append(res.FlagSet, flag_incorrect_pool) + return res, nil + } + + data := store.ProcessVouchers(swapFromList) + + // Store all from list voucher data + dataMap := map[storedb.DataTyp]string{ + storedb.DATA_POOL_FROM_SYMBOLS: data.Symbols, + storedb.DATA_POOL_FROM_BALANCES: data.Balances, + storedb.DATA_POOL_FROM_DECIMALS: data.Decimals, + storedb.DATA_POOL_FROM_ADDRESSES: data.Addresses, + } + + for key, value := range dataMap { + if err := h.prefixDb.Put(ctx, []byte(storedb.ToBytes(key)), []byte(value)); err != nil { + return res, nil + } + } + + res.Content = h.ReplaceSeparatorFunc(data.Symbols) + + return res, nil +} + +// LoadSwapFromList returns a list of possible vouchers to swap to +func (h *MenuHandlers) LoadSwapToList(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + + userStore := h.userdataStore + publicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", storedb.DATA_PUBLIC_KEY, "error", err) + return res, err + } + + flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher") + flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") + + inputStr := string(input) + if inputStr == "0" { + return res, nil + } + + metadata, err := store.GetSwapFromVoucherData(ctx, h.prefixDb, inputStr) + if err != nil { + return res, fmt.Errorf("failed to retrieve swap from voucher data: %v", err) + } + + if metadata == nil { + res.FlagSet = append(res.FlagSet, flag_incorrect_voucher) + return res, nil + } + + // Store the active swap from data + if err := store.UpdateSwapFromVoucherData(ctx, userStore, sessionId, metadata); err != nil { + logg.ErrorCtxf(ctx, "failed on UpdateSwapFromVoucherData", "error", err) + return res, err + } + + res.FlagReset = append(res.FlagReset, flag_incorrect_voucher) + + activePoolAddress, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_ADDRESS) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read activePoolAddress entry with", "key", storedb.DATA_ACTIVE_POOL_ADDRESS, "error", err) + return res, err + } + + // call the api using the activePoolAddress and publicKey to get a list of SwapToSymbolsData + swapToList, err := h.accountService.GetPoolSwappableVouchers(ctx, string(activePoolAddress), string(publicKey)) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) + return res, err + } + + // Return if there are no vouchers + if len(swapToList) == 0 { + return res, nil + } + + data := store.ProcessVouchers(swapToList) + + // Store all to list voucher data + dataMap := map[storedb.DataTyp]string{ + storedb.DATA_POOL_TO_SYMBOLS: data.Symbols, + storedb.DATA_POOL_TO_BALANCES: data.Balances, + storedb.DATA_POOL_TO_DECIMALS: data.Decimals, + storedb.DATA_POOL_TO_ADDRESSES: data.Addresses, + } + + for key, value := range dataMap { + if err := h.prefixDb.Put(ctx, []byte(storedb.ToBytes(key)), []byte(value)); err != nil { + return res, nil + } + } + + res.Content = h.ReplaceSeparatorFunc(data.Symbols) + + return res, nil +} + +// SwapMaxLimit returns the max FROM token +// check if max/tokenDecimals > 0.1 for UX purposes and to prevent swapping of dust values +func (h *MenuHandlers) SwapMaxLimit(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + + flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher") + flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") + flag_low_swap_amount, _ := h.flagManager.GetFlag("flag_low_swap_amount") + + res.FlagReset = append(res.FlagReset, flag_incorrect_voucher) + res.FlagReset = append(res.FlagSet, flag_low_swap_amount) + + inputStr := string(input) + if inputStr == "0" { + return res, nil + } + + metadata, err := store.GetSwapToVoucherData(ctx, h.prefixDb, inputStr) + if err != nil { + return res, fmt.Errorf("failed to retrieve swap to voucher data: %v", err) + } + if metadata == nil { + res.FlagSet = append(res.FlagSet, flag_incorrect_voucher) + return res, nil + } + + userStore := h.userdataStore + + // Store the active swap_to data + if err := store.UpdateSwapToVoucherData(ctx, userStore, sessionId, metadata); err != nil { + logg.ErrorCtxf(ctx, "failed on UpdateSwapToVoucherData", "error", err) + return res, err + } + + swapData, err := store.ReadSwapData(ctx, userStore, sessionId) + if err != nil { + return res, err + } + + // call the api using the ActivePoolAddress, ActiveSwapFromAddress, ActiveSwapToAddress and PublicKey to get the swap max limit + r, err := h.accountService.GetSwapFromTokenMaxLimit(ctx, swapData.ActivePoolAddress, swapData.ActiveSwapFromAddress, swapData.ActiveSwapToAddress, swapData.PublicKey) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) + return res, err + } + + // Scale down the amount + maxAmountStr := store.ScaleDownBalance(r.Max, swapData.ActiveSwapFromDecimal) + if err != nil { + return res, err + } + + maxAmountFloat, err := strconv.ParseFloat(maxAmountStr, 64) + if err != nil { + logg.ErrorCtxf(ctx, "failed to parse maxAmountStr as float", "value", maxAmountStr, "error", err) + return res, err + } + + // Format to 2 decimal places + maxStr := fmt.Sprintf("%.2f", maxAmountFloat) + + if maxAmountFloat < 0.1 { + // return with low amount flag + res.Content = maxStr + res.FlagSet = append(res.FlagSet, flag_low_swap_amount) + return res, nil + } + + err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, []byte(maxStr)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write swap max amount entry with", "key", storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, "value", maxStr, "error", err) + return res, err + } + + res.Content = fmt.Sprintf( + "Maximum: %s\n\nEnter amount of %s to swap for %s:", + maxStr, swapData.ActiveSwapFromSym, swapData.ActiveSwapToSym, + ) + + return res, nil +} + +// SwapPreview displays the swap preview and estimates +func (h *MenuHandlers) SwapPreview(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + + inputStr := string(input) + if inputStr == "0" { + return res, nil + } + + flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount") + + code := codeFromCtx(ctx) + l := gotext.NewLocale(translationDir, code) + l.AddDomain("default") + + userStore := h.userdataStore + + swapData, err := store.ReadSwapPreviewData(ctx, userStore, sessionId) + if err != nil { + return res, err + } + + maxValue, err := strconv.ParseFloat(swapData.ActiveSwapMaxAmount, 64) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to convert the swapMaxAmount to a float", "error", err) + return res, err + } + + inputAmount, err := strconv.ParseFloat(inputStr, 64) + if err != nil || inputAmount > maxValue { + res.FlagSet = append(res.FlagSet, flag_invalid_amount) + res.Content = inputStr + return res, nil + } + + finalAmountStr, err := store.ParseAndScaleAmount(inputStr, swapData.ActiveSwapFromDecimal) + if err != nil { + return res, err + } + + err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_AMOUNT, []byte(finalAmountStr)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write swap amount entry with", "key", storedb.DATA_ACTIVE_SWAP_AMOUNT, "value", finalAmountStr, "error", err) + return res, err + } + + // call the API to get the quote + r, err := h.accountService.GetPoolSwapQuote(ctx, finalAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress) + if err != nil { + flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") + res.FlagSet = append(res.FlagSet, flag_api_error) + res.Content = l.Get("Your request failed. Please try again later.") + logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err) + return res, nil + } + + // Scale down the quoted amount + quoteAmountStr := store.ScaleDownBalance(r.OutValue, swapData.ActiveSwapToDecimal) + qouteAmount, err := strconv.ParseFloat(quoteAmountStr, 64) + if err != nil { + logg.ErrorCtxf(ctx, "failed to parse quoteAmountStr as float", "value", quoteAmountStr, "error", err) + return res, err + } + + // Format to 2 decimal places + qouteStr := fmt.Sprintf("%.2f", qouteAmount) + + res.Content = fmt.Sprintf( + "You will swap:\n%s %s for %s %s:", + inputStr, swapData.ActiveSwapFromSym, qouteStr, swapData.ActiveSwapToSym, + ) + + return res, nil +} + +// InitiateSwap calls the poolSwap and returns a confirmation based on the result. +func (h *MenuHandlers) InitiateSwap(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + var err error + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + + flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") + + code := codeFromCtx(ctx) + l := gotext.NewLocale(translationDir, code) + l.AddDomain("default") + + userStore := h.userdataStore + + swapData, err := store.ReadSwapPreviewData(ctx, userStore, sessionId) + if err != nil { + return res, err + } + + swapAmount, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SWAP_AMOUNT) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read swapAmount entry with", "key", storedb.DATA_ACTIVE_SWAP_AMOUNT, "error", err) + return res, err + } + + swapAmountStr := string(swapAmount) + + // Call the poolSwap API + r, err := h.accountService.PoolSwap(ctx, swapAmountStr, swapData.PublicKey, swapData.ActiveSwapFromAddress, swapData.ActivePoolAddress, swapData.ActiveSwapToAddress) + if err != nil { + flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") + res.FlagSet = append(res.FlagSet, flag_api_error) + res.Content = l.Get("Your request failed. Please try again later.") + logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err) + return res, nil + } + + trackingId := r.TrackingId + logg.InfoCtxf(ctx, "poolSwap", "trackingId", trackingId) + + res.Content = l.Get( + "Your request has been sent. You will receive an SMS when your %s %s has been swapped for %s.", + swapAmountStr, + swapData.ActiveSwapFromSym, + swapData.ActiveSwapToSym, + ) + + res.FlagReset = append(res.FlagReset, flag_account_authorized) + return res, nil +} diff --git a/handlers/local.go b/handlers/local.go index 7d66c7d..50d6489 100644 --- a/handlers/local.go +++ b/handlers/local.go @@ -124,6 +124,12 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountService) ls.DbRs.AddLocalFunc("set_back", appHandlers.SetBack) ls.DbRs.AddLocalFunc("show_blocked_account", appHandlers.ShowBlockedAccount) ls.DbRs.AddLocalFunc("clear_temporary_value", appHandlers.ClearTemporaryValue) + ls.DbRs.AddLocalFunc("get_pools", appHandlers.GetPools) + ls.DbRs.AddLocalFunc("swap_from_list", appHandlers.LoadSwapFromList) + ls.DbRs.AddLocalFunc("swap_to_list", appHandlers.LoadSwapToList) + ls.DbRs.AddLocalFunc("swap_max_limit", appHandlers.SwapMaxLimit) + ls.DbRs.AddLocalFunc("swap_preview", appHandlers.SwapPreview) + ls.DbRs.AddLocalFunc("initiate_swap", appHandlers.InitiateSwap) ls.first = appHandlers.Init return appHandlers, nil diff --git a/services/registration/locale/swa/default.po b/services/registration/locale/swa/default.po index 6155063..8f10e7d 100644 --- a/services/registration/locale/swa/default.po +++ b/services/registration/locale/swa/default.po @@ -30,3 +30,6 @@ msgstr "Salio la Kikundi: 0.00" msgid "Symbol: %s\nBalance: %s" msgstr "Sarafu: %s\nSalio: %s" + +msgid "Your request has been sent. You will receive an SMS when your %s %s has been swapped for %s." +msgstr "Ombi lako limetumwa. Utapokea SMS wakati %s %s yako itakapobadilishwa kuwa %s." diff --git a/services/registration/low_swap_amount b/services/registration/low_swap_amount new file mode 100644 index 0000000..d3e50f7 --- /dev/null +++ b/services/registration/low_swap_amount @@ -0,0 +1 @@ +Available amount {{.swap_max_limit}} is too low, please try again: \ No newline at end of file diff --git a/services/registration/low_swap_amount.vis b/services/registration/low_swap_amount.vis new file mode 100644 index 0000000..336ccde --- /dev/null +++ b/services/registration/low_swap_amount.vis @@ -0,0 +1,6 @@ +MAP swap_max_limit +MOUT retry 1 +MOUT quit 9 +HALT +INCMP _ 1 +INCMP quit 9 diff --git a/services/registration/low_swap_amount_swa b/services/registration/low_swap_amount_swa new file mode 100644 index 0000000..2d11dd9 --- /dev/null +++ b/services/registration/low_swap_amount_swa @@ -0,0 +1 @@ +Kiasi kinachopatikana {{.swap_max_limit}} ni cha chini sana, tafadhali jaribu tena: \ No newline at end of file diff --git a/services/registration/main.vis b/services/registration/main.vis index 5996c97..69f8b00 100644 --- a/services/registration/main.vis +++ b/services/registration/main.vis @@ -9,14 +9,16 @@ RELOAD check_balance CATCH api_failure flag_api_call_error 1 MAP check_balance MOUT send 1 -MOUT vouchers 2 -MOUT account 3 -MOUT help 4 +MOUT swap 2 +MOUT vouchers 3 +MOUT account 4 +MOUT help 5 MOUT quit 9 HALT INCMP send 1 -INCMP my_vouchers 2 -INCMP my_account 3 -INCMP help 4 +INCMP pool_swap 2 +INCMP my_vouchers 3 +INCMP my_account 4 +INCMP help 5 INCMP quit 9 INCMP . * diff --git a/services/registration/pool_swap b/services/registration/pool_swap new file mode 100644 index 0000000..21d0739 --- /dev/null +++ b/services/registration/pool_swap @@ -0,0 +1,2 @@ +Enter number or pool symbol to swap from: +{{.get_pools}} \ No newline at end of file diff --git a/services/registration/pool_swap.vis b/services/registration/pool_swap.vis new file mode 100644 index 0000000..dd98b0d --- /dev/null +++ b/services/registration/pool_swap.vis @@ -0,0 +1,8 @@ +LOAD get_pools 100 +MAP get_pools +MOUT back 0 +MOUT quit 99 +HALT +INCMP _ 0 +INCMP quit 99 +INCMP swap_from_list * diff --git a/services/registration/pool_swap_swa b/services/registration/pool_swap_swa new file mode 100644 index 0000000..e12dc90 --- /dev/null +++ b/services/registration/pool_swap_swa @@ -0,0 +1,2 @@ +Chagua bwawa la sarafu la kubadilishana: +{{.get_pools}} \ No newline at end of file diff --git a/services/registration/pp.csv b/services/registration/pp.csv index aa1eb05..17f8460 100644 --- a/services/registration/pp.csv +++ b/services/registration/pp.csv @@ -29,4 +29,5 @@ flag,flag_location_set,35,this is set when the location of the profile is set flag,flag_offerings_set,36,this is set when the offerings of the profile is set flag,flag_back_set,37,this is set when it is a back navigation flag,flag_account_blocked,38,this is set when an account has been blocked after the allowed incorrect PIN attempts have been exceeded - +flag,flag_incorrect_pool,39,this is set when the user selects an invalid pool +flag,flag_low_swap_amount,40,this is set when the swap max limit is less than 0.1 diff --git a/services/registration/swap_from_list b/services/registration/swap_from_list new file mode 100644 index 0000000..e0cc3d2 --- /dev/null +++ b/services/registration/swap_from_list @@ -0,0 +1,2 @@ +Select number or symbol to swap FROM: +{{.swap_from_list}} \ No newline at end of file diff --git a/services/registration/swap_from_list.vis b/services/registration/swap_from_list.vis new file mode 100644 index 0000000..e316e6e --- /dev/null +++ b/services/registration/swap_from_list.vis @@ -0,0 +1,7 @@ +LOAD swap_from_list 0 +CATCH _ flag_incorrect_pool 1 +MAP swap_from_list +MOUT back 0 +HALT +INCMP _ 0 +INCMP swap_to_list * diff --git a/services/registration/swap_from_list_swa b/services/registration/swap_from_list_swa new file mode 100644 index 0000000..ef716fd --- /dev/null +++ b/services/registration/swap_from_list_swa @@ -0,0 +1,2 @@ +Chagua nambari au ishara ya sarafu kubadilisha KUTOKA: +{{.swap_from_list}} \ No newline at end of file diff --git a/services/registration/swap_initiated.vis b/services/registration/swap_initiated.vis new file mode 100644 index 0000000..69ffc6d --- /dev/null +++ b/services/registration/swap_initiated.vis @@ -0,0 +1,3 @@ +LOAD reset_incorrect 6 +LOAD initiate_swap 0 +HALT diff --git a/services/registration/swap_limit b/services/registration/swap_limit new file mode 100644 index 0000000..4c9797c --- /dev/null +++ b/services/registration/swap_limit @@ -0,0 +1 @@ +{{.swap_max_limit}} \ No newline at end of file diff --git a/services/registration/swap_limit.vis b/services/registration/swap_limit.vis new file mode 100644 index 0000000..b96f1f5 --- /dev/null +++ b/services/registration/swap_limit.vis @@ -0,0 +1,6 @@ +RELOAD swap_max_limit +MAP swap_max_limit +MOUT back 0 +HALT +INCMP _ 0 +INCMP swap_preview * diff --git a/services/registration/swap_limit_swa b/services/registration/swap_limit_swa new file mode 100644 index 0000000..4c9797c --- /dev/null +++ b/services/registration/swap_limit_swa @@ -0,0 +1 @@ +{{.swap_max_limit}} \ No newline at end of file diff --git a/services/registration/swap_menu b/services/registration/swap_menu new file mode 100644 index 0000000..a5daabf --- /dev/null +++ b/services/registration/swap_menu @@ -0,0 +1 @@ +Swap \ No newline at end of file diff --git a/services/registration/swap_preview b/services/registration/swap_preview new file mode 100644 index 0000000..fb008e4 --- /dev/null +++ b/services/registration/swap_preview @@ -0,0 +1,3 @@ +{{.swap_preview}} + +Please enter your PIN to confirm: \ No newline at end of file diff --git a/services/registration/swap_preview.vis b/services/registration/swap_preview.vis new file mode 100644 index 0000000..901de26 --- /dev/null +++ b/services/registration/swap_preview.vis @@ -0,0 +1,12 @@ +LOAD swap_preview 0 +MAP swap_preview +MOUT back 0 +MOUT quit 9 +LOAD authorize_account 6 +HALT +RELOAD authorize_account +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH . flag_account_authorized 0 +INCMP _ 0 +INCMP quit 9 +INCMP swap_initiated * diff --git a/services/registration/swap_preview_swa b/services/registration/swap_preview_swa new file mode 100644 index 0000000..4c6d567 --- /dev/null +++ b/services/registration/swap_preview_swa @@ -0,0 +1,3 @@ +{{.swap_preview}} + +Tafadhali weka PIN yako kudhibitisha: \ No newline at end of file diff --git a/services/registration/swap_to_list b/services/registration/swap_to_list new file mode 100644 index 0000000..86f56c3 --- /dev/null +++ b/services/registration/swap_to_list @@ -0,0 +1,2 @@ +Select number or symbol to swap TO: +{{.swap_to_list}} \ No newline at end of file diff --git a/services/registration/swap_to_list.vis b/services/registration/swap_to_list.vis new file mode 100644 index 0000000..de1836e --- /dev/null +++ b/services/registration/swap_to_list.vis @@ -0,0 +1,11 @@ +LOAD swap_to_list 0 +CATCH _ flag_incorrect_voucher 1 +MAP swap_to_list +MOUT back 0 +HALT +LOAD swap_max_limit 64 +RELOAD swap_max_limit +CATCH . flag_incorrect_voucher 1 +CATCH low_swap_amount flag_low_swap_amount 1 +INCMP _ 0 +INCMP swap_limit * diff --git a/services/registration/swap_to_list_swa b/services/registration/swap_to_list_swa new file mode 100644 index 0000000..42da7d3 --- /dev/null +++ b/services/registration/swap_to_list_swa @@ -0,0 +1,2 @@ +Chagua nambari au ishara ya sarafu kubadilisha KWENDA: +{{.swap_to_list}} \ No newline at end of file diff --git a/store/db/db.go b/store/db/db.go index 10f360a..50a09cd 100644 --- a/store/db/db.go +++ b/store/db/db.go @@ -63,6 +63,24 @@ const ( DATA_INITIAL_LANGUAGE_CODE //Fully qualified account alias string DATA_ACCOUNT_ALIAS + // Holds the active pool contract address for the swap + DATA_ACTIVE_POOL_ADDRESS + // Currently active swap from symbol for the swap + DATA_ACTIVE_SWAP_FROM_SYM + // Currently active swap from decimal count for the swap + DATA_ACTIVE_SWAP_FROM_DECIMAL + // Holds the active swap from contract address for the swap + DATA_ACTIVE_SWAP_FROM_ADDRESS + // Currently active swap from to for the swap + DATA_ACTIVE_SWAP_TO_SYM + // Currently active swap to decimal count for the swap + DATA_ACTIVE_SWAP_TO_DECIMAL + // Holds the active pool contract address for the swap + DATA_ACTIVE_SWAP_TO_ADDRESS + // Holds the max swap amount for the swap + DATA_ACTIVE_SWAP_MAX_AMOUNT + // Holds the active swap amount for the swap + DATA_ACTIVE_SWAP_AMOUNT ) const ( @@ -101,6 +119,31 @@ const ( DATA_TRANSACTIONS = 1024 + iota ) +const ( + // List of voucher symbols in the top pools context. + DATA_POOL_NAMES = 2048 + iota + // List of symbols in the top pools context. + DATA_POOL_SYMBOLS + // List of contact addresses in the top pools context + DATA_POOL_ADDRESSES + // List of swap from voucher symbols in the user context. + DATA_POOL_FROM_SYMBOLS + // List of swap from balances for vouchers valid in the pools context. + DATA_POOL_FROM_BALANCES + // List of swap from decimal counts for vouchers valid in the pools context. + DATA_POOL_FROM_DECIMALS + // List of swap from EVM addresses for vouchers valid in the pools context. + DATA_POOL_FROM_ADDRESSES + // List of swap to voucher symbols in the user context. + DATA_POOL_TO_SYMBOLS + // List of swap to balances for vouchers valid in the pools context. + DATA_POOL_TO_BALANCES + // List of swap to decimal counts for vouchers valid in the pools context. + DATA_POOL_TO_DECIMALS + // List of swap to EVM addresses for vouchers valid in the pools context. + DATA_POOL_TO_ADDRESSES +) + var ( logg = logging.NewVanilla().WithDomain("urdt-common") ) diff --git a/store/pools.go b/store/pools.go new file mode 100644 index 0000000..39e3966 --- /dev/null +++ b/store/pools.go @@ -0,0 +1,93 @@ +package store + +import ( + "context" + "fmt" + "strings" + + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +// PoolsMetadata helps organize data fields +type PoolsMetadata struct { + PoolNames string + PoolSymbols string + PoolContractAdrresses string +} + +// ProcessPools converts pools into formatted strings +func ProcessPools(pools []dataserviceapi.PoolDetails) PoolsMetadata { + var data PoolsMetadata + var poolNames, poolSymbols, poolContractAdrresses []string + + for i, p := range pools { + poolNames = append(poolNames, fmt.Sprintf("%d:%s", i+1, p.PoolName)) + poolSymbols = append(poolSymbols, fmt.Sprintf("%d:%s", i+1, p.PoolSymbol)) + poolContractAdrresses = append(poolContractAdrresses, fmt.Sprintf("%d:%s", i+1, p.PoolContractAdrress)) + } + + data.PoolNames = strings.Join(poolNames, "\n") + data.PoolSymbols = strings.Join(poolSymbols, "\n") + data.PoolContractAdrresses = strings.Join(poolContractAdrresses, "\n") + + return data +} + +// GetPoolData retrieves and matches pool data +// if no match is found, it fetches the API with the symbol +func GetPoolData(ctx context.Context, db storedb.PrefixDb, input string) (*dataserviceapi.PoolDetails, error) { + keys := []storedb.DataTyp{ + storedb.DATA_POOL_NAMES, + storedb.DATA_POOL_SYMBOLS, + storedb.DATA_POOL_ADDRESSES, + } + data := make(map[storedb.DataTyp]string) + + for _, key := range keys { + value, err := db.Get(ctx, storedb.ToBytes(key)) + if err != nil { + return nil, fmt.Errorf("failed to get prefix key %x: %v", storedb.ToBytes(key), err) + } + data[key] = string(value) + } + + name, symbol, address := MatchPool(input, + data[storedb.DATA_POOL_NAMES], + data[storedb.DATA_POOL_SYMBOLS], + data[storedb.DATA_POOL_ADDRESSES], + ) + + if symbol == "" { + return nil, nil + } + + return &dataserviceapi.PoolDetails{ + PoolName: string(name), + PoolSymbol: string(symbol), + PoolContractAdrress: string(address), + }, nil +} + +// MatchPool finds the matching pool name, symbol and pool contract address based on the input. +func MatchPool(input, names, symbols, addresses string) (name, symbol, address string) { + nameList := strings.Split(names, "\n") + symList := strings.Split(symbols, "\n") + addrList := strings.Split(addresses, "\n") + + 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(nameList) { + name = strings.SplitN(nameList[i], ":", 2)[1] + } + if i < len(addrList) { + address = strings.SplitN(addrList[i], ":", 2)[1] + } + break + } + } + return +} diff --git a/store/swap.go b/store/swap.go new file mode 100644 index 0000000..49fb561 --- /dev/null +++ b/store/swap.go @@ -0,0 +1,207 @@ +package store + +import ( + "context" + "errors" + "fmt" + "reflect" + + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +type SwapData struct { + PublicKey string + ActivePoolAddress string + ActiveSwapFromSym string + ActiveSwapFromDecimal string + ActiveSwapFromAddress string + ActiveSwapToSym string + ActiveSwapToAddress string +} + +type SwapPreviewData struct { + PublicKey string + ActiveSwapMaxAmount string + ActiveSwapFromDecimal string + ActivePoolAddress string + ActiveSwapFromAddress string + ActiveSwapFromSym string + ActiveSwapToAddress string + ActiveSwapToSym string + ActiveSwapToDecimal string +} + +func ReadSwapData(ctx context.Context, store DataStore, sessionId string) (SwapData, error) { + data := SwapData{} + fieldToKey := map[string]storedb.DataTyp{ + "PublicKey": storedb.DATA_PUBLIC_KEY, + "ActivePoolAddress": storedb.DATA_ACTIVE_POOL_ADDRESS, + "ActiveSwapFromSym": storedb.DATA_ACTIVE_SWAP_FROM_SYM, + "ActiveSwapFromDecimal": storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL, + "ActiveSwapFromAddress": storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS, + "ActiveSwapToSym": storedb.DATA_ACTIVE_SWAP_TO_SYM, + "ActiveSwapToAddress": storedb.DATA_ACTIVE_SWAP_TO_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 ReadSwapPreviewData(ctx context.Context, store DataStore, sessionId string) (SwapPreviewData, error) { + data := SwapPreviewData{} + fieldToKey := map[string]storedb.DataTyp{ + "PublicKey": storedb.DATA_PUBLIC_KEY, + "ActiveSwapMaxAmount": storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT, + "ActiveSwapFromDecimal": storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL, + "ActivePoolAddress": storedb.DATA_ACTIVE_POOL_ADDRESS, + "ActiveSwapFromAddress": storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS, + "ActiveSwapFromSym": storedb.DATA_ACTIVE_SWAP_FROM_SYM, + "ActiveSwapToAddress": storedb.DATA_ACTIVE_SWAP_TO_ADDRESS, + "ActiveSwapToSym": storedb.DATA_ACTIVE_SWAP_TO_SYM, + "ActiveSwapToDecimal": storedb.DATA_ACTIVE_SWAP_TO_DECIMAL, + } + + 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 +} + +// GetSwapFromVoucherData retrieves and matches swap from voucher data +func GetSwapFromVoucherData(ctx context.Context, db storedb.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) { + keys := []storedb.DataTyp{ + storedb.DATA_POOL_FROM_SYMBOLS, + storedb.DATA_POOL_FROM_BALANCES, + storedb.DATA_POOL_FROM_DECIMALS, + storedb.DATA_POOL_FROM_ADDRESSES, + } + data := make(map[storedb.DataTyp]string) + + for _, key := range keys { + value, err := db.Get(ctx, storedb.ToBytes(key)) + if err != nil { + return nil, fmt.Errorf("failed to get prefix key %x: %v", storedb.ToBytes(key), err) + } + data[key] = string(value) + } + + symbol, balance, decimal, address := MatchVoucher(input, + data[storedb.DATA_POOL_FROM_SYMBOLS], + data[storedb.DATA_POOL_FROM_BALANCES], + data[storedb.DATA_POOL_FROM_DECIMALS], + data[storedb.DATA_POOL_FROM_ADDRESSES], + ) + + if symbol == "" { + return nil, nil + } + + return &dataserviceapi.TokenHoldings{ + TokenSymbol: string(symbol), + Balance: string(balance), + TokenDecimals: string(decimal), + ContractAddress: string(address), + }, nil +} + +// UpdateSwapFromVoucherData updates the active swap to voucher data in the DataStore. +func UpdateSwapFromVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { + logg.TraceCtxf(ctx, "dtal", "data", data) + // Active swap from voucher data entries + activeEntries := map[storedb.DataTyp][]byte{ + storedb.DATA_ACTIVE_SWAP_FROM_SYM: []byte(data.TokenSymbol), + storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL: []byte(data.TokenDecimals), + storedb.DATA_ACTIVE_SWAP_FROM_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 +} + +// GetSwapToVoucherData retrieves and matches voucher data +func GetSwapToVoucherData(ctx context.Context, db storedb.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) { + keys := []storedb.DataTyp{ + storedb.DATA_POOL_TO_SYMBOLS, + storedb.DATA_POOL_TO_BALANCES, + storedb.DATA_POOL_TO_DECIMALS, + storedb.DATA_POOL_TO_ADDRESSES, + } + data := make(map[storedb.DataTyp]string) + + for _, key := range keys { + value, err := db.Get(ctx, storedb.ToBytes(key)) + if err != nil { + return nil, fmt.Errorf("failed to get prefix key %x: %v", storedb.ToBytes(key), err) + } + data[key] = string(value) + } + + symbol, balance, decimal, address := MatchVoucher(input, + data[storedb.DATA_POOL_TO_SYMBOLS], + data[storedb.DATA_POOL_TO_BALANCES], + data[storedb.DATA_POOL_TO_DECIMALS], + data[storedb.DATA_POOL_TO_ADDRESSES], + ) + + if symbol == "" { + return nil, nil + } + + return &dataserviceapi.TokenHoldings{ + TokenSymbol: string(symbol), + Balance: string(balance), + TokenDecimals: string(decimal), + ContractAddress: string(address), + }, nil +} + +// UpdateSwapToVoucherData updates the active swap to voucher data in the DataStore. +func UpdateSwapToVoucherData(ctx context.Context, store DataStore, sessionId string, data *dataserviceapi.TokenHoldings) error { + logg.TraceCtxf(ctx, "dtal", "data", data) + // Active swap to voucher data entries + activeEntries := map[storedb.DataTyp][]byte{ + storedb.DATA_ACTIVE_SWAP_TO_SYM: []byte(data.TokenSymbol), + storedb.DATA_ACTIVE_SWAP_TO_DECIMAL: []byte(data.TokenDecimals), + storedb.DATA_ACTIVE_SWAP_TO_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 +} diff --git a/store/swap_test.go b/store/swap_test.go new file mode 100644 index 0000000..ee96066 --- /dev/null +++ b/store/swap_test.go @@ -0,0 +1,206 @@ +package store + +import ( + "context" + "testing" + + visedb "git.defalsify.org/vise.git/db" + memdb "git.defalsify.org/vise.git/db/mem" + storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db" + "github.com/alecthomas/assert/v2" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" + "github.com/stretchr/testify/require" +) + +func TestReadSwapData(t *testing.T) { + sessionId := "session123" + publicKey := "0X13242618721" + ctx, store := InitializeTestDb(t) + + // Test swap data + swapData := map[storedb.DataTyp]string{ + storedb.DATA_PUBLIC_KEY: publicKey, + storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e", + storedb.DATA_ACTIVE_SWAP_FROM_SYM: "AMANI", + storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL: "6", + storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe", + storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD", + storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a", + } + + // Store the data + for key, value := range swapData { + if err := store.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil { + t.Fatal(err) + } + } + + expectedResult := SwapData{ + PublicKey: "0X13242618721", + ActivePoolAddress: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e", + ActiveSwapFromSym: "AMANI", + ActiveSwapFromDecimal: "6", + ActiveSwapFromAddress: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe", + ActiveSwapToSym: "cUSD", + ActiveSwapToAddress: "0x765DE816845861e75A25fCA122bb6898B8B1282a", + } + + data, err := ReadSwapData(ctx, store, sessionId) + + assert.NoError(t, err) + assert.Equal(t, expectedResult, data) +} + +func TestReadSwapPreviewData(t *testing.T) { + sessionId := "session123" + publicKey := "0X13242618721" + ctx, store := InitializeTestDb(t) + + // Test swap preview data + swapPreviewData := map[storedb.DataTyp]string{ + storedb.DATA_PUBLIC_KEY: publicKey, + storedb.DATA_ACTIVE_SWAP_MAX_AMOUNT: "1339482", + storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL: "6", + storedb.DATA_ACTIVE_POOL_ADDRESS: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e", + storedb.DATA_ACTIVE_SWAP_FROM_ADDRESS: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe", + storedb.DATA_ACTIVE_SWAP_FROM_SYM: "AMANI", + storedb.DATA_ACTIVE_SWAP_TO_ADDRESS: "0x765DE816845861e75A25fCA122bb6898B8B1282a", + storedb.DATA_ACTIVE_SWAP_TO_SYM: "cUSD", + storedb.DATA_ACTIVE_SWAP_TO_DECIMAL: "18", + } + + // Store the data + for key, value := range swapPreviewData { + if err := store.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil { + t.Fatal(err) + } + } + + expectedResult := SwapPreviewData{ + PublicKey: "0X13242618721", + ActiveSwapMaxAmount: "1339482", + ActiveSwapFromDecimal: "6", + ActivePoolAddress: "0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e", + ActiveSwapFromAddress: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe", + ActiveSwapFromSym: "AMANI", + ActiveSwapToAddress: "0x765DE816845861e75A25fCA122bb6898B8B1282a", + ActiveSwapToSym: "cUSD", + ActiveSwapToDecimal: "18", + } + + data, err := ReadSwapPreviewData(ctx, store, sessionId) + + assert.NoError(t, err) + assert.Equal(t, expectedResult, data) +} + +func TestGetSwapFromVoucherData(t *testing.T) { + ctx := context.Background() + + db := memdb.NewMemDb() + err := db.Connect(ctx, "") + if err != nil { + t.Fatal(err) + } + + prefix := storedb.ToBytes(visedb.DATATYPE_USERDATA) + spdb := storedb.NewSubPrefixDb(db, prefix) + + // Test pool swap data + mockData := map[storedb.DataTyp][]byte{ + storedb.DATA_POOL_FROM_SYMBOLS: []byte("1:AMANI\n2:AMUA"), + storedb.DATA_POOL_FROM_BALANCES: []byte("1:\n2:"), + storedb.DATA_POOL_FROM_DECIMALS: []byte("1:6\n2:4"), + storedb.DATA_POOL_FROM_ADDRESSES: []byte("1:0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe\n2:0xF0C3C7581b8b96B59a97daEc8Bd48247cE078674"), + } + + // Put the data + for key, value := range mockData { + err = spdb.Put(ctx, []byte(storedb.ToBytes(key)), []byte(value)) + if err != nil { + t.Fatal(err) + } + } + + result, err := GetSwapFromVoucherData(ctx, spdb, "1") + + assert.NoError(t, err) + assert.Equal(t, "AMANI", result.TokenSymbol) + assert.Equal(t, "", result.Balance) + assert.Equal(t, "6", result.TokenDecimals) + assert.Equal(t, "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe", result.ContractAddress) +} + +func TestUpdateSwapFromVoucherData(t *testing.T) { + ctx, store := InitializeTestDb(t) + sessionId := "session123" + + // New swap from voucher data + newData := &dataserviceapi.TokenHoldings{ + TokenSymbol: "AMANI", + TokenDecimals: "6", + ContractAddress: "0xc7B78Ac9ACB9E025C8234621FC515bC58179dEAe", + } + + // Old temporary data + tempData := &dataserviceapi.TokenHoldings{ + TokenSymbol: "OLD", + TokenDecimals: "8", + ContractAddress: "0xold", + } + require.NoError(t, StoreTemporaryVoucher(ctx, store, sessionId, tempData)) + + // Execute update + err := UpdateSwapFromVoucherData(ctx, store, sessionId, newData) + require.NoError(t, err) + + // Verify active swap from data was stored correctly + activeEntries := map[storedb.DataTyp][]byte{ + storedb.DATA_ACTIVE_SWAP_FROM_SYM: []byte(newData.TokenSymbol), + storedb.DATA_ACTIVE_SWAP_FROM_DECIMAL: []byte(newData.TokenDecimals), + storedb.DATA_ACTIVE_SWAP_FROM_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 swap from data mismatch for key %v", key) + } +} + +func TestGetSwapToVoucherData(t *testing.T) { + ctx := context.Background() + + db := memdb.NewMemDb() + err := db.Connect(ctx, "") + if err != nil { + t.Fatal(err) + } + + prefix := storedb.ToBytes(visedb.DATATYPE_USERDATA) + spdb := storedb.NewSubPrefixDb(db, prefix) + + // Test pool swap data + mockData := map[storedb.DataTyp][]byte{ + storedb.DATA_POOL_TO_SYMBOLS: []byte("1:cUSD\n2:AMUA"), + storedb.DATA_POOL_TO_BALANCES: []byte("1:\n2:"), + storedb.DATA_POOL_TO_DECIMALS: []byte("1:6\n2:4"), + storedb.DATA_POOL_TO_ADDRESSES: []byte("1:0xc7B78Ac9ACB9E025C8234621\n2:0xF0C3C7581b8b96B59a97daEc8Bd48247cE078674"), + } + + // Put the data + for key, value := range mockData { + err = spdb.Put(ctx, []byte(storedb.ToBytes(key)), []byte(value)) + if err != nil { + t.Fatal(err) + } + } + + result, err := GetSwapToVoucherData(ctx, spdb, "1") + + assert.NoError(t, err) + assert.Equal(t, "cUSD", result.TokenSymbol) + assert.Equal(t, "", result.Balance) + assert.Equal(t, "6", result.TokenDecimals) + assert.Equal(t, "0xc7B78Ac9ACB9E025C8234621", result.ContractAddress) +} \ No newline at end of file diff --git a/store/tokens.go b/store/tokens.go index 7c3ad0c..a7770c7 100644 --- a/store/tokens.go +++ b/store/tokens.go @@ -64,7 +64,7 @@ func ReadTransactionData(ctx context.Context, store DataStore, sessionId string) return data, errors.New("invalid struct field: " + fieldName) } - value, err := readStringEntry(ctx, store, sessionId, key) + value, err := ReadStringEntry(ctx, store, sessionId, key) if err != nil { return data, err } @@ -74,7 +74,7 @@ func ReadTransactionData(ctx context.Context, store DataStore, sessionId string) return data, nil } -func readStringEntry(ctx context.Context, store DataStore, sessionId string, key storedb.DataTyp) (string, error) { +func ReadStringEntry(ctx context.Context, store DataStore, sessionId string, key storedb.DataTyp) (string, error) { entry, err := store.ReadEntry(ctx, sessionId, key) if err != nil { return "", err