Compare commits
13 Commits
9c16e27645
...
bf85ca062c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf85ca062c | ||
|
|
3b04753f4e | ||
|
|
5f4184760d | ||
|
|
5e1da9617f | ||
|
|
c4d34c5663 | ||
|
|
34420ca2fb | ||
|
|
d16703197d | ||
|
|
35897f9aa1 | ||
|
|
f59ee55084 | ||
|
|
8bf86c5c93 | ||
|
|
5e8ff2adb7 | ||
|
|
da57b1d2e0 | ||
| 2ef9ac24a1 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -3,4 +3,9 @@
|
|||||||
*.pb.gw.go
|
*.pb.gw.go
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
analysis_options.yaml
|
||||||
|
devtools_options.yaml
|
||||||
|
untranslated.txt
|
||||||
|
generate_protos.sh
|
||||||
update_dep.sh
|
update_dep.sh
|
||||||
|
.vscode/
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
depends_on:
|
|
||||||
- bff
|
|
||||||
- billing_fees
|
|
||||||
- chain_gateway
|
|
||||||
- db
|
|
||||||
- frontend
|
|
||||||
- fx_ingestor
|
|
||||||
- fx_oracle
|
|
||||||
- ledger
|
|
||||||
- nats
|
|
||||||
- notification
|
|
||||||
- payments_orchestrator
|
|
||||||
|
|
||||||
when:
|
|
||||||
event: push
|
|
||||||
branch: main
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: bump-version
|
|
||||||
image: alpine:latest
|
|
||||||
environment:
|
|
||||||
GIT_AUTHOR_NAME: woodpecker
|
|
||||||
GIT_AUTHOR_EMAIL: ci@sendico.io
|
|
||||||
GIT_COMMITTER_NAME: woodpecker
|
|
||||||
GIT_COMMITTER_EMAIL: ci@sendico.io
|
|
||||||
commands:
|
|
||||||
- set -euo pipefail
|
|
||||||
- apk add --no-cache git
|
|
||||||
# make sure git knows who commits
|
|
||||||
- git config user.name "$GIT_AUTHOR_NAME"
|
|
||||||
- git config user.email "$GIT_AUTHOR_EMAIL"
|
|
||||||
# run your script (must do commit + push)
|
|
||||||
- sh ci/scripts/common/bump_version.sh
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- CHAIN_GATEWAY_IMAGE_PATH: chain/gateway
|
- CHAIN_GATEWAY_IMAGE_PATH: gateway/chain
|
||||||
CHAIN_GATEWAY_DOCKERFILE: ci/prod/compose/chain_gateway.dockerfile
|
CHAIN_GATEWAY_DOCKERFILE: ci/prod/compose/chain_gateway.dockerfile
|
||||||
CHAIN_GATEWAY_MONGO_SECRET_PATH: sendico/db
|
CHAIN_GATEWAY_MONGO_SECRET_PATH: sendico/db
|
||||||
CHAIN_GATEWAY_RPC_SECRET_PATH: sendico/chain/gateway
|
CHAIN_GATEWAY_RPC_SECRET_PATH: sendico/gateway/chain
|
||||||
CHAIN_GATEWAY_WALLET_SECRET_PATH: sendico/chain/gateway/wallet
|
CHAIN_GATEWAY_WALLET_SECRET_PATH: sendico/gateway/chain/wallet
|
||||||
CHAIN_GATEWAY_VAULT_SECRET_PATH: sendico/chain/gateway/vault
|
CHAIN_GATEWAY_VAULT_SECRET_PATH: sendico/gateway/chain/vault
|
||||||
CHAIN_GATEWAY_ENV: prod
|
CHAIN_GATEWAY_ENV: prod
|
||||||
|
|
||||||
when:
|
when:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ require (
|
|||||||
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/google/uuid v1.6.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-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
@@ -49,6 +49,6 @@ require (
|
|||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.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/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.10
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -212,8 +212,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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
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-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
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/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 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fake implements Client for tests.
|
|
||||||
type Fake struct {
|
|
||||||
CreateManagedWalletFn func(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error)
|
|
||||||
GetManagedWalletFn func(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error)
|
|
||||||
ListManagedWalletsFn func(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error)
|
|
||||||
GetWalletBalanceFn func(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error)
|
|
||||||
SubmitTransferFn func(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error)
|
|
||||||
GetTransferFn func(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error)
|
|
||||||
ListTransfersFn func(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error)
|
|
||||||
EstimateTransferFeeFn func(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error)
|
|
||||||
CloseFn func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) {
|
|
||||||
if f.CreateManagedWalletFn != nil {
|
|
||||||
return f.CreateManagedWalletFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.CreateManagedWalletResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) {
|
|
||||||
if f.GetManagedWalletFn != nil {
|
|
||||||
return f.GetManagedWalletFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.GetManagedWalletResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) {
|
|
||||||
if f.ListManagedWalletsFn != nil {
|
|
||||||
return f.ListManagedWalletsFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.ListManagedWalletsResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) {
|
|
||||||
if f.GetWalletBalanceFn != nil {
|
|
||||||
return f.GetWalletBalanceFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.GetWalletBalanceResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
|
|
||||||
if f.SubmitTransferFn != nil {
|
|
||||||
return f.SubmitTransferFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.SubmitTransferResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) {
|
|
||||||
if f.GetTransferFn != nil {
|
|
||||||
return f.GetTransferFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.GetTransferResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) {
|
|
||||||
if f.ListTransfersFn != nil {
|
|
||||||
return f.ListTransfersFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.ListTransfersResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) {
|
|
||||||
if f.EstimateTransferFeeFn != nil {
|
|
||||||
return f.EstimateTransferFeeFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.EstimateTransferFeeResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) Close() error {
|
|
||||||
if f.CloseFn != nil {
|
|
||||||
return f.CloseFn()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package gateway
|
|
||||||
|
|
||||||
import moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
||||||
|
|
||||||
func cloneMoney(m *moneyv1.Money) *moneyv1.Money {
|
|
||||||
if m == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &moneyv1.Money{Amount: m.GetAmount(), Currency: m.GetCurrency()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneMetadata(input map[string]string) map[string]string {
|
|
||||||
if len(input) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
clone := make(map[string]string, len(input))
|
|
||||||
for k, v := range input {
|
|
||||||
clone[k] = v
|
|
||||||
}
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
package gateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
|
||||||
"github.com/tech/sendico/pkg/api/routers"
|
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
|
||||||
|
|
||||||
type serviceError string
|
|
||||||
|
|
||||||
func (e serviceError) Error() string {
|
|
||||||
return string(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
errStorageUnavailable = serviceError("chain_gateway: storage not initialised")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service implements the ChainGatewayService RPC contract.
|
|
||||||
type Service struct {
|
|
||||||
logger mlogger.Logger
|
|
||||||
storage storage.Repository
|
|
||||||
producer msg.Producer
|
|
||||||
clock clockpkg.Clock
|
|
||||||
|
|
||||||
networks map[string]Network
|
|
||||||
serviceWallet ServiceWallet
|
|
||||||
keyManager keymanager.Manager
|
|
||||||
executor TransferExecutor
|
|
||||||
|
|
||||||
gatewayv1.UnimplementedChainGatewayServiceServer
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewService constructs the chain gateway service skeleton.
|
|
||||||
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
|
|
||||||
svc := &Service{
|
|
||||||
logger: logger.Named("chain_gateway"),
|
|
||||||
storage: repo,
|
|
||||||
producer: producer,
|
|
||||||
clock: clockpkg.System{},
|
|
||||||
networks: map[string]Network{},
|
|
||||||
}
|
|
||||||
|
|
||||||
initMetrics()
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
if opt != nil {
|
|
||||||
opt(svc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if svc.clock == nil {
|
|
||||||
svc.clock = clockpkg.System{}
|
|
||||||
}
|
|
||||||
if svc.networks == nil {
|
|
||||||
svc.networks = map[string]Network{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return svc
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register wires the service onto the provided gRPC router.
|
|
||||||
func (s *Service) Register(router routers.GRPC) error {
|
|
||||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
|
||||||
gatewayv1.RegisterChainGatewayServiceServer(reg, s)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "CreateManagedWallet", s.createManagedWalletHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "GetManagedWallet", s.getManagedWalletHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "ListManagedWallets", s.listManagedWalletsHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "GetWalletBalance", s.getWalletBalanceHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "SubmitTransfer", s.submitTransferHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "GetTransfer", s.getTransferHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "ListTransfers", s.listTransfersHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "EstimateTransferFee", s.estimateTransferFeeHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ensureRepository(ctx context.Context) error {
|
|
||||||
if s.storage == nil {
|
|
||||||
return errStorageUnavailable
|
|
||||||
}
|
|
||||||
return s.storage.Ping(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func executeUnary[TReq any, TResp any](ctx context.Context, svc *Service, method string, handler func(context.Context, *TReq) gsresponse.Responder[TResp], req *TReq) (*TResp, error) {
|
|
||||||
start := svc.clock.Now()
|
|
||||||
resp, err := gsresponse.Unary(svc.logger, mservice.ChainGateway, handler)(ctx, req)
|
|
||||||
observeRPC(method, err, svc.clock.Now().Sub(start))
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveContractAddress(tokens []TokenContract, symbol string) string {
|
|
||||||
upper := strings.ToUpper(symbol)
|
|
||||||
for _, token := range tokens {
|
|
||||||
if strings.EqualFold(token.Symbol, upper) && token.ContractAddress != "" {
|
|
||||||
return strings.ToLower(token.ContractAddress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateWalletRef() string {
|
|
||||||
return primitive.NewObjectID().Hex()
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateTransferRef() string {
|
|
||||||
return primitive.NewObjectID().Hex()
|
|
||||||
}
|
|
||||||
|
|
||||||
func chainKeyFromEnum(chain gatewayv1.ChainNetwork) (string, gatewayv1.ChainNetwork) {
|
|
||||||
if name, ok := gatewayv1.ChainNetwork_name[int32(chain)]; ok {
|
|
||||||
key := strings.ToLower(strings.TrimPrefix(name, "CHAIN_NETWORK_"))
|
|
||||||
return key, chain
|
|
||||||
}
|
|
||||||
return "", gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
|
||||||
}
|
|
||||||
|
|
||||||
func chainEnumFromName(name string) gatewayv1.ChainNetwork {
|
|
||||||
if name == "" {
|
|
||||||
return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
|
||||||
}
|
|
||||||
upper := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(name, " ", "_"), "-", "_"))
|
|
||||||
key := "CHAIN_NETWORK_" + upper
|
|
||||||
if val, ok := gatewayv1.ChainNetwork_value[key]; ok {
|
|
||||||
return gatewayv1.ChainNetwork(val)
|
|
||||||
}
|
|
||||||
return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
|
||||||
}
|
|
||||||
|
|
||||||
func managedWalletStatusToProto(status model.ManagedWalletStatus) gatewayv1.ManagedWalletStatus {
|
|
||||||
switch status {
|
|
||||||
case model.ManagedWalletStatusActive:
|
|
||||||
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE
|
|
||||||
case model.ManagedWalletStatusSuspended:
|
|
||||||
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED
|
|
||||||
case model.ManagedWalletStatusClosed:
|
|
||||||
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED
|
|
||||||
default:
|
|
||||||
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_STATUS_UNSPECIFIED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func transferStatusToModel(status gatewayv1.TransferStatus) model.TransferStatus {
|
|
||||||
switch status {
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_PENDING:
|
|
||||||
return model.TransferStatusPending
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_SIGNING:
|
|
||||||
return model.TransferStatusSigning
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_SUBMITTED:
|
|
||||||
return model.TransferStatusSubmitted
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_CONFIRMED:
|
|
||||||
return model.TransferStatusConfirmed
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_FAILED:
|
|
||||||
return model.TransferStatusFailed
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_CANCELLED:
|
|
||||||
return model.TransferStatusCancelled
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func transferStatusToProto(status model.TransferStatus) gatewayv1.TransferStatus {
|
|
||||||
switch status {
|
|
||||||
case model.TransferStatusPending:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_PENDING
|
|
||||||
case model.TransferStatusSigning:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_SIGNING
|
|
||||||
case model.TransferStatusSubmitted:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_SUBMITTED
|
|
||||||
case model.TransferStatusConfirmed:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_CONFIRMED
|
|
||||||
case model.TransferStatusFailed:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_FAILED
|
|
||||||
case model.TransferStatusCancelled:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_CANCELLED
|
|
||||||
default:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
package gateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Service) submitTransferHandler(ctx context.Context, req *gatewayv1.SubmitTransferRequest) gsresponse.Responder[gatewayv1.SubmitTransferResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
||||||
}
|
|
||||||
|
|
||||||
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
|
||||||
if idempotencyKey == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
|
||||||
}
|
|
||||||
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
|
||||||
if organizationRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
|
||||||
}
|
|
||||||
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
|
|
||||||
if sourceWalletRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
|
|
||||||
}
|
|
||||||
amount := req.GetAmount()
|
|
||||||
if amount == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
|
|
||||||
}
|
|
||||||
amountCurrency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
|
|
||||||
if amountCurrency == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount.currency is required"))
|
|
||||||
}
|
|
||||||
amountValue := strings.TrimSpace(amount.GetAmount())
|
|
||||||
if amountValue == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required"))
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceWallet, err := s.storage.Wallets().Get(ctx, sourceWalletRef)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
|
||||||
return gsresponse.NotFound[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if !strings.EqualFold(sourceWallet.OrganizationRef, organizationRef) {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref mismatch with wallet"))
|
|
||||||
}
|
|
||||||
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
|
|
||||||
networkCfg, ok := s.networks[networkKey]
|
|
||||||
if !ok {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
|
||||||
}
|
|
||||||
|
|
||||||
destination, err := s.resolveDestination(ctx, req.GetDestination(), sourceWallet)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
|
||||||
return gsresponse.NotFound[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fees, feeSum, err := convertFees(req.GetFees(), amountCurrency)
|
|
||||||
if err != nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
amountDec, err := decimal.NewFromString(amountValue)
|
|
||||||
if err != nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("invalid amount"))
|
|
||||||
}
|
|
||||||
netDec := amountDec.Sub(feeSum)
|
|
||||||
if netDec.IsNegative() {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("fees exceed amount"))
|
|
||||||
}
|
|
||||||
|
|
||||||
netAmount := cloneMoney(amount)
|
|
||||||
netAmount.Amount = netDec.String()
|
|
||||||
|
|
||||||
transfer := &model.Transfer{
|
|
||||||
IdempotencyKey: idempotencyKey,
|
|
||||||
TransferRef: generateTransferRef(),
|
|
||||||
OrganizationRef: organizationRef,
|
|
||||||
SourceWalletRef: sourceWalletRef,
|
|
||||||
Destination: destination,
|
|
||||||
Network: sourceWallet.Network,
|
|
||||||
TokenSymbol: sourceWallet.TokenSymbol,
|
|
||||||
ContractAddress: sourceWallet.ContractAddress,
|
|
||||||
RequestedAmount: cloneMoney(amount),
|
|
||||||
NetAmount: netAmount,
|
|
||||||
Fees: fees,
|
|
||||||
Status: model.TransferStatusPending,
|
|
||||||
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
|
||||||
LastStatusAt: s.clock.Now().UTC(),
|
|
||||||
}
|
|
||||||
|
|
||||||
saved, err := s.storage.Transfers().Create(ctx, transfer)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrDataConflict) {
|
|
||||||
s.logger.Debug("transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", idempotencyKey))
|
|
||||||
return gsresponse.Success(&gatewayv1.SubmitTransferResponse{Transfer: s.toProtoTransfer(saved)})
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.executor != nil {
|
|
||||||
s.launchTransferExecution(saved.TransferRef, sourceWalletRef, networkCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gsresponse.Success(&gatewayv1.SubmitTransferResponse{Transfer: s.toProtoTransfer(saved)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) getTransferHandler(ctx context.Context, req *gatewayv1.GetTransferRequest) gsresponse.Responder[gatewayv1.GetTransferResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
||||||
}
|
|
||||||
transferRef := strings.TrimSpace(req.GetTransferRef())
|
|
||||||
if transferRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("transfer_ref is required"))
|
|
||||||
}
|
|
||||||
transfer, err := s.storage.Transfers().Get(ctx, transferRef)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
|
||||||
return gsresponse.NotFound[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Success(&gatewayv1.GetTransferResponse{Transfer: s.toProtoTransfer(transfer)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) listTransfersHandler(ctx context.Context, req *gatewayv1.ListTransfersRequest) gsresponse.Responder[gatewayv1.ListTransfersResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.ListTransfersResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
filter := model.TransferFilter{}
|
|
||||||
if req != nil {
|
|
||||||
filter.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
|
|
||||||
filter.DestinationWalletRef = strings.TrimSpace(req.GetDestinationWalletRef())
|
|
||||||
if status := transferStatusToModel(req.GetStatus()); status != "" {
|
|
||||||
filter.Status = status
|
|
||||||
}
|
|
||||||
if page := req.GetPage(); page != nil {
|
|
||||||
filter.Cursor = strings.TrimSpace(page.GetCursor())
|
|
||||||
filter.Limit = page.GetLimit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := s.storage.Transfers().List(ctx, filter)
|
|
||||||
if err != nil {
|
|
||||||
return gsresponse.Auto[gatewayv1.ListTransfersResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
protoTransfers := make([]*gatewayv1.Transfer, 0, len(result.Items))
|
|
||||||
for _, transfer := range result.Items {
|
|
||||||
protoTransfers = append(protoTransfers, s.toProtoTransfer(transfer))
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &gatewayv1.ListTransfersResponse{
|
|
||||||
Transfers: protoTransfers,
|
|
||||||
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
|
|
||||||
}
|
|
||||||
return gsresponse.Success(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) estimateTransferFeeHandler(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) gsresponse.Responder[gatewayv1.EstimateTransferFeeResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.EstimateTransferFeeResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil || req.GetAmount() == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.EstimateTransferFeeResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
|
|
||||||
}
|
|
||||||
currency := req.GetAmount().GetCurrency()
|
|
||||||
fee := &moneyv1.Money{
|
|
||||||
Currency: currency,
|
|
||||||
Amount: "0",
|
|
||||||
}
|
|
||||||
resp := &gatewayv1.EstimateTransferFeeResponse{
|
|
||||||
NetworkFee: fee,
|
|
||||||
EstimationContext: "not_implemented",
|
|
||||||
}
|
|
||||||
return gsresponse.Success(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) toProtoTransfer(transfer *model.Transfer) *gatewayv1.Transfer {
|
|
||||||
if transfer == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
destination := &gatewayv1.TransferDestination{}
|
|
||||||
if transfer.Destination.ManagedWalletRef != "" {
|
|
||||||
destination.Destination = &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: transfer.Destination.ManagedWalletRef}
|
|
||||||
} else if transfer.Destination.ExternalAddress != "" {
|
|
||||||
destination.Destination = &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: transfer.Destination.ExternalAddress}
|
|
||||||
}
|
|
||||||
destination.Memo = transfer.Destination.Memo
|
|
||||||
|
|
||||||
protoFees := make([]*gatewayv1.ServiceFeeBreakdown, 0, len(transfer.Fees))
|
|
||||||
for _, fee := range transfer.Fees {
|
|
||||||
protoFees = append(protoFees, &gatewayv1.ServiceFeeBreakdown{
|
|
||||||
FeeCode: fee.FeeCode,
|
|
||||||
Amount: cloneMoney(fee.Amount),
|
|
||||||
Description: fee.Description,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
asset := &gatewayv1.Asset{
|
|
||||||
Chain: chainEnumFromName(transfer.Network),
|
|
||||||
TokenSymbol: transfer.TokenSymbol,
|
|
||||||
ContractAddress: transfer.ContractAddress,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &gatewayv1.Transfer{
|
|
||||||
TransferRef: transfer.TransferRef,
|
|
||||||
IdempotencyKey: transfer.IdempotencyKey,
|
|
||||||
OrganizationRef: transfer.OrganizationRef,
|
|
||||||
SourceWalletRef: transfer.SourceWalletRef,
|
|
||||||
Destination: destination,
|
|
||||||
Asset: asset,
|
|
||||||
RequestedAmount: cloneMoney(transfer.RequestedAmount),
|
|
||||||
NetAmount: cloneMoney(transfer.NetAmount),
|
|
||||||
Fees: protoFees,
|
|
||||||
Status: transferStatusToProto(transfer.Status),
|
|
||||||
TransactionHash: transfer.TxHash,
|
|
||||||
FailureReason: transfer.FailureReason,
|
|
||||||
CreatedAt: timestamppb.New(transfer.CreatedAt.UTC()),
|
|
||||||
UpdatedAt: timestamppb.New(transfer.UpdatedAt.UTC()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) resolveDestination(ctx context.Context, dest *gatewayv1.TransferDestination, source *model.ManagedWallet) (model.TransferDestination, error) {
|
|
||||||
if dest == nil {
|
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
|
||||||
}
|
|
||||||
managedRef := strings.TrimSpace(dest.GetManagedWalletRef())
|
|
||||||
external := strings.TrimSpace(dest.GetExternalAddress())
|
|
||||||
if managedRef != "" && external != "" {
|
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination must be managed_wallet_ref or external_address")
|
|
||||||
}
|
|
||||||
if managedRef != "" {
|
|
||||||
wallet, err := s.storage.Wallets().Get(ctx, managedRef)
|
|
||||||
if err != nil {
|
|
||||||
return model.TransferDestination{}, err
|
|
||||||
}
|
|
||||||
if !strings.EqualFold(wallet.Network, source.Network) {
|
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet network mismatch")
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet missing deposit address")
|
|
||||||
}
|
|
||||||
return model.TransferDestination{
|
|
||||||
ManagedWalletRef: wallet.WalletRef,
|
|
||||||
Memo: strings.TrimSpace(dest.GetMemo()),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
if external == "" {
|
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
|
||||||
}
|
|
||||||
return model.TransferDestination{
|
|
||||||
ExternalAddress: strings.ToLower(external),
|
|
||||||
Memo: strings.TrimSpace(dest.GetMemo()),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertFees(fees []*gatewayv1.ServiceFeeBreakdown, currency string) ([]model.ServiceFee, decimal.Decimal, error) {
|
|
||||||
result := make([]model.ServiceFee, 0, len(fees))
|
|
||||||
sum := decimal.NewFromInt(0)
|
|
||||||
for _, fee := range fees {
|
|
||||||
if fee == nil || fee.GetAmount() == nil {
|
|
||||||
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
|
|
||||||
}
|
|
||||||
amtCurrency := strings.ToUpper(strings.TrimSpace(fee.GetAmount().GetCurrency()))
|
|
||||||
if amtCurrency != strings.ToUpper(currency) {
|
|
||||||
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee currency mismatch")
|
|
||||||
}
|
|
||||||
amtValue := strings.TrimSpace(fee.GetAmount().GetAmount())
|
|
||||||
if amtValue == "" {
|
|
||||||
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
|
|
||||||
}
|
|
||||||
dec, err := decimal.NewFromString(amtValue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, decimal.Decimal{}, merrors.InvalidArgument("invalid fee amount")
|
|
||||||
}
|
|
||||||
if dec.IsNegative() {
|
|
||||||
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount must be non-negative")
|
|
||||||
}
|
|
||||||
sum = sum.Add(dec)
|
|
||||||
result = append(result, model.ServiceFee{
|
|
||||||
FeeCode: strings.TrimSpace(fee.GetFeeCode()),
|
|
||||||
Amount: cloneMoney(fee.GetAmount()),
|
|
||||||
Description: strings.TrimSpace(fee.GetDescription()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result, sum, nil
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
package gateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Service) createManagedWalletHandler(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) gsresponse.Responder[gatewayv1.CreateManagedWalletResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
||||||
}
|
|
||||||
|
|
||||||
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
|
||||||
if idempotencyKey == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
|
||||||
}
|
|
||||||
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
|
||||||
if organizationRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
|
||||||
}
|
|
||||||
ownerRef := strings.TrimSpace(req.GetOwnerRef())
|
|
||||||
if ownerRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("owner_ref is required"))
|
|
||||||
}
|
|
||||||
|
|
||||||
asset := req.GetAsset()
|
|
||||||
if asset == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required"))
|
|
||||||
}
|
|
||||||
|
|
||||||
chainKey, _ := chainKeyFromEnum(asset.GetChain())
|
|
||||||
if chainKey == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
|
||||||
}
|
|
||||||
networkCfg, ok := s.networks[chainKey]
|
|
||||||
if !ok {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenSymbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
|
|
||||||
if tokenSymbol == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("asset.token_symbol is required"))
|
|
||||||
}
|
|
||||||
contractAddress := strings.ToLower(strings.TrimSpace(asset.GetContractAddress()))
|
|
||||||
if contractAddress == "" {
|
|
||||||
contractAddress = resolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
|
|
||||||
if contractAddress == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
walletRef := generateWalletRef()
|
|
||||||
if s.keyManager == nil {
|
|
||||||
return gsresponse.Internal[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.Internal("key manager not configured"))
|
|
||||||
}
|
|
||||||
|
|
||||||
keyInfo, err := s.keyManager.CreateManagedWalletKey(ctx, walletRef, chainKey)
|
|
||||||
if err != nil {
|
|
||||||
return gsresponse.Auto[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if keyInfo == nil || strings.TrimSpace(keyInfo.Address) == "" {
|
|
||||||
return gsresponse.Internal[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address"))
|
|
||||||
}
|
|
||||||
|
|
||||||
wallet := &model.ManagedWallet{
|
|
||||||
IdempotencyKey: idempotencyKey,
|
|
||||||
WalletRef: walletRef,
|
|
||||||
OrganizationRef: organizationRef,
|
|
||||||
OwnerRef: ownerRef,
|
|
||||||
Network: chainKey,
|
|
||||||
TokenSymbol: tokenSymbol,
|
|
||||||
ContractAddress: contractAddress,
|
|
||||||
DepositAddress: strings.ToLower(keyInfo.Address),
|
|
||||||
KeyReference: keyInfo.KeyID,
|
|
||||||
Status: model.ManagedWalletStatusActive,
|
|
||||||
Metadata: cloneMetadata(req.GetMetadata()),
|
|
||||||
}
|
|
||||||
|
|
||||||
created, err := s.storage.Wallets().Create(ctx, wallet)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrDataConflict) {
|
|
||||||
s.logger.Debug("wallet already exists", zap.String("wallet_ref", walletRef), zap.String("idempotency_key", idempotencyKey))
|
|
||||||
return gsresponse.Success(&gatewayv1.CreateManagedWalletResponse{Wallet: s.toProtoManagedWallet(created)})
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gsresponse.Success(&gatewayv1.CreateManagedWalletResponse{Wallet: s.toProtoManagedWallet(created)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) getManagedWalletHandler(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) gsresponse.Responder[gatewayv1.GetManagedWalletResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
||||||
}
|
|
||||||
walletRef := strings.TrimSpace(req.GetWalletRef())
|
|
||||||
if walletRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
|
||||||
}
|
|
||||||
wallet, err := s.storage.Wallets().Get(ctx, walletRef)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
|
||||||
return gsresponse.NotFound[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Success(&gatewayv1.GetManagedWalletResponse{Wallet: s.toProtoManagedWallet(wallet)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) listManagedWalletsHandler(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) gsresponse.Responder[gatewayv1.ListManagedWalletsResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.ListManagedWalletsResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
filter := model.ManagedWalletFilter{}
|
|
||||||
if req != nil {
|
|
||||||
filter.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
|
||||||
filter.OwnerRef = strings.TrimSpace(req.GetOwnerRef())
|
|
||||||
if asset := req.GetAsset(); asset != nil {
|
|
||||||
filter.Network, _ = chainKeyFromEnum(asset.GetChain())
|
|
||||||
filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol())
|
|
||||||
}
|
|
||||||
if page := req.GetPage(); page != nil {
|
|
||||||
filter.Cursor = strings.TrimSpace(page.GetCursor())
|
|
||||||
filter.Limit = page.GetLimit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := s.storage.Wallets().List(ctx, filter)
|
|
||||||
if err != nil {
|
|
||||||
return gsresponse.Auto[gatewayv1.ListManagedWalletsResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
protoWallets := make([]*gatewayv1.ManagedWallet, 0, len(result.Items))
|
|
||||||
for _, wallet := range result.Items {
|
|
||||||
protoWallets = append(protoWallets, s.toProtoManagedWallet(wallet))
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &gatewayv1.ListManagedWalletsResponse{
|
|
||||||
Wallets: protoWallets,
|
|
||||||
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
|
|
||||||
}
|
|
||||||
return gsresponse.Success(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) getWalletBalanceHandler(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) gsresponse.Responder[gatewayv1.GetWalletBalanceResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
||||||
}
|
|
||||||
walletRef := strings.TrimSpace(req.GetWalletRef())
|
|
||||||
if walletRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
|
||||||
}
|
|
||||||
balance, err := s.storage.Wallets().GetBalance(ctx, walletRef)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
|
||||||
return gsresponse.NotFound[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Success(&gatewayv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(balance)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) toProtoManagedWallet(wallet *model.ManagedWallet) *gatewayv1.ManagedWallet {
|
|
||||||
if wallet == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
asset := &gatewayv1.Asset{
|
|
||||||
Chain: chainEnumFromName(wallet.Network),
|
|
||||||
TokenSymbol: wallet.TokenSymbol,
|
|
||||||
ContractAddress: wallet.ContractAddress,
|
|
||||||
}
|
|
||||||
return &gatewayv1.ManagedWallet{
|
|
||||||
WalletRef: wallet.WalletRef,
|
|
||||||
OrganizationRef: wallet.OrganizationRef,
|
|
||||||
OwnerRef: wallet.OwnerRef,
|
|
||||||
Asset: asset,
|
|
||||||
DepositAddress: wallet.DepositAddress,
|
|
||||||
Status: managedWalletStatusToProto(wallet.Status),
|
|
||||||
Metadata: cloneMetadata(wallet.Metadata),
|
|
||||||
CreatedAt: timestamppb.New(wallet.CreatedAt.UTC()),
|
|
||||||
UpdatedAt: timestamppb.New(wallet.UpdatedAt.UTC()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toProtoWalletBalance(balance *model.WalletBalance) *gatewayv1.WalletBalance {
|
|
||||||
if balance == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &gatewayv1.WalletBalance{
|
|
||||||
Available: cloneMoney(balance.Available),
|
|
||||||
PendingInbound: cloneMoney(balance.PendingInbound),
|
|
||||||
PendingOutbound: cloneMoney(balance.PendingOutbound),
|
|
||||||
CalculatedAt: timestamppb.New(balance.CalculatedAt.UTC()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,7 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/google/uuid v1.6.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-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
@@ -49,7 +49,7 @@ require (
|
|||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.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
|
google.golang.org/grpc v1.77.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -212,8 +212,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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
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-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
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/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 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
||||||
github.com/golang/snappy v1.0.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/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
@@ -50,5 +50,5 @@ require (
|
|||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -212,8 +212,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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
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-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
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/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 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ require (
|
|||||||
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
|
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/google/uuid v1.6.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/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
|
|||||||
@@ -51,8 +51,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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ root = "./../.."
|
|||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/chain/gateway/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.BuildDate=$(date)'\""
|
cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/gateway/chain/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.BuildDate=$(date)'\""
|
||||||
bin = "./app"
|
bin = "./app"
|
||||||
full_bin = "./app --debug --config.file=config.yml"
|
full_bin = "./app --debug --config.file=config.yml"
|
||||||
include_ext = ["go", "yaml", "yml"]
|
include_ext = ["go", "yaml", "yml"]
|
||||||
exclude_dir = ["chain/gateway/tmp", "pkg/.git", "chain/gateway/env"]
|
exclude_dir = ["gateway/chain/tmp", "pkg/.git", "gateway/chain/env"]
|
||||||
exclude_regex = ["_test\\.go"]
|
exclude_regex = ["_test\\.go"]
|
||||||
exclude_unchanged = true
|
exclude_unchanged = true
|
||||||
follow_symlink = true
|
follow_symlink = true
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
@@ -16,26 +16,26 @@ import (
|
|||||||
|
|
||||||
// Client exposes typed helpers around the chain gateway gRPC API.
|
// Client exposes typed helpers around the chain gateway gRPC API.
|
||||||
type Client interface {
|
type Client interface {
|
||||||
CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error)
|
CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
|
||||||
GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error)
|
GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error)
|
||||||
ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error)
|
ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
|
||||||
GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error)
|
GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
|
||||||
SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error)
|
SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error)
|
||||||
GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error)
|
GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error)
|
||||||
ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error)
|
ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error)
|
||||||
EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error)
|
EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error)
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type grpcGatewayClient interface {
|
type grpcGatewayClient interface {
|
||||||
CreateManagedWallet(ctx context.Context, in *gatewayv1.CreateManagedWalletRequest, opts ...grpc.CallOption) (*gatewayv1.CreateManagedWalletResponse, error)
|
CreateManagedWallet(ctx context.Context, in *chainv1.CreateManagedWalletRequest, opts ...grpc.CallOption) (*chainv1.CreateManagedWalletResponse, error)
|
||||||
GetManagedWallet(ctx context.Context, in *gatewayv1.GetManagedWalletRequest, opts ...grpc.CallOption) (*gatewayv1.GetManagedWalletResponse, error)
|
GetManagedWallet(ctx context.Context, in *chainv1.GetManagedWalletRequest, opts ...grpc.CallOption) (*chainv1.GetManagedWalletResponse, error)
|
||||||
ListManagedWallets(ctx context.Context, in *gatewayv1.ListManagedWalletsRequest, opts ...grpc.CallOption) (*gatewayv1.ListManagedWalletsResponse, error)
|
ListManagedWallets(ctx context.Context, in *chainv1.ListManagedWalletsRequest, opts ...grpc.CallOption) (*chainv1.ListManagedWalletsResponse, error)
|
||||||
GetWalletBalance(ctx context.Context, in *gatewayv1.GetWalletBalanceRequest, opts ...grpc.CallOption) (*gatewayv1.GetWalletBalanceResponse, error)
|
GetWalletBalance(ctx context.Context, in *chainv1.GetWalletBalanceRequest, opts ...grpc.CallOption) (*chainv1.GetWalletBalanceResponse, error)
|
||||||
SubmitTransfer(ctx context.Context, in *gatewayv1.SubmitTransferRequest, opts ...grpc.CallOption) (*gatewayv1.SubmitTransferResponse, error)
|
SubmitTransfer(ctx context.Context, in *chainv1.SubmitTransferRequest, opts ...grpc.CallOption) (*chainv1.SubmitTransferResponse, error)
|
||||||
GetTransfer(ctx context.Context, in *gatewayv1.GetTransferRequest, opts ...grpc.CallOption) (*gatewayv1.GetTransferResponse, error)
|
GetTransfer(ctx context.Context, in *chainv1.GetTransferRequest, opts ...grpc.CallOption) (*chainv1.GetTransferResponse, error)
|
||||||
ListTransfers(ctx context.Context, in *gatewayv1.ListTransfersRequest, opts ...grpc.CallOption) (*gatewayv1.ListTransfersResponse, error)
|
ListTransfers(ctx context.Context, in *chainv1.ListTransfersRequest, opts ...grpc.CallOption) (*chainv1.ListTransfersResponse, error)
|
||||||
EstimateTransferFee(ctx context.Context, in *gatewayv1.EstimateTransferFeeRequest, opts ...grpc.CallOption) (*gatewayv1.EstimateTransferFeeResponse, error)
|
EstimateTransferFee(ctx context.Context, in *chainv1.EstimateTransferFeeRequest, opts ...grpc.CallOption) (*chainv1.EstimateTransferFeeResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type chainGatewayClient struct {
|
type chainGatewayClient struct {
|
||||||
@@ -71,7 +71,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
|
|||||||
return &chainGatewayClient{
|
return &chainGatewayClient{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
client: gatewayv1.NewChainGatewayServiceClient(conn),
|
client: chainv1.NewChainGatewayServiceClient(conn),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,49 +91,49 @@ func (c *chainGatewayClient) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) {
|
func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.CreateManagedWallet(ctx, req)
|
return c.client.CreateManagedWallet(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) {
|
func (c *chainGatewayClient) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetManagedWallet(ctx, req)
|
return c.client.GetManagedWallet(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) {
|
func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.ListManagedWallets(ctx, req)
|
return c.client.ListManagedWallets(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) {
|
func (c *chainGatewayClient) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetWalletBalance(ctx, req)
|
return c.client.GetWalletBalance(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
|
func (c *chainGatewayClient) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.SubmitTransfer(ctx, req)
|
return c.client.SubmitTransfer(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) {
|
func (c *chainGatewayClient) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetTransfer(ctx, req)
|
return c.client.GetTransfer(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) {
|
func (c *chainGatewayClient) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.ListTransfers(ctx, req)
|
return c.client.ListTransfers(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) {
|
func (c *chainGatewayClient) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.EstimateTransferFee(ctx, req)
|
return c.client.EstimateTransferFee(ctx, req)
|
||||||
83
api/gateway/chain/client/fake.go
Normal file
83
api/gateway/chain/client/fake.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fake implements Client for tests.
|
||||||
|
type Fake struct {
|
||||||
|
CreateManagedWalletFn func(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
|
||||||
|
GetManagedWalletFn func(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error)
|
||||||
|
ListManagedWalletsFn func(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
|
||||||
|
GetWalletBalanceFn func(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
|
||||||
|
SubmitTransferFn func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error)
|
||||||
|
GetTransferFn func(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error)
|
||||||
|
ListTransfersFn func(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error)
|
||||||
|
EstimateTransferFeeFn func(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error)
|
||||||
|
CloseFn func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
||||||
|
if f.CreateManagedWalletFn != nil {
|
||||||
|
return f.CreateManagedWalletFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.CreateManagedWalletResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
|
||||||
|
if f.GetManagedWalletFn != nil {
|
||||||
|
return f.GetManagedWalletFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.GetManagedWalletResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
|
||||||
|
if f.ListManagedWalletsFn != nil {
|
||||||
|
return f.ListManagedWalletsFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.ListManagedWalletsResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||||
|
if f.GetWalletBalanceFn != nil {
|
||||||
|
return f.GetWalletBalanceFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.GetWalletBalanceResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||||
|
if f.SubmitTransferFn != nil {
|
||||||
|
return f.SubmitTransferFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.SubmitTransferResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
||||||
|
if f.GetTransferFn != nil {
|
||||||
|
return f.GetTransferFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.GetTransferResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
|
||||||
|
if f.ListTransfersFn != nil {
|
||||||
|
return f.ListTransfersFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.ListTransfersResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||||
|
if f.EstimateTransferFeeFn != nil {
|
||||||
|
return f.EstimateTransferFeeFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.EstimateTransferFeeResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) Close() error {
|
||||||
|
if f.CloseFn != nil {
|
||||||
|
return f.CloseFn()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -54,4 +54,4 @@ key_management:
|
|||||||
token_env: VAULT_TOKEN
|
token_env: VAULT_TOKEN
|
||||||
namespace: ""
|
namespace: ""
|
||||||
mount_path: kv
|
mount_path: kv
|
||||||
key_prefix: chain/gateway/wallets
|
key_prefix: gateway/chain/wallets
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module github.com/tech/sendico/chain/gateway
|
module github.com/tech/sendico/gateway/chain
|
||||||
|
|
||||||
go 1.25.3
|
go 1.25.3
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/hashicorp/vault/api"
|
"github.com/hashicorp/vault/api"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
)
|
)
|
||||||
@@ -7,11 +7,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
vaultmanager "github.com/tech/sendico/chain/gateway/internal/keymanager/vault"
|
vaultmanager "github.com/tech/sendico/gateway/chain/internal/keymanager/vault"
|
||||||
gatewayservice "github.com/tech/sendico/chain/gateway/internal/service/gateway"
|
gatewayservice "github.com/tech/sendico/gateway/chain/internal/service/gateway"
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
gatewayshared "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
gatewaymongo "github.com/tech/sendico/chain/gateway/storage/mongo"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
|
gatewaymongo "github.com/tech/sendico/gateway/chain/storage/mongo"
|
||||||
"github.com/tech/sendico/pkg/api/routers"
|
"github.com/tech/sendico/pkg/api/routers"
|
||||||
"github.com/tech/sendico/pkg/db"
|
"github.com/tech/sendico/pkg/db"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
@@ -154,8 +155,8 @@ func (i *Imp) loadConfig() (*config, error) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewayservice.Network {
|
func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewayshared.Network {
|
||||||
result := make([]gatewayservice.Network, 0, len(chains))
|
result := make([]gatewayshared.Network, 0, len(chains))
|
||||||
for _, chain := range chains {
|
for _, chain := range chains {
|
||||||
if strings.TrimSpace(chain.Name) == "" {
|
if strings.TrimSpace(chain.Name) == "" {
|
||||||
logger.Warn("skipping unnamed chain configuration")
|
logger.Warn("skipping unnamed chain configuration")
|
||||||
@@ -165,7 +166,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewa
|
|||||||
if rpcURL == "" {
|
if rpcURL == "" {
|
||||||
logger.Warn("chain RPC endpoint not configured", zap.String("chain", chain.Name), zap.String("env", chain.RPCURLEnv))
|
logger.Warn("chain RPC endpoint not configured", zap.String("chain", chain.Name), zap.String("env", chain.RPCURLEnv))
|
||||||
}
|
}
|
||||||
contracts := make([]gatewayservice.TokenContract, 0, len(chain.Tokens))
|
contracts := make([]gatewayshared.TokenContract, 0, len(chain.Tokens))
|
||||||
for _, token := range chain.Tokens {
|
for _, token := range chain.Tokens {
|
||||||
symbol := strings.TrimSpace(token.Symbol)
|
symbol := strings.TrimSpace(token.Symbol)
|
||||||
if symbol == "" {
|
if symbol == "" {
|
||||||
@@ -185,13 +186,13 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewa
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
contracts = append(contracts, gatewayservice.TokenContract{
|
contracts = append(contracts, gatewayshared.TokenContract{
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
ContractAddress: addr,
|
ContractAddress: addr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, gatewayservice.Network{
|
result = append(result, gatewayshared.Network{
|
||||||
Name: chain.Name,
|
Name: chain.Name,
|
||||||
RPCURL: rpcURL,
|
RPCURL: rpcURL,
|
||||||
ChainID: chain.ChainID,
|
ChainID: chain.ChainID,
|
||||||
@@ -202,7 +203,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewa
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewayservice.ServiceWallet {
|
func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewayshared.ServiceWallet {
|
||||||
address := strings.TrimSpace(cfg.Address)
|
address := strings.TrimSpace(cfg.Address)
|
||||||
if address == "" && cfg.AddressEnv != "" {
|
if address == "" && cfg.AddressEnv != "" {
|
||||||
address = strings.TrimSpace(os.Getenv(cfg.AddressEnv))
|
address = strings.TrimSpace(os.Getenv(cfg.AddressEnv))
|
||||||
@@ -221,7 +222,7 @@ func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewa
|
|||||||
logger.Warn("service wallet private key not configured", zap.String("env", cfg.PrivateKeyEnv))
|
logger.Warn("service wallet private key not configured", zap.String("env", cfg.PrivateKeyEnv))
|
||||||
}
|
}
|
||||||
|
|
||||||
return gatewayservice.ServiceWallet{
|
return gatewayshared.ServiceWallet{
|
||||||
Network: cfg.Chain,
|
Network: cfg.Chain,
|
||||||
Address: address,
|
Address: address,
|
||||||
PrivateKey: privateKey,
|
PrivateKey: privateKey,
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
serverimp "github.com/tech/sendico/chain/gateway/internal/server/internal"
|
serverimp "github.com/tech/sendico/gateway/chain/internal/server/internal"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/server"
|
"github.com/tech/sendico/pkg/server"
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/transfer"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/wallet"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Unary[TReq any, TResp any] interface {
|
||||||
|
Execute(context.Context, *TReq) gsresponse.Responder[TResp]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
CreateManagedWallet Unary[chainv1.CreateManagedWalletRequest, chainv1.CreateManagedWalletResponse]
|
||||||
|
GetManagedWallet Unary[chainv1.GetManagedWalletRequest, chainv1.GetManagedWalletResponse]
|
||||||
|
ListManagedWallets Unary[chainv1.ListManagedWalletsRequest, chainv1.ListManagedWalletsResponse]
|
||||||
|
GetWalletBalance Unary[chainv1.GetWalletBalanceRequest, chainv1.GetWalletBalanceResponse]
|
||||||
|
|
||||||
|
SubmitTransfer Unary[chainv1.SubmitTransferRequest, chainv1.SubmitTransferResponse]
|
||||||
|
GetTransfer Unary[chainv1.GetTransferRequest, chainv1.GetTransferResponse]
|
||||||
|
ListTransfers Unary[chainv1.ListTransfersRequest, chainv1.ListTransfersResponse]
|
||||||
|
EstimateTransfer Unary[chainv1.EstimateTransferFeeRequest, chainv1.EstimateTransferFeeResponse]
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegistryDeps struct {
|
||||||
|
Wallet wallet.Deps
|
||||||
|
Transfer transfer.Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry(deps RegistryDeps) Registry {
|
||||||
|
return Registry{
|
||||||
|
CreateManagedWallet: wallet.NewCreateManagedWallet(deps.Wallet.WithLogger("wallet.create")),
|
||||||
|
GetManagedWallet: wallet.NewGetManagedWallet(deps.Wallet.WithLogger("wallet.get")),
|
||||||
|
ListManagedWallets: wallet.NewListManagedWallets(deps.Wallet.WithLogger("wallet.list")),
|
||||||
|
GetWalletBalance: wallet.NewGetWalletBalance(deps.Wallet.WithLogger("wallet.balance")),
|
||||||
|
SubmitTransfer: transfer.NewSubmitTransfer(deps.Transfer.WithLogger("transfer.submit")),
|
||||||
|
GetTransfer: transfer.NewGetTransfer(deps.Transfer.WithLogger("transfer.get")),
|
||||||
|
ListTransfers: transfer.NewListTransfers(deps.Transfer.WithLogger("transfer.list")),
|
||||||
|
EstimateTransfer: transfer.NewEstimateTransfer(deps.Transfer.WithLogger("transfer.estimate_fee")),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convertFees(fees []*chainv1.ServiceFeeBreakdown, currency string) ([]model.ServiceFee, decimal.Decimal, error) {
|
||||||
|
result := make([]model.ServiceFee, 0, len(fees))
|
||||||
|
sum := decimal.NewFromInt(0)
|
||||||
|
for _, fee := range fees {
|
||||||
|
if fee == nil || fee.GetAmount() == nil {
|
||||||
|
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
|
||||||
|
}
|
||||||
|
amtCurrency := strings.ToUpper(strings.TrimSpace(fee.GetAmount().GetCurrency()))
|
||||||
|
if amtCurrency != strings.ToUpper(currency) {
|
||||||
|
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee currency mismatch")
|
||||||
|
}
|
||||||
|
amtValue := strings.TrimSpace(fee.GetAmount().GetAmount())
|
||||||
|
if amtValue == "" {
|
||||||
|
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
|
||||||
|
}
|
||||||
|
dec, err := decimal.NewFromString(amtValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, decimal.Decimal{}, merrors.InvalidArgument("invalid fee amount")
|
||||||
|
}
|
||||||
|
if dec.IsNegative() {
|
||||||
|
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount must be non-negative")
|
||||||
|
}
|
||||||
|
sum = sum.Add(dec)
|
||||||
|
result = append(result, model.ServiceFee{
|
||||||
|
FeeCode: strings.TrimSpace(fee.GetFeeCode()),
|
||||||
|
Amount: shared.CloneMoney(fee.GetAmount()),
|
||||||
|
Description: strings.TrimSpace(fee.GetDescription()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, sum, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
Logger mlogger.Logger
|
||||||
|
Networks map[string]shared.Network
|
||||||
|
Storage storage.Repository
|
||||||
|
Clock clockpkg.Clock
|
||||||
|
EnsureRepository func(context.Context) error
|
||||||
|
LaunchExecution func(transferRef, sourceWalletRef string, network shared.Network)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Deps) WithLogger(name string) Deps {
|
||||||
|
if d.Logger != nil {
|
||||||
|
d.Logger = d.Logger.Named(name)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolveDestination(ctx context.Context, deps Deps, dest *chainv1.TransferDestination, source *model.ManagedWallet) (model.TransferDestination, error) {
|
||||||
|
if dest == nil {
|
||||||
|
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
||||||
|
}
|
||||||
|
managedRef := strings.TrimSpace(dest.GetManagedWalletRef())
|
||||||
|
external := strings.TrimSpace(dest.GetExternalAddress())
|
||||||
|
if managedRef != "" && external != "" {
|
||||||
|
deps.Logger.Warn("both managed and external destination provided")
|
||||||
|
return model.TransferDestination{}, merrors.InvalidArgument("destination must be managed_wallet_ref or external_address")
|
||||||
|
}
|
||||||
|
if managedRef != "" {
|
||||||
|
wallet, err := deps.Storage.Wallets().Get(ctx, managedRef)
|
||||||
|
if err != nil {
|
||||||
|
deps.Logger.Warn("destination wallet lookup failed", zap.Error(err), zap.String("managed_wallet_ref", managedRef))
|
||||||
|
return model.TransferDestination{}, err
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(wallet.Network, source.Network) {
|
||||||
|
deps.Logger.Warn("destination wallet network mismatch", zap.String("source_network", source.Network), zap.String("dest_network", wallet.Network))
|
||||||
|
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet network mismatch")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
||||||
|
deps.Logger.Warn("destination wallet missing deposit address", zap.String("managed_wallet_ref", managedRef))
|
||||||
|
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet missing deposit address")
|
||||||
|
}
|
||||||
|
return model.TransferDestination{
|
||||||
|
ManagedWalletRef: wallet.WalletRef,
|
||||||
|
Memo: strings.TrimSpace(dest.GetMemo()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if external == "" {
|
||||||
|
deps.Logger.Warn("destination external address missing")
|
||||||
|
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
||||||
|
}
|
||||||
|
return model.TransferDestination{
|
||||||
|
ExternalAddress: strings.ToLower(external),
|
||||||
|
Memo: strings.TrimSpace(dest.GetMemo()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func destinationAddress(ctx context.Context, deps Deps, dest model.TransferDestination) (string, error) {
|
||||||
|
if ref := strings.TrimSpace(dest.ManagedWalletRef); ref != "" {
|
||||||
|
wallet, err := deps.Storage.Wallets().Get(ctx, ref)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
||||||
|
return "", merrors.Internal("destination wallet missing deposit address")
|
||||||
|
}
|
||||||
|
return wallet.DepositAddress, nil
|
||||||
|
}
|
||||||
|
if addr := strings.TrimSpace(dest.ExternalAddress); addr != "" {
|
||||||
|
return strings.ToLower(addr), nil
|
||||||
|
}
|
||||||
|
return "", merrors.InvalidArgument("transfer destination address not resolved")
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum"
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"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/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type estimateTransferFeeCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEstimateTransfer(deps Deps) *estimateTransferFeeCommand {
|
||||||
|
return &estimateTransferFeeCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *estimateTransferFeeCommand) Execute(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) gsresponse.Responder[chainv1.EstimateTransferFeeResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("request is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
|
||||||
|
if sourceWalletRef == "" {
|
||||||
|
c.deps.Logger.Warn("source wallet ref missing")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
|
||||||
|
}
|
||||||
|
amount := req.GetAmount()
|
||||||
|
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
|
||||||
|
c.deps.Logger.Warn("amount missing or incomplete")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceWallet, err := c.deps.Storage.Wallets().Get(ctx, sourceWalletRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("source wallet not found", zap.String("source_wallet_ref", sourceWalletRef))
|
||||||
|
return gsresponse.NotFound[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage get wallet failed", zap.Error(err), zap.String("source_wallet_ref", sourceWalletRef))
|
||||||
|
return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
|
||||||
|
networkCfg, ok := c.deps.Networks[networkKey]
|
||||||
|
if !ok {
|
||||||
|
c.deps.Logger.Warn("unsupported chain", zap.String("network", networkKey))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
dest, err := resolveDestination(ctx, c.deps, req.GetDestination(), sourceWallet)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("destination not found", zap.String("destination_wallet_ref", req.GetDestination().GetManagedWalletRef()))
|
||||||
|
return gsresponse.NotFound[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("invalid destination", zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
destinationAddress, err := destinationAddress(ctx, c.deps, dest)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("failed to resolve destination address", zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
feeMoney, err := estimateNetworkFee(ctx, c.deps.Logger, networkCfg, sourceWallet, destinationAddress, amount)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("fee estimation failed", zap.Error(err))
|
||||||
|
return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &chainv1.EstimateTransferFeeResponse{
|
||||||
|
NetworkFee: feeMoney,
|
||||||
|
EstimationContext: "erc20_transfer",
|
||||||
|
}
|
||||||
|
return gsresponse.Success(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func estimateNetworkFee(ctx context.Context, logger mlogger.Logger, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
|
||||||
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
|
if rpcURL == "" {
|
||||||
|
return nil, merrors.InvalidArgument("network rpc url not configured")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(wallet.ContractAddress) == "" {
|
||||||
|
return nil, merrors.NotImplemented("native token transfers not supported")
|
||||||
|
}
|
||||||
|
if !common.IsHexAddress(wallet.ContractAddress) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid token contract address")
|
||||||
|
}
|
||||||
|
if !common.IsHexAddress(wallet.DepositAddress) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid source wallet address")
|
||||||
|
}
|
||||||
|
if !common.IsHexAddress(destination) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid destination address")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := ethclient.DialContext(ctx, rpcURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to connect to rpc: " + err.Error())
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tokenABI, err := abi.JSON(strings.NewReader(erc20TransferABI))
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to parse erc20 abi: " + err.Error())
|
||||||
|
}
|
||||||
|
tokenAddr := common.HexToAddress(wallet.ContractAddress)
|
||||||
|
toAddr := common.HexToAddress(destination)
|
||||||
|
fromAddr := common.HexToAddress(wallet.DepositAddress)
|
||||||
|
|
||||||
|
decimals, err := erc20Decimals(timeoutCtx, client, tokenABI, tokenAddr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("failed to read token decimals", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
amountBase, err := toBaseUnits(strings.TrimSpace(amount.GetAmount()), decimals)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err := tokenABI.Pack("transfer", toAddr, amountBase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to encode transfer call: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
gasPrice, err := client.SuggestGasPrice(timeoutCtx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to suggest gas price: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
callMsg := ethereum.CallMsg{
|
||||||
|
From: fromAddr,
|
||||||
|
To: &tokenAddr,
|
||||||
|
GasPrice: gasPrice,
|
||||||
|
Data: input,
|
||||||
|
}
|
||||||
|
gasLimit, err := client.EstimateGas(timeoutCtx, callMsg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to estimate gas: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fee := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasLimit))
|
||||||
|
feeDec := decimal.NewFromBigInt(fee, 0)
|
||||||
|
|
||||||
|
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
||||||
|
if currency == "" {
|
||||||
|
currency = strings.ToUpper(network.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &moneyv1.Money{
|
||||||
|
Currency: currency,
|
||||||
|
Amount: feeDec.String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func erc20Decimals(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address) (uint8, error) {
|
||||||
|
callData, err := tokenABI.Pack("decimals")
|
||||||
|
if err != nil {
|
||||||
|
return 0, merrors.Internal("failed to encode decimals call: " + err.Error())
|
||||||
|
}
|
||||||
|
msg := ethereum.CallMsg{
|
||||||
|
To: &token,
|
||||||
|
Data: callData,
|
||||||
|
}
|
||||||
|
output, err := client.CallContract(ctx, msg, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, merrors.Internal("decimals call failed: " + err.Error())
|
||||||
|
}
|
||||||
|
values, err := tokenABI.Unpack("decimals", output)
|
||||||
|
if err != nil {
|
||||||
|
return 0, merrors.Internal("failed to unpack decimals: " + err.Error())
|
||||||
|
}
|
||||||
|
if len(values) == 0 {
|
||||||
|
return 0, merrors.Internal("decimals call returned no data")
|
||||||
|
}
|
||||||
|
decimals, ok := values[0].(uint8)
|
||||||
|
if !ok {
|
||||||
|
return 0, merrors.Internal("decimals call returned unexpected type")
|
||||||
|
}
|
||||||
|
return decimals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBaseUnits(amount string, decimals uint8) (*big.Int, error) {
|
||||||
|
value, err := decimal.NewFromString(strings.TrimSpace(amount))
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.InvalidArgument("invalid amount " + amount + ": " + err.Error())
|
||||||
|
}
|
||||||
|
if value.IsNegative() {
|
||||||
|
return nil, merrors.InvalidArgument("amount must be positive")
|
||||||
|
}
|
||||||
|
multiplier := decimal.NewFromInt(1).Shift(int32(decimals))
|
||||||
|
scaled := value.Mul(multiplier)
|
||||||
|
if !scaled.Equal(scaled.Truncate(0)) {
|
||||||
|
return nil, merrors.InvalidArgument("amount " + amount + " exceeds token precision")
|
||||||
|
}
|
||||||
|
return scaled.BigInt(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const erc20TransferABI = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "decimals",
|
||||||
|
"outputs": [{ "name": "", "type": "uint8" }],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{ "name": "_to", "type": "address" },
|
||||||
|
{ "name": "_value", "type": "uint256" }
|
||||||
|
],
|
||||||
|
"name": "transfer",
|
||||||
|
"outputs": [{ "name": "", "type": "bool" }],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
]`
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getTransferCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGetTransfer(deps Deps) *getTransferCommand {
|
||||||
|
return &getTransferCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *getTransferCommand) Execute(ctx context.Context, req *chainv1.GetTransferRequest) gsresponse.Responder[chainv1.GetTransferResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
transferRef := strings.TrimSpace(req.GetTransferRef())
|
||||||
|
if transferRef == "" {
|
||||||
|
c.deps.Logger.Warn("transfer_ref missing")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("transfer_ref is required"))
|
||||||
|
}
|
||||||
|
transfer, err := c.deps.Storage.Transfers().Get(ctx, transferRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("not found", zap.String("transfer_ref", transferRef))
|
||||||
|
return gsresponse.NotFound[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("transfer_ref", transferRef))
|
||||||
|
return gsresponse.Auto[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
return gsresponse.Success(&chainv1.GetTransferResponse{Transfer: toProtoTransfer(transfer)})
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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/mservice"
|
||||||
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listTransfersCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListTransfers(deps Deps) *listTransfersCommand {
|
||||||
|
return &listTransfersCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *listTransfersCommand) Execute(ctx context.Context, req *chainv1.ListTransfersRequest) gsresponse.Responder[chainv1.ListTransfersResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.ListTransfersResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
filter := model.TransferFilter{}
|
||||||
|
if req != nil {
|
||||||
|
filter.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
|
||||||
|
filter.DestinationWalletRef = strings.TrimSpace(req.GetDestinationWalletRef())
|
||||||
|
if status := shared.TransferStatusToModel(req.GetStatus()); status != "" {
|
||||||
|
filter.Status = status
|
||||||
|
}
|
||||||
|
if page := req.GetPage(); page != nil {
|
||||||
|
filter.Cursor = strings.TrimSpace(page.GetCursor())
|
||||||
|
filter.Limit = page.GetLimit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := c.deps.Storage.Transfers().List(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("storage list failed", zap.Error(err))
|
||||||
|
return gsresponse.Auto[chainv1.ListTransfersResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
protoTransfers := make([]*chainv1.Transfer, 0, len(result.Items))
|
||||||
|
for _, transfer := range result.Items {
|
||||||
|
protoTransfers = append(protoTransfers, toProtoTransfer(transfer))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &chainv1.ListTransfersResponse{
|
||||||
|
Transfers: protoTransfers,
|
||||||
|
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
|
||||||
|
}
|
||||||
|
return gsresponse.Success(resp)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toProtoTransfer(transfer *model.Transfer) *chainv1.Transfer {
|
||||||
|
if transfer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
destination := &chainv1.TransferDestination{}
|
||||||
|
if transfer.Destination.ManagedWalletRef != "" {
|
||||||
|
destination.Destination = &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: transfer.Destination.ManagedWalletRef}
|
||||||
|
} else if transfer.Destination.ExternalAddress != "" {
|
||||||
|
destination.Destination = &chainv1.TransferDestination_ExternalAddress{ExternalAddress: transfer.Destination.ExternalAddress}
|
||||||
|
}
|
||||||
|
destination.Memo = transfer.Destination.Memo
|
||||||
|
|
||||||
|
protoFees := make([]*chainv1.ServiceFeeBreakdown, 0, len(transfer.Fees))
|
||||||
|
for _, fee := range transfer.Fees {
|
||||||
|
protoFees = append(protoFees, &chainv1.ServiceFeeBreakdown{
|
||||||
|
FeeCode: fee.FeeCode,
|
||||||
|
Amount: shared.CloneMoney(fee.Amount),
|
||||||
|
Description: fee.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
asset := &chainv1.Asset{
|
||||||
|
Chain: shared.ChainEnumFromName(transfer.Network),
|
||||||
|
TokenSymbol: transfer.TokenSymbol,
|
||||||
|
ContractAddress: transfer.ContractAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &chainv1.Transfer{
|
||||||
|
TransferRef: transfer.TransferRef,
|
||||||
|
IdempotencyKey: transfer.IdempotencyKey,
|
||||||
|
OrganizationRef: transfer.OrganizationRef,
|
||||||
|
SourceWalletRef: transfer.SourceWalletRef,
|
||||||
|
Destination: destination,
|
||||||
|
Asset: asset,
|
||||||
|
RequestedAmount: shared.CloneMoney(transfer.RequestedAmount),
|
||||||
|
NetAmount: shared.CloneMoney(transfer.NetAmount),
|
||||||
|
Fees: protoFees,
|
||||||
|
Status: shared.TransferStatusToProto(transfer.Status),
|
||||||
|
TransactionHash: transfer.TxHash,
|
||||||
|
FailureReason: transfer.FailureReason,
|
||||||
|
CreatedAt: timestamppb.New(transfer.CreatedAt.UTC()),
|
||||||
|
UpdatedAt: timestamppb.New(transfer.UpdatedAt.UTC()),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"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"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type submitTransferCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubmitTransfer(deps Deps) *submitTransferCommand {
|
||||||
|
return &submitTransferCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.SubmitTransferRequest) gsresponse.Responder[chainv1.SubmitTransferResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
|
||||||
|
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
||||||
|
if idempotencyKey == "" {
|
||||||
|
c.deps.Logger.Warn("missing idempotency key")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
||||||
|
}
|
||||||
|
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
||||||
|
if organizationRef == "" {
|
||||||
|
c.deps.Logger.Warn("missing organization ref")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
||||||
|
}
|
||||||
|
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
|
||||||
|
if sourceWalletRef == "" {
|
||||||
|
c.deps.Logger.Warn("missing source wallet ref")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
|
||||||
|
}
|
||||||
|
amount := req.GetAmount()
|
||||||
|
if amount == nil {
|
||||||
|
c.deps.Logger.Warn("missing amount")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
|
||||||
|
}
|
||||||
|
amountCurrency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
|
||||||
|
if amountCurrency == "" {
|
||||||
|
c.deps.Logger.Warn("missing amount currency")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount.currency is required"))
|
||||||
|
}
|
||||||
|
amountValue := strings.TrimSpace(amount.GetAmount())
|
||||||
|
if amountValue == "" {
|
||||||
|
c.deps.Logger.Warn("missing amount value")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceWallet, err := c.deps.Storage.Wallets().Get(ctx, sourceWalletRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("source wallet not found", zap.String("source_wallet_ref", sourceWalletRef))
|
||||||
|
return gsresponse.NotFound[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage get wallet failed", zap.Error(err), zap.String("source_wallet_ref", sourceWalletRef))
|
||||||
|
return gsresponse.Auto[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sourceWallet.OrganizationRef, organizationRef) {
|
||||||
|
c.deps.Logger.Warn("organization mismatch", zap.String("wallet_org", sourceWallet.OrganizationRef), zap.String("req_org", organizationRef))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref mismatch with wallet"))
|
||||||
|
}
|
||||||
|
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
|
||||||
|
networkCfg, ok := c.deps.Networks[networkKey]
|
||||||
|
if !ok {
|
||||||
|
c.deps.Logger.Warn("unsupported chain", zap.String("network", networkKey))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
destination, err := resolveDestination(ctx, c.deps, req.GetDestination(), sourceWallet)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("destination not found", zap.String("destination_wallet_ref", req.GetDestination().GetManagedWalletRef()))
|
||||||
|
return gsresponse.NotFound[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("invalid destination", zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fees, feeSum, err := convertFees(req.GetFees(), amountCurrency)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("fee conversion failed", zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
amountDec, err := decimal.NewFromString(amountValue)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("invalid amount", zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("invalid amount"))
|
||||||
|
}
|
||||||
|
netDec := amountDec.Sub(feeSum)
|
||||||
|
if netDec.IsNegative() {
|
||||||
|
c.deps.Logger.Warn("fees exceed amount", zap.String("amount", amountValue), zap.String("fee_sum", feeSum.String()))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("fees exceed amount"))
|
||||||
|
}
|
||||||
|
|
||||||
|
netAmount := shared.CloneMoney(amount)
|
||||||
|
netAmount.Amount = netDec.String()
|
||||||
|
|
||||||
|
transfer := &model.Transfer{
|
||||||
|
IdempotencyKey: idempotencyKey,
|
||||||
|
TransferRef: shared.GenerateTransferRef(),
|
||||||
|
OrganizationRef: organizationRef,
|
||||||
|
SourceWalletRef: sourceWalletRef,
|
||||||
|
Destination: destination,
|
||||||
|
Network: sourceWallet.Network,
|
||||||
|
TokenSymbol: sourceWallet.TokenSymbol,
|
||||||
|
ContractAddress: sourceWallet.ContractAddress,
|
||||||
|
RequestedAmount: shared.CloneMoney(amount),
|
||||||
|
NetAmount: netAmount,
|
||||||
|
Fees: fees,
|
||||||
|
Status: model.TransferStatusPending,
|
||||||
|
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
||||||
|
LastStatusAt: c.deps.Clock.Now().UTC(),
|
||||||
|
}
|
||||||
|
|
||||||
|
saved, err := c.deps.Storage.Transfers().Create(ctx, transfer)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrDataConflict) {
|
||||||
|
c.deps.Logger.Debug("transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", idempotencyKey))
|
||||||
|
return gsresponse.Success(&chainv1.SubmitTransferResponse{Transfer: toProtoTransfer(saved)})
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage create failed", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
|
||||||
|
return gsresponse.Auto[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.deps.LaunchExecution != nil {
|
||||||
|
c.deps.LaunchExecution(saved.TransferRef, sourceWalletRef, networkCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gsresponse.Success(&chainv1.SubmitTransferResponse{Transfer: toProtoTransfer(saved)})
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getWalletBalanceCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGetWalletBalance(deps Deps) *getWalletBalanceCommand {
|
||||||
|
return &getWalletBalanceCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *getWalletBalanceCommand) Execute(ctx context.Context, req *chainv1.GetWalletBalanceRequest) gsresponse.Responder[chainv1.GetWalletBalanceResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
walletRef := strings.TrimSpace(req.GetWalletRef())
|
||||||
|
if walletRef == "" {
|
||||||
|
c.deps.Logger.Warn("wallet_ref missing")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
||||||
|
}
|
||||||
|
wallet, err := c.deps.Storage.Wallets().Get(ctx, walletRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("not found", zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.NotFound[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(stored)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{Balance: onChainBalanceToProto(balance)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func onChainBalanceToProto(balance *moneyv1.Money) *chainv1.WalletBalance {
|
||||||
|
if balance == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
zero := &moneyv1.Money{Currency: balance.Currency, Amount: "0"}
|
||||||
|
return &chainv1.WalletBalance{
|
||||||
|
Available: balance,
|
||||||
|
PendingInbound: zero,
|
||||||
|
PendingOutbound: zero,
|
||||||
|
CalculatedAt: timestamppb.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createManagedWalletCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCreateManagedWallet(deps Deps) *createManagedWalletCommand {
|
||||||
|
return &createManagedWalletCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.CreateManagedWalletRequest) gsresponse.Responder[chainv1.CreateManagedWalletResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
|
||||||
|
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
||||||
|
if idempotencyKey == "" {
|
||||||
|
c.deps.Logger.Warn("missing idempotency key")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
||||||
|
}
|
||||||
|
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
||||||
|
if organizationRef == "" {
|
||||||
|
c.deps.Logger.Warn("missing organization ref")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
||||||
|
}
|
||||||
|
ownerRef := strings.TrimSpace(req.GetOwnerRef())
|
||||||
|
if ownerRef == "" {
|
||||||
|
c.deps.Logger.Warn("missing owner ref")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("owner_ref is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
asset := req.GetAsset()
|
||||||
|
if asset == nil {
|
||||||
|
c.deps.Logger.Warn("missing asset")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
chainKey, _ := shared.ChainKeyFromEnum(asset.GetChain())
|
||||||
|
if chainKey == "" {
|
||||||
|
c.deps.Logger.Warn("unsupported chain", zap.Any("chain", asset.GetChain()))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
||||||
|
}
|
||||||
|
networkCfg, ok := c.deps.Networks[chainKey]
|
||||||
|
if !ok {
|
||||||
|
c.deps.Logger.Warn("unsupported chain in config", zap.String("chain", chainKey))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenSymbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
|
||||||
|
if tokenSymbol == "" {
|
||||||
|
c.deps.Logger.Warn("missing token symbol")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset.token_symbol is required"))
|
||||||
|
}
|
||||||
|
contractAddress := strings.ToLower(strings.TrimSpace(asset.GetContractAddress()))
|
||||||
|
if contractAddress == "" {
|
||||||
|
contractAddress = shared.ResolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
|
||||||
|
if contractAddress == "" {
|
||||||
|
c.deps.Logger.Warn("unsupported token", zap.String("token", tokenSymbol), zap.String("chain", chainKey))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walletRef := shared.GenerateWalletRef()
|
||||||
|
if c.deps.KeyManager == nil {
|
||||||
|
c.deps.Logger.Warn("key manager missing")
|
||||||
|
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("key manager not configured"))
|
||||||
|
}
|
||||||
|
|
||||||
|
keyInfo, err := c.deps.KeyManager.CreateManagedWalletKey(ctx, walletRef, chainKey)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("key manager error", zap.Error(err))
|
||||||
|
return gsresponse.Auto[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if keyInfo == nil || strings.TrimSpace(keyInfo.Address) == "" {
|
||||||
|
c.deps.Logger.Warn("key manager returned empty address")
|
||||||
|
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address"))
|
||||||
|
}
|
||||||
|
|
||||||
|
wallet := &model.ManagedWallet{
|
||||||
|
IdempotencyKey: idempotencyKey,
|
||||||
|
WalletRef: walletRef,
|
||||||
|
OrganizationRef: organizationRef,
|
||||||
|
OwnerRef: ownerRef,
|
||||||
|
Network: chainKey,
|
||||||
|
TokenSymbol: tokenSymbol,
|
||||||
|
ContractAddress: contractAddress,
|
||||||
|
DepositAddress: strings.ToLower(keyInfo.Address),
|
||||||
|
KeyReference: keyInfo.KeyID,
|
||||||
|
Status: model.ManagedWalletStatusActive,
|
||||||
|
Metadata: shared.CloneMetadata(req.GetMetadata()),
|
||||||
|
}
|
||||||
|
|
||||||
|
created, err := c.deps.Storage.Wallets().Create(ctx, wallet)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrDataConflict) {
|
||||||
|
c.deps.Logger.Debug("wallet already exists", zap.String("wallet_ref", walletRef), zap.String("idempotency_key", idempotencyKey))
|
||||||
|
return gsresponse.Success(&chainv1.CreateManagedWalletResponse{Wallet: toProtoManagedWallet(created)})
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage create failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.Auto[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gsresponse.Success(&chainv1.CreateManagedWalletResponse{Wallet: toProtoManagedWallet(created)})
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
Logger mlogger.Logger
|
||||||
|
Networks map[string]shared.Network
|
||||||
|
KeyManager keymanager.Manager
|
||||||
|
Storage storage.Repository
|
||||||
|
EnsureRepository func(context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Deps) WithLogger(name string) Deps {
|
||||||
|
if d.Logger != nil {
|
||||||
|
d.Logger = d.Logger.Named(name)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getManagedWalletCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGetManagedWallet(deps Deps) *getManagedWalletCommand {
|
||||||
|
return &getManagedWalletCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *getManagedWalletCommand) Execute(ctx context.Context, req *chainv1.GetManagedWalletRequest) gsresponse.Responder[chainv1.GetManagedWalletResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
walletRef := strings.TrimSpace(req.GetWalletRef())
|
||||||
|
if walletRef == "" {
|
||||||
|
c.deps.Logger.Warn("wallet_ref missing")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
||||||
|
}
|
||||||
|
wallet, err := c.deps.Storage.Wallets().Get(ctx, walletRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("not found", zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.NotFound[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.Auto[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
return gsresponse.Success(&chainv1.GetManagedWalletResponse{Wallet: toProtoManagedWallet(wallet)})
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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/mservice"
|
||||||
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listManagedWalletsCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListManagedWallets(deps Deps) *listManagedWalletsCommand {
|
||||||
|
return &listManagedWalletsCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *listManagedWalletsCommand) Execute(ctx context.Context, req *chainv1.ListManagedWalletsRequest) gsresponse.Responder[chainv1.ListManagedWalletsResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.ListManagedWalletsResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
filter := model.ManagedWalletFilter{}
|
||||||
|
if req != nil {
|
||||||
|
filter.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
||||||
|
filter.OwnerRef = strings.TrimSpace(req.GetOwnerRef())
|
||||||
|
if asset := req.GetAsset(); asset != nil {
|
||||||
|
filter.Network, _ = shared.ChainKeyFromEnum(asset.GetChain())
|
||||||
|
filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol())
|
||||||
|
}
|
||||||
|
if page := req.GetPage(); page != nil {
|
||||||
|
filter.Cursor = strings.TrimSpace(page.GetCursor())
|
||||||
|
filter.Limit = page.GetLimit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := c.deps.Storage.Wallets().List(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("storage list failed", zap.Error(err))
|
||||||
|
return gsresponse.Auto[chainv1.ListManagedWalletsResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
protoWallets := make([]*chainv1.ManagedWallet, 0, len(result.Items))
|
||||||
|
for _, wallet := range result.Items {
|
||||||
|
protoWallets = append(protoWallets, toProtoManagedWallet(wallet))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &chainv1.ListManagedWalletsResponse{
|
||||||
|
Wallets: protoWallets,
|
||||||
|
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
|
||||||
|
}
|
||||||
|
return gsresponse.Success(resp)
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum"
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func onChainWalletBalance(ctx context.Context, deps Deps, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||||
|
network := deps.Networks[strings.ToLower(strings.TrimSpace(wallet.Network))]
|
||||||
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
|
if rpcURL == "" {
|
||||||
|
return nil, merrors.Internal("network rpc url is not configured")
|
||||||
|
}
|
||||||
|
contract := strings.TrimSpace(wallet.ContractAddress)
|
||||||
|
if contract == "" || !common.IsHexAddress(contract) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid contract address")
|
||||||
|
}
|
||||||
|
if wallet.DepositAddress == "" || !common.IsHexAddress(wallet.DepositAddress) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid wallet address")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := ethclient.DialContext(ctx, rpcURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to connect rpc: " + err.Error())
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tokenABI, err := abi.JSON(strings.NewReader(erc20ABIJSON))
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to parse erc20 abi: " + err.Error())
|
||||||
|
}
|
||||||
|
tokenAddr := common.HexToAddress(contract)
|
||||||
|
walletAddr := common.HexToAddress(wallet.DepositAddress)
|
||||||
|
|
||||||
|
decimals, err := readDecimals(timeoutCtx, client, tokenABI, tokenAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bal, err := readBalanceOf(timeoutCtx, client, tokenABI, tokenAddr, walletAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := decimal.NewFromBigInt(bal, 0).Shift(-int32(decimals))
|
||||||
|
return &moneyv1.Money{Currency: wallet.TokenSymbol, Amount: dec.String()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDecimals(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address) (uint8, error) {
|
||||||
|
data, err := tokenABI.Pack("decimals")
|
||||||
|
if err != nil {
|
||||||
|
return 0, merrors.Internal("failed to encode decimals call: " + err.Error())
|
||||||
|
}
|
||||||
|
msg := ethereum.CallMsg{To: &token, Data: data}
|
||||||
|
out, err := client.CallContract(ctx, msg, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, merrors.Internal("decimals call failed: " + err.Error())
|
||||||
|
}
|
||||||
|
values, err := tokenABI.Unpack("decimals", out)
|
||||||
|
if err != nil || len(values) == 0 {
|
||||||
|
return 0, merrors.Internal("failed to unpack decimals")
|
||||||
|
}
|
||||||
|
if val, ok := values[0].(uint8); ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, merrors.Internal("decimals returned unexpected type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBalanceOf(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address, wallet common.Address) (*big.Int, error) {
|
||||||
|
data, err := tokenABI.Pack("balanceOf", wallet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to encode balanceOf: " + err.Error())
|
||||||
|
}
|
||||||
|
msg := ethereum.CallMsg{To: &token, Data: data}
|
||||||
|
out, err := client.CallContract(ctx, msg, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("balanceOf call failed: " + err.Error())
|
||||||
|
}
|
||||||
|
values, err := tokenABI.Unpack("balanceOf", out)
|
||||||
|
if err != nil || len(values) == 0 {
|
||||||
|
return nil, merrors.Internal("failed to unpack balanceOf")
|
||||||
|
}
|
||||||
|
raw, ok := values[0].(*big.Int)
|
||||||
|
if !ok {
|
||||||
|
return nil, merrors.Internal("balanceOf returned unexpected type")
|
||||||
|
}
|
||||||
|
return decimal.NewFromBigInt(raw, 0).BigInt(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const erc20ABIJSON = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "decimals",
|
||||||
|
"outputs": [{ "name": "", "type": "uint8" }],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [{ "name": "_owner", "type": "address" }],
|
||||||
|
"name": "balanceOf",
|
||||||
|
"outputs": [{ "name": "balance", "type": "uint256" }],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
]`
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toProtoManagedWallet(wallet *model.ManagedWallet) *chainv1.ManagedWallet {
|
||||||
|
if wallet == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
asset := &chainv1.Asset{
|
||||||
|
Chain: shared.ChainEnumFromName(wallet.Network),
|
||||||
|
TokenSymbol: wallet.TokenSymbol,
|
||||||
|
ContractAddress: wallet.ContractAddress,
|
||||||
|
}
|
||||||
|
return &chainv1.ManagedWallet{
|
||||||
|
WalletRef: wallet.WalletRef,
|
||||||
|
OrganizationRef: wallet.OrganizationRef,
|
||||||
|
OwnerRef: wallet.OwnerRef,
|
||||||
|
Asset: asset,
|
||||||
|
DepositAddress: wallet.DepositAddress,
|
||||||
|
Status: shared.ManagedWalletStatusToProto(wallet.Status),
|
||||||
|
Metadata: shared.CloneMetadata(wallet.Metadata),
|
||||||
|
CreatedAt: timestamppb.New(wallet.CreatedAt.UTC()),
|
||||||
|
UpdatedAt: timestamppb.New(wallet.UpdatedAt.UTC()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toProtoWalletBalance(balance *model.WalletBalance) *chainv1.WalletBalance {
|
||||||
|
if balance == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &chainv1.WalletBalance{
|
||||||
|
Available: shared.CloneMoney(balance.Available),
|
||||||
|
PendingInbound: shared.CloneMoney(balance.PendingInbound),
|
||||||
|
PendingOutbound: shared.CloneMoney(balance.PendingOutbound),
|
||||||
|
CalculatedAt: timestamppb.New(balance.CalculatedAt.UTC()),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,18 +14,19 @@ import (
|
|||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/ethclient"
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TransferExecutor handles on-chain submission of transfers.
|
// TransferExecutor handles on-chain submission of transfers.
|
||||||
type TransferExecutor interface {
|
type TransferExecutor interface {
|
||||||
SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network Network) (string, error)
|
SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network shared.Network) (string, error)
|
||||||
AwaitConfirmation(ctx context.Context, network Network, txHash string) (*types.Receipt, error)
|
AwaitConfirmation(ctx context.Context, network shared.Network, txHash string) (*types.Receipt, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOnChainExecutor constructs a TransferExecutor that talks to an EVM-compatible chain.
|
// NewOnChainExecutor constructs a TransferExecutor that talks to an EVM-compatible chain.
|
||||||
@@ -45,7 +46,7 @@ type onChainExecutor struct {
|
|||||||
clients map[string]*ethclient.Client
|
clients map[string]*ethclient.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network Network) (string, error) {
|
func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network shared.Network) (string, error) {
|
||||||
if o.keyManager == nil {
|
if o.keyManager == nil {
|
||||||
o.logger.Error("key manager not configured")
|
o.logger.Error("key manager not configured")
|
||||||
return "", executorInternal("key manager is not configured", nil)
|
return "", executorInternal("key manager is not configured", nil)
|
||||||
@@ -237,7 +238,7 @@ func (o *onChainExecutor) getClient(ctx context.Context, rpcURL string) (*ethcli
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network Network, txHash string) (*types.Receipt, error) {
|
func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network shared.Network, txHash string) (*types.Receipt, error) {
|
||||||
if strings.TrimSpace(txHash) == "" {
|
if strings.TrimSpace(txHash) == "" {
|
||||||
o.logger.Warn("missing transaction hash for confirmation", zap.String("network", network.Name))
|
o.logger.Warn("missing transaction hash for confirmation", zap.String("network", network.Name))
|
||||||
return nil, executorInvalid("tx hash is required")
|
return nil, executorInvalid("tx hash is required")
|
||||||
@@ -3,35 +3,14 @@ package gateway
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option configures the Service.
|
// Option configures the Service.
|
||||||
type Option func(*Service)
|
type Option func(*Service)
|
||||||
|
|
||||||
// Network describes a supported blockchain network and known token contracts.
|
|
||||||
type Network struct {
|
|
||||||
Name string
|
|
||||||
RPCURL string
|
|
||||||
ChainID uint64
|
|
||||||
NativeToken string
|
|
||||||
TokenConfigs []TokenContract
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenContract captures the metadata needed to work with a specific on-chain token.
|
|
||||||
type TokenContract struct {
|
|
||||||
Symbol string
|
|
||||||
ContractAddress string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceWallet captures the managed service wallet configuration.
|
|
||||||
type ServiceWallet struct {
|
|
||||||
Network string
|
|
||||||
Address string
|
|
||||||
PrivateKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithKeyManager configures the service key manager.
|
// WithKeyManager configures the service key manager.
|
||||||
func WithKeyManager(manager keymanager.Manager) Option {
|
func WithKeyManager(manager keymanager.Manager) Option {
|
||||||
return func(s *Service) {
|
return func(s *Service) {
|
||||||
@@ -47,13 +26,13 @@ func WithTransferExecutor(executor TransferExecutor) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithNetworks configures supported blockchain networks.
|
// WithNetworks configures supported blockchain networks.
|
||||||
func WithNetworks(networks []Network) Option {
|
func WithNetworks(networks []shared.Network) Option {
|
||||||
return func(s *Service) {
|
return func(s *Service) {
|
||||||
if len(networks) == 0 {
|
if len(networks) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.networks == nil {
|
if s.networks == nil {
|
||||||
s.networks = make(map[string]Network, len(networks))
|
s.networks = make(map[string]shared.Network, len(networks))
|
||||||
}
|
}
|
||||||
for _, network := range networks {
|
for _, network := range networks {
|
||||||
if network.Name == "" {
|
if network.Name == "" {
|
||||||
@@ -61,7 +40,7 @@ func WithNetworks(networks []Network) Option {
|
|||||||
}
|
}
|
||||||
clone := network
|
clone := network
|
||||||
if clone.TokenConfigs == nil {
|
if clone.TokenConfigs == nil {
|
||||||
clone.TokenConfigs = []TokenContract{}
|
clone.TokenConfigs = []shared.TokenContract{}
|
||||||
}
|
}
|
||||||
for i := range clone.TokenConfigs {
|
for i := range clone.TokenConfigs {
|
||||||
clone.TokenConfigs[i].Symbol = strings.ToUpper(strings.TrimSpace(clone.TokenConfigs[i].Symbol))
|
clone.TokenConfigs[i].Symbol = strings.ToUpper(strings.TrimSpace(clone.TokenConfigs[i].Symbol))
|
||||||
@@ -74,7 +53,7 @@ func WithNetworks(networks []Network) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithServiceWallet configures the service wallet binding.
|
// WithServiceWallet configures the service wallet binding.
|
||||||
func WithServiceWallet(wallet ServiceWallet) Option {
|
func WithServiceWallet(wallet shared.ServiceWallet) Option {
|
||||||
return func(s *Service) {
|
return func(s *Service) {
|
||||||
s.serviceWallet = wallet
|
s.serviceWallet = wallet
|
||||||
}
|
}
|
||||||
153
api/gateway/chain/internal/service/gateway/service.go
Normal file
153
api/gateway/chain/internal/service/gateway/service.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/transfer"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/wallet"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serviceError string
|
||||||
|
|
||||||
|
func (e serviceError) Error() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errStorageUnavailable = serviceError("chain_gateway: storage not initialised")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service implements the ChainGatewayService RPC contract.
|
||||||
|
type Service struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
storage storage.Repository
|
||||||
|
producer msg.Producer
|
||||||
|
clock clockpkg.Clock
|
||||||
|
|
||||||
|
networks map[string]shared.Network
|
||||||
|
serviceWallet shared.ServiceWallet
|
||||||
|
keyManager keymanager.Manager
|
||||||
|
executor TransferExecutor
|
||||||
|
commands commands.Registry
|
||||||
|
|
||||||
|
chainv1.UnimplementedChainGatewayServiceServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService constructs the chain gateway service skeleton.
|
||||||
|
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
|
||||||
|
svc := &Service{
|
||||||
|
logger: logger.Named("service"),
|
||||||
|
storage: repo,
|
||||||
|
producer: producer,
|
||||||
|
clock: clockpkg.System{},
|
||||||
|
networks: map[string]shared.Network{},
|
||||||
|
}
|
||||||
|
|
||||||
|
initMetrics()
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
if opt != nil {
|
||||||
|
opt(svc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if svc.clock == nil {
|
||||||
|
svc.clock = clockpkg.System{}
|
||||||
|
}
|
||||||
|
if svc.networks == nil {
|
||||||
|
svc.networks = map[string]shared.Network{}
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.commands = commands.NewRegistry(commands.RegistryDeps{
|
||||||
|
Wallet: commandsWalletDeps(svc),
|
||||||
|
Transfer: commandsTransferDeps(svc),
|
||||||
|
})
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register wires the service onto the provided gRPC router.
|
||||||
|
func (s *Service) Register(router routers.GRPC) error {
|
||||||
|
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||||
|
chainv1.RegisterChainGatewayServiceServer(reg, s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "CreateManagedWallet", s.commands.CreateManagedWallet.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "GetManagedWallet", s.commands.GetManagedWallet.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "ListManagedWallets", s.commands.ListManagedWallets.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "GetWalletBalance", s.commands.GetWalletBalance.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "SubmitTransfer", s.commands.SubmitTransfer.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "GetTransfer", s.commands.GetTransfer.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "ListTransfers", s.commands.ListTransfers.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "EstimateTransferFee", s.commands.EstimateTransfer.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ensureRepository(ctx context.Context) error {
|
||||||
|
if s.storage == nil {
|
||||||
|
return errStorageUnavailable
|
||||||
|
}
|
||||||
|
return s.storage.Ping(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandsWalletDeps(s *Service) wallet.Deps {
|
||||||
|
return wallet.Deps{
|
||||||
|
Logger: s.logger.Named("command"),
|
||||||
|
Networks: s.networks,
|
||||||
|
KeyManager: s.keyManager,
|
||||||
|
Storage: s.storage,
|
||||||
|
EnsureRepository: s.ensureRepository,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandsTransferDeps(s *Service) transfer.Deps {
|
||||||
|
return transfer.Deps{
|
||||||
|
Logger: s.logger.Named("transfer_cmd"),
|
||||||
|
Networks: s.networks,
|
||||||
|
Storage: s.storage,
|
||||||
|
Clock: s.clock,
|
||||||
|
EnsureRepository: s.ensureRepository,
|
||||||
|
LaunchExecution: s.launchTransferExecution,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeUnary[TReq any, TResp any](ctx context.Context, svc *Service, method string, handler func(context.Context, *TReq) gsresponse.Responder[TResp], req *TReq) (*TResp, error) {
|
||||||
|
start := svc.clock.Now()
|
||||||
|
resp, err := gsresponse.Unary(svc.logger, mservice.ChainGateway, handler)(ctx, req)
|
||||||
|
observeRPC(method, err, svc.clock.Now().Sub(start))
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
@@ -11,15 +11,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
igatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
ichainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
@@ -40,12 +41,12 @@ func TestCreateManagedWallet_Idempotent(t *testing.T) {
|
|||||||
svc, repo := newTestService(t)
|
svc, repo := newTestService(t)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
req := &igatewayv1.CreateManagedWalletRequest{
|
req := &ichainv1.CreateManagedWalletRequest{
|
||||||
IdempotencyKey: "idem-1",
|
IdempotencyKey: "idem-1",
|
||||||
OrganizationRef: "org-1",
|
OrganizationRef: "org-1",
|
||||||
OwnerRef: "owner-1",
|
OwnerRef: "owner-1",
|
||||||
Asset: &igatewayv1.Asset{
|
Asset: &ichainv1.Asset{
|
||||||
Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
||||||
TokenSymbol: "USDC",
|
TokenSymbol: "USDC",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -69,12 +70,12 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// create source wallet
|
// create source wallet
|
||||||
srcResp, err := svc.CreateManagedWallet(ctx, &igatewayv1.CreateManagedWalletRequest{
|
srcResp, err := svc.CreateManagedWallet(ctx, &ichainv1.CreateManagedWalletRequest{
|
||||||
IdempotencyKey: "idem-src",
|
IdempotencyKey: "idem-src",
|
||||||
OrganizationRef: "org-1",
|
OrganizationRef: "org-1",
|
||||||
OwnerRef: "owner-1",
|
OwnerRef: "owner-1",
|
||||||
Asset: &igatewayv1.Asset{
|
Asset: &ichainv1.Asset{
|
||||||
Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
||||||
TokenSymbol: "USDC",
|
TokenSymbol: "USDC",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -82,27 +83,27 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|||||||
srcRef := srcResp.GetWallet().GetWalletRef()
|
srcRef := srcResp.GetWallet().GetWalletRef()
|
||||||
|
|
||||||
// destination wallet
|
// destination wallet
|
||||||
dstResp, err := svc.CreateManagedWallet(ctx, &igatewayv1.CreateManagedWalletRequest{
|
dstResp, err := svc.CreateManagedWallet(ctx, &ichainv1.CreateManagedWalletRequest{
|
||||||
IdempotencyKey: "idem-dst",
|
IdempotencyKey: "idem-dst",
|
||||||
OrganizationRef: "org-1",
|
OrganizationRef: "org-1",
|
||||||
OwnerRef: "owner-2",
|
OwnerRef: "owner-2",
|
||||||
Asset: &igatewayv1.Asset{
|
Asset: &ichainv1.Asset{
|
||||||
Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
||||||
TokenSymbol: "USDC",
|
TokenSymbol: "USDC",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
dstRef := dstResp.GetWallet().GetWalletRef()
|
dstRef := dstResp.GetWallet().GetWalletRef()
|
||||||
|
|
||||||
transferResp, err := svc.SubmitTransfer(ctx, &igatewayv1.SubmitTransferRequest{
|
transferResp, err := svc.SubmitTransfer(ctx, &ichainv1.SubmitTransferRequest{
|
||||||
IdempotencyKey: "transfer-1",
|
IdempotencyKey: "transfer-1",
|
||||||
OrganizationRef: "org-1",
|
OrganizationRef: "org-1",
|
||||||
SourceWalletRef: srcRef,
|
SourceWalletRef: srcRef,
|
||||||
Destination: &igatewayv1.TransferDestination{
|
Destination: &ichainv1.TransferDestination{
|
||||||
Destination: &igatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef},
|
Destination: &ichainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef},
|
||||||
},
|
},
|
||||||
Amount: &moneyv1.Money{Currency: "USDC", Amount: "100"},
|
Amount: &moneyv1.Money{Currency: "USDC", Amount: "100"},
|
||||||
Fees: []*igatewayv1.ServiceFeeBreakdown{
|
Fees: []*ichainv1.ServiceFeeBreakdown{
|
||||||
{
|
{
|
||||||
FeeCode: "service",
|
FeeCode: "service",
|
||||||
Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"},
|
Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"},
|
||||||
@@ -118,12 +119,12 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|||||||
require.Equal(t, model.TransferStatusPending, stored.Status)
|
require.Equal(t, model.TransferStatusPending, stored.Status)
|
||||||
|
|
||||||
// GetTransfer
|
// GetTransfer
|
||||||
getResp, err := svc.GetTransfer(ctx, &igatewayv1.GetTransferRequest{TransferRef: stored.TransferRef})
|
getResp, err := svc.GetTransfer(ctx, &ichainv1.GetTransferRequest{TransferRef: stored.TransferRef})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, stored.TransferRef, getResp.GetTransfer().GetTransferRef())
|
require.Equal(t, stored.TransferRef, getResp.GetTransfer().GetTransferRef())
|
||||||
|
|
||||||
// ListTransfers
|
// ListTransfers
|
||||||
listResp, err := svc.ListTransfers(ctx, &igatewayv1.ListTransfersRequest{
|
listResp, err := svc.ListTransfers(ctx, &ichainv1.ListTransfersRequest{
|
||||||
SourceWalletRef: srcRef,
|
SourceWalletRef: srcRef,
|
||||||
Page: &paginationv1.CursorPageRequest{Limit: 10},
|
Page: &paginationv1.CursorPageRequest{Limit: 10},
|
||||||
})
|
})
|
||||||
@@ -136,7 +137,7 @@ func TestGetWalletBalance_NotFound(t *testing.T) {
|
|||||||
svc, _ := newTestService(t)
|
svc, _ := newTestService(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
_, err := svc.GetWalletBalance(ctx, &igatewayv1.GetWalletBalanceRequest{WalletRef: "missing"})
|
_, err := svc.GetWalletBalance(ctx, &ichainv1.GetWalletBalanceRequest{WalletRef: "missing"})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
st, _ := status.FromError(err)
|
st, _ := status.FromError(err)
|
||||||
require.Equal(t, codes.NotFound, st.Code())
|
require.Equal(t, codes.NotFound, st.Code())
|
||||||
@@ -530,13 +531,13 @@ func newTestService(_ *testing.T) (*Service, *inMemoryRepository) {
|
|||||||
logger := zap.NewNop()
|
logger := zap.NewNop()
|
||||||
svc := NewService(logger, repo, nil,
|
svc := NewService(logger, repo, nil,
|
||||||
WithKeyManager(&fakeKeyManager{}),
|
WithKeyManager(&fakeKeyManager{}),
|
||||||
WithNetworks([]Network{{
|
WithNetworks([]shared.Network{{
|
||||||
Name: "ethereum_mainnet",
|
Name: "ethereum_mainnet",
|
||||||
TokenConfigs: []TokenContract{
|
TokenConfigs: []shared.TokenContract{
|
||||||
{Symbol: "USDC", ContractAddress: "0xusdc"},
|
{Symbol: "USDC", ContractAddress: "0xusdc"},
|
||||||
},
|
},
|
||||||
}}),
|
}}),
|
||||||
WithServiceWallet(ServiceWallet{Network: "ethereum_mainnet", Address: "0xservice"}),
|
WithServiceWallet(shared.ServiceWallet{Network: "ethereum_mainnet", Address: "0xservice"}),
|
||||||
)
|
)
|
||||||
return svc, repo
|
return svc, repo
|
||||||
}
|
}
|
||||||
142
api/gateway/chain/internal/service/gateway/shared/helpers.go
Normal file
142
api/gateway/chain/internal/service/gateway/shared/helpers.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CloneMoney defensively copies a Money proto.
|
||||||
|
func CloneMoney(m *moneyv1.Money) *moneyv1.Money {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &moneyv1.Money{Amount: m.GetAmount(), Currency: m.GetCurrency()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloneMetadata defensively copies metadata maps.
|
||||||
|
func CloneMetadata(input map[string]string) map[string]string {
|
||||||
|
if len(input) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
clone := make(map[string]string, len(input))
|
||||||
|
for k, v := range input {
|
||||||
|
clone[k] = v
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveContractAddress finds a token contract for a symbol in a case-insensitive manner.
|
||||||
|
func ResolveContractAddress(tokens []TokenContract, symbol string) string {
|
||||||
|
upper := strings.ToUpper(symbol)
|
||||||
|
for _, token := range tokens {
|
||||||
|
if strings.EqualFold(token.Symbol, upper) && token.ContractAddress != "" {
|
||||||
|
return strings.ToLower(token.ContractAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateWalletRef() string {
|
||||||
|
return primitive.NewObjectID().Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateTransferRef() string {
|
||||||
|
return primitive.NewObjectID().Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChainKeyFromEnum(chain chainv1.ChainNetwork) (string, chainv1.ChainNetwork) {
|
||||||
|
if name, ok := chainv1.ChainNetwork_name[int32(chain)]; ok {
|
||||||
|
key := strings.ToLower(strings.TrimPrefix(name, "CHAIN_NETWORK_"))
|
||||||
|
return key, chain
|
||||||
|
}
|
||||||
|
return "", chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChainEnumFromName(name string) chainv1.ChainNetwork {
|
||||||
|
if name == "" {
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
||||||
|
}
|
||||||
|
upper := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(name, " ", "_"), "-", "_"))
|
||||||
|
key := "CHAIN_NETWORK_" + upper
|
||||||
|
if val, ok := chainv1.ChainNetwork_value[key]; ok {
|
||||||
|
return chainv1.ChainNetwork(val)
|
||||||
|
}
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
func ManagedWalletStatusToProto(status model.ManagedWalletStatus) chainv1.ManagedWalletStatus {
|
||||||
|
switch status {
|
||||||
|
case model.ManagedWalletStatusActive:
|
||||||
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE
|
||||||
|
case model.ManagedWalletStatusSuspended:
|
||||||
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED
|
||||||
|
case model.ManagedWalletStatusClosed:
|
||||||
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED
|
||||||
|
default:
|
||||||
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TransferStatusToModel(status chainv1.TransferStatus) model.TransferStatus {
|
||||||
|
switch status {
|
||||||
|
case chainv1.TransferStatus_TRANSFER_PENDING:
|
||||||
|
return model.TransferStatusPending
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SIGNING:
|
||||||
|
return model.TransferStatusSigning
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
||||||
|
return model.TransferStatusSubmitted
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
||||||
|
return model.TransferStatusConfirmed
|
||||||
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
|
return model.TransferStatusFailed
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
|
return model.TransferStatusCancelled
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TransferStatusToProto(status model.TransferStatus) chainv1.TransferStatus {
|
||||||
|
switch status {
|
||||||
|
case model.TransferStatusPending:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_PENDING
|
||||||
|
case model.TransferStatusSigning:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_SIGNING
|
||||||
|
case model.TransferStatusSubmitted:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_SUBMITTED
|
||||||
|
case model.TransferStatusConfirmed:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
||||||
|
case model.TransferStatusFailed:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_FAILED
|
||||||
|
case model.TransferStatusCancelled:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||||
|
default:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network describes a supported blockchain network and known token contracts.
|
||||||
|
type Network struct {
|
||||||
|
Name string
|
||||||
|
RPCURL string
|
||||||
|
ChainID uint64
|
||||||
|
NativeToken string
|
||||||
|
TokenConfigs []TokenContract
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenContract captures the metadata needed to work with a specific on-chain token.
|
||||||
|
type TokenContract struct {
|
||||||
|
Symbol string
|
||||||
|
ContractAddress string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceWallet captures the managed service wallet configuration.
|
||||||
|
type ServiceWallet struct {
|
||||||
|
Network string
|
||||||
|
Address string
|
||||||
|
PrivateKey string
|
||||||
|
}
|
||||||
@@ -7,17 +7,19 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, network Network) {
|
func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, network shared.Network) {
|
||||||
if s.executor == nil {
|
if s.executor == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go func(ref, walletRef string, net Network) {
|
go func(ref, walletRef string, net shared.Network) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, n
|
|||||||
}(transferRef, sourceWalletRef, network)
|
}(transferRef, sourceWalletRef, network)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWalletRef string, network Network) error {
|
func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWalletRef string, network shared.Network) error {
|
||||||
transfer, err := s.storage.Transfers().Get(ctx, transferRef)
|
transfer, err := s.storage.Transfers().Get(ctx, transferRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/tech/sendico/chain/gateway/internal/appversion"
|
"github.com/tech/sendico/gateway/chain/internal/appversion"
|
||||||
si "github.com/tech/sendico/chain/gateway/internal/server"
|
si "github.com/tech/sendico/gateway/chain/internal/server"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/server"
|
"github.com/tech/sendico/pkg/server"
|
||||||
smain "github.com/tech/sendico/pkg/server/main"
|
smain "github.com/tech/sendico/pkg/server/main"
|
||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/mongo/store"
|
"github.com/tech/sendico/gateway/chain/storage/mongo/store"
|
||||||
"github.com/tech/sendico/pkg/db"
|
"github.com/tech/sendico/pkg/db"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/db/repository"
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/db/repository"
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/db/repository"
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
@@ -3,7 +3,7 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type storageError string
|
type storageError string
|
||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
|
|
||||||
// Fake implements Client for tests.
|
// Fake implements Client for tests.
|
||||||
type Fake struct {
|
type Fake struct {
|
||||||
|
CreateAccountFn func(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
|
||||||
|
ListAccountsFn func(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
|
||||||
PostCreditWithChargesFn func(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
|
PostCreditWithChargesFn func(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
|
||||||
PostDebitWithChargesFn func(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
|
PostDebitWithChargesFn func(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
|
||||||
TransferInternalFn func(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
|
TransferInternalFn func(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
|
||||||
@@ -18,6 +20,20 @@ type Fake struct {
|
|||||||
CloseFn func() error
|
CloseFn func() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fake) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
||||||
|
if f.CreateAccountFn != nil {
|
||||||
|
return f.CreateAccountFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &ledgerv1.CreateAccountResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
|
||||||
|
if f.ListAccountsFn != nil {
|
||||||
|
return f.ListAccountsFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &ledgerv1.ListAccountsResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fake) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
func (f *Fake) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||||
if f.PostCreditWithChargesFn != nil {
|
if f.PostCreditWithChargesFn != nil {
|
||||||
return f.PostCreditWithChargesFn(ctx, req)
|
return f.PostCreditWithChargesFn(ctx, req)
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat
|
|||||||
func parseCursor(cursor string) (int, error) {
|
func parseCursor(cursor string) (int, error) {
|
||||||
decoded, err := base64.StdEncoding.DecodeString(cursor)
|
decoded, err := base64.StdEncoding.DecodeString(cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, merrors.InvalidArgumentWrap(err, "invalid cursor base64 encoding")
|
return 0, merrors.InvalidArgumentWrap(err, "invalid base64")
|
||||||
}
|
}
|
||||||
parts := strings.Split(string(decoded), ":")
|
parts := strings.Split(string(decoded), ":")
|
||||||
if len(parts) != 2 || parts[0] != "offset" {
|
if len(parts) != 2 || parts[0] != "offset" {
|
||||||
@@ -257,7 +257,7 @@ func parseCursor(cursor string) (int, error) {
|
|||||||
}
|
}
|
||||||
offset, err := strconv.Atoi(parts[1])
|
offset, err := strconv.Atoi(parts[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, merrors.InvalidArgumentWrap(err, "invalid cursor offset")
|
return 0, merrors.InvalidArgumentWrap(err, "invalid offset")
|
||||||
}
|
}
|
||||||
return offset, nil
|
return offset, nil
|
||||||
}
|
}
|
||||||
|
|||||||
1
api/notification/.gitignore
vendored
1
api/notification/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
notification
|
notification
|
||||||
|
.gocache
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ replace github.com/tech/sendico/pkg => ../../pkg
|
|||||||
|
|
||||||
replace github.com/tech/sendico/billing/fees => ../../billing/fees
|
replace github.com/tech/sendico/billing/fees => ../../billing/fees
|
||||||
|
|
||||||
replace github.com/tech/sendico/chain/gateway => ../../chain/gateway
|
replace github.com/tech/sendico/gateway/chain => ../../gateway/chain
|
||||||
|
|
||||||
replace github.com/tech/sendico/fx/oracle => ../../fx/oracle
|
replace github.com/tech/sendico/fx/oracle => ../../fx/oracle
|
||||||
|
|
||||||
@@ -15,8 +15,8 @@ replace github.com/tech/sendico/ledger => ../../ledger
|
|||||||
require (
|
require (
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/shopspring/decimal v1.4.0
|
github.com/shopspring/decimal v1.4.0
|
||||||
github.com/tech/sendico/chain/gateway v0.0.0-00010101000000-000000000000
|
|
||||||
github.com/tech/sendico/fx/oracle v0.0.0-00010101000000-000000000000
|
github.com/tech/sendico/fx/oracle v0.0.0-00010101000000-000000000000
|
||||||
|
github.com/tech/sendico/gateway/chain v0.0.0-00010101000000-000000000000
|
||||||
github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000
|
github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000
|
||||||
github.com/tech/sendico/pkg v0.1.0
|
github.com/tech/sendico/pkg v0.1.0
|
||||||
go.mongodb.org/mongo-driver v1.17.6
|
go.mongodb.org/mongo-driver v1.17.6
|
||||||
@@ -36,7 +36,7 @@ require (
|
|||||||
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
github.com/go-chi/chi/v5 v5.2.3 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
github.com/google/uuid v1.6.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-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
@@ -59,5 +59,5 @@ require (
|
|||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.31.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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -215,8 +215,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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
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-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
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/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 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
chainclient "github.com/tech/sendico/chain/gateway/client"
|
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||||
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
||||||
ledgerclient "github.com/tech/sendico/ledger/client"
|
ledgerclient "github.com/tech/sendico/ledger/client"
|
||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
@@ -327,11 +327,11 @@ func protoFailureFromModel(code model.PaymentFailureCode) orchestratorv1.Payment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneAsset(asset *gatewayv1.Asset) *gatewayv1.Asset {
|
func cloneAsset(asset *chainv1.Asset) *chainv1.Asset {
|
||||||
if asset == nil {
|
if asset == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &gatewayv1.Asset{
|
return &chainv1.Asset{
|
||||||
Chain: asset.GetChain(),
|
Chain: asset.GetChain(),
|
||||||
TokenSymbol: asset.GetTokenSymbol(),
|
TokenSymbol: asset.GetTokenSymbol(),
|
||||||
ContractAddress: asset.GetContractAddress(),
|
ContractAddress: asset.GetContractAddress(),
|
||||||
@@ -358,11 +358,11 @@ func cloneFXQuote(quote *oraclev1.Quote) *oraclev1.Quote {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneNetworkEstimate(resp *gatewayv1.EstimateTransferFeeResponse) *gatewayv1.EstimateTransferFeeResponse {
|
func cloneNetworkEstimate(resp *chainv1.EstimateTransferFeeResponse) *chainv1.EstimateTransferFeeResponse {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if cloned, ok := proto.Clone(resp).(*gatewayv1.EstimateTransferFeeResponse); ok {
|
if cloned, ok := proto.Clone(resp).(*chainv1.EstimateTransferFeeResponse); ok {
|
||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
@@ -29,7 +29,7 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
|
|||||||
}
|
}
|
||||||
feeTotal := extractFeeTotal(feeQuote.GetLines(), amount.GetCurrency())
|
feeTotal := extractFeeTotal(feeQuote.GetLines(), amount.GetCurrency())
|
||||||
|
|
||||||
var networkFee *gatewayv1.EstimateTransferFeeResponse
|
var networkFee *chainv1.EstimateTransferFeeResponse
|
||||||
if shouldEstimateNetworkFee(intent) {
|
if shouldEstimateNetworkFee(intent) {
|
||||||
networkFee, err = s.estimateNetworkFee(ctx, intent)
|
networkFee, err = s.estimateNetworkFee(ctx, intent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -90,25 +90,25 @@ func (s *Service) quoteFees(ctx context.Context, orgRef string, req *orchestrato
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) estimateNetworkFee(ctx context.Context, intent *orchestratorv1.PaymentIntent) (*gatewayv1.EstimateTransferFeeResponse, error) {
|
func (s *Service) estimateNetworkFee(ctx context.Context, intent *orchestratorv1.PaymentIntent) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||||
if !s.gateway.available() {
|
if !s.gateway.available() {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &gatewayv1.EstimateTransferFeeRequest{
|
req := &chainv1.EstimateTransferFeeRequest{
|
||||||
Amount: cloneMoney(intent.GetAmount()),
|
Amount: cloneMoney(intent.GetAmount()),
|
||||||
}
|
}
|
||||||
if src := intent.GetSource().GetManagedWallet(); src != nil {
|
if src := intent.GetSource().GetManagedWallet(); src != nil {
|
||||||
req.SourceWalletRef = strings.TrimSpace(src.GetManagedWalletRef())
|
req.SourceWalletRef = strings.TrimSpace(src.GetManagedWalletRef())
|
||||||
}
|
}
|
||||||
if dst := intent.GetDestination().GetManagedWallet(); dst != nil {
|
if dst := intent.GetDestination().GetManagedWallet(); dst != nil {
|
||||||
req.Destination = &gatewayv1.TransferDestination{
|
req.Destination = &chainv1.TransferDestination{
|
||||||
Destination: &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(dst.GetManagedWalletRef())},
|
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(dst.GetManagedWalletRef())},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dst := intent.GetDestination().GetExternalChain(); dst != nil {
|
if dst := intent.GetDestination().GetExternalChain(); dst != nil {
|
||||||
req.Destination = &gatewayv1.TransferDestination{
|
req.Destination = &chainv1.TransferDestination{
|
||||||
Destination: &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(dst.GetAddress())},
|
Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(dst.GetAddress())},
|
||||||
Memo: strings.TrimSpace(dst.GetMemo()),
|
Memo: strings.TrimSpace(dst.GetMemo()),
|
||||||
}
|
}
|
||||||
req.Asset = dst.GetAsset()
|
req.Asset = dst.GetAsset()
|
||||||
@@ -320,7 +320,7 @@ func (s *Service) applyFX(ctx context.Context, payment *model.Payment, quote *or
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) submitChainTransfer(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote) (*gatewayv1.SubmitTransferResponse, error) {
|
func (s *Service) submitChainTransfer(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote) (*chainv1.SubmitTransferResponse, error) {
|
||||||
intent := payment.Intent
|
intent := payment.Intent
|
||||||
source := intent.Source.ManagedWallet
|
source := intent.Source.ManagedWallet
|
||||||
destination := intent.Destination
|
destination := intent.Destination
|
||||||
@@ -336,7 +336,7 @@ func (s *Service) submitChainTransfer(ctx context.Context, payment *model.Paymen
|
|||||||
return nil, merrors.InvalidArgument("chain: amount is required")
|
return nil, merrors.InvalidArgument("chain: amount is required")
|
||||||
}
|
}
|
||||||
fees := feeBreakdownFromQuote(quote)
|
fees := feeBreakdownFromQuote(quote)
|
||||||
req := &gatewayv1.SubmitTransferRequest{
|
req := &chainv1.SubmitTransferRequest{
|
||||||
IdempotencyKey: payment.IdempotencyKey,
|
IdempotencyKey: payment.IdempotencyKey,
|
||||||
OrganizationRef: payment.OrganizationRef.Hex(),
|
OrganizationRef: payment.OrganizationRef.Hex(),
|
||||||
SourceWalletRef: strings.TrimSpace(source.ManagedWalletRef),
|
SourceWalletRef: strings.TrimSpace(source.ManagedWalletRef),
|
||||||
@@ -437,21 +437,21 @@ func hasManagedWallet(endpoint model.PaymentEndpoint) bool {
|
|||||||
return endpoint.Type == model.EndpointTypeManagedWallet && endpoint.ManagedWallet != nil && strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) != ""
|
return endpoint.Type == model.EndpointTypeManagedWallet && endpoint.ManagedWallet != nil && strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func toGatewayDestination(endpoint model.PaymentEndpoint) (*gatewayv1.TransferDestination, error) {
|
func toGatewayDestination(endpoint model.PaymentEndpoint) (*chainv1.TransferDestination, error) {
|
||||||
switch endpoint.Type {
|
switch endpoint.Type {
|
||||||
case model.EndpointTypeManagedWallet:
|
case model.EndpointTypeManagedWallet:
|
||||||
if endpoint.ManagedWallet == nil || strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) == "" {
|
if endpoint.ManagedWallet == nil || strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) == "" {
|
||||||
return nil, merrors.InvalidArgument("chain: destination managed wallet is required")
|
return nil, merrors.InvalidArgument("chain: destination managed wallet is required")
|
||||||
}
|
}
|
||||||
return &gatewayv1.TransferDestination{
|
return &chainv1.TransferDestination{
|
||||||
Destination: &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef)},
|
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef)},
|
||||||
}, nil
|
}, nil
|
||||||
case model.EndpointTypeExternalChain:
|
case model.EndpointTypeExternalChain:
|
||||||
if endpoint.ExternalChain == nil || strings.TrimSpace(endpoint.ExternalChain.Address) == "" {
|
if endpoint.ExternalChain == nil || strings.TrimSpace(endpoint.ExternalChain.Address) == "" {
|
||||||
return nil, merrors.InvalidArgument("chain: external address is required")
|
return nil, merrors.InvalidArgument("chain: external address is required")
|
||||||
}
|
}
|
||||||
return &gatewayv1.TransferDestination{
|
return &chainv1.TransferDestination{
|
||||||
Destination: &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(endpoint.ExternalChain.Address)},
|
Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(endpoint.ExternalChain.Address)},
|
||||||
Memo: strings.TrimSpace(endpoint.ExternalChain.Memo),
|
Memo: strings.TrimSpace(endpoint.ExternalChain.Memo),
|
||||||
}, nil
|
}, nil
|
||||||
default:
|
default:
|
||||||
@@ -459,7 +459,7 @@ func toGatewayDestination(endpoint model.PaymentEndpoint) (*gatewayv1.TransferDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyTransferStatus(event *gatewayv1.TransferStatusChangedEvent, payment *model.Payment) {
|
func applyTransferStatus(event *chainv1.TransferStatusChangedEvent, payment *model.Payment) {
|
||||||
if payment.Execution == nil {
|
if payment.Execution == nil {
|
||||||
payment.Execution = &model.ExecutionRefs{}
|
payment.Execution = &model.ExecutionRefs{}
|
||||||
}
|
}
|
||||||
@@ -473,21 +473,21 @@ func applyTransferStatus(event *gatewayv1.TransferStatusChangedEvent, payment *m
|
|||||||
reason = strings.TrimSpace(transfer.GetFailureReason())
|
reason = strings.TrimSpace(transfer.GetFailureReason())
|
||||||
}
|
}
|
||||||
switch transfer.GetStatus() {
|
switch transfer.GetStatus() {
|
||||||
case gatewayv1.TransferStatus_TRANSFER_CONFIRMED:
|
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
||||||
payment.State = model.PaymentStateSettled
|
payment.State = model.PaymentStateSettled
|
||||||
payment.FailureCode = model.PaymentFailureCodeUnspecified
|
payment.FailureCode = model.PaymentFailureCodeUnspecified
|
||||||
payment.FailureReason = ""
|
payment.FailureReason = ""
|
||||||
case gatewayv1.TransferStatus_TRANSFER_FAILED:
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
payment.State = model.PaymentStateFailed
|
payment.State = model.PaymentStateFailed
|
||||||
payment.FailureCode = model.PaymentFailureCodeChain
|
payment.FailureCode = model.PaymentFailureCodeChain
|
||||||
payment.FailureReason = reason
|
payment.FailureReason = reason
|
||||||
case gatewayv1.TransferStatus_TRANSFER_CANCELLED:
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
payment.State = model.PaymentStateCancelled
|
payment.State = model.PaymentStateCancelled
|
||||||
payment.FailureCode = model.PaymentFailureCodePolicy
|
payment.FailureCode = model.PaymentFailureCodePolicy
|
||||||
payment.FailureReason = reason
|
payment.FailureReason = reason
|
||||||
case gatewayv1.TransferStatus_TRANSFER_SIGNING,
|
case chainv1.TransferStatus_TRANSFER_SIGNING,
|
||||||
gatewayv1.TransferStatus_TRANSFER_PENDING,
|
chainv1.TransferStatus_TRANSFER_PENDING,
|
||||||
gatewayv1.TransferStatus_TRANSFER_SUBMITTED:
|
chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
||||||
payment.State = model.PaymentStateSubmitted
|
payment.State = model.PaymentStateSubmitted
|
||||||
default:
|
default:
|
||||||
// retain previous state
|
// retain previous state
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
|
accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func cloneMoney(input *moneyv1.Money) *moneyv1.Money {
|
func cloneMoney(input *moneyv1.Money) *moneyv1.Money {
|
||||||
@@ -108,7 +108,7 @@ func extractFeeTotal(lines []*feesv1.DerivedPostingLine, currency string) *money
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeAggregates(base, fee *moneyv1.Money, network *gatewayv1.EstimateTransferFeeResponse) (*moneyv1.Money, *moneyv1.Money) {
|
func computeAggregates(base, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse) (*moneyv1.Money, *moneyv1.Money) {
|
||||||
if base == nil {
|
if base == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -219,12 +219,12 @@ func ledgerLineTypeFromAccounting(lineType accountingv1.PostingLineType) ledgerv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*gatewayv1.ServiceFeeBreakdown {
|
func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*chainv1.ServiceFeeBreakdown {
|
||||||
if quote == nil {
|
if quote == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
lines := quote.GetFeeLines()
|
lines := quote.GetFeeLines()
|
||||||
breakdown := make([]*gatewayv1.ServiceFeeBreakdown, 0, len(lines)+1)
|
breakdown := make([]*chainv1.ServiceFeeBreakdown, 0, len(lines)+1)
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if line == nil {
|
if line == nil {
|
||||||
continue
|
continue
|
||||||
@@ -241,7 +241,7 @@ func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*gatewayv1.Serv
|
|||||||
code = line.GetLineType().String()
|
code = line.GetLineType().String()
|
||||||
}
|
}
|
||||||
desc := strings.TrimSpace(line.GetMeta()["description"])
|
desc := strings.TrimSpace(line.GetMeta()["description"])
|
||||||
breakdown = append(breakdown, &gatewayv1.ServiceFeeBreakdown{
|
breakdown = append(breakdown, &chainv1.ServiceFeeBreakdown{
|
||||||
FeeCode: code,
|
FeeCode: code,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
@@ -250,7 +250,7 @@ func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*gatewayv1.Serv
|
|||||||
if quote.GetNetworkFee() != nil && quote.GetNetworkFee().GetNetworkFee() != nil {
|
if quote.GetNetworkFee() != nil && quote.GetNetworkFee().GetNetworkFee() != nil {
|
||||||
networkAmount := cloneMoney(quote.GetNetworkFee().GetNetworkFee())
|
networkAmount := cloneMoney(quote.GetNetworkFee().GetNetworkFee())
|
||||||
if networkAmount != nil {
|
if networkAmount != nil {
|
||||||
breakdown = append(breakdown, &gatewayv1.ServiceFeeBreakdown{
|
breakdown = append(breakdown, &chainv1.ServiceFeeBreakdown{
|
||||||
FeeCode: "network_fee",
|
FeeCode: "network_fee",
|
||||||
Amount: networkAmount,
|
Amount: networkAmount,
|
||||||
Description: strings.TrimSpace(quote.GetNetworkFee().GetEstimationContext()),
|
Description: strings.TrimSpace(quote.GetNetworkFee().GetEstimationContext()),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package orchestrator
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
chainclient "github.com/tech/sendico/chain/gateway/client"
|
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||||
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
||||||
ledgerclient "github.com/tech/sendico/ledger/client"
|
ledgerclient "github.com/tech/sendico/ledger/client"
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
chainclient "github.com/tech/sendico/chain/gateway/client"
|
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||||
ledgerclient "github.com/tech/sendico/ledger/client"
|
ledgerclient "github.com/tech/sendico/ledger/client"
|
||||||
"github.com/tech/sendico/payments/orchestrator/storage"
|
"github.com/tech/sendico/payments/orchestrator/storage"
|
||||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
mo "github.com/tech/sendico/pkg/model"
|
mo "github.com/tech/sendico/pkg/model"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
@@ -88,7 +88,7 @@ func TestExecutePayment_ChainFailure(t *testing.T) {
|
|||||||
clock: testClock{now: time.Now()},
|
clock: testClock{now: time.Now()},
|
||||||
storage: repo,
|
storage: repo,
|
||||||
gateway: gatewayDependency{client: &chainclient.Fake{
|
gateway: gatewayDependency{client: &chainclient.Fake{
|
||||||
SubmitTransferFn: func(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
|
SubmitTransferFn: func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||||
return nil, errors.New("chain failure")
|
return nil, errors.New("chain failure")
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
@@ -147,10 +147,10 @@ func TestProcessTransferUpdateHandler_Settled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req := &orchestratorv1.ProcessTransferUpdateRequest{
|
req := &orchestratorv1.ProcessTransferUpdateRequest{
|
||||||
Event: &gatewayv1.TransferStatusChangedEvent{
|
Event: &chainv1.TransferStatusChangedEvent{
|
||||||
Transfer: &gatewayv1.Transfer{
|
Transfer: &chainv1.Transfer{
|
||||||
TransferRef: "transfer-1",
|
TransferRef: "transfer-1",
|
||||||
Status: gatewayv1.TransferStatus_TRANSFER_CONFIRMED,
|
Status: chainv1.TransferStatus_TRANSFER_CONFIRMED,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ func TestProcessDepositObservedHandler_MatchesPayment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req := &orchestratorv1.ProcessDepositObservedRequest{
|
req := &orchestratorv1.ProcessDepositObservedRequest{
|
||||||
Event: &gatewayv1.WalletDepositObservedEvent{
|
Event: &chainv1.WalletDepositObservedEvent{
|
||||||
WalletRef: "wallet-dst",
|
WalletRef: "wallet-dst",
|
||||||
Amount: &moneyv1.Money{Currency: "USD", Amount: "40"},
|
Amount: &moneyv1.Money{Currency: "USD", Amount: "40"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,15 +67,15 @@ type LedgerEndpoint struct {
|
|||||||
|
|
||||||
// ManagedWalletEndpoint describes managed wallet routing.
|
// ManagedWalletEndpoint describes managed wallet routing.
|
||||||
type ManagedWalletEndpoint struct {
|
type ManagedWalletEndpoint struct {
|
||||||
ManagedWalletRef string `bson:"managedWalletRef" json:"managedWalletRef"`
|
ManagedWalletRef string `bson:"managedWalletRef" json:"managedWalletRef"`
|
||||||
Asset *gatewayv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"`
|
Asset *chainv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExternalChainEndpoint describes an external address.
|
// ExternalChainEndpoint describes an external address.
|
||||||
type ExternalChainEndpoint struct {
|
type ExternalChainEndpoint struct {
|
||||||
Asset *gatewayv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"`
|
Asset *chainv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"`
|
||||||
Address string `bson:"address" json:"address"`
|
Address string `bson:"address" json:"address"`
|
||||||
Memo string `bson:"memo,omitempty" json:"memo,omitempty"`
|
Memo string `bson:"memo,omitempty" json:"memo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PaymentEndpoint is a polymorphic payment destination/source.
|
// PaymentEndpoint is a polymorphic payment destination/source.
|
||||||
@@ -111,14 +111,14 @@ type PaymentIntent struct {
|
|||||||
|
|
||||||
// PaymentQuoteSnapshot stores the latest quote info.
|
// PaymentQuoteSnapshot stores the latest quote info.
|
||||||
type PaymentQuoteSnapshot struct {
|
type PaymentQuoteSnapshot struct {
|
||||||
DebitAmount *moneyv1.Money `bson:"debitAmount,omitempty" json:"debitAmount,omitempty"`
|
DebitAmount *moneyv1.Money `bson:"debitAmount,omitempty" json:"debitAmount,omitempty"`
|
||||||
ExpectedSettlementAmount *moneyv1.Money `bson:"expectedSettlementAmount,omitempty" json:"expectedSettlementAmount,omitempty"`
|
ExpectedSettlementAmount *moneyv1.Money `bson:"expectedSettlementAmount,omitempty" json:"expectedSettlementAmount,omitempty"`
|
||||||
ExpectedFeeTotal *moneyv1.Money `bson:"expectedFeeTotal,omitempty" json:"expectedFeeTotal,omitempty"`
|
ExpectedFeeTotal *moneyv1.Money `bson:"expectedFeeTotal,omitempty" json:"expectedFeeTotal,omitempty"`
|
||||||
FeeLines []*feesv1.DerivedPostingLine `bson:"feeLines,omitempty" json:"feeLines,omitempty"`
|
FeeLines []*feesv1.DerivedPostingLine `bson:"feeLines,omitempty" json:"feeLines,omitempty"`
|
||||||
FeeRules []*feesv1.AppliedRule `bson:"feeRules,omitempty" json:"feeRules,omitempty"`
|
FeeRules []*feesv1.AppliedRule `bson:"feeRules,omitempty" json:"feeRules,omitempty"`
|
||||||
FXQuote *oraclev1.Quote `bson:"fxQuote,omitempty" json:"fxQuote,omitempty"`
|
FXQuote *oraclev1.Quote `bson:"fxQuote,omitempty" json:"fxQuote,omitempty"`
|
||||||
NetworkFee *gatewayv1.EstimateTransferFeeResponse `bson:"networkFee,omitempty" json:"networkFee,omitempty"`
|
NetworkFee *chainv1.EstimateTransferFeeResponse `bson:"networkFee,omitempty" json:"networkFee,omitempty"`
|
||||||
FeeQuoteToken string `bson:"feeQuoteToken,omitempty" json:"feeQuoteToken,omitempty"`
|
FeeQuoteToken string `bson:"feeQuoteToken,omitempty" json:"feeQuoteToken,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecutionRefs links to downstream systems.
|
// ExecutionRefs links to downstream systems.
|
||||||
|
|||||||
2
api/pkg/.gitignore
vendored
2
api/pkg/.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
proto/billing
|
proto/billing
|
||||||
proto/common
|
proto/common
|
||||||
proto/chain
|
proto/chain
|
||||||
|
proto/gateway
|
||||||
proto/ledger
|
proto/ledger
|
||||||
proto/oracle
|
proto/oracle
|
||||||
proto/payments
|
proto/payments
|
||||||
|
.gocache
|
||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo"
|
mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo"
|
||||||
"github.com/tech/sendico/pkg/db/invitation"
|
"github.com/tech/sendico/pkg/db/invitation"
|
||||||
"github.com/tech/sendico/pkg/db/organization"
|
"github.com/tech/sendico/pkg/db/organization"
|
||||||
|
"github.com/tech/sendico/pkg/db/paymethod"
|
||||||
"github.com/tech/sendico/pkg/db/policy"
|
"github.com/tech/sendico/pkg/db/policy"
|
||||||
|
"github.com/tech/sendico/pkg/db/recipient"
|
||||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||||
"github.com/tech/sendico/pkg/db/role"
|
"github.com/tech/sendico/pkg/db/role"
|
||||||
"github.com/tech/sendico/pkg/db/transaction"
|
"github.com/tech/sendico/pkg/db/transaction"
|
||||||
@@ -23,6 +25,8 @@ type Factory interface {
|
|||||||
NewAccountDB() (account.DB, error)
|
NewAccountDB() (account.DB, error)
|
||||||
NewOrganizationDB() (organization.DB, error)
|
NewOrganizationDB() (organization.DB, error)
|
||||||
NewInvitationsDB() (invitation.DB, error)
|
NewInvitationsDB() (invitation.DB, error)
|
||||||
|
NewRecipientsDB() (recipient.DB, error)
|
||||||
|
NewPaymentMethodsDB() (paymethod.DB, error)
|
||||||
|
|
||||||
NewRolesDB() (role.DB, error)
|
NewRolesDB() (role.DB, error)
|
||||||
NewPoliciesDB() (policy.DB, error)
|
NewPoliciesDB() (policy.DB, error)
|
||||||
|
|||||||
@@ -15,13 +15,17 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/db/internal/mongo/confirmationdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/confirmationdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/invitationdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/invitationdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/organizationdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/organizationdb"
|
||||||
|
"github.com/tech/sendico/pkg/db/internal/mongo/paymethoddb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/policiesdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/policiesdb"
|
||||||
|
"github.com/tech/sendico/pkg/db/internal/mongo/recipientdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/refreshtokensdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/refreshtokensdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/rolesdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/rolesdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/transactionimp"
|
"github.com/tech/sendico/pkg/db/internal/mongo/transactionimp"
|
||||||
"github.com/tech/sendico/pkg/db/invitation"
|
"github.com/tech/sendico/pkg/db/invitation"
|
||||||
"github.com/tech/sendico/pkg/db/organization"
|
"github.com/tech/sendico/pkg/db/organization"
|
||||||
|
"github.com/tech/sendico/pkg/db/paymethod"
|
||||||
"github.com/tech/sendico/pkg/db/policy"
|
"github.com/tech/sendico/pkg/db/policy"
|
||||||
|
"github.com/tech/sendico/pkg/db/recipient"
|
||||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||||
"github.com/tech/sendico/pkg/db/repository"
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
"github.com/tech/sendico/pkg/db/role"
|
"github.com/tech/sendico/pkg/db/role"
|
||||||
@@ -201,6 +205,29 @@ func (db *DB) NewOrganizationDB() (organization.DB, error) {
|
|||||||
return organizationDB, nil
|
return organizationDB, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) NewRecipientsDB() (recipient.DB, error) {
|
||||||
|
pmdb, err := db.NewPaymentMethodsDB()
|
||||||
|
if err != nil {
|
||||||
|
db.logger.Warn("Failed to create payment methods database", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
create := func(ctx context.Context,
|
||||||
|
logger mlogger.Logger,
|
||||||
|
enforcer auth.Enforcer,
|
||||||
|
pdb policy.DB,
|
||||||
|
db *mongo.Database,
|
||||||
|
) (recipient.DB, error) {
|
||||||
|
return recipientdb.Create(ctx, logger, enforcer, pdb, pmdb, db)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newProtectedDB(db, create)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) NewPaymentMethodsDB() (paymethod.DB, error) {
|
||||||
|
return newProtectedDB(db, paymethoddb.Create)
|
||||||
|
}
|
||||||
|
|
||||||
func (db *DB) NewRefreshTokensDB() (refreshtokens.DB, error) {
|
func (db *DB) NewRefreshTokensDB() (refreshtokens.DB, error) {
|
||||||
return refreshtokensdb.Create(db.logger, db.db())
|
return refreshtokensdb.Create(db.logger, db.db())
|
||||||
}
|
}
|
||||||
|
|||||||
20
api/pkg/db/internal/mongo/paymethoddb/archived.go
Normal file
20
api/pkg/db/internal/mongo/paymethoddb/archived.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package paymethoddb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *PaymentMethodsDB) SetArchived(ctx context.Context, accountRef, organizationRef, objectRef primitive.ObjectID, isArchived, cascade bool) error {
|
||||||
|
// Use the ArchivableDB for the main archiving logic
|
||||||
|
if err := db.ArchivableDB.SetArchived(ctx, accountRef, objectRef, isArchived); err != nil {
|
||||||
|
db.DBImp.Logger.Warn("Failed to chnage object archive status", zap.Error(err),
|
||||||
|
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
|
||||||
|
mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
49
api/pkg/db/internal/mongo/paymethoddb/db.go
Normal file
49
api/pkg/db/internal/mongo/paymethoddb/db.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package paymethoddb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/auth"
|
||||||
|
"github.com/tech/sendico/pkg/db/policy"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaymentMethodsDB struct {
|
||||||
|
auth.ProtectedDBImp[*model.PaymentMethod]
|
||||||
|
auth.ArchivableDB[*model.PaymentMethod]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(ctx context.Context,
|
||||||
|
logger mlogger.Logger,
|
||||||
|
enforcer auth.Enforcer,
|
||||||
|
pdb policy.DB,
|
||||||
|
db *mongo.Database,
|
||||||
|
) (*PaymentMethodsDB, error) {
|
||||||
|
p, err := auth.CreateDBImp[*model.PaymentMethod](ctx, logger, pdb, enforcer, mservice.PaymentMethods, db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createEmpty := func() *model.PaymentMethod {
|
||||||
|
return &model.PaymentMethod{}
|
||||||
|
}
|
||||||
|
|
||||||
|
getArchivable := func(c *model.PaymentMethod) model.Archivable {
|
||||||
|
return &c.ArchivableBase
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &PaymentMethodsDB{
|
||||||
|
ProtectedDBImp: *p,
|
||||||
|
ArchivableDB: auth.NewArchivableDB(
|
||||||
|
p.DBImp,
|
||||||
|
logger,
|
||||||
|
p.Enforcer,
|
||||||
|
createEmpty,
|
||||||
|
getArchivable,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
28
api/pkg/db/internal/mongo/paymethoddb/list.go
Normal file
28
api/pkg/db/internal/mongo/paymethoddb/list.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package paymethoddb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
mauth "github.com/tech/sendico/pkg/mutil/db/auth"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *PaymentMethodsDB) List(ctx context.Context, accountRef, organizationRef, recipientRef primitive.ObjectID, cursor *model.ViewCursor) ([]model.PaymentMethod, error) {
|
||||||
|
res, err := mauth.GetProtectedObjects[model.PaymentMethod](
|
||||||
|
ctx,
|
||||||
|
db.DBImp.Logger,
|
||||||
|
accountRef, organizationRef, model.ActionRead,
|
||||||
|
repository.OrgFilter(organizationRef).And(repository.Filter("recipientRef", recipientRef)),
|
||||||
|
cursor,
|
||||||
|
db.Enforcer,
|
||||||
|
db.DBImp.Repository,
|
||||||
|
)
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
return []model.PaymentMethod{}, nil
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
57
api/pkg/db/internal/mongo/recipientdb/archived.go
Normal file
57
api/pkg/db/internal/mongo/recipientdb/archived.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package recipientdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *RecipientDB) SetArchived(ctx context.Context, accountRef, organizationRef, objectRef primitive.ObjectID, isArchived, cascade bool) error {
|
||||||
|
// Use the ArchivableDB for the main archiving logic
|
||||||
|
if err := db.ArchivableDB.SetArchived(ctx, accountRef, objectRef, isArchived); err != nil {
|
||||||
|
db.DBImp.Logger.Warn("Failed to change recipient archive status", zap.Error(err),
|
||||||
|
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
|
||||||
|
mzap.ObjRef("recipient_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cascade {
|
||||||
|
if err := db.setArchivedPaymentMethods(ctx, accountRef, organizationRef, objectRef, isArchived); err != nil {
|
||||||
|
db.DBImp.Logger.Warn("Failed to update payment methods archive status", zap.Error(err),
|
||||||
|
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
|
||||||
|
mzap.ObjRef("recipient_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *RecipientDB) setArchivedPaymentMethods(ctx context.Context, accountRef, organizationRef, recipientRef primitive.ObjectID, archived bool) error {
|
||||||
|
db.DBImp.Logger.Debug("Setting archived status for recipient payment methods", mzap.ObjRef("recipient_ref", recipientRef), zap.Bool("archived", archived))
|
||||||
|
|
||||||
|
db.DBImp.Logger.Debug("Applying archived status to payment methods for recipient", mzap.ObjRef("recipient_ref", recipientRef))
|
||||||
|
|
||||||
|
// Get all payMethods for the recipient
|
||||||
|
payMethods, err := db.pmdb.List(ctx, accountRef, organizationRef, recipientRef, nil)
|
||||||
|
if err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||||
|
db.DBImp.Logger.Warn("Failed to fetch payment methods for recipient", zap.Error(err), mzap.ObjRef("recipient_ref", recipientRef))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive each payment method
|
||||||
|
for _, pmethod := range payMethods {
|
||||||
|
if err := db.pmdb.SetArchived(ctx, accountRef, organizationRef, pmethod.ID, archived, true); err != nil {
|
||||||
|
db.DBImp.Logger.Warn("Failed to set archived status for payment method", zap.Error(err), mzap.ObjRef("payment_method_ref", pmethod.ID))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.DBImp.Logger.Debug("Successfully updated payment methods archived status", zap.Int("count", len(payMethods)), mzap.ObjRef("recipient_ref", recipientRef))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
56
api/pkg/db/internal/mongo/recipientdb/db.go
Normal file
56
api/pkg/db/internal/mongo/recipientdb/db.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package recipientdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/auth"
|
||||||
|
"github.com/tech/sendico/pkg/db/paymethod"
|
||||||
|
"github.com/tech/sendico/pkg/db/policy"
|
||||||
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecipientDB struct {
|
||||||
|
auth.ProtectedDBImp[*model.Recipient]
|
||||||
|
auth.ArchivableDB[*model.Recipient]
|
||||||
|
pmdb paymethod.DB
|
||||||
|
paymentMethodsRepo repository.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(ctx context.Context,
|
||||||
|
logger mlogger.Logger,
|
||||||
|
enforcer auth.Enforcer,
|
||||||
|
pdb policy.DB,
|
||||||
|
pmdb paymethod.DB,
|
||||||
|
db *mongo.Database,
|
||||||
|
) (*RecipientDB, error) {
|
||||||
|
p, err := auth.CreateDBImp[*model.Recipient](ctx, logger, pdb, enforcer, mservice.Organizations, db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createEmpty := func() *model.Recipient {
|
||||||
|
return &model.Recipient{}
|
||||||
|
}
|
||||||
|
|
||||||
|
getArchivable := func(c *model.Recipient) model.Archivable {
|
||||||
|
return &c.ArchivableBase
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &RecipientDB{
|
||||||
|
ProtectedDBImp: *p,
|
||||||
|
ArchivableDB: auth.NewArchivableDB(
|
||||||
|
p.DBImp,
|
||||||
|
p.DBImp.Logger,
|
||||||
|
enforcer,
|
||||||
|
createEmpty,
|
||||||
|
getArchivable,
|
||||||
|
),
|
||||||
|
paymentMethodsRepo: repository.CreateMongoRepository(db, string(mservice.PaymentMethods)),
|
||||||
|
pmdb: pmdb,
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
28
api/pkg/db/internal/mongo/recipientdb/list.go
Normal file
28
api/pkg/db/internal/mongo/recipientdb/list.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package recipientdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
mauth "github.com/tech/sendico/pkg/mutil/db/auth"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *RecipientDB) List(ctx context.Context, accountRef, organizationRef, _ primitive.ObjectID, cursor *model.ViewCursor) ([]model.Recipient, error) {
|
||||||
|
res, err := mauth.GetProtectedObjects[model.Recipient](
|
||||||
|
ctx,
|
||||||
|
db.DBImp.Logger,
|
||||||
|
accountRef, organizationRef, model.ActionRead,
|
||||||
|
repository.OrgFilter(organizationRef),
|
||||||
|
cursor,
|
||||||
|
db.Enforcer,
|
||||||
|
db.DBImp.Repository,
|
||||||
|
)
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
return []model.Recipient{}, nil
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
@@ -15,5 +15,5 @@ type DB interface {
|
|||||||
Decline(ctx context.Context, invitationRef primitive.ObjectID) error
|
Decline(ctx context.Context, invitationRef primitive.ObjectID) error
|
||||||
List(ctx context.Context, accountRef, organizationRef, _ primitive.ObjectID, cursor *model.ViewCursor) ([]model.Invitation, error)
|
List(ctx context.Context, accountRef, organizationRef, _ primitive.ObjectID, cursor *model.ViewCursor) ([]model.Invitation, error)
|
||||||
DeleteCascade(ctx context.Context, accountRef, statusRef primitive.ObjectID) error
|
DeleteCascade(ctx context.Context, accountRef, statusRef primitive.ObjectID) error
|
||||||
SetArchived(ctx context.Context, accountRef, organizationRef, statusRef primitive.ObjectID, archived, cascade bool) error
|
SetArchived(ctx context.Context, accountRef, organizationRef, invitationRef primitive.ObjectID, archived, cascade bool) error
|
||||||
}
|
}
|
||||||
|
|||||||
15
api/pkg/db/paymethod/db.go
Normal file
15
api/pkg/db/paymethod/db.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package paymethod
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/auth"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB interface {
|
||||||
|
auth.ProtectedDB[*model.PaymentMethod]
|
||||||
|
SetArchived(ctx context.Context, accountRef, organizationRef, methodRef primitive.ObjectID, archived, cascade bool) error
|
||||||
|
List(ctx context.Context, accountRef, organizationRef, recipientRef primitive.ObjectID, cursor *model.ViewCursor) ([]model.PaymentMethod, error)
|
||||||
|
}
|
||||||
15
api/pkg/db/recipient/db.go
Normal file
15
api/pkg/db/recipient/db.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package recipient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/auth"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB interface {
|
||||||
|
auth.ProtectedDB[*model.Recipient]
|
||||||
|
SetArchived(ctx context.Context, accountRef, organizationRef, recipientRef primitive.ObjectID, archived, cascade bool) error
|
||||||
|
List(ctx context.Context, accountRef, organizationRef, _ primitive.ObjectID, cursor *model.ViewCursor) ([]model.Recipient, error)
|
||||||
|
}
|
||||||
25
api/pkg/model/card.go
Normal file
25
api/pkg/model/card.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CardPaymentData struct {
|
||||||
|
Pan string `bson:"pan" json:"pan"`
|
||||||
|
FirstName string `bson:"firstName" json:"firstName"`
|
||||||
|
LastName string `bson:"lastName" json:"lastName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *PaymentMethod) AsCard() (*CardPaymentData, error) {
|
||||||
|
if m.Type != PaymentTypeCard {
|
||||||
|
return nil, merrors.InvalidArgument(fmt.Sprintf("payment method type is %s, not card", m.Type), "type")
|
||||||
|
}
|
||||||
|
var d CardPaymentData
|
||||||
|
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &d, nil
|
||||||
|
}
|
||||||
27
api/pkg/model/crypto_address.go
Normal file
27
api/pkg/model/crypto_address.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CryptoAddressPaymentData struct {
|
||||||
|
Address string `bson:"address" json:"address"`
|
||||||
|
Network string `bson:"network" json:"network"`
|
||||||
|
DestinationTag *string `bson:"destinationTag,omitempty" json:"destinationTag,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *PaymentMethod) AsCryptoAddress() (*CryptoAddressPaymentData, error) {
|
||||||
|
if m.Type != PaymentTypeCryptoAddress {
|
||||||
|
return nil, merrors.InvalidArgument(fmt.Sprintf("payment method type is %s, not cryptoAddress", m.Type), "type")
|
||||||
|
}
|
||||||
|
|
||||||
|
var d CryptoAddressPaymentData
|
||||||
|
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &d, nil
|
||||||
|
}
|
||||||
26
api/pkg/model/iban.go
Normal file
26
api/pkg/model/iban.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IbanPaymentData struct {
|
||||||
|
Iban string `bson:"iban" json:"iban"`
|
||||||
|
AccountHolder string `bson:"accountHolder" json:"accountHolder"`
|
||||||
|
Bic *string `bson:"bic,omitempty" json:"bic,omitempty"`
|
||||||
|
BankName *string `bson:"bankName,omitempty" json:"bankName,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *PaymentMethod) AsIban() (*IbanPaymentData, error) {
|
||||||
|
if m.Type != PaymentTypeIban {
|
||||||
|
return nil, merrors.InvalidArgument(fmt.Sprintf("payment method type is %s, not iban", m.Type), "type")
|
||||||
|
}
|
||||||
|
var d IbanPaymentData
|
||||||
|
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &d, nil
|
||||||
|
}
|
||||||
68
api/pkg/model/payment.go
Normal file
68
api/pkg/model/payment.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaymentType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PaymentTypeIban PaymentType = iota
|
||||||
|
PaymentTypeCard
|
||||||
|
PaymentTypeBankAccount
|
||||||
|
PaymentTypeWallet
|
||||||
|
PaymentTypeCryptoAddress
|
||||||
|
)
|
||||||
|
|
||||||
|
var paymentTypeToString = map[PaymentType]string{
|
||||||
|
PaymentTypeIban: "iban",
|
||||||
|
PaymentTypeCard: "card",
|
||||||
|
PaymentTypeBankAccount: "bankAccount",
|
||||||
|
PaymentTypeWallet: "wallet",
|
||||||
|
PaymentTypeCryptoAddress: "cryptoAddress",
|
||||||
|
}
|
||||||
|
|
||||||
|
var paymentTypeFromString = map[string]PaymentType{
|
||||||
|
"iban": PaymentTypeIban,
|
||||||
|
"card": PaymentTypeCard,
|
||||||
|
"bankAccount": PaymentTypeBankAccount,
|
||||||
|
"wallet": PaymentTypeWallet,
|
||||||
|
"cryptoAddress": PaymentTypeCryptoAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t PaymentType) String() string {
|
||||||
|
if v, ok := paymentTypeToString[t]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "iban"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t PaymentType) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(t.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PaymentType) UnmarshalJSON(data []byte) error {
|
||||||
|
var val string
|
||||||
|
if err := json.Unmarshal(data, &val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v, ok := paymentTypeFromString[val]
|
||||||
|
if !ok {
|
||||||
|
return merrors.InvalidArgument(fmt.Sprintf("unknown PaymentType: %q", val))
|
||||||
|
}
|
||||||
|
*t = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaymentMethod struct {
|
||||||
|
PermissionBound `bson:",inline" json:",inline"`
|
||||||
|
|
||||||
|
RecipientRef primitive.ObjectID `bson:"recipientRef" json:"recipientRef"`
|
||||||
|
Type PaymentType `bson:"type" json:"type"`
|
||||||
|
Data bson.Raw `bson:"data" json:"data"`
|
||||||
|
}
|
||||||
31
api/pkg/model/rba.go
Normal file
31
api/pkg/model/rba.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RussianBankAccountPaymentData struct {
|
||||||
|
RecipientName string `bson:"recipientName" json:"recipientName"`
|
||||||
|
Inn string `bson:"inn" json:"inn"`
|
||||||
|
Kpp string `bson:"kpp" json:"kpp"`
|
||||||
|
BankName string `bson:"bankName" json:"bankName"`
|
||||||
|
Bik string `bson:"bik" json:"bik"`
|
||||||
|
AccountNumber string `bson:"accountNumber" json:"accountNumber"`
|
||||||
|
CorrespondentAccount string `bson:"correspondentAccount" json:"correspondentAccount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *PaymentMethod) AsRussianBankAccount() (*RussianBankAccountPaymentData, error) {
|
||||||
|
if m.Type != PaymentTypeBankAccount {
|
||||||
|
return nil, merrors.InvalidArgument(fmt.Sprintf("payment method type is %s, not bankAccount", m.Type), "type")
|
||||||
|
}
|
||||||
|
|
||||||
|
var d RussianBankAccountPaymentData
|
||||||
|
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &d, nil
|
||||||
|
}
|
||||||
106
api/pkg/model/recipient.go
Normal file
106
api/pkg/model/recipient.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecipientStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
RecipientStatusReady RecipientStatus = iota
|
||||||
|
RecipientStatusRegistered
|
||||||
|
RecipientStatusNotRegistered
|
||||||
|
)
|
||||||
|
|
||||||
|
var recipientStatusToString = map[RecipientStatus]string{
|
||||||
|
RecipientStatusReady: "ready",
|
||||||
|
RecipientStatusRegistered: "registered",
|
||||||
|
RecipientStatusNotRegistered: "notRegistered",
|
||||||
|
}
|
||||||
|
|
||||||
|
var recipientStatusFromString = map[string]RecipientStatus{
|
||||||
|
"ready": RecipientStatusReady,
|
||||||
|
"registered": RecipientStatusRegistered,
|
||||||
|
"notRegistered": RecipientStatusNotRegistered,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s RecipientStatus) String() string {
|
||||||
|
if v, ok := recipientStatusToString[s]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "ready" // дефолт, можно поменять
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON: храним как строку ("ready" / "registered" / "notRegistered")
|
||||||
|
func (s RecipientStatus) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(s.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RecipientStatus) UnmarshalJSON(data []byte) error {
|
||||||
|
var val string
|
||||||
|
if err := json.Unmarshal(data, &val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v, ok := recipientStatusFromString[val]
|
||||||
|
if !ok {
|
||||||
|
return merrors.InvalidArgument(fmt.Sprintf("unknown RecipientStatus: %q", val))
|
||||||
|
}
|
||||||
|
*s = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecipientType { internal, external }
|
||||||
|
|
||||||
|
type RecipientType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
RecipientTypeInternal RecipientType = iota
|
||||||
|
RecipientTypeExternal
|
||||||
|
)
|
||||||
|
|
||||||
|
var recipientTypeToString = map[RecipientType]string{
|
||||||
|
RecipientTypeInternal: "internal",
|
||||||
|
RecipientTypeExternal: "external",
|
||||||
|
}
|
||||||
|
|
||||||
|
var recipientTypeFromString = map[string]RecipientType{
|
||||||
|
"internal": RecipientTypeInternal,
|
||||||
|
"external": RecipientTypeExternal,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t RecipientType) String() string {
|
||||||
|
if v, ok := recipientTypeToString[t]; ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "internal"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t RecipientType) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(t.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *RecipientType) UnmarshalJSON(data []byte) error {
|
||||||
|
var val string
|
||||||
|
if err := json.Unmarshal(data, &val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v, ok := recipientTypeFromString[val]
|
||||||
|
if !ok {
|
||||||
|
return merrors.InvalidArgument(fmt.Sprintf("unknown RecipientType: %q", val))
|
||||||
|
}
|
||||||
|
*t = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Recipient struct {
|
||||||
|
PermissionBound `bson:",inline" json:",inline"`
|
||||||
|
Describable `bson:",inline" json:",inline"`
|
||||||
|
Email string `bson:"email" json:"email"`
|
||||||
|
AvatarURL *string `bson:"avatarUrl,omitempty" json:"avatarUrl,omitempty"`
|
||||||
|
|
||||||
|
Status RecipientStatus `bson:"status" json:"status"`
|
||||||
|
Type RecipientType `bson:"type" json:"type"`
|
||||||
|
}
|
||||||
25
api/pkg/model/wallet.go
Normal file
25
api/pkg/model/wallet.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WalletPaymentData struct {
|
||||||
|
WalletID string `bson:"walletId" json:"walletId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *PaymentMethod) AsWallet() (*WalletPaymentData, error) {
|
||||||
|
if m.Type != PaymentTypeWallet {
|
||||||
|
return nil, merrors.InvalidArgument(fmt.Sprintf("payment method type is %s, not wallet", m.Type), "type")
|
||||||
|
}
|
||||||
|
|
||||||
|
var d WalletPaymentData
|
||||||
|
if err := bson.Unmarshal(m.Data, &d); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &d, nil
|
||||||
|
}
|
||||||
@@ -33,9 +33,11 @@ const (
|
|||||||
Notifications Type = "notifications" // Represents notifications sent to users
|
Notifications Type = "notifications" // Represents notifications sent to users
|
||||||
Organizations Type = "organizations" // Represents organizations in the system
|
Organizations Type = "organizations" // Represents organizations in the system
|
||||||
Payments Type = "payments" // Represents payments service
|
Payments Type = "payments" // Represents payments service
|
||||||
|
PaymentMethods Type = "payment_methods" // Represents payment methods service
|
||||||
Permissions Type = "permissions" // Represents permissiosns service
|
Permissions Type = "permissions" // Represents permissiosns service
|
||||||
Policies Type = "policies" // Represents access control policies
|
Policies Type = "policies" // Represents access control policies
|
||||||
PolicyAssignements Type = "policy_assignments" // Represents policy assignments database
|
PolicyAssignements Type = "policy_assignments" // Represents policy assignments database
|
||||||
|
Recipients Type = "recipients" // Represents payment recipients
|
||||||
RefreshTokens Type = "refresh_tokens" // Represents refresh tokens for authentication
|
RefreshTokens Type = "refresh_tokens" // Represents refresh tokens for authentication
|
||||||
Roles Type = "roles" // Represents roles in access control
|
Roles Type = "roles" // Represents roles in access control
|
||||||
Storage Type = "storage" // Represents statuses of tasks or projects
|
Storage Type = "storage" // Represents statuses of tasks or projects
|
||||||
|
|||||||
97
api/proto/common/gateway/v1/gateway.proto
Normal file
97
api/proto/common/gateway/v1/gateway.proto
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package common.gateway.v1;
|
||||||
|
option go_package = "github.com/tech/sendico/pkg/proto/common/gateway/v1;gatewayv1";
|
||||||
|
|
||||||
|
enum Operation {
|
||||||
|
OPERATION_UNSPECIFIED = 0;
|
||||||
|
OPERATION_AUTHORIZE = 1;
|
||||||
|
OPERATION_CAPTURE = 2;
|
||||||
|
OPERATION_REFUND = 3;
|
||||||
|
OPERATION_VOID = 4;
|
||||||
|
OPERATION_PAYOUT = 5;
|
||||||
|
OPERATION_TOKENIZE = 6;
|
||||||
|
OPERATION_VERIFY = 7; // zero-amount verification
|
||||||
|
OPERATION_GET_BALANCE = 8;
|
||||||
|
OPERATION_CREATE_ACCOUNT = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PaymentMethodType {
|
||||||
|
PM_UNSPECIFIED = 0;
|
||||||
|
PM_CARD = 1;
|
||||||
|
PM_SEPA = 2;
|
||||||
|
PM_ACH = 3;
|
||||||
|
PM_PIX = 4;
|
||||||
|
PM_WALLET = 5;
|
||||||
|
PM_CRYPTO = 6;
|
||||||
|
PM_LOCAL_BANK = 7; // generic local rails, refine later if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limits in minor units, e.g. cents
|
||||||
|
message AmountLimits {
|
||||||
|
int64 min_minor = 1;
|
||||||
|
int64 max_minor = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capabilities of a particular operation (e.g. "authorize")
|
||||||
|
message OperationCapabilities {
|
||||||
|
// If false or absent in the map -> operation not supported
|
||||||
|
bool supported = 1;
|
||||||
|
bool partial_allowed = 2; // partial capture/refund
|
||||||
|
bool supports_3ds = 3; // relevant mostly for AUTHORIZE/VERIFY
|
||||||
|
bool synchronous = 4; // true = immediate result, false = async/poll
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-method matrix entry
|
||||||
|
message MethodCapability {
|
||||||
|
PaymentMethodType method = 1;
|
||||||
|
|
||||||
|
// ISO 4217 currency codes, e.g. "EUR", "USD"
|
||||||
|
repeated string currencies = 2;
|
||||||
|
|
||||||
|
// ISO 3166-1 alpha-2 country codes where this method is available
|
||||||
|
repeated string countries = 3;
|
||||||
|
|
||||||
|
// Can the gateway tokenize this method (card token, wallet token, etc.)
|
||||||
|
bool tokenization_supported = 4;
|
||||||
|
|
||||||
|
// Optional per-method limits; if unset, use global amount_limits
|
||||||
|
AmountLimits amount_limits = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payout capabilities of this gateway
|
||||||
|
message PayoutCapabilities {
|
||||||
|
bool enabled = 1;
|
||||||
|
repeated string currencies = 2;
|
||||||
|
repeated string countries = 3;
|
||||||
|
AmountLimits amount_limits = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// High-level capability descriptor for a gateway
|
||||||
|
message GatewayCapabilities {
|
||||||
|
// For each operation, describe what exactly it can do.
|
||||||
|
// Map key uses the Operation enum name (e.g. "OPERATION_AUTHORIZE").
|
||||||
|
map<string, OperationCapabilities> operations = 1;
|
||||||
|
|
||||||
|
// For each payment method, list where and how it works
|
||||||
|
repeated MethodCapability methods = 2;
|
||||||
|
|
||||||
|
// Global amount limits (fallback if per-method limits not set)
|
||||||
|
AmountLimits amount_limits = 3;
|
||||||
|
|
||||||
|
// Payout-related capabilities (if any)
|
||||||
|
PayoutCapabilities payouts = 4;
|
||||||
|
|
||||||
|
// Free-form metadata / escape hatch
|
||||||
|
map<string, string> extra = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A specific gateway instance or config variant (e.g. stripe_eu_prod)
|
||||||
|
message GatewayDescriptor {
|
||||||
|
string id = 1; // "stripe_eu", "adyen_br", "local_bank_pl"
|
||||||
|
string provider = 2; // "stripe", "adyen", "local_bank"
|
||||||
|
string label = 3; // human-readable name
|
||||||
|
string version = 4; // config or integration version
|
||||||
|
string environment = 5; // "prod", "sandbox", "test"
|
||||||
|
|
||||||
|
GatewayCapabilities capabilities = 6;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package chain.gateway.v1;
|
package chain.gateway.v1;
|
||||||
|
|
||||||
option go_package = "github.com/tech/sendico/pkg/proto/chain/gateway/v1;gatewayv1";
|
option go_package = "github.com/tech/sendico/pkg/proto/gateway/chain/v1;chainv1";
|
||||||
|
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
import "common/money/v1/money.proto";
|
import "common/money/v1/money.proto";
|
||||||
@@ -10,7 +10,7 @@ import "common/fx/v1/fx.proto";
|
|||||||
import "common/trace/v1/trace.proto";
|
import "common/trace/v1/trace.proto";
|
||||||
import "common/pagination/v1/cursor.proto";
|
import "common/pagination/v1/cursor.proto";
|
||||||
import "billing/fees/v1/fees.proto";
|
import "billing/fees/v1/fees.proto";
|
||||||
import "chain/gateway/v1/gateway.proto";
|
import "gateway/chain/v1/chain.proto";
|
||||||
import "oracle/v1/oracle.proto";
|
import "oracle/v1/oracle.proto";
|
||||||
|
|
||||||
enum PaymentKind {
|
enum PaymentKind {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user