28 Commits

Author SHA1 Message Date
Arseni
dfbf36bf04 Fixes for build 2025-12-09 21:16:29 +03:00
Arseni
b16c295094 Account recovery is triggered once via a memoized Future, and errors are not swallowed inside the provider 2025-12-09 21:10:31 +03:00
Arseni
336687eccf Account state now survives reload before redirecting to login 2025-12-08 19:50:02 +03:00
f478219990 Merge pull request 'Top Up Balance logic and Added fixes for routing' (#31) from SEND001 into main
All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #31
2025-12-06 23:35:53 +00:00
Arseni
bf39b1d401 Top Up Balance logic and Added fixes for routing 2025-12-05 20:29:43 +03:00
f7bf3138ac Merge pull request 'balance cache' (#30) from balance-cache-#29 into main
All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
Reviewed-on: #30
2025-12-05 09:55:29 +00:00
Stephan D
7cb747f9a9 balance cache 2025-12-05 10:55:01 +01:00
f2658aea44 Merge pull request 'address book complete' (#28) from address-book-#16 into main
Some checks failed
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/mntx_gateway Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #28
2025-12-05 09:32:54 +00:00
Stephan D
5e49ee3244 address book complete 2025-12-05 10:27:55 +01:00
1073be187f Merge pull request 'fixed recipient storing problem' (#27) from address-book-#16 into main
All checks were successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #27
2025-12-05 08:38:31 +00:00
Stephan D
e854963fa6 fixed recipient storing problem 2025-12-05 09:37:51 +01:00
e5f283432b Merge pull request 'docker conflict resolved' (#26) from address-book-#16 into main
All checks were successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #26
2025-12-05 05:01:55 +00:00
Stephan D
d62a3413b2 docker conflict resolved 2025-12-05 06:01:23 +01:00
f720ba9bdf Merge pull request 'address-book-#16' (#25) from address-book-#16 into main
Some checks failed
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/fx_ingestor Pipeline failed
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/ledger Pipeline failed
ci/woodpecker/push/mntx_gateway Pipeline failed
ci/woodpecker/push/chain_gateway Pipeline failed
Reviewed-on: #25
2025-12-05 04:51:04 +00:00
Stephan D
98f254e34b docker conflict resolved 2025-12-05 05:50:34 +01:00
Stephan D
980bb96c74 relaxed healthcheck 2025-12-05 05:43:08 +01:00
4bb18f0210 Merge pull request 'fixed payment methods serialization deserialization' (#24) from address-book-#16 into main
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline failed
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline failed
ci/woodpecker/push/notification Pipeline failed
Reviewed-on: #24
2025-12-05 04:17:45 +00:00
Stephan D
574b40fe9f fixed payment methods serialization deserialization 2025-12-05 05:17:14 +01:00
a3a807e625 Merge pull request 'fixed port + recipient storing' (#23) from address-book-#16 into main
Some checks failed
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline failed
ci/woodpecker/push/ledger Pipeline failed
Reviewed-on: #23
2025-12-05 03:49:13 +00:00
Stephan D
3b047af7ca fixed port + recipient storing 2025-12-05 04:48:50 +01:00
36cc46577c Merge pull request 'recipient saving' (#22) from address-book-#16 into main
Some checks failed
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/mntx_gateway Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #22
2025-12-05 03:36:47 +00:00
Stephan D
e1da16448b recipient saving 2025-12-05 04:34:11 +01:00
fed6f39de6 Merge pull request 'address-book-#16' (#21) from address-book-#16 into main
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline failed
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #21
2025-12-05 02:41:11 +00:00
Stephan D
85fb567ed9 rewored monetix gateawy to port 8084 2025-12-05 03:39:59 +01:00
Stephan D
fd07c10cba version bump 2025-12-05 03:31:33 +01:00
57a48fe2a3 Merge pull request 'migration to address book service' (#20) from address-book-#16 into main
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline failed
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #20
2025-12-05 02:03:11 +00:00
d431317a50 Merge pull request 'migration to address book service' (#19) from address-book-#16 into main
Some checks failed
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/mntx_gateway Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/chain_gateway Pipeline failed
Reviewed-on: #19
2025-12-05 01:53:44 +00:00
4d03a6ead8 Merge pull request 'address-book-#16' (#18) from address-book-#16 into main
Some checks failed
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/mntx_gateway Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
Reviewed-on: #18
2025-12-05 01:32:54 +00:00
79 changed files with 1522 additions and 363 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ untranslated.txt
generate_protos.sh
update_dep.sh
.vscode/
GeneratedPluginRegistrant.swift

View File

@@ -55,3 +55,6 @@ key_management:
namespace: ""
mount_path: kv
key_prefix: gateway/chain/wallets
cache:
wallet_balance_ttl_seconds: 120

View File

@@ -54,7 +54,7 @@ require (
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -86,5 +86,5 @@ require (
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
)

View File

@@ -159,8 +159,8 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -362,8 +362,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -34,9 +34,10 @@ type Imp struct {
type config struct {
*grpcapp.Config `yaml:",inline"`
Chains []chainConfig `yaml:"chains"`
ServiceWallet serviceWalletConfig `yaml:"service_wallet"`
KeyManagement keymanager.Config `yaml:"key_management"`
Chains []chainConfig `yaml:"chains"`
ServiceWallet serviceWalletConfig `yaml:"service_wallet"`
KeyManagement keymanager.Config `yaml:"key_management"`
Settings gatewayservice.CacheSettings `yaml:"cache"`
}
type chainConfig struct {
@@ -111,11 +112,12 @@ func (i *Imp) Start() error {
gatewayservice.WithServiceWallet(walletConfig),
gatewayservice.WithKeyManager(keyManager),
gatewayservice.WithTransferExecutor(executor),
gatewayservice.WithSettings(cfg.Settings),
}
return gatewayservice.NewService(logger, repo, producer, opts...), nil
}
app, err := grpcapp.NewApp(i.logger, "chain_gateway", cfg.Config, i.debug, repoFactory, serviceFactory)
app, err := grpcapp.NewApp(i.logger, "chain", cfg.Config, i.debug, repoFactory, serviceFactory)
if err != nil {
return err
}

View File

@@ -4,7 +4,10 @@ import (
"context"
"errors"
"strings"
"time"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mservice"
@@ -14,6 +17,8 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)
const fallbackBalanceCacheTTL = 2 * time.Minute
type getWalletBalanceCommand struct {
deps Deps
}
@@ -48,30 +53,88 @@ func (c *getWalletBalanceCommand) Execute(ctx context.Context, req *chainv1.GetW
balance, chainErr := onChainWalletBalance(ctx, c.deps, wallet)
if chainErr != nil {
c.deps.Logger.Warn("on-chain balance fetch failed, falling back to stored balance", zap.Error(chainErr), zap.String("wallet_ref", walletRef))
c.deps.Logger.Warn("on-chain balance fetch failed, attempting cached balance", zap.Error(chainErr), zap.String("wallet_ref", walletRef))
stored, err := c.deps.Storage.Wallets().GetBalance(ctx, walletRef)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
c.deps.Logger.Warn("stored balance not found", zap.String("wallet_ref", walletRef))
return gsresponse.NotFound[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
c.deps.Logger.Warn("cached balance not found", zap.String("wallet_ref", walletRef))
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, chainErr)
}
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
}
if c.isCachedBalanceStale(stored) {
c.deps.Logger.Warn("cached balance is stale",
zap.String("wallet_ref", walletRef),
zap.Time("calculated_at", stored.CalculatedAt),
zap.Duration("ttl", c.cacheTTL()),
)
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, chainErr)
}
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(stored)})
}
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{Balance: onChainBalanceToProto(balance)})
calculatedAt := c.now()
c.persistCachedBalance(ctx, walletRef, balance, calculatedAt)
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{
Balance: onChainBalanceToProto(balance, calculatedAt),
})
}
func onChainBalanceToProto(balance *moneyv1.Money) *chainv1.WalletBalance {
func onChainBalanceToProto(balance *moneyv1.Money, calculatedAt time.Time) *chainv1.WalletBalance {
if balance == nil {
return nil
}
zero := &moneyv1.Money{Currency: balance.Currency, Amount: "0"}
zero := zeroMoney(balance.Currency)
return &chainv1.WalletBalance{
Available: balance,
PendingInbound: zero,
PendingOutbound: zero,
CalculatedAt: timestamppb.Now(),
CalculatedAt: timestamppb.New(calculatedAt.UTC()),
}
}
func (c *getWalletBalanceCommand) persistCachedBalance(ctx context.Context, walletRef string, available *moneyv1.Money, calculatedAt time.Time) {
if available == nil {
return
}
record := &model.WalletBalance{
WalletRef: walletRef,
Available: shared.CloneMoney(available),
PendingInbound: zeroMoney(available.Currency),
PendingOutbound: zeroMoney(available.Currency),
CalculatedAt: calculatedAt,
}
if err := c.deps.Storage.Wallets().SaveBalance(ctx, record); err != nil {
c.deps.Logger.Warn("failed to cache wallet balance", zap.String("wallet_ref", walletRef), zap.Error(err))
}
}
func (c *getWalletBalanceCommand) isCachedBalanceStale(balance *model.WalletBalance) bool {
if balance == nil || balance.CalculatedAt.IsZero() {
return true
}
return c.now().After(balance.CalculatedAt.Add(c.cacheTTL()))
}
func (c *getWalletBalanceCommand) cacheTTL() time.Duration {
if c.deps.BalanceCacheTTL > 0 {
return c.deps.BalanceCacheTTL
}
// Fallback to sane default if not configured.
return fallbackBalanceCacheTTL
}
func (c *getWalletBalanceCommand) now() time.Time {
if c.deps.Clock != nil {
return c.deps.Clock.Now().UTC()
}
return time.Now().UTC()
}
func zeroMoney(currency string) *moneyv1.Money {
if strings.TrimSpace(currency) == "" {
return nil
}
return &moneyv1.Money{Currency: currency, Amount: "0"}
}

View File

@@ -2,10 +2,12 @@ package wallet
import (
"context"
"time"
"github.com/tech/sendico/gateway/chain/internal/keymanager"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/gateway/chain/storage"
clockpkg "github.com/tech/sendico/pkg/clock"
"github.com/tech/sendico/pkg/mlogger"
)
@@ -14,6 +16,8 @@ type Deps struct {
Networks map[string]shared.Network
KeyManager keymanager.Manager
Storage storage.Repository
Clock clockpkg.Clock
BalanceCacheTTL time.Duration
EnsureRepository func(context.Context) error
}

View File

@@ -67,3 +67,10 @@ func WithClock(clk clockpkg.Clock) Option {
}
}
}
// WithSettings applies gateway settings.
func WithSettings(settings CacheSettings) Option {
return func(s *Service) {
s.settings = settings.withDefaults()
}
}

View File

@@ -36,6 +36,8 @@ type Service struct {
producer msg.Producer
clock clockpkg.Clock
settings CacheSettings
networks map[string]shared.Network
serviceWallet shared.ServiceWallet
keyManager keymanager.Manager
@@ -52,6 +54,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
storage: repo,
producer: producer,
clock: clockpkg.System{},
settings: defaultSettings(),
networks: map[string]shared.Network{},
}
@@ -69,6 +72,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
if svc.networks == nil {
svc.networks = map[string]shared.Network{}
}
svc.settings = svc.settings.withDefaults()
svc.commands = commands.NewRegistry(commands.RegistryDeps{
Wallet: commandsWalletDeps(svc),
@@ -130,6 +134,8 @@ func commandsWalletDeps(s *Service) wallet.Deps {
Networks: s.networks,
KeyManager: s.keyManager,
Storage: s.storage,
Clock: s.clock,
BalanceCacheTTL: s.settings.walletBalanceCacheTTL(),
EnsureRepository: s.ensureRepository,
}
}

View File

@@ -0,0 +1,30 @@
package gateway
import "time"
const defaultWalletBalanceCacheTTL = 120 * time.Second
// CacheSettings holds tunable gateway behaviour.
type CacheSettings struct {
WalletBalanceCacheTTLSeconds int `yaml:"wallet_balance_ttl_seconds"`
}
func defaultSettings() CacheSettings {
return CacheSettings{
WalletBalanceCacheTTLSeconds: int(defaultWalletBalanceCacheTTL.Seconds()),
}
}
func (s CacheSettings) withDefaults() CacheSettings {
if s.WalletBalanceCacheTTLSeconds <= 0 {
s.WalletBalanceCacheTTLSeconds = int(defaultWalletBalanceCacheTTL.Seconds())
}
return s
}
func (s CacheSettings) walletBalanceCacheTTL() time.Duration {
if s.WalletBalanceCacheTTLSeconds <= 0 {
return defaultWalletBalanceCacheTTL
}
return time.Duration(s.WalletBalanceCacheTTLSeconds) * time.Second
}

View File

@@ -13,5 +13,5 @@ func factory(logger mlogger.Logger, file string, debug bool) (server.Application
}
func main() {
smain.RunServer("main", appversion.Create(), factory)
smain.RunServer("gateway", appversion.Create(), factory)
}

View File

@@ -4,7 +4,7 @@ This service now supports Monetix “payout by card”.
## Runtime entry points
- gRPC: `MntxGatewayService.CreateCardPayout` and `GetCardPayoutStatus`.
- Callback HTTP server (default): `:8080/monetix/callback` for Monetix payout status notifications.
- Callback HTTP server (default): `:8084/monetix/callback` for Monetix payout status notifications.
- Metrics: Prometheus on `:9404/metrics`.
## Required config/env
@@ -13,7 +13,7 @@ This service now supports Monetix “payout by card”.
- `MONETIX_PROJECT_ID` integer project ID
- `MONETIX_SECRET_KEY` signature secret
- Optional: `allowed_currencies`, `require_customer_address`, `request_timeout_seconds`
- Callback server: `MNTX_GATEWAY_HTTP_PORT` (exposed as 8080), `http.callback.path`, optional `allowed_cidrs`
- Callback server: `MNTX_GATEWAY_HTTP_PORT` (exposed as 8084), `http.callback.path`, optional `allowed_cidrs`
## Outbound request (CreateCardPayout)
Payload is built per Monetix spec:

View File

@@ -34,7 +34,7 @@ monetix:
http:
callback:
address: ":8080"
address: ":8084"
path: "/monetix/callback"
allowed_cidrs: []
max_body_bytes: 1048576

View File

@@ -5,6 +5,7 @@ go 1.25.3
replace github.com/tech/sendico/pkg => ../../pkg
require (
github.com/go-chi/chi/v5 v5.2.3
github.com/google/uuid v1.6.0
github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0
@@ -22,7 +23,6 @@ require (
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect

View File

@@ -125,7 +125,7 @@ func (i *Imp) Start() error {
return svc, nil
}
app, err := grpcapp.NewApp(i.logger, "mntx_gateway", cfg.Config, i.debug, nil, serviceFactory)
app, err := grpcapp.NewApp(i.logger, "monetix", cfg.Config, i.debug, nil, serviceFactory)
if err != nil {
return err
}
@@ -163,7 +163,7 @@ func (i *Imp) loadConfig() (*config, error) {
}
if cfg.Metrics == nil {
cfg.Metrics = &grpcapp.MetricsConfig{Address: ":9404"}
cfg.Metrics = &grpcapp.MetricsConfig{Address: ":9405"}
}
return cfg, nil
@@ -226,7 +226,7 @@ type callbackRuntimeConfig struct {
func (i *Imp) resolveCallbackConfig(cfg callbackConfig) (callbackRuntimeConfig, error) {
addr := strings.TrimSpace(cfg.Address)
if addr == "" {
addr = ":8080"
addr = ":8084"
}
path := strings.TrimSpace(cfg.Path)
if path == "" {

View File

@@ -13,5 +13,5 @@ func factory(logger mlogger.Logger, file string, debug bool) (server.Application
}
func main() {
smain.RunServer("mntx_gateway", appversion.Create(), factory)
smain.RunServer("gateway", appversion.Create(), factory)
}

View File

@@ -27,7 +27,7 @@ require (
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -51,5 +51,5 @@ require (
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
)

View File

@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -214,8 +214,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -28,7 +28,7 @@ require (
github.com/go-test/deep v1.1.1 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
@@ -52,7 +52,7 @@ require (
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)

View File

@@ -65,8 +65,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -227,8 +227,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -5,10 +5,12 @@ import (
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/db/policy"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
)
type PaymentMethodsDB struct {
@@ -45,5 +47,13 @@ func Create(ctx context.Context,
getArchivable,
),
}
if err := res.DBImp.Repository.CreateIndex(&ri.Definition{
Keys: []ri.Key{{Field: "recipientRef", Sort: ri.Asc}},
}); err != nil {
res.DBImp.Logger.Error("Failed to create recipientRef index for payment methods", zap.Error(err))
return nil, err
}
return res, nil
}

View File

@@ -46,7 +46,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -93,6 +93,6 @@ require (
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -70,8 +70,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rH
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -269,8 +269,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -1,6 +1,7 @@
package model
import (
"bytes"
"encoding/json"
"fmt"
@@ -71,3 +72,73 @@ type PaymentMethod struct {
Data bson.Raw `bson:"data" json:"data"`
IsMain bool `bson:"isMain" json:"isMain"`
}
type paymentMethodJSON struct {
PermissionBound `json:",inline"`
Describable `json:",inline"`
RecipientRef primitive.ObjectID `json:"recipientRef"`
Type PaymentType `json:"type"`
Data json.RawMessage `json:"data"`
IsMain bool `json:"isMain"`
}
func (m PaymentMethod) MarshalJSON() ([]byte, error) {
var marshaledData json.RawMessage
if len(m.Data) > 0 {
var data bson.M
if err := bson.Unmarshal(m.Data, &data); err != nil {
return nil, err
}
if data != nil {
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
marshaledData = b
}
}
payload := paymentMethodJSON{
PermissionBound: m.PermissionBound,
Describable: m.Describable,
RecipientRef: m.RecipientRef,
Type: m.Type,
Data: marshaledData,
IsMain: m.IsMain,
}
return json.Marshal(payload)
}
func (m *PaymentMethod) UnmarshalJSON(data []byte) error {
var payload paymentMethodJSON
if err := json.Unmarshal(data, &payload); err != nil {
return err
}
m.PermissionBound = payload.PermissionBound
m.Describable = payload.Describable
m.RecipientRef = payload.RecipientRef
m.Type = payload.Type
m.IsMain = payload.IsMain
if len(payload.Data) == 0 || bytes.Equal(payload.Data, []byte("null")) {
m.Data = nil
return nil
}
var rawData map[string]any
if err := json.Unmarshal(payload.Data, &rawData); err != nil {
return err
}
raw, err := bson.Marshal(rawData)
if err != nil {
return err
}
m.Data = raw
return nil
}

View File

@@ -9,10 +9,10 @@ replace github.com/tech/sendico/ledger => ../ledger
replace github.com/tech/sendico/gateway/chain => ../gateway/chain
require (
github.com/aws/aws-sdk-go-v2 v1.40.0
github.com/aws/aws-sdk-go-v2/config v1.32.2
github.com/aws/aws-sdk-go-v2/credentials v1.19.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1
github.com/aws/aws-sdk-go-v2 v1.40.1
github.com/aws/aws-sdk-go-v2/config v1.32.3
github.com/aws/aws-sdk-go-v2/credentials v1.19.3
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/go-chi/jwtauth/v5 v5.3.3
@@ -43,21 +43,21 @@ require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 // indirect
github.com/aws/smithy-go v1.23.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.3 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@@ -79,7 +79,7 @@ require (
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
@@ -135,6 +135,6 @@ require (
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
)

View File

@@ -6,44 +6,44 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk=
github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4=
github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
github.com/aws/aws-sdk-go-v2 v1.40.1 h1:difXb4maDZkRH0x//Qkwcfpdg1XQVXEAEs2DdXldFFc=
github.com/aws/aws-sdk-go-v2 v1.40.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.3 h1:cpz7H2uMNTDa0h/5CYL5dLUEzPSLo2g0NkbxTRJtSSU=
github.com/aws/aws-sdk-go-v2/config v1.32.3/go.mod h1:srtPKaJJe3McW6T/+GMBZyIPc+SeqJsNPJsd4mOYZ6s=
github.com/aws/aws-sdk-go-v2/credentials v1.19.3 h1:01Ym72hK43hjwDeJUfi1l2oYLXBAOR8gNSZNmXmvuas=
github.com/aws/aws-sdk-go-v2/credentials v1.19.3/go.mod h1:55nWF/Sr9Zvls0bGnWkRxUdhzKqj9uRNlPvgV1vgxKc=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15 h1:utxLraaifrSBkeyII9mIbVwXXWrZdlPO7FIKmyLCEcY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15/go.mod h1:hW6zjYUDQwfz3icf4g2O41PHi77u10oAzJ84iSzR/lo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 h1:Y5YXgygXwDI5P4RkteB5yF7v35neH7LfJKBG+hzIons=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15/go.mod h1:K+/1EpG42dFSY7CBj+Fruzm8PsCGWTXJ3jdeJ659oGQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 h1:AvltKnW9ewxX2hFmQS0FyJH93aSvJVUEFvXfU+HWtSE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15/go.mod h1:3I4oCdZdmgrREhU74qS1dK9yZ62yumob+58AbFR4cQA=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 h1:OgQy/+0+Kc3khtqiEOk23xQAglXi3Tj0y5doOxbi5tg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15 h1:NLYTEyZmVZo0Qh183sC8nC+ydJXOOeIL/qI/sS3PdLY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.15/go.mod h1:Z803iB3B0bc8oJV8zH2PERLRfQUJ2n2BXISpsA4+O1M=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.6 h1:P1MU/SuhadGvg2jtviDXPEejU3jBNhoeeAlRadHzvHI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.6/go.mod h1:5KYaMG6wmVKMFBSfWoyG/zH8pWwzQFnKgpoSRlXHKdQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 h1:3/u/4yZOffg5jdNk1sDpOQ4Y+R6Xbh+GzpDrSZjuy3U=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15/go.mod h1:4Zkjq0FKjE78NKjabuM4tRXKFzUJWXgP0ItEZK8l7JU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15 h1:wsSQ4SVz5YE1crz0Ap7VBZrV4nNqZt4CIBBT8mnwoNc=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.15/go.mod h1:I7sditnFGtYMIqPRU1QoHZAUrXkGp4SczmlLwrNPlD0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0 h1:IrbE3B8O9pm3lsg96AXIN5MXX4pECEuExh/A0Du3AuI=
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.0/go.mod h1:/sJLzHtiiZvs6C1RbxS/anSAFwZD6oC6M/kotQzOiLw=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.3 h1:d/6xOGIllc/XW1lzG9a4AUBMmpLA9PXcQnVPTuHHcik=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.3/go.mod h1:fQ7E7Qj9GiW8y0ClD7cUJk3Bz5Iw8wZkWDHsTe8vDKs=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.6 h1:8sTTiw+9yuNXcfWeqKF2x01GqCF49CpP4Z9nKrrk/ts=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.6/go.mod h1:8WYg+Y40Sn3X2hioaaWAAIngndR8n1XFdRPPX+7QBaM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11 h1:E+KqWoVsSrj1tJ6I/fjDIu5xoS2Zacuu1zT+H7KtiIk=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11/go.mod h1:qyWHz+4lvkXcr3+PoGlGHEI+3DLLiU6/GdrFfMaAhB0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.3 h1:tzMkjh0yTChUqJDgGkcDdxvZDSrJ/WB6R6ymI5ehqJI=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.3/go.mod h1:T270C0R5sZNLbWUe8ueiAF42XSZxxPocTaGSgs5c/60=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
@@ -123,8 +123,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5uk
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -359,8 +359,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -66,11 +66,10 @@ MNTX_GATEWAY_DIR=mntx_gateway
MNTX_GATEWAY_COMPOSE_PROJECT=sendico-mntx-gateway
MNTX_GATEWAY_SERVICE_NAME=sendico_mntx_gateway
MNTX_GATEWAY_GRPC_PORT=50075
MNTX_GATEWAY_METRICS_PORT=9404
MNTX_GATEWAY_HTTP_PORT=8080
MNTX_GATEWAY_METRICS_PORT=9405
MNTX_GATEWAY_HTTP_PORT=8084
MONETIX_BASE_URL=https://api.txflux.com
# FX oracle stack
FX_ORACLE_DIR=fx_oracle
FX_ORACLE_COMPOSE_PROJECT=sendico-fx-oracle

View File

@@ -31,7 +31,7 @@ services:
- "0.0.0.0:${FRONTEND_HTTP_PORT}:80"
- "0.0.0.0:${FRONTEND_HTTPS_PORT}:443"
healthcheck:
test: ["CMD-SHELL","curl -sf http://localhost:80/ >/dev/null"]
test: ["CMD", "curl", "-f", "http://localhost:2019/config"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -37,6 +37,6 @@ COPY api/gateway/mntx/config.yml /app/config.yml
COPY api/gateway/mntx/entrypoint.sh /app/entrypoint.sh
COPY --from=build /out/mntx-gateway /app/mntx-gateway
RUN chmod +x /app/entrypoint.sh
EXPOSE 50075 9404 8080
EXPOSE 50075 9405 8084
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["/app/mntx-gateway","--config.file","/app/config.yml"]

View File

@@ -28,10 +28,10 @@ services:
command: ["--config.file", "/app/config.yml"]
ports:
- "0.0.0.0:${MNTX_GATEWAY_GRPC_PORT:-50075}:50075"
- "0.0.0.0:${MNTX_GATEWAY_METRICS_PORT:-9404}:9404"
- "0.0.0.0:${MNTX_GATEWAY_HTTP_PORT:-8080}:8080"
- "0.0.0.0:${MNTX_GATEWAY_METRICS_PORT:-9405}:9405"
- "0.0.0.0:${MNTX_GATEWAY_HTTP_PORT:-8084}:8084"
healthcheck:
test: ["CMD-SHELL","wget -qO- http://localhost:9404/health | grep -q '\"status\":\"ok\"'"]
test: ["CMD-SHELL","wget -qO- http://localhost:9405/health | grep -q '\"status\":\"ok\"'"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -4,3 +4,5 @@ import 'package:pshared/models/payment/type.dart';
abstract class PaymentMethodData {
PaymentType get type;
}
typedef MethodMap = Map<PaymentType, PaymentMethodData?>;

View File

@@ -84,14 +84,13 @@ Recipient newRecipient({
String? description,
String? avatarUrl,
bool isArchived = false,
}) =>
Recipient(
storable: newStorable(),
permissionBound: newPermissionBound(organizationBound: newOrganizationBound(organizationRef: organizationRef)),
describable: newDescribable(name: name, description: description),
email: email,
status: status,
type: type,
avatarUrl: avatarUrl,
isArchived: isArchived,
);
}) => Recipient(
storable: newStorable(),
permissionBound: newPermissionBound(organizationBound: newOrganizationBound(organizationRef: organizationRef)),
describable: newDescribable(name: name, description: description),
email: email,
status: status,
type: type,
avatarUrl: avatarUrl,
isArchived: isArchived,
);

View File

@@ -27,6 +27,7 @@ class AccountProvider extends ChangeNotifier {
Resource<Account?> get resource => _resource;
late LocaleProvider _localeProvider;
PendingLogin? _pendingLogin;
Future<void>? _restoreFuture;
Account? get account => _resource.data;
PendingLogin? get pendingLogin => _pendingLogin;
@@ -34,6 +35,7 @@ class AccountProvider extends ChangeNotifier {
bool get isLoading => _resource.isLoading;
Object? get error => _resource.error;
bool get isReady => (!isLoading) && (account != null);
Future<void>? get restoreFuture => _restoreFuture;
Account? currentUser() {
final acc = account;
@@ -220,4 +222,12 @@ class AccountProvider extends ChangeNotifier {
rethrow;
}
}
Future<void> restoreIfPossible() {
return _restoreFuture ??= () async {
final hasAuth = await AuthorizationService.isAuthorizationStored();
if (!hasAuth) return;
await restore();
}();
}
}

View File

@@ -1,7 +1,12 @@
import 'package:collection/collection.dart';
import 'package:pshared/data/mapper/payment/method.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/organization/bound.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/models/permissions/bound.dart';
import 'package:pshared/models/storable.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pshared/provider/template.dart';
@@ -49,6 +54,22 @@ class PaymentMethodsProvider extends GenericProvider<PaymentMethod> {
cascade: true,
);
Future<PaymentMethod> create({
required String reacipientRef,
required PaymentMethodData data,
required String name,
}) => createObject(
_organizations.current.id,
PaymentMethod(
storable: newStorable(),
permissionBound: newPermissionBound(
organizationBound: newOrganizationBound(organizationRef: _organizations.current.id),
),
recipientRef: reacipientRef,
data: data,
describable: newDescribable(name: name),
).toDTO().toJson(),
);
Future<void> makeMain(PaymentMethod method) {
// TODO: create separate backend method to manage main payment method

View File

@@ -1,7 +1,9 @@
import 'package:pshared/data/mapper/recipient/recipient.dart';
import 'package:pshared/models/recipient/filter.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/models/recipient/status.dart';
import 'package:pshared/models/recipient/type.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/template.dart';
import 'package:pshared/service/recipient/service.dart';
@@ -51,6 +53,20 @@ class RecipientsProvider extends GenericProvider<Recipient> {
notifyListeners();
}
Future<Recipient> create({
required String name,
required String email,
}) async => createObject(
_organizations.current.id,
newRecipient(
organizationRef: _organizations.current.id,
email: email,
name: name,
status: RecipientStatus.ready,
type: RecipientType.internal,
).toDTO().toJson(),
);
void updateProviders(OrganizationsProvider organizations) {
_organizations = organizations;
if (_organizations.isOrganizationSet) {

View File

@@ -43,7 +43,7 @@
}
handle @monetixSuccess {
rewrite * /monetix/callback
reverse_proxy sendico_mntx_gateway:8080
reverse_proxy sendico_mntx_gateway:8084
header Cache-Control "no-cache, no-store, must-revalidate"
}
@@ -54,7 +54,7 @@
}
handle @monetixFail {
rewrite * /monetix/callback
reverse_proxy sendico_mntx_gateway:8080
reverse_proxy sendico_mntx_gateway:8084
header Cache-Control "no-cache, no-store, must-revalidate"
}

View File

@@ -0,0 +1,112 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
class PayoutRoutes {
static const dashboard = 'dashboard';
static const sendPayout = payment;
static const recipients = 'payout-recipients';
static const addRecipient = 'payout-add-recipient';
static const payment = 'payout-payment';
static const settings = 'payout-settings';
static const reports = 'payout-reports';
static const methods = 'payout-methods';
static const editWallet = 'payout-edit-wallet';
static const walletTopUp = 'payout-wallet-top-up';
static const dashboardPath = '/dashboard';
static const recipientsPath = '/dashboard/recipients';
static const addRecipientPath = '/dashboard/recipients/add';
static const paymentPath = '/dashboard/payment';
static const settingsPath = '/dashboard/settings';
static const reportsPath = '/dashboard/reports';
static const methodsPath = '/dashboard/methods';
static const editWalletPath = '/dashboard/methods/edit';
static const walletTopUpPath = '/dashboard/wallet/top-up';
static String nameFor(PayoutDestination destination) {
switch (destination) {
case PayoutDestination.dashboard:
return dashboard;
case PayoutDestination.sendPayout:
return payment;
case PayoutDestination.recipients:
return recipients;
case PayoutDestination.addrecipient:
return addRecipient;
case PayoutDestination.payment:
return payment;
case PayoutDestination.settings:
return settings;
case PayoutDestination.reports:
return reports;
case PayoutDestination.methods:
return methods;
case PayoutDestination.editwallet:
return editWallet;
case PayoutDestination.walletTopUp:
return walletTopUp;
}
}
static String pathFor(PayoutDestination destination) {
switch (destination) {
case PayoutDestination.dashboard:
return dashboardPath;
case PayoutDestination.sendPayout:
return paymentPath;
case PayoutDestination.recipients:
return recipientsPath;
case PayoutDestination.addrecipient:
return addRecipientPath;
case PayoutDestination.payment:
return paymentPath;
case PayoutDestination.settings:
return settingsPath;
case PayoutDestination.reports:
return reportsPath;
case PayoutDestination.methods:
return methodsPath;
case PayoutDestination.editwallet:
return editWalletPath;
case PayoutDestination.walletTopUp:
return walletTopUpPath;
}
}
static PayoutDestination? destinationFor(String? routeName) {
switch (routeName) {
case dashboard:
return PayoutDestination.dashboard;
case sendPayout:
return PayoutDestination.payment;
case recipients:
return PayoutDestination.recipients;
case addRecipient:
return PayoutDestination.addrecipient;
case payment:
return PayoutDestination.payment;
case settings:
return PayoutDestination.settings;
case reports:
return PayoutDestination.reports;
case methods:
return PayoutDestination.methods;
case editWallet:
return PayoutDestination.editwallet;
case walletTopUp:
return PayoutDestination.walletTopUp;
default:
return null;
}
}
}
extension PayoutNavigation on BuildContext {
void goToPayout(PayoutDestination destination) => goNamed(PayoutRoutes.nameFor(destination));
void pushToPayout(PayoutDestination destination) => pushNamed(PayoutRoutes.nameFor(destination));
}

View File

@@ -0,0 +1,160 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/app/router/pages.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/pages/address_book/form/page.dart';
import 'package:pweb/pages/address_book/page/page.dart';
import 'package:pweb/pages/dashboard/dashboard.dart';
import 'package:pweb/pages/payment_methods/page.dart';
import 'package:pweb/pages/payout_page/page.dart';
import 'package:pweb/pages/payout_page/wallet/edit/page.dart';
import 'package:pweb/pages/report/page.dart';
import 'package:pweb/pages/settings/profile/page.dart';
import 'package:pweb/pages/wallet_top_up/page.dart';
import 'package:pweb/providers/page_selector.dart';
import 'package:pweb/widgets/error/snackbar.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/widgets/sidebar/page.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
RouteBase payoutShellRoute() => ShellRoute(
builder: (context, state, child) => PageSelector(
child: child,
routerState: state,
),
routes: [
GoRoute(
name: PayoutRoutes.dashboard,
path: routerPage(Pages.dashboard),
pageBuilder: (context, _) => NoTransitionPage(
child: DashboardPage(
onRecipientSelected: (recipient) => context
.read<PageSelectorProvider>()
.selectRecipient(context, recipient),
onGoToPaymentWithoutRecipient: (type) => context
.read<PageSelectorProvider>()
.startPaymentWithoutRecipient(context, type),
onTopUp: (wallet) => context
.read<PageSelectorProvider>()
.openWalletTopUp(context, wallet),
),
),
),
GoRoute(
name: PayoutRoutes.recipients,
path: PayoutRoutes.recipientsPath,
pageBuilder: (context, _) {
final loc = AppLocalizations.of(context)!;
return NoTransitionPage(
child: RecipientAddressBookPage(
onRecipientSelected: (recipient) => context
.read<PageSelectorProvider>()
.selectRecipient(context, recipient, fromList: true),
onAddRecipient: () => context
.read<PageSelectorProvider>()
.goToAddRecipient(context),
onEditRecipient: (recipient) => context
.read<PageSelectorProvider>()
.editRecipient(context, recipient, fromList: true),
onDeleteRecipient: (recipient) => executeActionWithNotification(
context: context,
action: () async =>
context.read<RecipientsProvider>().delete(recipient.id),
successMessage: loc.recipientDeletedSuccessfully,
errorMessage: loc.errorDeleteRecipient,
),
),
);
},
),
GoRoute(
name: PayoutRoutes.addRecipient,
path: PayoutRoutes.addRecipientPath,
pageBuilder: (context, _) {
final selector = context.read<PageSelectorProvider>();
final recipient = selector.recipientProvider.currentObject;
return NoTransitionPage(
child: AdressBookRecipientForm(
recipient: recipient,
onSaved: (_) => selector.selectPage(
context,
PayoutDestination.recipients,
),
),
);
},
),
GoRoute(
name: PayoutRoutes.payment,
path: PayoutRoutes.paymentPath,
pageBuilder: (context, _) => NoTransitionPage(
child: PaymentPage(
onBack: (_) => context
.read<PageSelectorProvider>()
.goBackFromPayment(context),
),
),
),
GoRoute(
name: PayoutRoutes.settings,
path: PayoutRoutes.settingsPath,
pageBuilder: (_, __) => const NoTransitionPage(
child: ProfileSettingsPage(),
),
),
GoRoute(
name: PayoutRoutes.reports,
path: PayoutRoutes.reportsPath,
pageBuilder: (_, __) => const NoTransitionPage(
child: OperationHistoryPage(),
),
),
GoRoute(
name: PayoutRoutes.methods,
path: PayoutRoutes.methodsPath,
pageBuilder: (context, _) => NoTransitionPage(
child: PaymentConfigPage(
onWalletTap: (wallet) => context
.read<PageSelectorProvider>()
.selectWallet(context, wallet),
),
),
),
GoRoute(
name: PayoutRoutes.editWallet,
path: PayoutRoutes.editWalletPath,
pageBuilder: (context, _) {
final provider = context.read<PageSelectorProvider>();
final wallet = provider.walletsProvider.selectedWallet;
final loc = AppLocalizations.of(context)!;
return NoTransitionPage(
child: wallet != null
? WalletEditPage(
onBack: () => provider.goBackFromWalletEdit(context),
)
: Center(child: Text(loc.noWalletSelected)),
);
},
),
GoRoute(
name: PayoutRoutes.walletTopUp,
path: PayoutRoutes.walletTopUpPath,
pageBuilder: (context, _) => NoTransitionPage(
child: WalletTopUpPage(
onBack: () => context
.read<PageSelectorProvider>()
.goBackFromWalletTopUp(context),
),
),
),
],
);

View File

@@ -1,13 +1,14 @@
import 'package:go_router/go_router.dart';
import 'package:pweb/app/router/pages.dart';
import 'package:pweb/app/router/page_params.dart';
import 'package:pweb/app/router/pages.dart';
import 'package:pweb/app/router/payout_shell.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/pages/2fa/page.dart';
import 'package:pweb/pages/errors/not_found.dart';
import 'package:pweb/pages/login/page.dart';
import 'package:pweb/pages/signup/page.dart';
import 'package:pweb/pages/verification/page.dart';
import 'package:pweb/widgets/sidebar/page.dart';
import 'package:pweb/pages/login/page.dart';
import 'package:pweb/pages/errors/not_found.dart';
GoRouter createRouter() => GoRouter(
@@ -16,40 +17,33 @@ GoRouter createRouter() => GoRouter(
GoRoute(
name: Pages.root.name,
path: routerPage(Pages.root),
builder: (_, _) => const LoginPage(),
routes: [
GoRoute(
name: Pages.login.name,
path: routerPage(Pages.login),
builder: (_, _) => const LoginPage(),
),
GoRoute(
name: Pages.dashboard.name,
path: routerPage(Pages.dashboard),
builder: (_, _) => const PageSelector(),
),
GoRoute(
name: Pages.sfactor.name,
path: routerPage(Pages.sfactor),
builder: (context, _) => TwoFactorCodePage(
onVerificationSuccess: () {
// trigger organization load
context.goNamed(Pages.dashboard.name);
},
),
),
GoRoute(
name: Pages.signup.name,
path: routerPage(Pages.signup),
builder: (_, _) => const SignUpPage(),
),
GoRoute(
name: Pages.verify.name,
path: '${routerPage(Pages.verify)}${routerAddParam(PageParams.token)}',
builder: (_, state) => AccountVerificationPage(token: state.pathParameters[PageParams.token.name]!),
),
],
builder: (_, __) => const LoginPage(),
),
GoRoute(
name: Pages.login.name,
path: routerPage(Pages.login),
builder: (_, __) => const LoginPage(),
),
GoRoute(
name: Pages.sfactor.name,
path: routerPage(Pages.sfactor),
builder: (context, _) => TwoFactorCodePage(
onVerificationSuccess: () => context.goNamed(PayoutRoutes.dashboard),
),
),
GoRoute(
name: Pages.signup.name,
path: routerPage(Pages.signup),
builder: (_, __) => const SignUpPage(),
),
GoRoute(
name: Pages.verify.name,
path: '${routerPage(Pages.verify)}${routerAddParam(PageParams.token)}',
builder: (_, state) => AccountVerificationPage(
token: state.pathParameters[PageParams.token.name]!,
),
),
payoutShellRoute(),
],
errorBuilder: (_, _) => const NotFoundPage(),
errorBuilder: (_, __) => const NotFoundPage(),
);

View File

@@ -21,6 +21,10 @@ extension WalletUiMapper on domain.WalletModel {
currency: currency,
isHidden: true,
calculatedAt: balance?.calculatedAt ?? DateTime.now(),
depositAddress: depositAddress,
network: asset.chain,
tokenSymbol: asset.tokenSymbol,
contractAddress: asset.contractAddress,
);
}
}

View File

@@ -429,6 +429,19 @@
"companyName": "Name of your company",
"companynameRequired": "Company name required",
"errorSaveRecipient": "Failed to save recipient",
"@errorSaveRecipient": {
"description": "Error message displayed when saving a recipient fails"
},
"recipientDeletedSuccessfully": "Recipient deleted successfully",
"@recipientDeletedSuccessfully": {
"description": "Success message displayed when a recipient is deleted"
},
"errorDeleteRecipient": "Failed to delete recipient",
"@errorDeleteRecipient": {
"description": "Error message displayed when deleting a recipient fails"
},
"errorSignUp": "Error occured while signing up, try again later",
"companyDescription": "Company Description",
"companyDescriptionHint": "Describe any of the fields of the Company's business",
@@ -456,6 +469,17 @@
"walletNameUpdateFailed": "Failed to update wallet name",
"walletNameSaved": "Wallet name saved",
"topUpBalance": "Top Up Balance",
"walletTopUpTitle": "Add funds to wallet",
"walletTopUpDetailsTitle": "Funding details",
"walletTopUpDescription": "Send funds to this address to increase your wallet balance.",
"walletTopUpAssetLabel": "Asset",
"walletTopUpNetworkLabel": "Network",
"walletTopUpAddressLabel": "Deposit address",
"walletTopUpQrLabel": "QR code for deposit",
"walletTopUpHint": "Only send funds on the specified network. Deposits may take a few minutes to confirm.",
"walletTopUpUnavailable": "Top-up details are unavailable for this wallet yet.",
"copyAddress": "Copy address",
"addressCopied": "Address copied",
"addFunctionality": "Add functionality",
"walletHistoryEmpty": "No history yet",
"colType": "Type",

View File

@@ -429,6 +429,19 @@
"companyName": "Название вашей компании",
"companynameRequired": "Необходимо указать название компании",
"errorSaveRecipient": "Не удалось сохранить получателя",
"@errorSaveRecipient": {
"description": "Сообщение об ошибке при неудачном сохранении получателя"
},
"recipientDeletedSuccessfully": "Получатель успешно удален",
"@recipientDeletedSuccessfully": {
"description": "Сообщение об успешном удалении получателя"
},
"errorDeleteRecipient": "Не удалось удалить получателя",
"@errorDeleteRecipient": {
"description": "Сообщение об ошибке при неудачном удалении получателя"
},
"errorSignUp": "Произошла ошибка при регистрации, попробуйте позже",
"companyDescription": "Описание компании",
"companyDescriptionHint": "Опишите любую из сфер деятельности компании",
@@ -457,6 +470,17 @@
"walletNameUpdateFailed": "Не удалось обновить название кошелька",
"walletNameSaved": "Название кошелька сохранено",
"topUpBalance": "Пополнить баланс",
"walletTopUpTitle": "Пополнение кошелька",
"walletTopUpDetailsTitle": "Данные для пополнения",
"walletTopUpDescription": "Отправьте средства на этот адрес, чтобы пополнить баланс кошелька.",
"walletTopUpAssetLabel": "Актив",
"walletTopUpNetworkLabel": "Сеть",
"walletTopUpAddressLabel": "Адрес для пополнения",
"walletTopUpQrLabel": "QR-код для пополнения",
"walletTopUpHint": "Отправляйте средства только в указанной сети. Подтверждение может занять несколько минут.",
"walletTopUpUnavailable": "Данные для пополнения пока недоступны для этого кошелька.",
"copyAddress": "Скопировать адрес",
"addressCopied": "Адрес скопирован",
"addFunctionality": "Добавить функциональность",
"walletHistoryEmpty": "История пуста",
"colType": "Тип",

View File

@@ -26,7 +26,6 @@ import 'package:pweb/providers/upload_history.dart';
import 'package:pweb/providers/wallets.dart';
import 'package:pweb/providers/wallet_transactions.dart';
// import 'package:pweb/services/amplitude.dart';
// import 'package:pweb/services/amplitude.dart';
import 'package:pweb/services/operations.dart';
import 'package:pweb/services/payments/history.dart';
import 'package:pweb/services/wallet_transactions.dart';

View File

@@ -9,6 +9,10 @@ class Wallet {
final Currency currency;
final bool isHidden;
final DateTime calculatedAt;
final String? depositAddress;
final String? network;
final String? tokenSymbol;
final String? contractAddress;
Wallet({
required this.id,
@@ -18,6 +22,10 @@ class Wallet {
required this.currency,
required this.calculatedAt,
this.isHidden = true,
this.depositAddress,
this.network,
this.tokenSymbol,
this.contractAddress,
});
Wallet copyWith({
@@ -27,6 +35,10 @@ class Wallet {
Currency? currency,
String? walletUserID,
bool? isHidden,
String? depositAddress,
String? network,
String? tokenSymbol,
String? contractAddress,
}) => Wallet(
id: id ?? this.id,
name: name ?? this.name,
@@ -35,5 +47,9 @@ class Wallet {
walletUserID: walletUserID ?? this.walletUserID,
isHidden: isHidden ?? this.isHidden,
calculatedAt: calculatedAt,
depositAddress: depositAddress ?? this.depositAddress,
network: network ?? this.network,
tokenSymbol: tokenSymbol ?? this.tokenSymbol,
contractAddress: contractAddress ?? this.contractAddress,
);
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/pages/payment_methods/form.dart';
@@ -9,8 +10,8 @@ import 'package:pweb/pages/payment_methods/icon.dart';
class AdressBookPaymentMethodTile extends StatefulWidget {
final PaymentType type;
final String title;
final Map<PaymentType, Object?> methods;
final ValueChanged<Object?> onChanged;
final MethodMap methods;
final ValueChanged<PaymentMethodData?> onChanged;
final double spacingM;
final double spacingS;

View File

@@ -1,16 +1,23 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/models/recipient/status.dart';
import 'package:pshared/models/recipient/type.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/address_book/form/view.dart';
// import 'package:pweb/services/amplitude.dart';
import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/utils/payment/label.dart';
import 'package:pweb/utils/snackbar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -31,7 +38,7 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
late TextEditingController _emailCtrl;
RecipientType _type = RecipientType.internal;
RecipientStatus _status = RecipientStatus.ready;
final Map<PaymentType, Object?> _methods = {};
final MethodMap _methods = {};
late PaymentMethodsProvider _methodsProvider;
Future<void> _loadMethods() async {
@@ -63,31 +70,54 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
_loadMethods();
}
Future<Recipient?> _doSave() async {
final recipients = context.read<RecipientsProvider>();
final methods = PaymentMethodsProvider();
final recipient = widget.recipient == null
? await recipients.create(
name: _nameCtrl.text,
email: _emailCtrl.text,
)
: widget.recipient!;
recipients.setCurrentObject(recipient.id);
//TODO : redesign work with recipients / payment methods
await methods.loadMethods(context.read<OrganizationsProvider>(), recipient.id);
for (final type in _methods.keys) {
final data = _methods[type]!;
final exising = methods.methods.firstWhereOrNull((m) => m.type == type);
if (exising != null) {
await methods.updateMethod(exising.copyWith(data: data));
} else {
await methods.create(
reacipientRef: recipient.id,
name: getPaymentTypeLabel(context, type),
data: data,
);
}
}
return recipient;
}
//TODO: Change when registration is ready
void _save() {
Future<void> _save() async {
if (!_formKey.currentState!.validate() || _methods.isEmpty) {
// AmplitudeService.recipientAddCompleted(
// _type,
// _status,
// _methods.keys.toSet(),
// );
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.recipientFormRule),
),
);
notifyUser(context, AppLocalizations.of(context)!.errorSaveRecipient);
return;
}
final recipient = newRecipient(
name: _nameCtrl.text,
email: _emailCtrl.text,
type: _type,
status: _status,
avatarUrl: null,
organizationRef: context.read<OrganizationsProvider>().current.id
// AmplitudeService.recipientAddCompleted(
// _type,
// _status,
// _methods.keys.toSet(),
// );
final recipient = await executeActionWithNotification(
context: context,
action: _doSave,
errorMessage: AppLocalizations.of(context)!.errorSaveRecipient,
successMessage: AppLocalizations.of(context)!.recipientFormRule,
);
widget.onSaved?.call(recipient);
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/status.dart';
@@ -20,10 +21,10 @@ class FormView extends StatelessWidget {
final TextEditingController emailCtrl;
final RecipientType type;
final RecipientStatus status;
final Map<PaymentType, Object?> methods;
final MethodMap methods;
final ValueChanged<RecipientType> onTypeChanged;
final ValueChanged<RecipientStatus> onStatusChanged;
final void Function(PaymentType, Object?) onMethodsChanged;
final void Function(PaymentType, PaymentMethodData?) onMethodsChanged;
final VoidCallback onSave;
final bool isEditing;
final VoidCallback onBack;

View File

@@ -42,10 +42,10 @@ class SaveButton extends StatelessWidget {
child: Text(
text ?? AppLocalizations.of(context)!.saveRecipient,
style: textStyle ??
theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.w600,
),
theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.w600,
),
),
),
),

View File

@@ -1,6 +1,11 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/permissions/action.dart' as perm;
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/models/resources.dart';
import 'package:pshared/provider/permissions.dart';
import 'package:pweb/pages/address_book/page/recipient/item.dart';
@@ -19,9 +24,16 @@ class RecipientAddressBookList extends StatelessWidget {
this.onDelete,
});
bool _checkPermissions(BuildContext context, Recipient recipient, perm.Action action) {
return context.read<PermissionsProvider>().canAccessResource(
ResourceType.recipients,
action: action,
objectRef: recipient.id,
);
}
@override
Widget build(BuildContext context) {
return ListView.builder(
Widget build(BuildContext context) => ListView.builder(
itemCount: filteredRecipients.length,
itemBuilder: (context, index) {
final recipient = filteredRecipients[index];
@@ -30,11 +42,14 @@ Widget build(BuildContext context) {
child: RecipientAddressBookItem(
recipient: recipient,
onTap: () => onSelected?.call(recipient),
onEdit: () => onEdit?.call(recipient),
onDelete: () => onDelete?.call(recipient),
onEdit: _checkPermissions(context, recipient, perm.Action.update)
? () => onEdit?.call(recipient)
: null,
onDelete: _checkPermissions(context, recipient, perm.Action.delete)
? () => onDelete?.call(recipient)
: null,
),
);
},
);
}
}

View File

@@ -18,12 +18,14 @@ class RecipientAddressBookPage extends StatefulWidget {
final ValueChanged<Recipient> onRecipientSelected;
final VoidCallback onAddRecipient;
final ValueChanged<Recipient>? onEditRecipient;
final ValueChanged<Recipient>? onDeleteRecipient;
const RecipientAddressBookPage({
super.key,
required this.onRecipientSelected,
required this.onAddRecipient,
this.onEditRecipient,
this.onDeleteRecipient,
});
static const double _expandedHeight = 550;
@@ -125,6 +127,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
child: RecipientAddressBookList(
filteredRecipients: provider.filteredRecipients,
onEdit: (recipient) => widget.onEditRecipient?.call(recipient),
onDelete: (recipient) => widget.onDeleteRecipient?.call(recipient),
onSelected: widget.onRecipientSelected,
),
),

View File

@@ -2,18 +2,22 @@ import 'package:flutter/material.dart';
class RecipientActions extends StatelessWidget {
final VoidCallback onEdit;
final VoidCallback onDelete;
final VoidCallback? onEdit;
final VoidCallback? onDelete;
const RecipientActions({super.key, required this.onEdit, required this.onDelete});
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(icon: Icon(Icons.edit, color: Theme.of(context).colorScheme.primary), onPressed: onEdit),
IconButton(icon: Icon(Icons.delete, color: Theme.of(context).colorScheme.error), onPressed: onDelete),
],
);
}
Widget build(BuildContext context) => Row(
children: [
IconButton(
icon: Icon(Icons.edit, color: Theme.of(context).colorScheme.primary),
onPressed: onEdit,
),
IconButton(
icon: Icon(Icons.delete, color: Theme.of(context).colorScheme.error),
onPressed: onDelete,
),
],
);
}

View File

@@ -12,8 +12,8 @@ import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
class RecipientAddressBookItem extends StatefulWidget {
final Recipient recipient;
final VoidCallback onTap;
final VoidCallback onEdit;
final VoidCallback onDelete;
final VoidCallback? onEdit;
final VoidCallback? onDelete;
final double borderRadius;
final double elevation;
@@ -82,7 +82,9 @@ class _RecipientAddressBookItemState extends State<RecipientAddressBookItem> {
),
if (_isHovered)
RecipientActions(
onEdit: widget.onEdit, onDelete: widget.onDelete),
onEdit: widget.onEdit,
onDelete: widget.onDelete,
),
],
),
SizedBox(height: widget.spacingBottom),

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pweb/models/wallet.dart';
import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart';
import 'package:pweb/providers/wallets.dart';
@@ -9,7 +10,9 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class BalanceWidget extends StatelessWidget {
const BalanceWidget({super.key});
final ValueChanged<Wallet> onTopUp;
const BalanceWidget({super.key, required this.onTopUp});
@override
Widget build(BuildContext context) {
@@ -30,6 +33,7 @@ class BalanceWidget extends StatelessWidget {
WalletCarousel(
wallets: wallets,
onWalletChanged: walletsProvider.selectWallet,
onTopUp: onTopUp,
);
}
}

View File

@@ -12,10 +12,12 @@ import 'package:pweb/providers/wallets.dart';
class WalletCard extends StatelessWidget {
final Wallet wallet;
final VoidCallback onTopUp;
const WalletCard({
super.key,
required this.wallet,
required this.onTopUp,
});
@override
@@ -43,7 +45,7 @@ class WalletCard extends StatelessWidget {
),
BalanceAddFunds(
onTopUp: () {
// TODO: Implement top-up functionality
onTopUp();
},
),
],

View File

@@ -12,11 +12,13 @@ import 'package:pweb/providers/carousel.dart';
class WalletCarousel extends StatefulWidget {
final List<Wallet> wallets;
final ValueChanged<Wallet> onWalletChanged;
final ValueChanged<Wallet> onTopUp;
const WalletCarousel({
super.key,
required this.wallets,
required this.onWalletChanged,
required this.onTopUp,
});
@override
@@ -33,6 +35,11 @@ class _WalletCarouselState extends State<WalletCarousel> {
_pageController = PageController(
viewportFraction: WalletCardConfig.viewportFraction,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.wallets.isNotEmpty) {
widget.onWalletChanged(widget.wallets[_currentPage]);
}
});
}
@override
@@ -83,7 +90,10 @@ class _WalletCarouselState extends State<WalletCarousel> {
itemBuilder: (context, index) {
return Padding(
padding: WalletCardConfig.cardPadding,
child: WalletCard(wallet: widget.wallets[index]),
child: WalletCard(
wallet: widget.wallets[index],
onTopUp: () => widget.onTopUp(widget.wallets[index]),
),
);
},
),

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/models/wallet.dart';
import 'package:pweb/pages/dashboard/buttons/balance/balance.dart';
import 'package:pweb/pages/dashboard/buttons/buttons.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/title.dart';
@@ -22,11 +23,13 @@ class AppSpacing {
class DashboardPage extends StatefulWidget {
final ValueChanged<Recipient> onRecipientSelected;
final void Function(PaymentType type) onGoToPaymentWithoutRecipient;
final ValueChanged<Wallet> onTopUp;
const DashboardPage({
super.key,
required this.onRecipientSelected,
required this.onGoToPaymentWithoutRecipient,
required this.onTopUp,
});
@override
@@ -75,7 +78,9 @@ class _DashboardPageState extends State<DashboardPage> {
],
),
const SizedBox(height: AppSpacing.medium),
BalanceWidget(),
BalanceWidget(
onTopUp: widget.onTopUp,
),
const SizedBox(height: AppSpacing.small),
if (_showContainerMultiple) TitleMultiplePayout(),
const SizedBox(height: AppSpacing.medium),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/pages/payment_methods/form.dart';
@@ -12,7 +13,7 @@ class PaymentDetailsSection extends StatelessWidget {
final bool isEditable;
final VoidCallback? onToggle;
final PaymentType? selectedType;
final Object? data;
final PaymentMethodData? data;
const PaymentDetailsSection({
super.key,
@@ -50,9 +51,7 @@ class PaymentDetailsSection extends StatelessWidget {
const SizedBox(height: toggleSpacing),
AnimatedCrossFade(
duration: animationDuration,
crossFadeState: isFormVisible
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
crossFadeState: isFormVisible ? CrossFadeState.showFirst : CrossFadeState.showSecond,
firstChild: PaymentMethodForm(
key: const ValueKey('formVisible'),
isEditable: isEditable,

View File

@@ -17,15 +17,34 @@ class AccountLoader extends StatelessWidget {
@override
Widget build(BuildContext context) => Consumer<AccountProvider>(builder: (context, provider, _) {
if (provider.isLoading) return const Center(child: CircularProgressIndicator());
if (provider.error != null) {
postNotifyUserOfErrorX(
context: context,
errorSituation: AppLocalizations.of(context)!.errorLogin,
exception: provider.error!,
);
navigateAndReplace(context, Pages.login);
if (provider.account != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
});
return child;
}
if (provider.error != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
postNotifyUserOfErrorX(
context: context,
errorSituation: AppLocalizations.of(context)!.errorLogin,
exception: provider.error!,
);
navigateAndReplace(context, Pages.login);
});
return const Center(child: CircularProgressIndicator());
}
if (provider.restoreFuture == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
provider.restoreIfPossible().catchError((error, stack) {
debugPrint('Account restore failed: $error');
});
});
return const Center(child: CircularProgressIndicator());
}
if (provider.isLoading) return const Center(child: CircularProgressIndicator());
if (provider.account == null) {
WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login));
return const Center(child: CircularProgressIndicator());

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/card.dart';
import 'package:pshared/models/payment/methods/crypto_address.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/iban.dart';
import 'package:pshared/models/payment/methods/russian_bank.dart';
import 'package:pshared/models/payment/methods/wallet.dart';
@@ -16,8 +17,8 @@ import 'package:pweb/pages/payment_methods/add/wallet.dart';
class PaymentMethodForm extends StatelessWidget {
final PaymentType? selectedType;
final ValueChanged<Object?> onChanged;
final Object? initialData;
final ValueChanged<PaymentMethodData?> onChanged;
final PaymentMethodData? initialData;
final bool isEditable;
const PaymentMethodForm({

View File

@@ -62,7 +62,7 @@ class _PaymentPageState extends State<PaymentPage> {
final recipientProvider = context.read<RecipientsProvider>();
recipientProvider.setCurrentObject(recipient.id);
pageSelector.selectRecipient(recipient);
pageSelector.selectRecipient(context, recipient);
_flowProvider.reset(pageSelector);
_clearSearchField();
}
@@ -72,7 +72,7 @@ class _PaymentPageState extends State<PaymentPage> {
final recipientProvider = context.read<RecipientsProvider>();
recipientProvider.setCurrentObject(null);
pageSelector.selectRecipient(null);
pageSelector.selectRecipient(context, null);
_flowProvider.reset(pageSelector);
_clearSearchField();
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
@@ -31,9 +32,9 @@ class PaymentInfoSection extends StatelessWidget {
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final hasRecipient = recipient != null;
final availableTypes = hasRecipient
final MethodMap availableTypes = hasRecipient
? pageSelector.getAvailablePaymentTypes()
: {for (final type in PaymentType.values) type: type};
: {for (final type in PaymentType.values) type: null};
if (hasRecipient && availableTypes.isEmpty) {
return Text(loc.recipientNoPaymentDetails);

View File

@@ -141,7 +141,7 @@ class PaymentBackButton extends StatelessWidget {
if (onBack != null) {
onBack!(pageSelector.selectedRecipient);
} else {
pageSelector.goBackFromPayment();
pageSelector.goBackFromPayment(context);
}
},
),

View File

@@ -25,7 +25,7 @@ class SendPayoutButton extends StatelessWidget {
final wallet = walletsProvider.selectedWallet;
if (wallet != null) {
pageSelectorProvider.startPaymentFromWallet(wallet);
pageSelectorProvider.startPaymentFromWallet(context, wallet);
}
},
child: Text(loc.payoutNavSendPayout),

View File

@@ -1,5 +1,9 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pweb/providers/page_selector.dart';
import 'package:pweb/providers/wallets.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -15,9 +19,14 @@ class TopUpButton extends StatelessWidget{
elevation: 0,
),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(loc.addFunctionality)),
);
final wallet = context.read<WalletsProvider>().selectedWallet;
if (wallet == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(loc.noWalletSelected)),
);
return;
}
context.read<PageSelectorProvider>().openWalletTopUp(context, wallet);
},
child: Text(loc.topUpBalance),
);

View File

@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class WalletTopUpAddressBlock extends StatelessWidget {
final String address;
final AppDimensions dimensions;
const WalletTopUpAddressBlock({
super.key,
required this.address,
required this.dimensions,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
loc.walletTopUpAddressLabel,
style: theme.textTheme.titleSmall,
),
TextButton.icon(
icon: const Icon(Icons.copy, size: 16),
label: Text(loc.copyAddress),
onPressed: () {
Clipboard.setData(ClipboardData(text: address));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(loc.addressCopied)),
);
},
),
],
),
const SizedBox(height: 6),
Container(
width: double.infinity,
padding: EdgeInsets.all(dimensions.paddingMedium),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
border: Border.all(color: theme.colorScheme.outlineVariant),
color: theme.colorScheme.surfaceVariant.withOpacity(0.4),
),
child: SelectableText(
address,
style: theme.textTheme.bodyLarge?.copyWith(
fontFeatures: const [FontFeature.tabularFigures()],
),
),
),
SizedBox(height: dimensions.paddingLarge),
Text(
loc.walletTopUpQrLabel,
style: theme.textTheme.titleSmall,
),
const SizedBox(height: 8),
Container(
padding: EdgeInsets.all(dimensions.paddingMedium),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
border: Border.all(color: theme.colorScheme.outlineVariant),
color: theme.colorScheme.surfaceVariant.withOpacity(0.4),
),
child: Center(
child: QrImageView(
data: address,
backgroundColor: theme.colorScheme.onSecondary,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: theme.colorScheme.onSurface,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: theme.colorScheme.onSurface,
),
size: 220,
),
),
),
],
);
}
}

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:pweb/models/wallet.dart';
import 'package:pweb/pages/wallet_top_up/details.dart';
import 'package:pweb/pages/wallet_top_up/header.dart';
import 'package:pweb/pages/wallet_top_up/meta.dart';
import 'package:pweb/utils/currency.dart';
import 'package:pweb/utils/dimensions.dart';
class WalletTopUpContent extends StatelessWidget {
final Wallet wallet;
final VoidCallback onBack;
const WalletTopUpContent({
super.key,
required this.wallet,
required this.onBack,
});
@override
Widget build(BuildContext context) {
final dimensions = AppDimensions();
final theme = Theme.of(context);
final address = _resolveAddress(wallet);
final network = wallet.network?.trim();
final assetLabel = wallet.tokenSymbol ?? currencyCodeToSymbol(wallet.currency);
return Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 960),
child: Padding(
padding: EdgeInsets.symmetric(vertical: dimensions.paddingLarge),
child: Material(
elevation: dimensions.elevationSmall,
color: theme.colorScheme.onSecondary,
borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium),
child: Padding(
padding: EdgeInsets.all(dimensions.paddingXLarge),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
WalletTopUpHeader(
onBack: onBack,
walletName: wallet.name,
),
SizedBox(height: dimensions.paddingLarge),
WalletTopUpMeta(
assetLabel: assetLabel,
network: network,
walletId: wallet.walletUserID,
),
SizedBox(height: dimensions.paddingXLarge),
WalletTopUpDetails(
address: address,
dimensions: dimensions,
),
],
),
),
),
),
),
),
);
}
String? _resolveAddress(Wallet wallet) {
final candidate = wallet.depositAddress?.trim();
if (candidate == null || candidate.isEmpty) return null;
return candidate;
}
}

View File

@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/wallet_top_up/address_block.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class WalletTopUpDetails extends StatelessWidget {
final String? address;
final AppDimensions dimensions;
const WalletTopUpDetails({
super.key,
required this.address,
required this.dimensions,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.walletTopUpDetailsTitle,
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 6),
Text(
loc.walletTopUpDescription,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
SizedBox(height: dimensions.paddingLarge),
if (address == null || address!.isEmpty)
Text(
loc.walletTopUpUnavailable,
style: theme.textTheme.bodyMedium,
)
else ...[
WalletTopUpAddressBlock(
address: address!,
dimensions: dimensions,
),
SizedBox(height: dimensions.paddingLarge),
Text(
loc.walletTopUpHint,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
],
);
}
}

View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class WalletTopUpHeader extends StatelessWidget {
final VoidCallback onBack;
final String walletName;
const WalletTopUpHeader({
super.key,
required this.onBack,
required this.walletName,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!;
return Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: onBack,
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.walletTopUpTitle,
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(
walletName,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
],
);
}
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:pweb/utils/dimensions.dart';
class WalletTopUpInfoChip extends StatelessWidget {
final String label;
final String value;
const WalletTopUpInfoChip({
super.key,
required this.label,
required this.value,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dimensions = AppDimensions();
return Container(
padding: EdgeInsets.all(dimensions.paddingMedium),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall),
border: Border.all(color: theme.colorScheme.outlineVariant),
color: theme.colorScheme.surfaceVariant.withOpacity(0.4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: theme.textTheme.labelMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
value,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/wallet_top_up/info_chip.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class WalletTopUpMeta extends StatelessWidget {
final String assetLabel;
final String walletId;
final String? network;
const WalletTopUpMeta({
super.key,
required this.assetLabel,
required this.walletId,
this.network,
});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final dimensions = AppDimensions();
return Wrap(
spacing: dimensions.paddingLarge,
runSpacing: dimensions.paddingLarge,
children: [
WalletTopUpInfoChip(label: loc.walletTopUpAssetLabel, value: assetLabel),
if (network != null && network!.isNotEmpty)
WalletTopUpInfoChip(label: loc.walletTopUpNetworkLabel, value: network!),
WalletTopUpInfoChip(label: loc.walletId, value: walletId),
],
);
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pweb/pages/wallet_top_up/content.dart';
import 'package:pweb/providers/wallets.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class WalletTopUpPage extends StatelessWidget {
final VoidCallback onBack;
const WalletTopUpPage({super.key, required this.onBack});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
return Consumer<WalletsProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.error != null) {
return Center(
child: Text(loc.notificationError(provider.error.toString())),
);
}
final wallet = provider.selectedWallet;
if (wallet == null) {
return Center(child: Text(loc.noWalletSelected));
}
return WalletTopUpContent(
wallet: wallet,
onBack: onBack,
);
},
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/methods/type.dart';
import 'package:pshared/models/payment/type.dart';
@@ -11,6 +12,7 @@ import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/models/wallet.dart';
import 'package:pweb/providers/wallets.dart';
//import 'package:pweb/services/amplitude.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
@@ -40,44 +42,93 @@ class PageSelectorProvider extends ChangeNotifier {
methodsProvider = methodsProv;
}
void selectPage(PayoutDestination dest) {
_selected = dest;
void syncDestination(PayoutDestination destination) {
if (_selected == destination) return;
_selected = destination;
notifyListeners();
}
void selectRecipient(Recipient? recipient, {bool fromList = false}) {
void selectPage(
BuildContext context,
PayoutDestination dest, {
bool replace = true,
}) {
_selected = dest;
notifyListeners();
_navigateTo(context, dest, replace: replace);
}
void selectRecipient(
BuildContext context,
Recipient? recipient, {
bool fromList = false,
}) {
final previousDestination = _selected;
recipientProvider.setCurrentObject(recipient?.id);
_cameFromRecipientList = fromList;
_setPreviousDestination();
_selected = PayoutDestination.payment;
notifyListeners();
if (previousDestination != PayoutDestination.payment) {
_navigateTo(context, PayoutDestination.payment, replace: false);
}
}
void editRecipient(Recipient? recipient, {bool fromList = false}) {
void editRecipient(
BuildContext context,
Recipient? recipient, {
bool fromList = false,
}) {
final previousDestination = _selected;
recipientProvider.setCurrentObject(recipient?.id);
_cameFromRecipientList = fromList;
_selected = PayoutDestination.addrecipient;
notifyListeners();
if (previousDestination != PayoutDestination.addrecipient) {
_navigateTo(context, PayoutDestination.addrecipient, replace: false);
}
}
void goToAddRecipient() {
void goToAddRecipient(BuildContext context) {
// AmplitudeService.recipientAddStarted();
final previousDestination = _selected;
recipientProvider.setCurrentObject(null);
_selected = PayoutDestination.addrecipient;
_cameFromRecipientList = false;
notifyListeners();
if (previousDestination != PayoutDestination.addrecipient) {
_navigateTo(context, PayoutDestination.addrecipient, replace: false);
}
}
void startPaymentWithoutRecipient(PaymentType type) {
void startPaymentWithoutRecipient(
BuildContext context,
PaymentType type,
) {
final previousDestination = _selected;
recipientProvider.setCurrentObject(null);
_type = type;
_cameFromRecipientList = false;
_setPreviousDestination();
_selected = PayoutDestination.payment;
notifyListeners();
if (previousDestination != PayoutDestination.payment) {
_navigateTo(context, PayoutDestination.payment, replace: false);
}
}
void goBackFromPayment() {
void goBackFromPayment(BuildContext context) {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
_navigateTo(
context,
_previousDestination ??
(_cameFromRecipientList
? PayoutDestination.recipients
: PayoutDestination.dashboard),
);
}
_selected = _previousDestination ??
(_cameFromRecipientList
? PayoutDestination.recipients
@@ -88,22 +139,55 @@ class PageSelectorProvider extends ChangeNotifier {
notifyListeners();
}
void goBackFromWalletEdit() {
selectPage(PayoutDestination.methods);
void goBackFromWalletEdit(BuildContext context) {
selectPage(context, PayoutDestination.methods);
}
void selectWallet(Wallet wallet) {
void selectWallet(BuildContext context, Wallet wallet) {
final previousDestination = _selected;
walletsProvider.selectWallet(wallet);
_selected = PayoutDestination.editwallet;
notifyListeners();
if (previousDestination != PayoutDestination.editwallet) {
_navigateTo(context, PayoutDestination.editwallet, replace: false);
}
}
void startPaymentFromWallet(Wallet wallet) {
void startPaymentFromWallet(BuildContext context, Wallet wallet) {
final previousDestination = _selected;
_type = PaymentType.wallet;
_cameFromRecipientList = false;
_setPreviousDestination();
_selected = PayoutDestination.payment;
notifyListeners();
if (previousDestination != PayoutDestination.payment) {
_navigateTo(context, PayoutDestination.payment, replace: false);
}
}
void openWalletTopUp(BuildContext context, Wallet wallet) {
final previousDestination = _selected;
_setPreviousDestination();
walletsProvider.selectWallet(wallet);
_selected = PayoutDestination.walletTopUp;
notifyListeners();
if (previousDestination != PayoutDestination.walletTopUp) {
_navigateTo(context, PayoutDestination.walletTopUp, replace: false);
}
}
void goBackFromWalletTopUp(BuildContext context) {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
_navigateTo(
context,
_previousDestination ?? PayoutDestination.dashboard,
);
}
_selected = _previousDestination ?? PayoutDestination.dashboard;
_previousDestination = null;
notifyListeners();
}
PaymentMethod? getPaymentMethodForWallet(Wallet wallet) {
@@ -112,12 +196,11 @@ class PageSelectorProvider extends ChangeNotifier {
}
return methodsProvider.methods.firstWhereOrNull(
(method) => method.type == PaymentType.wallet &&
(method.description?.contains(wallet.walletUserID) ?? false),
(method) => method.type == PaymentType.wallet && (method.description?.contains(wallet.walletUserID) ?? false),
);
}
Map<PaymentType, Object> getAvailablePaymentTypes() {
MethodMap getAvailablePaymentTypes() {
final recipient = selectedRecipient;
if ((recipient == null) || !methodsProvider.isReady) return {};
@@ -158,11 +241,24 @@ class PageSelectorProvider extends ChangeNotifier {
}
void _setPreviousDestination() {
if (_selected != PayoutDestination.payment) {
if (_selected != PayoutDestination.payment &&
_selected != PayoutDestination.walletTopUp) {
_previousDestination = _selected;
}
}
void _navigateTo(
BuildContext context,
PayoutDestination destination, {
bool replace = true,
}) {
if (replace) {
context.goToPayout(destination);
} else {
context.pushToPayout(destination);
}
}
Recipient? get selectedRecipient => recipientProvider.currentObject;
Wallet? get selectedWallet => walletsProvider.selectedWallet;
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
@@ -8,14 +9,14 @@ import 'package:pweb/providers/page_selector.dart';
class PaymentFlowProvider extends ChangeNotifier {
PaymentType _selectedType;
Object? _manualPaymentData;
PaymentMethodData? _manualPaymentData;
PaymentFlowProvider({
required PaymentType initialType,
}) : _selectedType = initialType;
PaymentType get selectedType => _selectedType;
Object? get manualPaymentData => _manualPaymentData;
PaymentMethodData? get manualPaymentData => _manualPaymentData;
void syncWithSelector(PageSelectorProvider selector) {
final recipient = selector.selectedRecipient;
@@ -53,7 +54,7 @@ class PaymentFlowProvider extends ChangeNotifier {
notifyListeners();
}
void setManualPaymentData(Object? data) {
void setManualPaymentData(PaymentMethodData? data) {
_manualPaymentData = data;
notifyListeners();
}

View File

@@ -1,6 +1,5 @@
import 'package:pshared/service/wallet.dart' as shared_wallet_service;
import 'package:pweb/models/currency.dart';
import 'package:pweb/models/wallet.dart';
import 'package:pweb/data/mappers/wallet_ui.dart';
@@ -10,27 +9,6 @@ abstract class WalletsService {
Future<double> getBalance(String organizationRef, String walletRef);
}
class MockWalletsService implements WalletsService {
final List<Wallet> _wallets = [
Wallet(id: '1124', walletUserID: 'WA-12345667', name: 'Main Wallet', balance: 10000000.0, currency: Currency.rub, calculatedAt: DateTime.now()),
Wallet(id: '2124', walletUserID: 'WA-76654321', name: 'Savings', balance: 2500.5, currency: Currency.usd, calculatedAt: DateTime.now()),
];
@override
Future<List<Wallet>> getWallets(String _) async {
return _wallets;
}
@override
Future<double> getBalance(String _, String walletRef) async {
final wallet = _wallets.firstWhere(
(w) => w.id == walletRef,
orElse: () => throw Exception('Wallet not found'),
);
return wallet.balance;
}
}
class ApiWalletsService implements WalletsService {
@override
Future<List<Wallet>> getWallets(String organizationRef) async {

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pshared/utils/snackbar.dart';
import 'package:pweb/utils/error/handler.dart';
import 'package:pweb/widgets/error/content.dart';
@@ -52,13 +53,18 @@ Future<T?> executeActionWithNotification<T>({
required BuildContext context,
required Future<T> Function() action,
required String errorMessage,
String? successMessage,
int delaySeconds = 3,
}) async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final localizations = AppLocalizations.of(context)!;
try {
return await action();
final result = await action();
if (successMessage != null) {
notifyUser(context, successMessage, delaySeconds: delaySeconds);
}
return result;
} catch (e) {
// Report the error using your existing notifier.
notifyUserOfErrorX(

View File

@@ -1,12 +1,13 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/utils/payment/label.dart';
class PaymentTypeSelector extends StatelessWidget {
final Map<PaymentType, Object> availableTypes;
final MethodMap availableTypes;
final PaymentType selectedType;
final ValueChanged<PaymentType> onSelected;

View File

@@ -12,7 +12,8 @@ enum PayoutDestination {
methods(Icons.credit_card, 'methods'),
payment(Icons.payment, 'payout'),
addrecipient(Icons.app_registration, 'add recipient'),
editwallet(Icons.wallet, 'edit wallet');
editwallet(Icons.wallet, 'edit wallet'),
walletTopUp(Icons.qr_code_2_outlined, 'wallet top up');
const PayoutDestination(this.icon, this.labelKey);
@@ -41,6 +42,8 @@ enum PayoutDestination {
return loc.addRecipient;
case PayoutDestination.editwallet:
return loc.editWallet;
case PayoutDestination.walletTopUp:
return loc.walletTopUpTitle;
}
}
}

View File

@@ -2,114 +2,61 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:pshared/models/resources.dart';
import 'package:pshared/provider/permissions.dart';
import 'package:pweb/pages/address_book/form/page.dart';
import 'package:pweb/pages/address_book/page/page.dart';
import 'package:pweb/pages/loader.dart';
import 'package:pweb/pages/payment_methods/page.dart';
import 'package:pweb/pages/payout_page/page.dart';
import 'package:pweb/pages/payout_page/wallet/edit/page.dart';
import 'package:pweb/pages/report/page.dart';
import 'package:pweb/pages/settings/profile/page.dart';
import 'package:pweb/pages/dashboard/dashboard.dart';
import 'package:pweb/providers/page_selector.dart';
import 'package:pweb/utils/logout.dart';
import 'package:pweb/widgets/appbar/app_bar.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/widgets/sidebar/sidebar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
import 'package:pweb/app/router/payout_routes.dart';
class PageSelector extends StatelessWidget {
const PageSelector({super.key});
final Widget child;
final GoRouterState routerState;
const PageSelector({
super.key,
required this.child,
required this.routerState,
});
@override
Widget build(BuildContext context) => PageViewLoader(
child: Builder(builder: (BuildContext context) {
final permissions = context.watch<PermissionsProvider>();
final permissions = context.read<PermissionsProvider>();
if (!permissions.isReady) return Center(child: CircularProgressIndicator());
final provider = context.watch<PageSelectorProvider>();
final loc = AppLocalizations.of(context)!;
final bool restrictedAccess = !permissions.canRead(ResourceType.chainWallets);
final allowedDestinations = restrictedAccess
? <PayoutDestination>{
PayoutDestination.settings,
PayoutDestination.methods,
PayoutDestination.editwallet,
PayoutDestination.walletTopUp,
}
: PayoutDestination.values.toSet();
final selected = allowedDestinations.contains(provider.selected)
? provider.selected
final routeDestination = _destinationFromState(routerState) ?? provider.selected;
final selected = allowedDestinations.contains(routeDestination)
? routeDestination
: (restrictedAccess ? PayoutDestination.settings : PayoutDestination.dashboard);
if (selected != provider.selected) {
WidgetsBinding.instance.addPostFrameCallback((_) => provider.selectPage(selected));
if (selected != routeDestination) {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.goToPayout(selected);
});
}
Widget content;
switch (selected) {
case PayoutDestination.dashboard:
content = DashboardPage(
onRecipientSelected: (recipient) => provider.selectRecipient(recipient),
onGoToPaymentWithoutRecipient: provider.startPaymentWithoutRecipient,
);
break;
case PayoutDestination.recipients:
content = RecipientAddressBookPage(
onRecipientSelected: (recipient) =>
provider.selectRecipient(recipient, fromList: true),
onAddRecipient: provider.goToAddRecipient,
onEditRecipient: provider.editRecipient,
);
break;
case PayoutDestination.addrecipient:
final recipient = provider.recipientProvider.currentObject;
content = AdressBookRecipientForm(
recipient: recipient,
onSaved: (_) => provider.selectPage(PayoutDestination.recipients),
);
break;
case PayoutDestination.payment:
content = PaymentPage(
onBack: (_) => provider.goBackFromPayment(),
);
break;
case PayoutDestination.settings:
content = ProfileSettingsPage();
break;
case PayoutDestination.reports:
content = OperationHistoryPage();
break;
case PayoutDestination.methods:
content = PaymentConfigPage(
onWalletTap: provider.selectWallet,
);
break;
case PayoutDestination.editwallet:
final wallet = provider.walletsProvider.selectedWallet;
content = wallet != null
? WalletEditPage(
onBack: provider.goBackFromWalletEdit,
)
: Center(child: Text(loc.noWalletSelected));
break;
default:
content = Text(selected.name);
if (provider.selected != selected) {
provider.syncDestination(selected);
}
return Scaffold(
@@ -126,14 +73,49 @@ class PageSelector extends StatelessWidget {
children: [
PayoutSidebar(
selected: selected,
onSelected: provider.selectPage,
onSelected: context.goToPayout,
onLogout: () => logoutUtil(context),
),
Expanded(child: content),
Expanded(child: child),
],
),
),
);
},
));
PayoutDestination? _destinationFromState(GoRouterState state) {
final byName = PayoutRoutes.destinationFor(state.name);
if (byName != null) return byName;
final location = state.matchedLocation;
if (location.startsWith(PayoutRoutes.editWalletPath)) {
return PayoutDestination.editwallet;
}
if (location.startsWith(PayoutRoutes.walletTopUpPath)) {
return PayoutDestination.walletTopUp;
}
if (location.startsWith(PayoutRoutes.methodsPath)) {
return PayoutDestination.methods;
}
if (location.startsWith(PayoutRoutes.paymentPath)) {
return PayoutDestination.payment;
}
if (location.startsWith(PayoutRoutes.addRecipientPath)) {
return PayoutDestination.addrecipient;
}
if (location.startsWith(PayoutRoutes.recipientsPath)) {
return PayoutDestination.recipients;
}
if (location.startsWith(PayoutRoutes.settingsPath)) {
return PayoutDestination.settings;
}
if (location.startsWith(PayoutRoutes.reportsPath)) {
return PayoutDestination.reports;
}
if (location.startsWith(PayoutRoutes.dashboardPath)) {
return PayoutDestination.dashboard;
}
return null;
}
}

View File

@@ -67,6 +67,7 @@ dependencies:
syncfusion_flutter_charts: ^31.2.10
flutter_multi_formatter: ^2.13.7
dotted_border: ^3.1.0
qr_flutter: ^4.1.0
@@ -96,7 +97,7 @@ flutter:
# To add assets to your application, add an assets section, like this:
assets:
- resources/logo.png
- resources/icon.png
- resources/logo.si
# An image asset can refer to one or more resolution-specific "variants", see