Compare commits

..

10 Commits

Author SHA1 Message Date
Alfred Kamanda
817b523135 Merge branch 'master' into cache-error-fix
Some checks failed
release / docker (push) Has been cancelled
2025-12-08 17:43:21 +03:00
e0e3d9b6cf Merge pull request 'mpesa-onramp-offramp' (#110) from mpesa-onramp-offramp into master
Reviewed-on: #110
2025-12-08 15:41:23 +01:00
Alfred Kamanda
2cabae1e74 use the ResetRoot config to clear the cache once a user quits
Some checks are pending
release / docker (push) Waiting to run
2025-12-08 17:29:13 +03:00
Alfred Kamanda
f949b83a51 change the go-vise source
Some checks failed
release / docker (push) Has been cancelled
2025-12-03 13:40:32 +03:00
Alfred Kamanda
d586c41cca add sleep for 1 second between requests
Some checks failed
release / docker (push) Has been cancelled
2025-12-03 13:12:24 +03:00
Alfred Kamanda
518baceee5 update the translations to add the approximation sign ~
Some checks failed
release / docker (push) Has been cancelled
2025-12-02 12:28:55 +03:00
Alfred Kamanda
1cb82e9099 call the mpesa rates API to get the rates 2025-12-02 12:28:28 +03:00
Alfred Kamanda
efc93397b2 use the updated sarafu-api 2025-12-02 12:26:52 +03:00
Alfred Kamanda
5b19b3409b remove the mpesa rates configs 2025-12-02 12:26:36 +03:00
Alfred Kamanda
2db97cde81 remove the hard-coded rates 2025-12-02 12:26:13 +03:00
11 changed files with 70 additions and 52 deletions

View File

@@ -32,10 +32,8 @@ INCLUDE_STABLES_PARAM=false
#Mpesa #Mpesa
DEFAULT_MPESA_ADDRESS=0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e DEFAULT_MPESA_ADDRESS=0x48a953cA5cf5298bc6f6Af3C608351f537AAcb9e
MPESA_RATE=129.5
MIN_MPESA_SEND_AMOUNT=100 MIN_MPESA_SEND_AMOUNT=100
MAX_MPESA_SEND_AMOUNT=250000 MAX_MPESA_SEND_AMOUNT=250000
MPESA_SEND_RATE=130.2
DEFAULT_MPESA_ASSET=cUSD DEFAULT_MPESA_ASSET=cUSD
MPESA_BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr MPESA_BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
MPESA_ONRAMP_BASE=https://pretium.v1.grassecon.net MPESA_ONRAMP_BASE=https://pretium.v1.grassecon.net

View File

@@ -11,7 +11,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libgdbm-dev \ libgdbm-dev \
git \ git \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN git clone https://git.defalsify.org/vise.git go-vise RUN git clone https://github.com/nolash/go-vise go-vise
COPY . ./sarafu-vise COPY . ./sarafu-vise
WORKDIR /build/sarafu-vise/services/registration WORKDIR /build/sarafu-vise/services/registration

View File

@@ -79,11 +79,12 @@ func main() {
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator, MenuSeparator: menuSeparator,
ResetOnEmptyInput: true, ResetOnEmptyInput: true,
ResetRoot: true, // clear the cache once a user quits
} }
if engineDebug { if engineDebug {

View File

@@ -94,11 +94,12 @@ func main() {
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator, MenuSeparator: menuSeparator,
ResetOnEmptyInput: true, ResetOnEmptyInput: true,
ResetRoot: true, // clear the cache once a user quits
} }
if engineDebug { if engineDebug {

View File

@@ -82,6 +82,7 @@ func main() {
MenuSeparator: menuSeparator, MenuSeparator: menuSeparator,
EngineDebug: engineDebug, EngineDebug: engineDebug,
ResetOnEmptyInput: true, ResetOnEmptyInput: true,
ResetRoot: true, // clear the cache once a user quits
} }
menuStorageService := storage.NewMenuStorageService(conns) menuStorageService := storage.NewMenuStorageService(conns)

View File

@@ -93,15 +93,6 @@ func DefaultMpesaAddress() string {
return env.GetEnv("DEFAULT_MPESA_ADDRESS", "") return env.GetEnv("DEFAULT_MPESA_ADDRESS", "")
} }
func MpesaRate() float64 {
v := env.GetEnv("MPESA_RATE", "129.5")
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return 129.5 // fallback default
}
return f
}
func MinMpesaSendAmount() float64 { func MinMpesaSendAmount() float64 {
v := env.GetEnv("MIN_MPESA_SEND_AMOUNT", "100") v := env.GetEnv("MIN_MPESA_SEND_AMOUNT", "100")
f, err := strconv.ParseFloat(v, 64) f, err := strconv.ParseFloat(v, 64)
@@ -120,15 +111,6 @@ func MaxMpesaSendAmount() float64 {
return f return f
} }
func MpesaSendRate() float64 {
v := env.GetEnv("MPESA_SEND_RATE", "130.2")
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return 130.2 // fallback default
}
return f
}
func DefaultMpesaAsset() string { func DefaultMpesaAsset() string {
return env.GetEnv("DEFAULT_MPESA_ASSET", "") return env.GetEnv("DEFAULT_MPESA_ASSET", "")
} }

2
go.mod
View File

@@ -7,7 +7,7 @@ toolchain go1.24.10
require ( require (
git.defalsify.org/vise.git v0.3.2-0.20250528124150-03bf7bfc1b66 git.defalsify.org/vise.git v0.3.2-0.20250528124150-03bf7bfc1b66
git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20251127132814-8ceadabbc215 git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20251127132814-8ceadabbc215
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251128071248-bfdeef125576 git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251202085112-45469d4ba326
git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306 git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306
git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694 git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694
github.com/alecthomas/assert/v2 v2.2.2 github.com/alecthomas/assert/v2 v2.2.2

2
go.sum
View File

@@ -4,6 +4,8 @@ git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20251127132814-8cea
git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20251127132814-8ceadabbc215/go.mod h1:wgQJZGIS6QuNLHqDhcsvehsbn5PvgV7aziRebMnJi60= git.grassecon.net/grassrootseconomics/common v0.9.0-beta.1.0.20251127132814-8ceadabbc215/go.mod h1:wgQJZGIS6QuNLHqDhcsvehsbn5PvgV7aziRebMnJi60=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251128071248-bfdeef125576 h1:Ov4zENfEnzuU4ZpsNGbFjog9NUM0h1A7RYwWkmHRJWo= git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251128071248-bfdeef125576 h1:Ov4zENfEnzuU4ZpsNGbFjog9NUM0h1A7RYwWkmHRJWo=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251128071248-bfdeef125576/go.mod h1:h/y/lJNJAVTcIzAxCMXXw8Dh2aoLxBFZ6F1nTB8C0nU= git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251128071248-bfdeef125576/go.mod h1:h/y/lJNJAVTcIzAxCMXXw8Dh2aoLxBFZ6F1nTB8C0nU=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251202085112-45469d4ba326 h1:qH4QulgncvAD7b/YeHGPxcDJTBIychPeoZJACefYryI=
git.grassecon.net/grassrootseconomics/sarafu-api v0.9.0-beta.1.0.20251202085112-45469d4ba326/go.mod h1:h/y/lJNJAVTcIzAxCMXXw8Dh2aoLxBFZ6F1nTB8C0nU=
git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306 h1:Jo+yWysWw/N5BJQtAyEMN8ePVvAyPHv+JG4lQti+1N4= git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306 h1:Jo+yWysWw/N5BJQtAyEMN8ePVvAyPHv+JG4lQti+1N4=
git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306/go.mod h1:FdLwYtzsjOIcDiW4uDgDYnB4Wdzq12uJUe0QHSSPbSo= git.grassecon.net/grassrootseconomics/visedriver v0.9.0-beta.2.0.20250408094335-e2d1f65bb306/go.mod h1:FdLwYtzsjOIcDiW4uDgDYnB4Wdzq12uJUe0QHSSPbSo=
git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694 h1:DjJlBSz0S13acft5XZDWk7ZYnzElym0xLMYEVgyNJ+E= git.grassecon.net/grassrootseconomics/visedriver-africastalking v0.0.0-20250129070628-5a539172c694 h1:DjJlBSz0S13acft5XZDWk7ZYnzElym0xLMYEVgyNJ+E=

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"strconv" "strconv"
"time"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
"git.grassecon.net/grassrootseconomics/common/hex" "git.grassecon.net/grassrootseconomics/common/hex"
@@ -45,7 +46,15 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
return res, err return res, err
} }
rate := config.MpesaRate() // call the mpesa rates API to get the rates
rates, err := h.accountService.GetMpesaOnrampRates(ctx)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on GetMpesaOnrampRates", "error", err)
return res, nil
}
txType := "swap" txType := "swap"
mpesaAddress := config.DefaultMpesaAddress() mpesaAddress := config.DefaultMpesaAddress()
@@ -84,7 +93,7 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
} }
activeFloat, _ := strconv.ParseFloat(string(activeBal), 64) activeFloat, _ := strconv.ParseFloat(string(activeBal), 64)
ksh := fmt.Sprintf("%f", activeFloat*rate) ksh := fmt.Sprintf("%f", activeFloat*rates.Buy)
kshFormatted, _ := store.TruncateDecimalString(ksh, 0) kshFormatted, _ := store.TruncateDecimalString(ksh, 0)
@@ -158,7 +167,7 @@ func (h *MenuHandlers) GetMpesaMaxLimit(ctx context.Context, sym string, input [
return res, err return res, err
} }
maxKsh := maxFloat * rate maxKsh := maxFloat * rates.Buy
kshStr := fmt.Sprintf("%f", maxKsh) kshStr := fmt.Sprintf("%f", maxKsh)
kshFormatted, _ := store.TruncateDecimalString(kshStr, 0) kshFormatted, _ := store.TruncateDecimalString(kshStr, 0)
@@ -192,7 +201,15 @@ func (h *MenuHandlers) GetMpesaPreview(ctx context.Context, sym string, input []
l.AddDomain("default") l.AddDomain("default")
userStore := h.userdataStore userStore := h.userdataStore
rate := config.MpesaRate()
// call the mpesa rates API to get the rates
rates, err := h.accountService.GetMpesaOnrampRates(ctx)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on GetMpesaOnrampRates", "error", err)
return res, nil
}
// Input in Ksh // Input in Ksh
kshAmount, err := strconv.ParseFloat(inputStr, 64) kshAmount, err := strconv.ParseFloat(inputStr, 64)
@@ -202,8 +219,8 @@ func (h *MenuHandlers) GetMpesaPreview(ctx context.Context, sym string, input []
return res, nil return res, nil
} }
// divide by the rate // divide by the buy rate
inputAmount := kshAmount / rate inputAmount := kshAmount / rates.Buy
// store the user's raw input amount in the temporary value // store the user's raw input amount in the temporary value
err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(inputStr)) err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(inputStr))
@@ -252,7 +269,7 @@ func (h *MenuHandlers) GetMpesaPreview(ctx context.Context, sym string, input []
} }
res.Content = l.Get( res.Content = l.Get(
"You are sending %s %s in order to receive %s ksh", "You are sending %s %s in order to receive ~ %s ksh",
qouteInputAmount, swapData.ActiveSwapFromSym, inputStr, qouteInputAmount, swapData.ActiveSwapFromSym, inputStr,
) )
@@ -311,7 +328,7 @@ func (h *MenuHandlers) GetMpesaPreview(ctx context.Context, sym string, input []
qouteInputAmount, _ := store.TruncateDecimalString(quoteInputStr, 2) qouteInputAmount, _ := store.TruncateDecimalString(quoteInputStr, 2)
res.Content = l.Get( res.Content = l.Get(
"You are sending %s %s in order to receive %s ksh", "You are sending %s %s in order to receive ~ %s ksh",
qouteInputAmount, swapData.ActiveSwapFromSym, inputStr, qouteInputAmount, swapData.ActiveSwapFromSym, inputStr,
) )
@@ -370,7 +387,7 @@ func (h *MenuHandlers) InitiateGetMpesa(ctx context.Context, sym string, input [
logg.InfoCtxf(ctx, "TokenTransfer normal", "trackingId", tokenTransfer.TrackingId) logg.InfoCtxf(ctx, "TokenTransfer normal", "trackingId", tokenTransfer.TrackingId)
res.Content = l.Get("Your request has been sent. You will receive %s ksh", data.TemporaryValue) res.Content = l.Get("Your request has been sent. You will receive ~ %s ksh", data.TemporaryValue)
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil return res, nil
@@ -395,7 +412,10 @@ func (h *MenuHandlers) InitiateGetMpesa(ctx context.Context, sym string, input [
return res, nil return res, nil
} }
logg.InfoCtxf(ctx, "poolSwap", "swapTrackingId", poolSwap.TrackingId) logg.InfoCtxf(ctx, "mpesa poolSwap before transfer", "swapTrackingId", poolSwap.TrackingId)
// TODO: remove this temporary time delay and replace with a swap and send endpoint
time.Sleep(1 * time.Second)
finalKshStr, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE) finalKshStr, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if err != nil { if err != nil {
@@ -418,7 +438,7 @@ func (h *MenuHandlers) InitiateGetMpesa(ctx context.Context, sym string, input [
logg.InfoCtxf(ctx, "final TokenTransfer after swap", "trackingId", tokenTransfer.TrackingId) logg.InfoCtxf(ctx, "final TokenTransfer after swap", "trackingId", tokenTransfer.TrackingId)
res.Content = l.Get("Your request has been sent. You will receive %s ksh", finalKshStr) res.Content = l.Get("Your request has been sent. You will receive ~ %s ksh", finalKshStr)
res.FlagReset = append(res.FlagReset, flag_account_authorized) res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil return res, nil
} }
@@ -468,13 +488,22 @@ func (h *MenuHandlers) SendMpesaPreview(ctx context.Context, sym string, input [
} }
flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount") flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount")
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
code := codeFromCtx(ctx) code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code) l := gotext.NewLocale(translationDir, code)
l.AddDomain("default") l.AddDomain("default")
userStore := h.userdataStore userStore := h.userdataStore
sendRate := config.MpesaSendRate()
// call the mpesa rates API to get the rates
rates, err := h.accountService.GetMpesaOnrampRates(ctx)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on GetMpesaOnrampRates", "error", err)
return res, nil
}
// Input in Ksh // Input in Ksh
kshAmount, err := strconv.ParseFloat(inputStr, 64) kshAmount, err := strconv.ParseFloat(inputStr, 64)
@@ -502,12 +531,12 @@ func (h *MenuHandlers) SendMpesaPreview(ctx context.Context, sym string, input [
return res, err return res, err
} }
estimateValue := kshAmount / sendRate estimateValue := kshAmount / rates.Sell
estimateStr := fmt.Sprintf("%f", estimateValue) estimateStr := fmt.Sprintf("%f", estimateValue)
estimateFormatted, _ := store.TruncateDecimalString(estimateStr, 2) estimateFormatted, _ := store.TruncateDecimalString(estimateStr, 2)
res.Content = l.Get( res.Content = l.Get(
"You will get a prompt for your M-Pesa PIN shortly to send %s ksh and receive %s cUSD", "You will get a prompt for your M-Pesa PIN shortly to send %s ksh and receive ~ %s cUSD",
inputStr, estimateFormatted, inputStr, estimateFormatted,
) )

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/resource"
@@ -842,7 +843,10 @@ func (h *MenuHandlers) TransactionInitiateSwap(ctx context.Context, sym string,
} }
swapTrackingId := poolSwap.TrackingId swapTrackingId := poolSwap.TrackingId
logg.InfoCtxf(ctx, "poolSwap", "swapTrackingId", swapTrackingId) logg.InfoCtxf(ctx, "send poolSwap before transfer", "swapTrackingId", swapTrackingId)
// TODO: remove this temporary time delay and replace with a swap and send endpoint
time.Sleep(1 * time.Second)
// Initiate a send // Initiate a send
recipientPublicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT) recipientPublicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_RECIPIENT)
@@ -874,7 +878,7 @@ func (h *MenuHandlers) TransactionInitiateSwap(ctx context.Context, sym string,
} }
trackingId := tokenTransfer.TrackingId trackingId := tokenTransfer.TrackingId
logg.InfoCtxf(ctx, "TokenTransfer", "trackingId", trackingId) logg.InfoCtxf(ctx, "send TokenTransfer after swap", "trackingId", trackingId)
res.Content = l.Get( res.Content = l.Get(
"Your request has been sent. %s will receive %s %s from %s.", "Your request has been sent. %s will receive %s %s from %s.",

View File

@@ -58,17 +58,17 @@ msgstr "%s atapokea %s %s"
msgid "Enter the amount of M-Pesa to get: (Max %s Ksh)\n" msgid "Enter the amount of M-Pesa to get: (Max %s Ksh)\n"
msgstr "Weka kiasi cha M-Pesa cha kupata: (Kikomo %s Ksh)\n" msgstr "Weka kiasi cha M-Pesa cha kupata: (Kikomo %s Ksh)\n"
msgid "You are sending %s %s in order to receive %s ksh" msgid "You are sending %s %s in order to receive ~ %s ksh"
msgstr "Unatuma %s %s ili upoke %s ksh" msgstr "Unatuma ~ %s %s ili upoke %s ksh"
msgid "Your request has been sent. You will receive %s ksh" msgid "Your request has been sent. You will receive ~ %s ksh"
msgstr "Ombi lako limetumwa. Utapokea %s ksh" msgstr "Ombi lako limetumwa. Utapokea ~ %s ksh"
msgid "Enter the amount of M-Pesa to send: (Minimum %s Ksh)\n" msgid "Enter the amount of M-Pesa to send: (Minimum %s Ksh)\n"
msgstr "Weka kiasi cha M-Pesa cha kutuma: (Kima cha chini %s Ksh)\n" msgstr "Weka kiasi cha M-Pesa cha kutuma: (Kima cha chini %s Ksh)\n"
msgid "You will get a prompt for your M-Pesa PIN shortly to send %s ksh and receive %s cUSD" msgid "You will get a prompt for your M-Pesa PIN shortly to send %s ksh and receive ~ %s cUSD"
msgstr "Utapokea kidokezo cha PIN yako ya M-Pesa hivi karibuni kutuma %s ksh na kupokea %s cUSD" msgstr "Utapokea kidokezo cha PIN yako ya M-Pesa hivi karibuni kutuma %s ksh na kupokea ~ %s cUSD"
msgid "Your request has been sent. Thank you for using Sarafu" msgid "Your request has been sent. Thank you for using Sarafu"
msgstr "Ombi lako limetumwa. Asante kwa kutumia huduma ya Sarafu" msgstr "Ombi lako limetumwa. Asante kwa kutumia huduma ya Sarafu"