13 Commits

Author SHA1 Message Date
Stephan D
bf85ca062c restucturization of recipients payment methods
All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
2025-12-04 14:42:25 +01:00
Arseni
3b04753f4e Revert "Merge branch 'devKA' into devka (resolve conflicts)"
Some checks are pending
ci/woodpecker/push/bump_version Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/frontend Pipeline is running
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline is running
This reverts commit 5f4184760d, reversing
changes made to 5e1da9617f.

Reverting changes on main
2025-12-04 15:38:01 +03:00
Arseni
5f4184760d Merge branch 'devKA' into devka (resolve conflicts)
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version unknown status
2025-12-04 14:08:32 +03:00
Stephan D
5e1da9617f +address book service
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-12-01 21:20:10 +01:00
Stephan D
c4d34c5663 added wallet management localizations
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-26 23:25:31 +01:00
Stephan D
34420ca2fb +gas estimation
Some checks failed
ci/woodpecker/push/db Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/bff Pipeline failed
2025-11-26 23:19:29 +01:00
Stephan D
d16703197d onchain balance getter implementation
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-26 22:25:15 +01:00
Stephan D
35897f9aa1 impreoved commands logging
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-26 21:45:38 +01:00
Stephan D
f59ee55084 removed untranslated.txt from repo
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-26 19:36:47 +01:00
Stephan D
8bf86c5c93 removed obsolete files
Some checks failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/billing_fees Pipeline failed
ci/woodpecker/push/chain_gateway Pipeline is pending
ci/woodpecker/push/db Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/bff Pipeline failed
2025-11-26 19:35:35 +01:00
Stephan D
5e8ff2adb7 removed CORS restrictions
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/payments_orchestrator Pipeline failed
2025-11-26 19:02:30 +01:00
Stephan D
da57b1d2e0 fixed providers initialization
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
2025-11-26 18:17:30 +01:00
2ef9ac24a1 Merge pull request 'Added Localizations and ran small fixes' (#6) from devKA into main
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #6
2025-11-26 14:00:56 +00:00
177 changed files with 3815 additions and 1516 deletions

7
.gitignore vendored
View File

@@ -3,4 +3,9 @@
*.pb.gw.go *.pb.gw.go
pubspec.lock pubspec.lock
.DS_Store .DS_Store
update_dep.sh analysis_options.yaml
devtools_options.yaml
untranslated.txt
generate_protos.sh
update_dep.sh
.vscode/

View File

@@ -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

View File

@@ -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:

View File

@@ -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
) )

View File

@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/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=

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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()),
}
}

View File

@@ -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
) )

View File

@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/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=

View File

@@ -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
) )

View File

@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/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=

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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)

View 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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"
) )

View File

@@ -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,

View File

@@ -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"
) )

View File

@@ -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")),
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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"
}
]`

View File

@@ -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)})
}

View File

@@ -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)
}

View File

@@ -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()),
}
}

View File

@@ -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)})
}

View File

@@ -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(),
}
}

View File

@@ -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)})
}

View File

@@ -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
}

View File

@@ -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)})
}

View File

@@ -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)
}

View File

@@ -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"
}
]`

View File

@@ -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()),
}
}

View File

@@ -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")

View File

@@ -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
} }

View 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
}

View File

@@ -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
} }

View 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
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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)

View File

@@ -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
} }

View File

@@ -1 +1,2 @@
notification notification
.gocache

View File

@@ -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
) )

View File

@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/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=

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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()),

View File

@@ -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"

View File

@@ -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"},
}, },

View File

@@ -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.

4
api/pkg/.gitignore vendored
View File

@@ -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

View File

@@ -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)

View File

@@ -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())
} }

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@@ -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
} }

View 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)
}

View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
}

View File

@@ -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

View 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;
}

View File

@@ -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";
@@ -198,7 +198,7 @@ message WalletDepositObservedEvent {
message TransferStatusChangedEvent { message TransferStatusChangedEvent {
Transfer transfer = 1; Transfer transfer = 1;
string reason = 2; string reason = 2;
} }
service ChainGatewayService { service ChainGatewayService {

View File

@@ -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