Compare commits
41 Commits
72d8da1fe8
...
devKA
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf85ca062c | ||
|
|
3b04753f4e | ||
|
|
5f4184760d | ||
|
|
5e1da9617f | ||
|
|
9c16e27645 | ||
|
|
c4d34c5663 | ||
|
|
34420ca2fb | ||
|
|
d16703197d | ||
|
|
35897f9aa1 | ||
|
|
f59ee55084 | ||
|
|
8bf86c5c93 | ||
|
|
5e8ff2adb7 | ||
|
|
da57b1d2e0 | ||
| 2ef9ac24a1 | |||
|
|
44446c6ad4 | ||
|
|
357af99564 | ||
|
|
48ccbb1c82 | ||
|
|
68f0a1048f | ||
|
|
be913bf96c | ||
|
|
8e1d4bef59 | ||
|
|
c6da138184 | ||
|
|
d78619bccf | ||
|
|
85b780b57e | ||
|
|
1f31fedc3a | ||
|
|
bdf3a01f80 | ||
|
|
d126d5d5de | ||
|
|
fcb5ab4f2c | ||
|
|
26a1e284b2 | ||
|
|
fc0600d6c4 | ||
|
|
b855404999 | ||
|
|
e3a8fb4f2d | ||
|
|
d65e442cb6 | ||
|
|
b4f6f63871 | ||
|
|
803683be7c | ||
|
|
72271cfc9a | ||
|
|
cd79355e69 | ||
|
|
3f84f8c609 | ||
|
|
ae15e1887b | ||
|
|
8a41785b1d | ||
|
|
56abc10dce | ||
| d8a3a5550d |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,3 +3,9 @@
|
|||||||
*.pb.gw.go
|
*.pb.gw.go
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
analysis_options.yaml
|
||||||
|
devtools_options.yaml
|
||||||
|
untranslated.txt
|
||||||
|
generate_protos.sh
|
||||||
|
update_dep.sh
|
||||||
|
.vscode/
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
depends_on:
|
|
||||||
- bff
|
|
||||||
- billing_fees
|
|
||||||
- chain_gateway
|
|
||||||
- db
|
|
||||||
- frontend
|
|
||||||
- fx_ingestor
|
|
||||||
- fx_oracle
|
|
||||||
- ledger
|
|
||||||
- nats
|
|
||||||
- notification
|
|
||||||
- payments_orchestrator
|
|
||||||
|
|
||||||
when:
|
|
||||||
event: push
|
|
||||||
branch: main
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: bump-version
|
|
||||||
image: alpine:latest
|
|
||||||
environment:
|
|
||||||
GIT_AUTHOR_NAME: woodpecker
|
|
||||||
GIT_AUTHOR_EMAIL: ci@sendico.io
|
|
||||||
GIT_COMMITTER_NAME: woodpecker
|
|
||||||
GIT_COMMITTER_EMAIL: ci@sendico.io
|
|
||||||
commands:
|
|
||||||
- set -euo pipefail
|
|
||||||
- apk add --no-cache git
|
|
||||||
# make sure git knows who commits
|
|
||||||
- git config user.name "$GIT_AUTHOR_NAME"
|
|
||||||
- git config user.email "$GIT_AUTHOR_EMAIL"
|
|
||||||
# run your script (must do commit + push)
|
|
||||||
- sh ci/scripts/common/bump_version.sh
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- CHAIN_GATEWAY_IMAGE_PATH: chain/gateway
|
- CHAIN_GATEWAY_IMAGE_PATH: gateway/chain
|
||||||
CHAIN_GATEWAY_DOCKERFILE: ci/prod/compose/chain_gateway.dockerfile
|
CHAIN_GATEWAY_DOCKERFILE: ci/prod/compose/chain_gateway.dockerfile
|
||||||
CHAIN_GATEWAY_MONGO_SECRET_PATH: sendico/db
|
CHAIN_GATEWAY_MONGO_SECRET_PATH: sendico/db
|
||||||
CHAIN_GATEWAY_RPC_SECRET_PATH: sendico/chain/gateway
|
CHAIN_GATEWAY_RPC_SECRET_PATH: sendico/gateway/chain
|
||||||
CHAIN_GATEWAY_WALLET_SECRET_PATH: sendico/chain/gateway/wallet
|
CHAIN_GATEWAY_WALLET_SECRET_PATH: sendico/gateway/chain/wallet
|
||||||
CHAIN_GATEWAY_VAULT_SECRET_PATH: sendico/chain/gateway/vault
|
CHAIN_GATEWAY_VAULT_SECRET_PATH: sendico/gateway/chain/vault
|
||||||
CHAIN_GATEWAY_ENV: prod
|
CHAIN_GATEWAY_ENV: prod
|
||||||
|
|
||||||
when:
|
when:
|
||||||
|
|||||||
@@ -25,21 +25,21 @@ 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
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.47.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.4 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.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-20251111163417-95abcf5c77ba // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
google.golang.org/protobuf v1.36.10
|
google.golang.org/protobuf v1.36.10
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -97,8 +97,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -141,8 +141,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
|
|||||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
@@ -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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
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-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fake implements Client for tests.
|
|
||||||
type Fake struct {
|
|
||||||
CreateManagedWalletFn func(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error)
|
|
||||||
GetManagedWalletFn func(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error)
|
|
||||||
ListManagedWalletsFn func(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error)
|
|
||||||
GetWalletBalanceFn func(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error)
|
|
||||||
SubmitTransferFn func(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error)
|
|
||||||
GetTransferFn func(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error)
|
|
||||||
ListTransfersFn func(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error)
|
|
||||||
EstimateTransferFeeFn func(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error)
|
|
||||||
CloseFn func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) {
|
|
||||||
if f.CreateManagedWalletFn != nil {
|
|
||||||
return f.CreateManagedWalletFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.CreateManagedWalletResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) {
|
|
||||||
if f.GetManagedWalletFn != nil {
|
|
||||||
return f.GetManagedWalletFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.GetManagedWalletResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) {
|
|
||||||
if f.ListManagedWalletsFn != nil {
|
|
||||||
return f.ListManagedWalletsFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.ListManagedWalletsResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) {
|
|
||||||
if f.GetWalletBalanceFn != nil {
|
|
||||||
return f.GetWalletBalanceFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.GetWalletBalanceResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
|
|
||||||
if f.SubmitTransferFn != nil {
|
|
||||||
return f.SubmitTransferFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.SubmitTransferResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) {
|
|
||||||
if f.GetTransferFn != nil {
|
|
||||||
return f.GetTransferFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.GetTransferResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) {
|
|
||||||
if f.ListTransfersFn != nil {
|
|
||||||
return f.ListTransfersFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.ListTransfersResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) {
|
|
||||||
if f.EstimateTransferFeeFn != nil {
|
|
||||||
return f.EstimateTransferFeeFn(ctx, req)
|
|
||||||
}
|
|
||||||
return &gatewayv1.EstimateTransferFeeResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fake) Close() error {
|
|
||||||
if f.CloseFn != nil {
|
|
||||||
return f.CloseFn()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package gateway
|
|
||||||
|
|
||||||
import moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
||||||
|
|
||||||
func cloneMoney(m *moneyv1.Money) *moneyv1.Money {
|
|
||||||
if m == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &moneyv1.Money{Amount: m.GetAmount(), Currency: m.GetCurrency()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneMetadata(input map[string]string) map[string]string {
|
|
||||||
if len(input) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
clone := make(map[string]string, len(input))
|
|
||||||
for k, v := range input {
|
|
||||||
clone[k] = v
|
|
||||||
}
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
package gateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
|
||||||
"github.com/tech/sendico/pkg/api/routers"
|
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
|
||||||
|
|
||||||
type serviceError string
|
|
||||||
|
|
||||||
func (e serviceError) Error() string {
|
|
||||||
return string(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
errStorageUnavailable = serviceError("chain_gateway: storage not initialised")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service implements the ChainGatewayService RPC contract.
|
|
||||||
type Service struct {
|
|
||||||
logger mlogger.Logger
|
|
||||||
storage storage.Repository
|
|
||||||
producer msg.Producer
|
|
||||||
clock clockpkg.Clock
|
|
||||||
|
|
||||||
networks map[string]Network
|
|
||||||
serviceWallet ServiceWallet
|
|
||||||
keyManager keymanager.Manager
|
|
||||||
executor TransferExecutor
|
|
||||||
|
|
||||||
gatewayv1.UnimplementedChainGatewayServiceServer
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewService constructs the chain gateway service skeleton.
|
|
||||||
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
|
|
||||||
svc := &Service{
|
|
||||||
logger: logger.Named("chain_gateway"),
|
|
||||||
storage: repo,
|
|
||||||
producer: producer,
|
|
||||||
clock: clockpkg.System{},
|
|
||||||
networks: map[string]Network{},
|
|
||||||
}
|
|
||||||
|
|
||||||
initMetrics()
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
if opt != nil {
|
|
||||||
opt(svc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if svc.clock == nil {
|
|
||||||
svc.clock = clockpkg.System{}
|
|
||||||
}
|
|
||||||
if svc.networks == nil {
|
|
||||||
svc.networks = map[string]Network{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return svc
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register wires the service onto the provided gRPC router.
|
|
||||||
func (s *Service) Register(router routers.GRPC) error {
|
|
||||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
|
||||||
gatewayv1.RegisterChainGatewayServiceServer(reg, s)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "CreateManagedWallet", s.createManagedWalletHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "GetManagedWallet", s.getManagedWalletHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "ListManagedWallets", s.listManagedWalletsHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "GetWalletBalance", s.getWalletBalanceHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "SubmitTransfer", s.submitTransferHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "GetTransfer", s.getTransferHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "ListTransfers", s.listTransfersHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) {
|
|
||||||
return executeUnary(ctx, s, "EstimateTransferFee", s.estimateTransferFeeHandler, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ensureRepository(ctx context.Context) error {
|
|
||||||
if s.storage == nil {
|
|
||||||
return errStorageUnavailable
|
|
||||||
}
|
|
||||||
return s.storage.Ping(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func executeUnary[TReq any, TResp any](ctx context.Context, svc *Service, method string, handler func(context.Context, *TReq) gsresponse.Responder[TResp], req *TReq) (*TResp, error) {
|
|
||||||
start := svc.clock.Now()
|
|
||||||
resp, err := gsresponse.Unary(svc.logger, mservice.ChainGateway, handler)(ctx, req)
|
|
||||||
observeRPC(method, err, svc.clock.Now().Sub(start))
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveContractAddress(tokens []TokenContract, symbol string) string {
|
|
||||||
upper := strings.ToUpper(symbol)
|
|
||||||
for _, token := range tokens {
|
|
||||||
if strings.EqualFold(token.Symbol, upper) && token.ContractAddress != "" {
|
|
||||||
return strings.ToLower(token.ContractAddress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateWalletRef() string {
|
|
||||||
return primitive.NewObjectID().Hex()
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateTransferRef() string {
|
|
||||||
return primitive.NewObjectID().Hex()
|
|
||||||
}
|
|
||||||
|
|
||||||
func chainKeyFromEnum(chain gatewayv1.ChainNetwork) (string, gatewayv1.ChainNetwork) {
|
|
||||||
if name, ok := gatewayv1.ChainNetwork_name[int32(chain)]; ok {
|
|
||||||
key := strings.ToLower(strings.TrimPrefix(name, "CHAIN_NETWORK_"))
|
|
||||||
return key, chain
|
|
||||||
}
|
|
||||||
return "", gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
|
||||||
}
|
|
||||||
|
|
||||||
func chainEnumFromName(name string) gatewayv1.ChainNetwork {
|
|
||||||
if name == "" {
|
|
||||||
return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
|
||||||
}
|
|
||||||
upper := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(name, " ", "_"), "-", "_"))
|
|
||||||
key := "CHAIN_NETWORK_" + upper
|
|
||||||
if val, ok := gatewayv1.ChainNetwork_value[key]; ok {
|
|
||||||
return gatewayv1.ChainNetwork(val)
|
|
||||||
}
|
|
||||||
return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
|
||||||
}
|
|
||||||
|
|
||||||
func managedWalletStatusToProto(status model.ManagedWalletStatus) gatewayv1.ManagedWalletStatus {
|
|
||||||
switch status {
|
|
||||||
case model.ManagedWalletStatusActive:
|
|
||||||
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE
|
|
||||||
case model.ManagedWalletStatusSuspended:
|
|
||||||
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED
|
|
||||||
case model.ManagedWalletStatusClosed:
|
|
||||||
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED
|
|
||||||
default:
|
|
||||||
return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_STATUS_UNSPECIFIED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func transferStatusToModel(status gatewayv1.TransferStatus) model.TransferStatus {
|
|
||||||
switch status {
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_PENDING:
|
|
||||||
return model.TransferStatusPending
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_SIGNING:
|
|
||||||
return model.TransferStatusSigning
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_SUBMITTED:
|
|
||||||
return model.TransferStatusSubmitted
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_CONFIRMED:
|
|
||||||
return model.TransferStatusConfirmed
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_FAILED:
|
|
||||||
return model.TransferStatusFailed
|
|
||||||
case gatewayv1.TransferStatus_TRANSFER_CANCELLED:
|
|
||||||
return model.TransferStatusCancelled
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func transferStatusToProto(status model.TransferStatus) gatewayv1.TransferStatus {
|
|
||||||
switch status {
|
|
||||||
case model.TransferStatusPending:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_PENDING
|
|
||||||
case model.TransferStatusSigning:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_SIGNING
|
|
||||||
case model.TransferStatusSubmitted:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_SUBMITTED
|
|
||||||
case model.TransferStatusConfirmed:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_CONFIRMED
|
|
||||||
case model.TransferStatusFailed:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_FAILED
|
|
||||||
case model.TransferStatusCancelled:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_CANCELLED
|
|
||||||
default:
|
|
||||||
return gatewayv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
package gateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Service) submitTransferHandler(ctx context.Context, req *gatewayv1.SubmitTransferRequest) gsresponse.Responder[gatewayv1.SubmitTransferResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
||||||
}
|
|
||||||
|
|
||||||
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
|
||||||
if idempotencyKey == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
|
||||||
}
|
|
||||||
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
|
||||||
if organizationRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
|
||||||
}
|
|
||||||
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
|
|
||||||
if sourceWalletRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
|
|
||||||
}
|
|
||||||
amount := req.GetAmount()
|
|
||||||
if amount == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
|
|
||||||
}
|
|
||||||
amountCurrency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
|
|
||||||
if amountCurrency == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount.currency is required"))
|
|
||||||
}
|
|
||||||
amountValue := strings.TrimSpace(amount.GetAmount())
|
|
||||||
if amountValue == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required"))
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceWallet, err := s.storage.Wallets().Get(ctx, sourceWalletRef)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
|
||||||
return gsresponse.NotFound[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if !strings.EqualFold(sourceWallet.OrganizationRef, organizationRef) {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref mismatch with wallet"))
|
|
||||||
}
|
|
||||||
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
|
|
||||||
networkCfg, ok := s.networks[networkKey]
|
|
||||||
if !ok {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
|
||||||
}
|
|
||||||
|
|
||||||
destination, err := s.resolveDestination(ctx, req.GetDestination(), sourceWallet)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
|
||||||
return gsresponse.NotFound[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fees, feeSum, err := convertFees(req.GetFees(), amountCurrency)
|
|
||||||
if err != nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
amountDec, err := decimal.NewFromString(amountValue)
|
|
||||||
if err != nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("invalid amount"))
|
|
||||||
}
|
|
||||||
netDec := amountDec.Sub(feeSum)
|
|
||||||
if netDec.IsNegative() {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("fees exceed amount"))
|
|
||||||
}
|
|
||||||
|
|
||||||
netAmount := cloneMoney(amount)
|
|
||||||
netAmount.Amount = netDec.String()
|
|
||||||
|
|
||||||
transfer := &model.Transfer{
|
|
||||||
IdempotencyKey: idempotencyKey,
|
|
||||||
TransferRef: generateTransferRef(),
|
|
||||||
OrganizationRef: organizationRef,
|
|
||||||
SourceWalletRef: sourceWalletRef,
|
|
||||||
Destination: destination,
|
|
||||||
Network: sourceWallet.Network,
|
|
||||||
TokenSymbol: sourceWallet.TokenSymbol,
|
|
||||||
ContractAddress: sourceWallet.ContractAddress,
|
|
||||||
RequestedAmount: cloneMoney(amount),
|
|
||||||
NetAmount: netAmount,
|
|
||||||
Fees: fees,
|
|
||||||
Status: model.TransferStatusPending,
|
|
||||||
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
|
||||||
LastStatusAt: s.clock.Now().UTC(),
|
|
||||||
}
|
|
||||||
|
|
||||||
saved, err := s.storage.Transfers().Create(ctx, transfer)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrDataConflict) {
|
|
||||||
s.logger.Debug("transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", idempotencyKey))
|
|
||||||
return gsresponse.Success(&gatewayv1.SubmitTransferResponse{Transfer: s.toProtoTransfer(saved)})
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.executor != nil {
|
|
||||||
s.launchTransferExecution(saved.TransferRef, sourceWalletRef, networkCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gsresponse.Success(&gatewayv1.SubmitTransferResponse{Transfer: s.toProtoTransfer(saved)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) getTransferHandler(ctx context.Context, req *gatewayv1.GetTransferRequest) gsresponse.Responder[gatewayv1.GetTransferResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
||||||
}
|
|
||||||
transferRef := strings.TrimSpace(req.GetTransferRef())
|
|
||||||
if transferRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("transfer_ref is required"))
|
|
||||||
}
|
|
||||||
transfer, err := s.storage.Transfers().Get(ctx, transferRef)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
|
||||||
return gsresponse.NotFound[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Success(&gatewayv1.GetTransferResponse{Transfer: s.toProtoTransfer(transfer)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) listTransfersHandler(ctx context.Context, req *gatewayv1.ListTransfersRequest) gsresponse.Responder[gatewayv1.ListTransfersResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.ListTransfersResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
filter := model.TransferFilter{}
|
|
||||||
if req != nil {
|
|
||||||
filter.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
|
|
||||||
filter.DestinationWalletRef = strings.TrimSpace(req.GetDestinationWalletRef())
|
|
||||||
if status := transferStatusToModel(req.GetStatus()); status != "" {
|
|
||||||
filter.Status = status
|
|
||||||
}
|
|
||||||
if page := req.GetPage(); page != nil {
|
|
||||||
filter.Cursor = strings.TrimSpace(page.GetCursor())
|
|
||||||
filter.Limit = page.GetLimit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := s.storage.Transfers().List(ctx, filter)
|
|
||||||
if err != nil {
|
|
||||||
return gsresponse.Auto[gatewayv1.ListTransfersResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
protoTransfers := make([]*gatewayv1.Transfer, 0, len(result.Items))
|
|
||||||
for _, transfer := range result.Items {
|
|
||||||
protoTransfers = append(protoTransfers, s.toProtoTransfer(transfer))
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &gatewayv1.ListTransfersResponse{
|
|
||||||
Transfers: protoTransfers,
|
|
||||||
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
|
|
||||||
}
|
|
||||||
return gsresponse.Success(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) estimateTransferFeeHandler(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) gsresponse.Responder[gatewayv1.EstimateTransferFeeResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.EstimateTransferFeeResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil || req.GetAmount() == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.EstimateTransferFeeResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
|
|
||||||
}
|
|
||||||
currency := req.GetAmount().GetCurrency()
|
|
||||||
fee := &moneyv1.Money{
|
|
||||||
Currency: currency,
|
|
||||||
Amount: "0",
|
|
||||||
}
|
|
||||||
resp := &gatewayv1.EstimateTransferFeeResponse{
|
|
||||||
NetworkFee: fee,
|
|
||||||
EstimationContext: "not_implemented",
|
|
||||||
}
|
|
||||||
return gsresponse.Success(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) toProtoTransfer(transfer *model.Transfer) *gatewayv1.Transfer {
|
|
||||||
if transfer == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
destination := &gatewayv1.TransferDestination{}
|
|
||||||
if transfer.Destination.ManagedWalletRef != "" {
|
|
||||||
destination.Destination = &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: transfer.Destination.ManagedWalletRef}
|
|
||||||
} else if transfer.Destination.ExternalAddress != "" {
|
|
||||||
destination.Destination = &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: transfer.Destination.ExternalAddress}
|
|
||||||
}
|
|
||||||
destination.Memo = transfer.Destination.Memo
|
|
||||||
|
|
||||||
protoFees := make([]*gatewayv1.ServiceFeeBreakdown, 0, len(transfer.Fees))
|
|
||||||
for _, fee := range transfer.Fees {
|
|
||||||
protoFees = append(protoFees, &gatewayv1.ServiceFeeBreakdown{
|
|
||||||
FeeCode: fee.FeeCode,
|
|
||||||
Amount: cloneMoney(fee.Amount),
|
|
||||||
Description: fee.Description,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
asset := &gatewayv1.Asset{
|
|
||||||
Chain: chainEnumFromName(transfer.Network),
|
|
||||||
TokenSymbol: transfer.TokenSymbol,
|
|
||||||
ContractAddress: transfer.ContractAddress,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &gatewayv1.Transfer{
|
|
||||||
TransferRef: transfer.TransferRef,
|
|
||||||
IdempotencyKey: transfer.IdempotencyKey,
|
|
||||||
OrganizationRef: transfer.OrganizationRef,
|
|
||||||
SourceWalletRef: transfer.SourceWalletRef,
|
|
||||||
Destination: destination,
|
|
||||||
Asset: asset,
|
|
||||||
RequestedAmount: cloneMoney(transfer.RequestedAmount),
|
|
||||||
NetAmount: cloneMoney(transfer.NetAmount),
|
|
||||||
Fees: protoFees,
|
|
||||||
Status: transferStatusToProto(transfer.Status),
|
|
||||||
TransactionHash: transfer.TxHash,
|
|
||||||
FailureReason: transfer.FailureReason,
|
|
||||||
CreatedAt: timestamppb.New(transfer.CreatedAt.UTC()),
|
|
||||||
UpdatedAt: timestamppb.New(transfer.UpdatedAt.UTC()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) resolveDestination(ctx context.Context, dest *gatewayv1.TransferDestination, source *model.ManagedWallet) (model.TransferDestination, error) {
|
|
||||||
if dest == nil {
|
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
|
||||||
}
|
|
||||||
managedRef := strings.TrimSpace(dest.GetManagedWalletRef())
|
|
||||||
external := strings.TrimSpace(dest.GetExternalAddress())
|
|
||||||
if managedRef != "" && external != "" {
|
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination must be managed_wallet_ref or external_address")
|
|
||||||
}
|
|
||||||
if managedRef != "" {
|
|
||||||
wallet, err := s.storage.Wallets().Get(ctx, managedRef)
|
|
||||||
if err != nil {
|
|
||||||
return model.TransferDestination{}, err
|
|
||||||
}
|
|
||||||
if !strings.EqualFold(wallet.Network, source.Network) {
|
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet network mismatch")
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet missing deposit address")
|
|
||||||
}
|
|
||||||
return model.TransferDestination{
|
|
||||||
ManagedWalletRef: wallet.WalletRef,
|
|
||||||
Memo: strings.TrimSpace(dest.GetMemo()),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
if external == "" {
|
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
|
||||||
}
|
|
||||||
return model.TransferDestination{
|
|
||||||
ExternalAddress: strings.ToLower(external),
|
|
||||||
Memo: strings.TrimSpace(dest.GetMemo()),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertFees(fees []*gatewayv1.ServiceFeeBreakdown, currency string) ([]model.ServiceFee, decimal.Decimal, error) {
|
|
||||||
result := make([]model.ServiceFee, 0, len(fees))
|
|
||||||
sum := decimal.NewFromInt(0)
|
|
||||||
for _, fee := range fees {
|
|
||||||
if fee == nil || fee.GetAmount() == nil {
|
|
||||||
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
|
|
||||||
}
|
|
||||||
amtCurrency := strings.ToUpper(strings.TrimSpace(fee.GetAmount().GetCurrency()))
|
|
||||||
if amtCurrency != strings.ToUpper(currency) {
|
|
||||||
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee currency mismatch")
|
|
||||||
}
|
|
||||||
amtValue := strings.TrimSpace(fee.GetAmount().GetAmount())
|
|
||||||
if amtValue == "" {
|
|
||||||
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
|
|
||||||
}
|
|
||||||
dec, err := decimal.NewFromString(amtValue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, decimal.Decimal{}, merrors.InvalidArgument("invalid fee amount")
|
|
||||||
}
|
|
||||||
if dec.IsNegative() {
|
|
||||||
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount must be non-negative")
|
|
||||||
}
|
|
||||||
sum = sum.Add(dec)
|
|
||||||
result = append(result, model.ServiceFee{
|
|
||||||
FeeCode: strings.TrimSpace(fee.GetFeeCode()),
|
|
||||||
Amount: cloneMoney(fee.GetAmount()),
|
|
||||||
Description: strings.TrimSpace(fee.GetDescription()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result, sum, nil
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
package gateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Service) createManagedWalletHandler(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) gsresponse.Responder[gatewayv1.CreateManagedWalletResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
||||||
}
|
|
||||||
|
|
||||||
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
|
||||||
if idempotencyKey == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
|
||||||
}
|
|
||||||
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
|
||||||
if organizationRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
|
||||||
}
|
|
||||||
ownerRef := strings.TrimSpace(req.GetOwnerRef())
|
|
||||||
if ownerRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("owner_ref is required"))
|
|
||||||
}
|
|
||||||
|
|
||||||
asset := req.GetAsset()
|
|
||||||
if asset == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required"))
|
|
||||||
}
|
|
||||||
|
|
||||||
chainKey, _ := chainKeyFromEnum(asset.GetChain())
|
|
||||||
if chainKey == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
|
||||||
}
|
|
||||||
networkCfg, ok := s.networks[chainKey]
|
|
||||||
if !ok {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenSymbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
|
|
||||||
if tokenSymbol == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("asset.token_symbol is required"))
|
|
||||||
}
|
|
||||||
contractAddress := strings.ToLower(strings.TrimSpace(asset.GetContractAddress()))
|
|
||||||
if contractAddress == "" {
|
|
||||||
contractAddress = resolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
|
|
||||||
if contractAddress == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
walletRef := generateWalletRef()
|
|
||||||
if s.keyManager == nil {
|
|
||||||
return gsresponse.Internal[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.Internal("key manager not configured"))
|
|
||||||
}
|
|
||||||
|
|
||||||
keyInfo, err := s.keyManager.CreateManagedWalletKey(ctx, walletRef, chainKey)
|
|
||||||
if err != nil {
|
|
||||||
return gsresponse.Auto[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if keyInfo == nil || strings.TrimSpace(keyInfo.Address) == "" {
|
|
||||||
return gsresponse.Internal[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address"))
|
|
||||||
}
|
|
||||||
|
|
||||||
wallet := &model.ManagedWallet{
|
|
||||||
IdempotencyKey: idempotencyKey,
|
|
||||||
WalletRef: walletRef,
|
|
||||||
OrganizationRef: organizationRef,
|
|
||||||
OwnerRef: ownerRef,
|
|
||||||
Network: chainKey,
|
|
||||||
TokenSymbol: tokenSymbol,
|
|
||||||
ContractAddress: contractAddress,
|
|
||||||
DepositAddress: strings.ToLower(keyInfo.Address),
|
|
||||||
KeyReference: keyInfo.KeyID,
|
|
||||||
Status: model.ManagedWalletStatusActive,
|
|
||||||
Metadata: cloneMetadata(req.GetMetadata()),
|
|
||||||
}
|
|
||||||
|
|
||||||
created, err := s.storage.Wallets().Create(ctx, wallet)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrDataConflict) {
|
|
||||||
s.logger.Debug("wallet already exists", zap.String("wallet_ref", walletRef), zap.String("idempotency_key", idempotencyKey))
|
|
||||||
return gsresponse.Success(&gatewayv1.CreateManagedWalletResponse{Wallet: s.toProtoManagedWallet(created)})
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gsresponse.Success(&gatewayv1.CreateManagedWalletResponse{Wallet: s.toProtoManagedWallet(created)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) getManagedWalletHandler(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) gsresponse.Responder[gatewayv1.GetManagedWalletResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
||||||
}
|
|
||||||
walletRef := strings.TrimSpace(req.GetWalletRef())
|
|
||||||
if walletRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
|
||||||
}
|
|
||||||
wallet, err := s.storage.Wallets().Get(ctx, walletRef)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
|
||||||
return gsresponse.NotFound[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Success(&gatewayv1.GetManagedWalletResponse{Wallet: s.toProtoManagedWallet(wallet)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) listManagedWalletsHandler(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) gsresponse.Responder[gatewayv1.ListManagedWalletsResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.ListManagedWalletsResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
filter := model.ManagedWalletFilter{}
|
|
||||||
if req != nil {
|
|
||||||
filter.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
|
||||||
filter.OwnerRef = strings.TrimSpace(req.GetOwnerRef())
|
|
||||||
if asset := req.GetAsset(); asset != nil {
|
|
||||||
filter.Network, _ = chainKeyFromEnum(asset.GetChain())
|
|
||||||
filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol())
|
|
||||||
}
|
|
||||||
if page := req.GetPage(); page != nil {
|
|
||||||
filter.Cursor = strings.TrimSpace(page.GetCursor())
|
|
||||||
filter.Limit = page.GetLimit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := s.storage.Wallets().List(ctx, filter)
|
|
||||||
if err != nil {
|
|
||||||
return gsresponse.Auto[gatewayv1.ListManagedWalletsResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
protoWallets := make([]*gatewayv1.ManagedWallet, 0, len(result.Items))
|
|
||||||
for _, wallet := range result.Items {
|
|
||||||
protoWallets = append(protoWallets, s.toProtoManagedWallet(wallet))
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &gatewayv1.ListManagedWalletsResponse{
|
|
||||||
Wallets: protoWallets,
|
|
||||||
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
|
|
||||||
}
|
|
||||||
return gsresponse.Success(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) getWalletBalanceHandler(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) gsresponse.Responder[gatewayv1.GetWalletBalanceResponse] {
|
|
||||||
if err := s.ensureRepository(ctx); err != nil {
|
|
||||||
return gsresponse.Unavailable[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
if req == nil {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
|
||||||
}
|
|
||||||
walletRef := strings.TrimSpace(req.GetWalletRef())
|
|
||||||
if walletRef == "" {
|
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
|
||||||
}
|
|
||||||
balance, err := s.storage.Wallets().GetBalance(ctx, walletRef)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
|
||||||
return gsresponse.NotFound[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Auto[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err)
|
|
||||||
}
|
|
||||||
return gsresponse.Success(&gatewayv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(balance)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) toProtoManagedWallet(wallet *model.ManagedWallet) *gatewayv1.ManagedWallet {
|
|
||||||
if wallet == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
asset := &gatewayv1.Asset{
|
|
||||||
Chain: chainEnumFromName(wallet.Network),
|
|
||||||
TokenSymbol: wallet.TokenSymbol,
|
|
||||||
ContractAddress: wallet.ContractAddress,
|
|
||||||
}
|
|
||||||
return &gatewayv1.ManagedWallet{
|
|
||||||
WalletRef: wallet.WalletRef,
|
|
||||||
OrganizationRef: wallet.OrganizationRef,
|
|
||||||
OwnerRef: wallet.OwnerRef,
|
|
||||||
Asset: asset,
|
|
||||||
DepositAddress: wallet.DepositAddress,
|
|
||||||
Status: managedWalletStatusToProto(wallet.Status),
|
|
||||||
Metadata: cloneMetadata(wallet.Metadata),
|
|
||||||
CreatedAt: timestamppb.New(wallet.CreatedAt.UTC()),
|
|
||||||
UpdatedAt: timestamppb.New(wallet.UpdatedAt.UTC()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toProtoWalletBalance(balance *model.WalletBalance) *gatewayv1.WalletBalance {
|
|
||||||
if balance == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &gatewayv1.WalletBalance{
|
|
||||||
Available: cloneMoney(balance.Available),
|
|
||||||
PendingInbound: cloneMoney(balance.PendingInbound),
|
|
||||||
PendingOutbound: cloneMoney(balance.PendingOutbound),
|
|
||||||
CalculatedAt: timestamppb.New(balance.CalculatedAt.UTC()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,20 +25,20 @@ 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
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.47.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.4 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
go.mongodb.org/mongo-driver v1.17.6 // 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-20251111163417-95abcf5c77ba // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
google.golang.org/grpc v1.77.0 // indirect
|
google.golang.org/grpc v1.77.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -97,8 +97,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -141,8 +141,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
|
|||||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
@@ -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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
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-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -27,20 +27,20 @@ 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
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.47.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.4 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.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-20251111163417-95abcf5c77ba // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -97,8 +97,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -141,8 +141,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
|
|||||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
@@ -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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
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-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ 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
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
@@ -109,8 +109,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
|
|||||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ root = "./../.."
|
|||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/chain/gateway/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.BuildDate=$(date)'\""
|
cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/gateway/chain/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/gateway/chain/internal/appversion.BuildDate=$(date)'\""
|
||||||
bin = "./app"
|
bin = "./app"
|
||||||
full_bin = "./app --debug --config.file=config.yml"
|
full_bin = "./app --debug --config.file=config.yml"
|
||||||
include_ext = ["go", "yaml", "yml"]
|
include_ext = ["go", "yaml", "yml"]
|
||||||
exclude_dir = ["chain/gateway/tmp", "pkg/.git", "chain/gateway/env"]
|
exclude_dir = ["gateway/chain/tmp", "pkg/.git", "gateway/chain/env"]
|
||||||
exclude_regex = ["_test\\.go"]
|
exclude_regex = ["_test\\.go"]
|
||||||
exclude_unchanged = true
|
exclude_unchanged = true
|
||||||
follow_symlink = true
|
follow_symlink = true
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
@@ -16,26 +16,26 @@ import (
|
|||||||
|
|
||||||
// Client exposes typed helpers around the chain gateway gRPC API.
|
// Client exposes typed helpers around the chain gateway gRPC API.
|
||||||
type Client interface {
|
type Client interface {
|
||||||
CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error)
|
CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
|
||||||
GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error)
|
GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error)
|
||||||
ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error)
|
ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
|
||||||
GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error)
|
GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
|
||||||
SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error)
|
SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error)
|
||||||
GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error)
|
GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error)
|
||||||
ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error)
|
ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error)
|
||||||
EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error)
|
EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error)
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type grpcGatewayClient interface {
|
type grpcGatewayClient interface {
|
||||||
CreateManagedWallet(ctx context.Context, in *gatewayv1.CreateManagedWalletRequest, opts ...grpc.CallOption) (*gatewayv1.CreateManagedWalletResponse, error)
|
CreateManagedWallet(ctx context.Context, in *chainv1.CreateManagedWalletRequest, opts ...grpc.CallOption) (*chainv1.CreateManagedWalletResponse, error)
|
||||||
GetManagedWallet(ctx context.Context, in *gatewayv1.GetManagedWalletRequest, opts ...grpc.CallOption) (*gatewayv1.GetManagedWalletResponse, error)
|
GetManagedWallet(ctx context.Context, in *chainv1.GetManagedWalletRequest, opts ...grpc.CallOption) (*chainv1.GetManagedWalletResponse, error)
|
||||||
ListManagedWallets(ctx context.Context, in *gatewayv1.ListManagedWalletsRequest, opts ...grpc.CallOption) (*gatewayv1.ListManagedWalletsResponse, error)
|
ListManagedWallets(ctx context.Context, in *chainv1.ListManagedWalletsRequest, opts ...grpc.CallOption) (*chainv1.ListManagedWalletsResponse, error)
|
||||||
GetWalletBalance(ctx context.Context, in *gatewayv1.GetWalletBalanceRequest, opts ...grpc.CallOption) (*gatewayv1.GetWalletBalanceResponse, error)
|
GetWalletBalance(ctx context.Context, in *chainv1.GetWalletBalanceRequest, opts ...grpc.CallOption) (*chainv1.GetWalletBalanceResponse, error)
|
||||||
SubmitTransfer(ctx context.Context, in *gatewayv1.SubmitTransferRequest, opts ...grpc.CallOption) (*gatewayv1.SubmitTransferResponse, error)
|
SubmitTransfer(ctx context.Context, in *chainv1.SubmitTransferRequest, opts ...grpc.CallOption) (*chainv1.SubmitTransferResponse, error)
|
||||||
GetTransfer(ctx context.Context, in *gatewayv1.GetTransferRequest, opts ...grpc.CallOption) (*gatewayv1.GetTransferResponse, error)
|
GetTransfer(ctx context.Context, in *chainv1.GetTransferRequest, opts ...grpc.CallOption) (*chainv1.GetTransferResponse, error)
|
||||||
ListTransfers(ctx context.Context, in *gatewayv1.ListTransfersRequest, opts ...grpc.CallOption) (*gatewayv1.ListTransfersResponse, error)
|
ListTransfers(ctx context.Context, in *chainv1.ListTransfersRequest, opts ...grpc.CallOption) (*chainv1.ListTransfersResponse, error)
|
||||||
EstimateTransferFee(ctx context.Context, in *gatewayv1.EstimateTransferFeeRequest, opts ...grpc.CallOption) (*gatewayv1.EstimateTransferFeeResponse, error)
|
EstimateTransferFee(ctx context.Context, in *chainv1.EstimateTransferFeeRequest, opts ...grpc.CallOption) (*chainv1.EstimateTransferFeeResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type chainGatewayClient struct {
|
type chainGatewayClient struct {
|
||||||
@@ -71,7 +71,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
|
|||||||
return &chainGatewayClient{
|
return &chainGatewayClient{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
client: gatewayv1.NewChainGatewayServiceClient(conn),
|
client: chainv1.NewChainGatewayServiceClient(conn),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,49 +91,49 @@ func (c *chainGatewayClient) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) {
|
func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.CreateManagedWallet(ctx, req)
|
return c.client.CreateManagedWallet(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) {
|
func (c *chainGatewayClient) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetManagedWallet(ctx, req)
|
return c.client.GetManagedWallet(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) {
|
func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.ListManagedWallets(ctx, req)
|
return c.client.ListManagedWallets(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) {
|
func (c *chainGatewayClient) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetWalletBalance(ctx, req)
|
return c.client.GetWalletBalance(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
|
func (c *chainGatewayClient) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.SubmitTransfer(ctx, req)
|
return c.client.SubmitTransfer(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) {
|
func (c *chainGatewayClient) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.GetTransfer(ctx, req)
|
return c.client.GetTransfer(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) {
|
func (c *chainGatewayClient) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.ListTransfers(ctx, req)
|
return c.client.ListTransfers(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chainGatewayClient) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) {
|
func (c *chainGatewayClient) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return c.client.EstimateTransferFee(ctx, req)
|
return c.client.EstimateTransferFee(ctx, req)
|
||||||
83
api/gateway/chain/client/fake.go
Normal file
83
api/gateway/chain/client/fake.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fake implements Client for tests.
|
||||||
|
type Fake struct {
|
||||||
|
CreateManagedWalletFn func(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
|
||||||
|
GetManagedWalletFn func(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error)
|
||||||
|
ListManagedWalletsFn func(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
|
||||||
|
GetWalletBalanceFn func(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
|
||||||
|
SubmitTransferFn func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error)
|
||||||
|
GetTransferFn func(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error)
|
||||||
|
ListTransfersFn func(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error)
|
||||||
|
EstimateTransferFeeFn func(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error)
|
||||||
|
CloseFn func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
||||||
|
if f.CreateManagedWalletFn != nil {
|
||||||
|
return f.CreateManagedWalletFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.CreateManagedWalletResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
|
||||||
|
if f.GetManagedWalletFn != nil {
|
||||||
|
return f.GetManagedWalletFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.GetManagedWalletResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
|
||||||
|
if f.ListManagedWalletsFn != nil {
|
||||||
|
return f.ListManagedWalletsFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.ListManagedWalletsResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||||
|
if f.GetWalletBalanceFn != nil {
|
||||||
|
return f.GetWalletBalanceFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.GetWalletBalanceResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||||
|
if f.SubmitTransferFn != nil {
|
||||||
|
return f.SubmitTransferFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.SubmitTransferResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
||||||
|
if f.GetTransferFn != nil {
|
||||||
|
return f.GetTransferFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.GetTransferResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
|
||||||
|
if f.ListTransfersFn != nil {
|
||||||
|
return f.ListTransfersFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.ListTransfersResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||||
|
if f.EstimateTransferFeeFn != nil {
|
||||||
|
return f.EstimateTransferFeeFn(ctx, req)
|
||||||
|
}
|
||||||
|
return &chainv1.EstimateTransferFeeResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fake) Close() error {
|
||||||
|
if f.CloseFn != nil {
|
||||||
|
return f.CloseFn()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -54,4 +54,4 @@ key_management:
|
|||||||
token_env: VAULT_TOKEN
|
token_env: VAULT_TOKEN
|
||||||
namespace: ""
|
namespace: ""
|
||||||
mount_path: kv
|
mount_path: kv
|
||||||
key_prefix: chain/gateway/wallets
|
key_prefix: gateway/chain/wallets
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module github.com/tech/sendico/chain/gateway
|
module github.com/tech/sendico/gateway/chain
|
||||||
|
|
||||||
go 1.25.3
|
go 1.25.3
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ require (
|
|||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.47.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
@@ -73,7 +73,7 @@ require (
|
|||||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
@@ -86,5 +86,5 @@ require (
|
|||||||
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
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||||
)
|
)
|
||||||
@@ -209,8 +209,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
@@ -283,8 +283,8 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
|||||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
@@ -362,8 +362,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/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=
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/hashicorp/vault/api"
|
"github.com/hashicorp/vault/api"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
)
|
)
|
||||||
@@ -7,11 +7,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
vaultmanager "github.com/tech/sendico/chain/gateway/internal/keymanager/vault"
|
vaultmanager "github.com/tech/sendico/gateway/chain/internal/keymanager/vault"
|
||||||
gatewayservice "github.com/tech/sendico/chain/gateway/internal/service/gateway"
|
gatewayservice "github.com/tech/sendico/gateway/chain/internal/service/gateway"
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
gatewayshared "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
gatewaymongo "github.com/tech/sendico/chain/gateway/storage/mongo"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
|
gatewaymongo "github.com/tech/sendico/gateway/chain/storage/mongo"
|
||||||
"github.com/tech/sendico/pkg/api/routers"
|
"github.com/tech/sendico/pkg/api/routers"
|
||||||
"github.com/tech/sendico/pkg/db"
|
"github.com/tech/sendico/pkg/db"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
@@ -154,8 +155,8 @@ func (i *Imp) loadConfig() (*config, error) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewayservice.Network {
|
func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewayshared.Network {
|
||||||
result := make([]gatewayservice.Network, 0, len(chains))
|
result := make([]gatewayshared.Network, 0, len(chains))
|
||||||
for _, chain := range chains {
|
for _, chain := range chains {
|
||||||
if strings.TrimSpace(chain.Name) == "" {
|
if strings.TrimSpace(chain.Name) == "" {
|
||||||
logger.Warn("skipping unnamed chain configuration")
|
logger.Warn("skipping unnamed chain configuration")
|
||||||
@@ -165,7 +166,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewa
|
|||||||
if rpcURL == "" {
|
if rpcURL == "" {
|
||||||
logger.Warn("chain RPC endpoint not configured", zap.String("chain", chain.Name), zap.String("env", chain.RPCURLEnv))
|
logger.Warn("chain RPC endpoint not configured", zap.String("chain", chain.Name), zap.String("env", chain.RPCURLEnv))
|
||||||
}
|
}
|
||||||
contracts := make([]gatewayservice.TokenContract, 0, len(chain.Tokens))
|
contracts := make([]gatewayshared.TokenContract, 0, len(chain.Tokens))
|
||||||
for _, token := range chain.Tokens {
|
for _, token := range chain.Tokens {
|
||||||
symbol := strings.TrimSpace(token.Symbol)
|
symbol := strings.TrimSpace(token.Symbol)
|
||||||
if symbol == "" {
|
if symbol == "" {
|
||||||
@@ -185,13 +186,13 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewa
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
contracts = append(contracts, gatewayservice.TokenContract{
|
contracts = append(contracts, gatewayshared.TokenContract{
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
ContractAddress: addr,
|
ContractAddress: addr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, gatewayservice.Network{
|
result = append(result, gatewayshared.Network{
|
||||||
Name: chain.Name,
|
Name: chain.Name,
|
||||||
RPCURL: rpcURL,
|
RPCURL: rpcURL,
|
||||||
ChainID: chain.ChainID,
|
ChainID: chain.ChainID,
|
||||||
@@ -202,7 +203,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewa
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewayservice.ServiceWallet {
|
func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewayshared.ServiceWallet {
|
||||||
address := strings.TrimSpace(cfg.Address)
|
address := strings.TrimSpace(cfg.Address)
|
||||||
if address == "" && cfg.AddressEnv != "" {
|
if address == "" && cfg.AddressEnv != "" {
|
||||||
address = strings.TrimSpace(os.Getenv(cfg.AddressEnv))
|
address = strings.TrimSpace(os.Getenv(cfg.AddressEnv))
|
||||||
@@ -221,7 +222,7 @@ func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewa
|
|||||||
logger.Warn("service wallet private key not configured", zap.String("env", cfg.PrivateKeyEnv))
|
logger.Warn("service wallet private key not configured", zap.String("env", cfg.PrivateKeyEnv))
|
||||||
}
|
}
|
||||||
|
|
||||||
return gatewayservice.ServiceWallet{
|
return gatewayshared.ServiceWallet{
|
||||||
Network: cfg.Chain,
|
Network: cfg.Chain,
|
||||||
Address: address,
|
Address: address,
|
||||||
PrivateKey: privateKey,
|
PrivateKey: privateKey,
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
serverimp "github.com/tech/sendico/chain/gateway/internal/server/internal"
|
serverimp "github.com/tech/sendico/gateway/chain/internal/server/internal"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/server"
|
"github.com/tech/sendico/pkg/server"
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/transfer"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/wallet"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Unary[TReq any, TResp any] interface {
|
||||||
|
Execute(context.Context, *TReq) gsresponse.Responder[TResp]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
CreateManagedWallet Unary[chainv1.CreateManagedWalletRequest, chainv1.CreateManagedWalletResponse]
|
||||||
|
GetManagedWallet Unary[chainv1.GetManagedWalletRequest, chainv1.GetManagedWalletResponse]
|
||||||
|
ListManagedWallets Unary[chainv1.ListManagedWalletsRequest, chainv1.ListManagedWalletsResponse]
|
||||||
|
GetWalletBalance Unary[chainv1.GetWalletBalanceRequest, chainv1.GetWalletBalanceResponse]
|
||||||
|
|
||||||
|
SubmitTransfer Unary[chainv1.SubmitTransferRequest, chainv1.SubmitTransferResponse]
|
||||||
|
GetTransfer Unary[chainv1.GetTransferRequest, chainv1.GetTransferResponse]
|
||||||
|
ListTransfers Unary[chainv1.ListTransfersRequest, chainv1.ListTransfersResponse]
|
||||||
|
EstimateTransfer Unary[chainv1.EstimateTransferFeeRequest, chainv1.EstimateTransferFeeResponse]
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegistryDeps struct {
|
||||||
|
Wallet wallet.Deps
|
||||||
|
Transfer transfer.Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry(deps RegistryDeps) Registry {
|
||||||
|
return Registry{
|
||||||
|
CreateManagedWallet: wallet.NewCreateManagedWallet(deps.Wallet.WithLogger("wallet.create")),
|
||||||
|
GetManagedWallet: wallet.NewGetManagedWallet(deps.Wallet.WithLogger("wallet.get")),
|
||||||
|
ListManagedWallets: wallet.NewListManagedWallets(deps.Wallet.WithLogger("wallet.list")),
|
||||||
|
GetWalletBalance: wallet.NewGetWalletBalance(deps.Wallet.WithLogger("wallet.balance")),
|
||||||
|
SubmitTransfer: transfer.NewSubmitTransfer(deps.Transfer.WithLogger("transfer.submit")),
|
||||||
|
GetTransfer: transfer.NewGetTransfer(deps.Transfer.WithLogger("transfer.get")),
|
||||||
|
ListTransfers: transfer.NewListTransfers(deps.Transfer.WithLogger("transfer.list")),
|
||||||
|
EstimateTransfer: transfer.NewEstimateTransfer(deps.Transfer.WithLogger("transfer.estimate_fee")),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convertFees(fees []*chainv1.ServiceFeeBreakdown, currency string) ([]model.ServiceFee, decimal.Decimal, error) {
|
||||||
|
result := make([]model.ServiceFee, 0, len(fees))
|
||||||
|
sum := decimal.NewFromInt(0)
|
||||||
|
for _, fee := range fees {
|
||||||
|
if fee == nil || fee.GetAmount() == nil {
|
||||||
|
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
|
||||||
|
}
|
||||||
|
amtCurrency := strings.ToUpper(strings.TrimSpace(fee.GetAmount().GetCurrency()))
|
||||||
|
if amtCurrency != strings.ToUpper(currency) {
|
||||||
|
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee currency mismatch")
|
||||||
|
}
|
||||||
|
amtValue := strings.TrimSpace(fee.GetAmount().GetAmount())
|
||||||
|
if amtValue == "" {
|
||||||
|
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required")
|
||||||
|
}
|
||||||
|
dec, err := decimal.NewFromString(amtValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, decimal.Decimal{}, merrors.InvalidArgument("invalid fee amount")
|
||||||
|
}
|
||||||
|
if dec.IsNegative() {
|
||||||
|
return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount must be non-negative")
|
||||||
|
}
|
||||||
|
sum = sum.Add(dec)
|
||||||
|
result = append(result, model.ServiceFee{
|
||||||
|
FeeCode: strings.TrimSpace(fee.GetFeeCode()),
|
||||||
|
Amount: shared.CloneMoney(fee.GetAmount()),
|
||||||
|
Description: strings.TrimSpace(fee.GetDescription()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, sum, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
Logger mlogger.Logger
|
||||||
|
Networks map[string]shared.Network
|
||||||
|
Storage storage.Repository
|
||||||
|
Clock clockpkg.Clock
|
||||||
|
EnsureRepository func(context.Context) error
|
||||||
|
LaunchExecution func(transferRef, sourceWalletRef string, network shared.Network)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Deps) WithLogger(name string) Deps {
|
||||||
|
if d.Logger != nil {
|
||||||
|
d.Logger = d.Logger.Named(name)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolveDestination(ctx context.Context, deps Deps, dest *chainv1.TransferDestination, source *model.ManagedWallet) (model.TransferDestination, error) {
|
||||||
|
if dest == nil {
|
||||||
|
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
||||||
|
}
|
||||||
|
managedRef := strings.TrimSpace(dest.GetManagedWalletRef())
|
||||||
|
external := strings.TrimSpace(dest.GetExternalAddress())
|
||||||
|
if managedRef != "" && external != "" {
|
||||||
|
deps.Logger.Warn("both managed and external destination provided")
|
||||||
|
return model.TransferDestination{}, merrors.InvalidArgument("destination must be managed_wallet_ref or external_address")
|
||||||
|
}
|
||||||
|
if managedRef != "" {
|
||||||
|
wallet, err := deps.Storage.Wallets().Get(ctx, managedRef)
|
||||||
|
if err != nil {
|
||||||
|
deps.Logger.Warn("destination wallet lookup failed", zap.Error(err), zap.String("managed_wallet_ref", managedRef))
|
||||||
|
return model.TransferDestination{}, err
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(wallet.Network, source.Network) {
|
||||||
|
deps.Logger.Warn("destination wallet network mismatch", zap.String("source_network", source.Network), zap.String("dest_network", wallet.Network))
|
||||||
|
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet network mismatch")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
||||||
|
deps.Logger.Warn("destination wallet missing deposit address", zap.String("managed_wallet_ref", managedRef))
|
||||||
|
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet missing deposit address")
|
||||||
|
}
|
||||||
|
return model.TransferDestination{
|
||||||
|
ManagedWalletRef: wallet.WalletRef,
|
||||||
|
Memo: strings.TrimSpace(dest.GetMemo()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if external == "" {
|
||||||
|
deps.Logger.Warn("destination external address missing")
|
||||||
|
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
||||||
|
}
|
||||||
|
return model.TransferDestination{
|
||||||
|
ExternalAddress: strings.ToLower(external),
|
||||||
|
Memo: strings.TrimSpace(dest.GetMemo()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func destinationAddress(ctx context.Context, deps Deps, dest model.TransferDestination) (string, error) {
|
||||||
|
if ref := strings.TrimSpace(dest.ManagedWalletRef); ref != "" {
|
||||||
|
wallet, err := deps.Storage.Wallets().Get(ctx, ref)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
||||||
|
return "", merrors.Internal("destination wallet missing deposit address")
|
||||||
|
}
|
||||||
|
return wallet.DepositAddress, nil
|
||||||
|
}
|
||||||
|
if addr := strings.TrimSpace(dest.ExternalAddress); addr != "" {
|
||||||
|
return strings.ToLower(addr), nil
|
||||||
|
}
|
||||||
|
return "", merrors.InvalidArgument("transfer destination address not resolved")
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum"
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type estimateTransferFeeCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEstimateTransfer(deps Deps) *estimateTransferFeeCommand {
|
||||||
|
return &estimateTransferFeeCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *estimateTransferFeeCommand) Execute(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) gsresponse.Responder[chainv1.EstimateTransferFeeResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("request is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
|
||||||
|
if sourceWalletRef == "" {
|
||||||
|
c.deps.Logger.Warn("source wallet ref missing")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
|
||||||
|
}
|
||||||
|
amount := req.GetAmount()
|
||||||
|
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
|
||||||
|
c.deps.Logger.Warn("amount missing or incomplete")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceWallet, err := c.deps.Storage.Wallets().Get(ctx, sourceWalletRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("source wallet not found", zap.String("source_wallet_ref", sourceWalletRef))
|
||||||
|
return gsresponse.NotFound[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage get wallet failed", zap.Error(err), zap.String("source_wallet_ref", sourceWalletRef))
|
||||||
|
return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
|
||||||
|
networkCfg, ok := c.deps.Networks[networkKey]
|
||||||
|
if !ok {
|
||||||
|
c.deps.Logger.Warn("unsupported chain", zap.String("network", networkKey))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
dest, err := resolveDestination(ctx, c.deps, req.GetDestination(), sourceWallet)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("destination not found", zap.String("destination_wallet_ref", req.GetDestination().GetManagedWalletRef()))
|
||||||
|
return gsresponse.NotFound[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("invalid destination", zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
destinationAddress, err := destinationAddress(ctx, c.deps, dest)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("failed to resolve destination address", zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
feeMoney, err := estimateNetworkFee(ctx, c.deps.Logger, networkCfg, sourceWallet, destinationAddress, amount)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("fee estimation failed", zap.Error(err))
|
||||||
|
return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &chainv1.EstimateTransferFeeResponse{
|
||||||
|
NetworkFee: feeMoney,
|
||||||
|
EstimationContext: "erc20_transfer",
|
||||||
|
}
|
||||||
|
return gsresponse.Success(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func estimateNetworkFee(ctx context.Context, logger mlogger.Logger, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
|
||||||
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
|
if rpcURL == "" {
|
||||||
|
return nil, merrors.InvalidArgument("network rpc url not configured")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(wallet.ContractAddress) == "" {
|
||||||
|
return nil, merrors.NotImplemented("native token transfers not supported")
|
||||||
|
}
|
||||||
|
if !common.IsHexAddress(wallet.ContractAddress) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid token contract address")
|
||||||
|
}
|
||||||
|
if !common.IsHexAddress(wallet.DepositAddress) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid source wallet address")
|
||||||
|
}
|
||||||
|
if !common.IsHexAddress(destination) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid destination address")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := ethclient.DialContext(ctx, rpcURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to connect to rpc: " + err.Error())
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tokenABI, err := abi.JSON(strings.NewReader(erc20TransferABI))
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to parse erc20 abi: " + err.Error())
|
||||||
|
}
|
||||||
|
tokenAddr := common.HexToAddress(wallet.ContractAddress)
|
||||||
|
toAddr := common.HexToAddress(destination)
|
||||||
|
fromAddr := common.HexToAddress(wallet.DepositAddress)
|
||||||
|
|
||||||
|
decimals, err := erc20Decimals(timeoutCtx, client, tokenABI, tokenAddr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("failed to read token decimals", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
amountBase, err := toBaseUnits(strings.TrimSpace(amount.GetAmount()), decimals)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err := tokenABI.Pack("transfer", toAddr, amountBase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to encode transfer call: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
gasPrice, err := client.SuggestGasPrice(timeoutCtx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to suggest gas price: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
callMsg := ethereum.CallMsg{
|
||||||
|
From: fromAddr,
|
||||||
|
To: &tokenAddr,
|
||||||
|
GasPrice: gasPrice,
|
||||||
|
Data: input,
|
||||||
|
}
|
||||||
|
gasLimit, err := client.EstimateGas(timeoutCtx, callMsg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to estimate gas: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fee := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasLimit))
|
||||||
|
feeDec := decimal.NewFromBigInt(fee, 0)
|
||||||
|
|
||||||
|
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
||||||
|
if currency == "" {
|
||||||
|
currency = strings.ToUpper(network.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &moneyv1.Money{
|
||||||
|
Currency: currency,
|
||||||
|
Amount: feeDec.String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func erc20Decimals(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address) (uint8, error) {
|
||||||
|
callData, err := tokenABI.Pack("decimals")
|
||||||
|
if err != nil {
|
||||||
|
return 0, merrors.Internal("failed to encode decimals call: " + err.Error())
|
||||||
|
}
|
||||||
|
msg := ethereum.CallMsg{
|
||||||
|
To: &token,
|
||||||
|
Data: callData,
|
||||||
|
}
|
||||||
|
output, err := client.CallContract(ctx, msg, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, merrors.Internal("decimals call failed: " + err.Error())
|
||||||
|
}
|
||||||
|
values, err := tokenABI.Unpack("decimals", output)
|
||||||
|
if err != nil {
|
||||||
|
return 0, merrors.Internal("failed to unpack decimals: " + err.Error())
|
||||||
|
}
|
||||||
|
if len(values) == 0 {
|
||||||
|
return 0, merrors.Internal("decimals call returned no data")
|
||||||
|
}
|
||||||
|
decimals, ok := values[0].(uint8)
|
||||||
|
if !ok {
|
||||||
|
return 0, merrors.Internal("decimals call returned unexpected type")
|
||||||
|
}
|
||||||
|
return decimals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBaseUnits(amount string, decimals uint8) (*big.Int, error) {
|
||||||
|
value, err := decimal.NewFromString(strings.TrimSpace(amount))
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.InvalidArgument("invalid amount " + amount + ": " + err.Error())
|
||||||
|
}
|
||||||
|
if value.IsNegative() {
|
||||||
|
return nil, merrors.InvalidArgument("amount must be positive")
|
||||||
|
}
|
||||||
|
multiplier := decimal.NewFromInt(1).Shift(int32(decimals))
|
||||||
|
scaled := value.Mul(multiplier)
|
||||||
|
if !scaled.Equal(scaled.Truncate(0)) {
|
||||||
|
return nil, merrors.InvalidArgument("amount " + amount + " exceeds token precision")
|
||||||
|
}
|
||||||
|
return scaled.BigInt(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const erc20TransferABI = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "decimals",
|
||||||
|
"outputs": [{ "name": "", "type": "uint8" }],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{ "name": "_to", "type": "address" },
|
||||||
|
{ "name": "_value", "type": "uint256" }
|
||||||
|
],
|
||||||
|
"name": "transfer",
|
||||||
|
"outputs": [{ "name": "", "type": "bool" }],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
]`
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getTransferCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGetTransfer(deps Deps) *getTransferCommand {
|
||||||
|
return &getTransferCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *getTransferCommand) Execute(ctx context.Context, req *chainv1.GetTransferRequest) gsresponse.Responder[chainv1.GetTransferResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
transferRef := strings.TrimSpace(req.GetTransferRef())
|
||||||
|
if transferRef == "" {
|
||||||
|
c.deps.Logger.Warn("transfer_ref missing")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("transfer_ref is required"))
|
||||||
|
}
|
||||||
|
transfer, err := c.deps.Storage.Transfers().Get(ctx, transferRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("not found", zap.String("transfer_ref", transferRef))
|
||||||
|
return gsresponse.NotFound[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("transfer_ref", transferRef))
|
||||||
|
return gsresponse.Auto[chainv1.GetTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
return gsresponse.Success(&chainv1.GetTransferResponse{Transfer: toProtoTransfer(transfer)})
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listTransfersCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListTransfers(deps Deps) *listTransfersCommand {
|
||||||
|
return &listTransfersCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *listTransfersCommand) Execute(ctx context.Context, req *chainv1.ListTransfersRequest) gsresponse.Responder[chainv1.ListTransfersResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.ListTransfersResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
filter := model.TransferFilter{}
|
||||||
|
if req != nil {
|
||||||
|
filter.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
|
||||||
|
filter.DestinationWalletRef = strings.TrimSpace(req.GetDestinationWalletRef())
|
||||||
|
if status := shared.TransferStatusToModel(req.GetStatus()); status != "" {
|
||||||
|
filter.Status = status
|
||||||
|
}
|
||||||
|
if page := req.GetPage(); page != nil {
|
||||||
|
filter.Cursor = strings.TrimSpace(page.GetCursor())
|
||||||
|
filter.Limit = page.GetLimit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := c.deps.Storage.Transfers().List(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("storage list failed", zap.Error(err))
|
||||||
|
return gsresponse.Auto[chainv1.ListTransfersResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
protoTransfers := make([]*chainv1.Transfer, 0, len(result.Items))
|
||||||
|
for _, transfer := range result.Items {
|
||||||
|
protoTransfers = append(protoTransfers, toProtoTransfer(transfer))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &chainv1.ListTransfersResponse{
|
||||||
|
Transfers: protoTransfers,
|
||||||
|
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
|
||||||
|
}
|
||||||
|
return gsresponse.Success(resp)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toProtoTransfer(transfer *model.Transfer) *chainv1.Transfer {
|
||||||
|
if transfer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
destination := &chainv1.TransferDestination{}
|
||||||
|
if transfer.Destination.ManagedWalletRef != "" {
|
||||||
|
destination.Destination = &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: transfer.Destination.ManagedWalletRef}
|
||||||
|
} else if transfer.Destination.ExternalAddress != "" {
|
||||||
|
destination.Destination = &chainv1.TransferDestination_ExternalAddress{ExternalAddress: transfer.Destination.ExternalAddress}
|
||||||
|
}
|
||||||
|
destination.Memo = transfer.Destination.Memo
|
||||||
|
|
||||||
|
protoFees := make([]*chainv1.ServiceFeeBreakdown, 0, len(transfer.Fees))
|
||||||
|
for _, fee := range transfer.Fees {
|
||||||
|
protoFees = append(protoFees, &chainv1.ServiceFeeBreakdown{
|
||||||
|
FeeCode: fee.FeeCode,
|
||||||
|
Amount: shared.CloneMoney(fee.Amount),
|
||||||
|
Description: fee.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
asset := &chainv1.Asset{
|
||||||
|
Chain: shared.ChainEnumFromName(transfer.Network),
|
||||||
|
TokenSymbol: transfer.TokenSymbol,
|
||||||
|
ContractAddress: transfer.ContractAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &chainv1.Transfer{
|
||||||
|
TransferRef: transfer.TransferRef,
|
||||||
|
IdempotencyKey: transfer.IdempotencyKey,
|
||||||
|
OrganizationRef: transfer.OrganizationRef,
|
||||||
|
SourceWalletRef: transfer.SourceWalletRef,
|
||||||
|
Destination: destination,
|
||||||
|
Asset: asset,
|
||||||
|
RequestedAmount: shared.CloneMoney(transfer.RequestedAmount),
|
||||||
|
NetAmount: shared.CloneMoney(transfer.NetAmount),
|
||||||
|
Fees: protoFees,
|
||||||
|
Status: shared.TransferStatusToProto(transfer.Status),
|
||||||
|
TransactionHash: transfer.TxHash,
|
||||||
|
FailureReason: transfer.FailureReason,
|
||||||
|
CreatedAt: timestamppb.New(transfer.CreatedAt.UTC()),
|
||||||
|
UpdatedAt: timestamppb.New(transfer.UpdatedAt.UTC()),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type submitTransferCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubmitTransfer(deps Deps) *submitTransferCommand {
|
||||||
|
return &submitTransferCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.SubmitTransferRequest) gsresponse.Responder[chainv1.SubmitTransferResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
|
||||||
|
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
||||||
|
if idempotencyKey == "" {
|
||||||
|
c.deps.Logger.Warn("missing idempotency key")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
||||||
|
}
|
||||||
|
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
||||||
|
if organizationRef == "" {
|
||||||
|
c.deps.Logger.Warn("missing organization ref")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
||||||
|
}
|
||||||
|
sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef())
|
||||||
|
if sourceWalletRef == "" {
|
||||||
|
c.deps.Logger.Warn("missing source wallet ref")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required"))
|
||||||
|
}
|
||||||
|
amount := req.GetAmount()
|
||||||
|
if amount == nil {
|
||||||
|
c.deps.Logger.Warn("missing amount")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required"))
|
||||||
|
}
|
||||||
|
amountCurrency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
|
||||||
|
if amountCurrency == "" {
|
||||||
|
c.deps.Logger.Warn("missing amount currency")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount.currency is required"))
|
||||||
|
}
|
||||||
|
amountValue := strings.TrimSpace(amount.GetAmount())
|
||||||
|
if amountValue == "" {
|
||||||
|
c.deps.Logger.Warn("missing amount value")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceWallet, err := c.deps.Storage.Wallets().Get(ctx, sourceWalletRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("source wallet not found", zap.String("source_wallet_ref", sourceWalletRef))
|
||||||
|
return gsresponse.NotFound[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage get wallet failed", zap.Error(err), zap.String("source_wallet_ref", sourceWalletRef))
|
||||||
|
return gsresponse.Auto[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sourceWallet.OrganizationRef, organizationRef) {
|
||||||
|
c.deps.Logger.Warn("organization mismatch", zap.String("wallet_org", sourceWallet.OrganizationRef), zap.String("req_org", organizationRef))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref mismatch with wallet"))
|
||||||
|
}
|
||||||
|
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
|
||||||
|
networkCfg, ok := c.deps.Networks[networkKey]
|
||||||
|
if !ok {
|
||||||
|
c.deps.Logger.Warn("unsupported chain", zap.String("network", networkKey))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
destination, err := resolveDestination(ctx, c.deps, req.GetDestination(), sourceWallet)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("destination not found", zap.String("destination_wallet_ref", req.GetDestination().GetManagedWalletRef()))
|
||||||
|
return gsresponse.NotFound[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("invalid destination", zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fees, feeSum, err := convertFees(req.GetFees(), amountCurrency)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("fee conversion failed", zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
amountDec, err := decimal.NewFromString(amountValue)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("invalid amount", zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("invalid amount"))
|
||||||
|
}
|
||||||
|
netDec := amountDec.Sub(feeSum)
|
||||||
|
if netDec.IsNegative() {
|
||||||
|
c.deps.Logger.Warn("fees exceed amount", zap.String("amount", amountValue), zap.String("fee_sum", feeSum.String()))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("fees exceed amount"))
|
||||||
|
}
|
||||||
|
|
||||||
|
netAmount := shared.CloneMoney(amount)
|
||||||
|
netAmount.Amount = netDec.String()
|
||||||
|
|
||||||
|
transfer := &model.Transfer{
|
||||||
|
IdempotencyKey: idempotencyKey,
|
||||||
|
TransferRef: shared.GenerateTransferRef(),
|
||||||
|
OrganizationRef: organizationRef,
|
||||||
|
SourceWalletRef: sourceWalletRef,
|
||||||
|
Destination: destination,
|
||||||
|
Network: sourceWallet.Network,
|
||||||
|
TokenSymbol: sourceWallet.TokenSymbol,
|
||||||
|
ContractAddress: sourceWallet.ContractAddress,
|
||||||
|
RequestedAmount: shared.CloneMoney(amount),
|
||||||
|
NetAmount: netAmount,
|
||||||
|
Fees: fees,
|
||||||
|
Status: model.TransferStatusPending,
|
||||||
|
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
||||||
|
LastStatusAt: c.deps.Clock.Now().UTC(),
|
||||||
|
}
|
||||||
|
|
||||||
|
saved, err := c.deps.Storage.Transfers().Create(ctx, transfer)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrDataConflict) {
|
||||||
|
c.deps.Logger.Debug("transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", idempotencyKey))
|
||||||
|
return gsresponse.Success(&chainv1.SubmitTransferResponse{Transfer: toProtoTransfer(saved)})
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage create failed", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
|
||||||
|
return gsresponse.Auto[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.deps.LaunchExecution != nil {
|
||||||
|
c.deps.LaunchExecution(saved.TransferRef, sourceWalletRef, networkCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gsresponse.Success(&chainv1.SubmitTransferResponse{Transfer: toProtoTransfer(saved)})
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getWalletBalanceCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGetWalletBalance(deps Deps) *getWalletBalanceCommand {
|
||||||
|
return &getWalletBalanceCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *getWalletBalanceCommand) Execute(ctx context.Context, req *chainv1.GetWalletBalanceRequest) gsresponse.Responder[chainv1.GetWalletBalanceResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
walletRef := strings.TrimSpace(req.GetWalletRef())
|
||||||
|
if walletRef == "" {
|
||||||
|
c.deps.Logger.Warn("wallet_ref missing")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
||||||
|
}
|
||||||
|
wallet, err := c.deps.Storage.Wallets().Get(ctx, walletRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("not found", zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.NotFound[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
balance, chainErr := onChainWalletBalance(ctx, c.deps, wallet)
|
||||||
|
if chainErr != nil {
|
||||||
|
c.deps.Logger.Warn("on-chain balance fetch failed, falling back to stored balance", zap.Error(chainErr), zap.String("wallet_ref", walletRef))
|
||||||
|
stored, err := c.deps.Storage.Wallets().GetBalance(ctx, walletRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("stored balance not found", zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.NotFound[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
return gsresponse.Auto[chainv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(stored)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return gsresponse.Success(&chainv1.GetWalletBalanceResponse{Balance: onChainBalanceToProto(balance)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func onChainBalanceToProto(balance *moneyv1.Money) *chainv1.WalletBalance {
|
||||||
|
if balance == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
zero := &moneyv1.Money{Currency: balance.Currency, Amount: "0"}
|
||||||
|
return &chainv1.WalletBalance{
|
||||||
|
Available: balance,
|
||||||
|
PendingInbound: zero,
|
||||||
|
PendingOutbound: zero,
|
||||||
|
CalculatedAt: timestamppb.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createManagedWalletCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCreateManagedWallet(deps Deps) *createManagedWalletCommand {
|
||||||
|
return &createManagedWalletCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.CreateManagedWalletRequest) gsresponse.Responder[chainv1.CreateManagedWalletResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
|
||||||
|
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
||||||
|
if idempotencyKey == "" {
|
||||||
|
c.deps.Logger.Warn("missing idempotency key")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
||||||
|
}
|
||||||
|
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
||||||
|
if organizationRef == "" {
|
||||||
|
c.deps.Logger.Warn("missing organization ref")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required"))
|
||||||
|
}
|
||||||
|
ownerRef := strings.TrimSpace(req.GetOwnerRef())
|
||||||
|
if ownerRef == "" {
|
||||||
|
c.deps.Logger.Warn("missing owner ref")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("owner_ref is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
asset := req.GetAsset()
|
||||||
|
if asset == nil {
|
||||||
|
c.deps.Logger.Warn("missing asset")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
chainKey, _ := shared.ChainKeyFromEnum(asset.GetChain())
|
||||||
|
if chainKey == "" {
|
||||||
|
c.deps.Logger.Warn("unsupported chain", zap.Any("chain", asset.GetChain()))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
||||||
|
}
|
||||||
|
networkCfg, ok := c.deps.Networks[chainKey]
|
||||||
|
if !ok {
|
||||||
|
c.deps.Logger.Warn("unsupported chain in config", zap.String("chain", chainKey))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenSymbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
|
||||||
|
if tokenSymbol == "" {
|
||||||
|
c.deps.Logger.Warn("missing token symbol")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset.token_symbol is required"))
|
||||||
|
}
|
||||||
|
contractAddress := strings.ToLower(strings.TrimSpace(asset.GetContractAddress()))
|
||||||
|
if contractAddress == "" {
|
||||||
|
contractAddress = shared.ResolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
|
||||||
|
if contractAddress == "" {
|
||||||
|
c.deps.Logger.Warn("unsupported token", zap.String("token", tokenSymbol), zap.String("chain", chainKey))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walletRef := shared.GenerateWalletRef()
|
||||||
|
if c.deps.KeyManager == nil {
|
||||||
|
c.deps.Logger.Warn("key manager missing")
|
||||||
|
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("key manager not configured"))
|
||||||
|
}
|
||||||
|
|
||||||
|
keyInfo, err := c.deps.KeyManager.CreateManagedWalletKey(ctx, walletRef, chainKey)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("key manager error", zap.Error(err))
|
||||||
|
return gsresponse.Auto[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if keyInfo == nil || strings.TrimSpace(keyInfo.Address) == "" {
|
||||||
|
c.deps.Logger.Warn("key manager returned empty address")
|
||||||
|
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address"))
|
||||||
|
}
|
||||||
|
|
||||||
|
wallet := &model.ManagedWallet{
|
||||||
|
IdempotencyKey: idempotencyKey,
|
||||||
|
WalletRef: walletRef,
|
||||||
|
OrganizationRef: organizationRef,
|
||||||
|
OwnerRef: ownerRef,
|
||||||
|
Network: chainKey,
|
||||||
|
TokenSymbol: tokenSymbol,
|
||||||
|
ContractAddress: contractAddress,
|
||||||
|
DepositAddress: strings.ToLower(keyInfo.Address),
|
||||||
|
KeyReference: keyInfo.KeyID,
|
||||||
|
Status: model.ManagedWalletStatusActive,
|
||||||
|
Metadata: shared.CloneMetadata(req.GetMetadata()),
|
||||||
|
}
|
||||||
|
|
||||||
|
created, err := c.deps.Storage.Wallets().Create(ctx, wallet)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrDataConflict) {
|
||||||
|
c.deps.Logger.Debug("wallet already exists", zap.String("wallet_ref", walletRef), zap.String("idempotency_key", idempotencyKey))
|
||||||
|
return gsresponse.Success(&chainv1.CreateManagedWalletResponse{Wallet: toProtoManagedWallet(created)})
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage create failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.Auto[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gsresponse.Success(&chainv1.CreateManagedWalletResponse{Wallet: toProtoManagedWallet(created)})
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Deps struct {
|
||||||
|
Logger mlogger.Logger
|
||||||
|
Networks map[string]shared.Network
|
||||||
|
KeyManager keymanager.Manager
|
||||||
|
Storage storage.Repository
|
||||||
|
EnsureRepository func(context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Deps) WithLogger(name string) Deps {
|
||||||
|
if d.Logger != nil {
|
||||||
|
d.Logger = d.Logger.Named(name)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getManagedWalletCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGetManagedWallet(deps Deps) *getManagedWalletCommand {
|
||||||
|
return &getManagedWalletCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *getManagedWalletCommand) Execute(ctx context.Context, req *chainv1.GetManagedWalletRequest) gsresponse.Responder[chainv1.GetManagedWalletResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
c.deps.Logger.Warn("nil request")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("nil request"))
|
||||||
|
}
|
||||||
|
walletRef := strings.TrimSpace(req.GetWalletRef())
|
||||||
|
if walletRef == "" {
|
||||||
|
c.deps.Logger.Warn("wallet_ref missing")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
||||||
|
}
|
||||||
|
wallet, err := c.deps.Storage.Wallets().Get(ctx, walletRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("not found", zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.NotFound[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.Auto[chainv1.GetManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
return gsresponse.Success(&chainv1.GetManagedWalletResponse{Wallet: toProtoManagedWallet(wallet)})
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listManagedWalletsCommand struct {
|
||||||
|
deps Deps
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListManagedWallets(deps Deps) *listManagedWalletsCommand {
|
||||||
|
return &listManagedWalletsCommand{deps: deps}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *listManagedWalletsCommand) Execute(ctx context.Context, req *chainv1.ListManagedWalletsRequest) gsresponse.Responder[chainv1.ListManagedWalletsResponse] {
|
||||||
|
if err := c.deps.EnsureRepository(ctx); err != nil {
|
||||||
|
c.deps.Logger.Warn("repository unavailable", zap.Error(err))
|
||||||
|
return gsresponse.Unavailable[chainv1.ListManagedWalletsResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
filter := model.ManagedWalletFilter{}
|
||||||
|
if req != nil {
|
||||||
|
filter.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
||||||
|
filter.OwnerRef = strings.TrimSpace(req.GetOwnerRef())
|
||||||
|
if asset := req.GetAsset(); asset != nil {
|
||||||
|
filter.Network, _ = shared.ChainKeyFromEnum(asset.GetChain())
|
||||||
|
filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol())
|
||||||
|
}
|
||||||
|
if page := req.GetPage(); page != nil {
|
||||||
|
filter.Cursor = strings.TrimSpace(page.GetCursor())
|
||||||
|
filter.Limit = page.GetLimit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := c.deps.Storage.Wallets().List(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("storage list failed", zap.Error(err))
|
||||||
|
return gsresponse.Auto[chainv1.ListManagedWalletsResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
protoWallets := make([]*chainv1.ManagedWallet, 0, len(result.Items))
|
||||||
|
for _, wallet := range result.Items {
|
||||||
|
protoWallets = append(protoWallets, toProtoManagedWallet(wallet))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &chainv1.ListManagedWalletsResponse{
|
||||||
|
Wallets: protoWallets,
|
||||||
|
Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor},
|
||||||
|
}
|
||||||
|
return gsresponse.Success(resp)
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum"
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func onChainWalletBalance(ctx context.Context, deps Deps, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||||
|
network := deps.Networks[strings.ToLower(strings.TrimSpace(wallet.Network))]
|
||||||
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
|
if rpcURL == "" {
|
||||||
|
return nil, merrors.Internal("network rpc url is not configured")
|
||||||
|
}
|
||||||
|
contract := strings.TrimSpace(wallet.ContractAddress)
|
||||||
|
if contract == "" || !common.IsHexAddress(contract) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid contract address")
|
||||||
|
}
|
||||||
|
if wallet.DepositAddress == "" || !common.IsHexAddress(wallet.DepositAddress) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid wallet address")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := ethclient.DialContext(ctx, rpcURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to connect rpc: " + err.Error())
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tokenABI, err := abi.JSON(strings.NewReader(erc20ABIJSON))
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to parse erc20 abi: " + err.Error())
|
||||||
|
}
|
||||||
|
tokenAddr := common.HexToAddress(contract)
|
||||||
|
walletAddr := common.HexToAddress(wallet.DepositAddress)
|
||||||
|
|
||||||
|
decimals, err := readDecimals(timeoutCtx, client, tokenABI, tokenAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bal, err := readBalanceOf(timeoutCtx, client, tokenABI, tokenAddr, walletAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := decimal.NewFromBigInt(bal, 0).Shift(-int32(decimals))
|
||||||
|
return &moneyv1.Money{Currency: wallet.TokenSymbol, Amount: dec.String()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDecimals(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address) (uint8, error) {
|
||||||
|
data, err := tokenABI.Pack("decimals")
|
||||||
|
if err != nil {
|
||||||
|
return 0, merrors.Internal("failed to encode decimals call: " + err.Error())
|
||||||
|
}
|
||||||
|
msg := ethereum.CallMsg{To: &token, Data: data}
|
||||||
|
out, err := client.CallContract(ctx, msg, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, merrors.Internal("decimals call failed: " + err.Error())
|
||||||
|
}
|
||||||
|
values, err := tokenABI.Unpack("decimals", out)
|
||||||
|
if err != nil || len(values) == 0 {
|
||||||
|
return 0, merrors.Internal("failed to unpack decimals")
|
||||||
|
}
|
||||||
|
if val, ok := values[0].(uint8); ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, merrors.Internal("decimals returned unexpected type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBalanceOf(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address, wallet common.Address) (*big.Int, error) {
|
||||||
|
data, err := tokenABI.Pack("balanceOf", wallet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to encode balanceOf: " + err.Error())
|
||||||
|
}
|
||||||
|
msg := ethereum.CallMsg{To: &token, Data: data}
|
||||||
|
out, err := client.CallContract(ctx, msg, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("balanceOf call failed: " + err.Error())
|
||||||
|
}
|
||||||
|
values, err := tokenABI.Unpack("balanceOf", out)
|
||||||
|
if err != nil || len(values) == 0 {
|
||||||
|
return nil, merrors.Internal("failed to unpack balanceOf")
|
||||||
|
}
|
||||||
|
raw, ok := values[0].(*big.Int)
|
||||||
|
if !ok {
|
||||||
|
return nil, merrors.Internal("balanceOf returned unexpected type")
|
||||||
|
}
|
||||||
|
return decimal.NewFromBigInt(raw, 0).BigInt(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const erc20ABIJSON = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "decimals",
|
||||||
|
"outputs": [{ "name": "", "type": "uint8" }],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [{ "name": "_owner", "type": "address" }],
|
||||||
|
"name": "balanceOf",
|
||||||
|
"outputs": [{ "name": "balance", "type": "uint256" }],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
]`
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toProtoManagedWallet(wallet *model.ManagedWallet) *chainv1.ManagedWallet {
|
||||||
|
if wallet == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
asset := &chainv1.Asset{
|
||||||
|
Chain: shared.ChainEnumFromName(wallet.Network),
|
||||||
|
TokenSymbol: wallet.TokenSymbol,
|
||||||
|
ContractAddress: wallet.ContractAddress,
|
||||||
|
}
|
||||||
|
return &chainv1.ManagedWallet{
|
||||||
|
WalletRef: wallet.WalletRef,
|
||||||
|
OrganizationRef: wallet.OrganizationRef,
|
||||||
|
OwnerRef: wallet.OwnerRef,
|
||||||
|
Asset: asset,
|
||||||
|
DepositAddress: wallet.DepositAddress,
|
||||||
|
Status: shared.ManagedWalletStatusToProto(wallet.Status),
|
||||||
|
Metadata: shared.CloneMetadata(wallet.Metadata),
|
||||||
|
CreatedAt: timestamppb.New(wallet.CreatedAt.UTC()),
|
||||||
|
UpdatedAt: timestamppb.New(wallet.UpdatedAt.UTC()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toProtoWalletBalance(balance *model.WalletBalance) *chainv1.WalletBalance {
|
||||||
|
if balance == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &chainv1.WalletBalance{
|
||||||
|
Available: shared.CloneMoney(balance.Available),
|
||||||
|
PendingInbound: shared.CloneMoney(balance.PendingInbound),
|
||||||
|
PendingOutbound: shared.CloneMoney(balance.PendingOutbound),
|
||||||
|
CalculatedAt: timestamppb.New(balance.CalculatedAt.UTC()),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,18 +14,19 @@ import (
|
|||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/ethclient"
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TransferExecutor handles on-chain submission of transfers.
|
// TransferExecutor handles on-chain submission of transfers.
|
||||||
type TransferExecutor interface {
|
type TransferExecutor interface {
|
||||||
SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network Network) (string, error)
|
SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network shared.Network) (string, error)
|
||||||
AwaitConfirmation(ctx context.Context, network Network, txHash string) (*types.Receipt, error)
|
AwaitConfirmation(ctx context.Context, network shared.Network, txHash string) (*types.Receipt, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOnChainExecutor constructs a TransferExecutor that talks to an EVM-compatible chain.
|
// NewOnChainExecutor constructs a TransferExecutor that talks to an EVM-compatible chain.
|
||||||
@@ -45,7 +46,7 @@ type onChainExecutor struct {
|
|||||||
clients map[string]*ethclient.Client
|
clients map[string]*ethclient.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network Network) (string, error) {
|
func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network shared.Network) (string, error) {
|
||||||
if o.keyManager == nil {
|
if o.keyManager == nil {
|
||||||
o.logger.Error("key manager not configured")
|
o.logger.Error("key manager not configured")
|
||||||
return "", executorInternal("key manager is not configured", nil)
|
return "", executorInternal("key manager is not configured", nil)
|
||||||
@@ -237,7 +238,7 @@ func (o *onChainExecutor) getClient(ctx context.Context, rpcURL string) (*ethcli
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network Network, txHash string) (*types.Receipt, error) {
|
func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network shared.Network, txHash string) (*types.Receipt, error) {
|
||||||
if strings.TrimSpace(txHash) == "" {
|
if strings.TrimSpace(txHash) == "" {
|
||||||
o.logger.Warn("missing transaction hash for confirmation", zap.String("network", network.Name))
|
o.logger.Warn("missing transaction hash for confirmation", zap.String("network", network.Name))
|
||||||
return nil, executorInvalid("tx hash is required")
|
return nil, executorInvalid("tx hash is required")
|
||||||
@@ -3,35 +3,14 @@ package gateway
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option configures the Service.
|
// Option configures the Service.
|
||||||
type Option func(*Service)
|
type Option func(*Service)
|
||||||
|
|
||||||
// Network describes a supported blockchain network and known token contracts.
|
|
||||||
type Network struct {
|
|
||||||
Name string
|
|
||||||
RPCURL string
|
|
||||||
ChainID uint64
|
|
||||||
NativeToken string
|
|
||||||
TokenConfigs []TokenContract
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenContract captures the metadata needed to work with a specific on-chain token.
|
|
||||||
type TokenContract struct {
|
|
||||||
Symbol string
|
|
||||||
ContractAddress string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceWallet captures the managed service wallet configuration.
|
|
||||||
type ServiceWallet struct {
|
|
||||||
Network string
|
|
||||||
Address string
|
|
||||||
PrivateKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithKeyManager configures the service key manager.
|
// WithKeyManager configures the service key manager.
|
||||||
func WithKeyManager(manager keymanager.Manager) Option {
|
func WithKeyManager(manager keymanager.Manager) Option {
|
||||||
return func(s *Service) {
|
return func(s *Service) {
|
||||||
@@ -47,13 +26,13 @@ func WithTransferExecutor(executor TransferExecutor) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithNetworks configures supported blockchain networks.
|
// WithNetworks configures supported blockchain networks.
|
||||||
func WithNetworks(networks []Network) Option {
|
func WithNetworks(networks []shared.Network) Option {
|
||||||
return func(s *Service) {
|
return func(s *Service) {
|
||||||
if len(networks) == 0 {
|
if len(networks) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.networks == nil {
|
if s.networks == nil {
|
||||||
s.networks = make(map[string]Network, len(networks))
|
s.networks = make(map[string]shared.Network, len(networks))
|
||||||
}
|
}
|
||||||
for _, network := range networks {
|
for _, network := range networks {
|
||||||
if network.Name == "" {
|
if network.Name == "" {
|
||||||
@@ -61,7 +40,7 @@ func WithNetworks(networks []Network) Option {
|
|||||||
}
|
}
|
||||||
clone := network
|
clone := network
|
||||||
if clone.TokenConfigs == nil {
|
if clone.TokenConfigs == nil {
|
||||||
clone.TokenConfigs = []TokenContract{}
|
clone.TokenConfigs = []shared.TokenContract{}
|
||||||
}
|
}
|
||||||
for i := range clone.TokenConfigs {
|
for i := range clone.TokenConfigs {
|
||||||
clone.TokenConfigs[i].Symbol = strings.ToUpper(strings.TrimSpace(clone.TokenConfigs[i].Symbol))
|
clone.TokenConfigs[i].Symbol = strings.ToUpper(strings.TrimSpace(clone.TokenConfigs[i].Symbol))
|
||||||
@@ -74,7 +53,7 @@ func WithNetworks(networks []Network) Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithServiceWallet configures the service wallet binding.
|
// WithServiceWallet configures the service wallet binding.
|
||||||
func WithServiceWallet(wallet ServiceWallet) Option {
|
func WithServiceWallet(wallet shared.ServiceWallet) Option {
|
||||||
return func(s *Service) {
|
return func(s *Service) {
|
||||||
s.serviceWallet = wallet
|
s.serviceWallet = wallet
|
||||||
}
|
}
|
||||||
153
api/gateway/chain/internal/service/gateway/service.go
Normal file
153
api/gateway/chain/internal/service/gateway/service.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/transfer"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/wallet"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers"
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serviceError string
|
||||||
|
|
||||||
|
func (e serviceError) Error() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errStorageUnavailable = serviceError("chain_gateway: storage not initialised")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service implements the ChainGatewayService RPC contract.
|
||||||
|
type Service struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
storage storage.Repository
|
||||||
|
producer msg.Producer
|
||||||
|
clock clockpkg.Clock
|
||||||
|
|
||||||
|
networks map[string]shared.Network
|
||||||
|
serviceWallet shared.ServiceWallet
|
||||||
|
keyManager keymanager.Manager
|
||||||
|
executor TransferExecutor
|
||||||
|
commands commands.Registry
|
||||||
|
|
||||||
|
chainv1.UnimplementedChainGatewayServiceServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService constructs the chain gateway service skeleton.
|
||||||
|
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
|
||||||
|
svc := &Service{
|
||||||
|
logger: logger.Named("service"),
|
||||||
|
storage: repo,
|
||||||
|
producer: producer,
|
||||||
|
clock: clockpkg.System{},
|
||||||
|
networks: map[string]shared.Network{},
|
||||||
|
}
|
||||||
|
|
||||||
|
initMetrics()
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
if opt != nil {
|
||||||
|
opt(svc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if svc.clock == nil {
|
||||||
|
svc.clock = clockpkg.System{}
|
||||||
|
}
|
||||||
|
if svc.networks == nil {
|
||||||
|
svc.networks = map[string]shared.Network{}
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.commands = commands.NewRegistry(commands.RegistryDeps{
|
||||||
|
Wallet: commandsWalletDeps(svc),
|
||||||
|
Transfer: commandsTransferDeps(svc),
|
||||||
|
})
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register wires the service onto the provided gRPC router.
|
||||||
|
func (s *Service) Register(router routers.GRPC) error {
|
||||||
|
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||||
|
chainv1.RegisterChainGatewayServiceServer(reg, s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "CreateManagedWallet", s.commands.CreateManagedWallet.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetManagedWallet(ctx context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "GetManagedWallet", s.commands.GetManagedWallet.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "ListManagedWallets", s.commands.ListManagedWallets.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "GetWalletBalance", s.commands.GetWalletBalance.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "SubmitTransfer", s.commands.SubmitTransfer.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "GetTransfer", s.commands.GetTransfer.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListTransfers(ctx context.Context, req *chainv1.ListTransfersRequest) (*chainv1.ListTransfersResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "ListTransfers", s.commands.ListTransfers.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) EstimateTransferFee(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||||
|
return executeUnary(ctx, s, "EstimateTransferFee", s.commands.EstimateTransfer.Execute, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ensureRepository(ctx context.Context) error {
|
||||||
|
if s.storage == nil {
|
||||||
|
return errStorageUnavailable
|
||||||
|
}
|
||||||
|
return s.storage.Ping(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandsWalletDeps(s *Service) wallet.Deps {
|
||||||
|
return wallet.Deps{
|
||||||
|
Logger: s.logger.Named("command"),
|
||||||
|
Networks: s.networks,
|
||||||
|
KeyManager: s.keyManager,
|
||||||
|
Storage: s.storage,
|
||||||
|
EnsureRepository: s.ensureRepository,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandsTransferDeps(s *Service) transfer.Deps {
|
||||||
|
return transfer.Deps{
|
||||||
|
Logger: s.logger.Named("transfer_cmd"),
|
||||||
|
Networks: s.networks,
|
||||||
|
Storage: s.storage,
|
||||||
|
Clock: s.clock,
|
||||||
|
EnsureRepository: s.ensureRepository,
|
||||||
|
LaunchExecution: s.launchTransferExecution,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeUnary[TReq any, TResp any](ctx context.Context, svc *Service, method string, handler func(context.Context, *TReq) gsresponse.Responder[TResp], req *TReq) (*TResp, error) {
|
||||||
|
start := svc.clock.Now()
|
||||||
|
resp, err := gsresponse.Unary(svc.logger, mservice.ChainGateway, handler)(ctx, req)
|
||||||
|
observeRPC(method, err, svc.clock.Now().Sub(start))
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
@@ -11,15 +11,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
igatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
ichainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
@@ -40,12 +41,12 @@ func TestCreateManagedWallet_Idempotent(t *testing.T) {
|
|||||||
svc, repo := newTestService(t)
|
svc, repo := newTestService(t)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
req := &igatewayv1.CreateManagedWalletRequest{
|
req := &ichainv1.CreateManagedWalletRequest{
|
||||||
IdempotencyKey: "idem-1",
|
IdempotencyKey: "idem-1",
|
||||||
OrganizationRef: "org-1",
|
OrganizationRef: "org-1",
|
||||||
OwnerRef: "owner-1",
|
OwnerRef: "owner-1",
|
||||||
Asset: &igatewayv1.Asset{
|
Asset: &ichainv1.Asset{
|
||||||
Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
||||||
TokenSymbol: "USDC",
|
TokenSymbol: "USDC",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -69,12 +70,12 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// create source wallet
|
// create source wallet
|
||||||
srcResp, err := svc.CreateManagedWallet(ctx, &igatewayv1.CreateManagedWalletRequest{
|
srcResp, err := svc.CreateManagedWallet(ctx, &ichainv1.CreateManagedWalletRequest{
|
||||||
IdempotencyKey: "idem-src",
|
IdempotencyKey: "idem-src",
|
||||||
OrganizationRef: "org-1",
|
OrganizationRef: "org-1",
|
||||||
OwnerRef: "owner-1",
|
OwnerRef: "owner-1",
|
||||||
Asset: &igatewayv1.Asset{
|
Asset: &ichainv1.Asset{
|
||||||
Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
||||||
TokenSymbol: "USDC",
|
TokenSymbol: "USDC",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -82,27 +83,27 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|||||||
srcRef := srcResp.GetWallet().GetWalletRef()
|
srcRef := srcResp.GetWallet().GetWalletRef()
|
||||||
|
|
||||||
// destination wallet
|
// destination wallet
|
||||||
dstResp, err := svc.CreateManagedWallet(ctx, &igatewayv1.CreateManagedWalletRequest{
|
dstResp, err := svc.CreateManagedWallet(ctx, &ichainv1.CreateManagedWalletRequest{
|
||||||
IdempotencyKey: "idem-dst",
|
IdempotencyKey: "idem-dst",
|
||||||
OrganizationRef: "org-1",
|
OrganizationRef: "org-1",
|
||||||
OwnerRef: "owner-2",
|
OwnerRef: "owner-2",
|
||||||
Asset: &igatewayv1.Asset{
|
Asset: &ichainv1.Asset{
|
||||||
Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
Chain: ichainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET,
|
||||||
TokenSymbol: "USDC",
|
TokenSymbol: "USDC",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
dstRef := dstResp.GetWallet().GetWalletRef()
|
dstRef := dstResp.GetWallet().GetWalletRef()
|
||||||
|
|
||||||
transferResp, err := svc.SubmitTransfer(ctx, &igatewayv1.SubmitTransferRequest{
|
transferResp, err := svc.SubmitTransfer(ctx, &ichainv1.SubmitTransferRequest{
|
||||||
IdempotencyKey: "transfer-1",
|
IdempotencyKey: "transfer-1",
|
||||||
OrganizationRef: "org-1",
|
OrganizationRef: "org-1",
|
||||||
SourceWalletRef: srcRef,
|
SourceWalletRef: srcRef,
|
||||||
Destination: &igatewayv1.TransferDestination{
|
Destination: &ichainv1.TransferDestination{
|
||||||
Destination: &igatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef},
|
Destination: &ichainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef},
|
||||||
},
|
},
|
||||||
Amount: &moneyv1.Money{Currency: "USDC", Amount: "100"},
|
Amount: &moneyv1.Money{Currency: "USDC", Amount: "100"},
|
||||||
Fees: []*igatewayv1.ServiceFeeBreakdown{
|
Fees: []*ichainv1.ServiceFeeBreakdown{
|
||||||
{
|
{
|
||||||
FeeCode: "service",
|
FeeCode: "service",
|
||||||
Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"},
|
Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"},
|
||||||
@@ -118,12 +119,12 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|||||||
require.Equal(t, model.TransferStatusPending, stored.Status)
|
require.Equal(t, model.TransferStatusPending, stored.Status)
|
||||||
|
|
||||||
// GetTransfer
|
// GetTransfer
|
||||||
getResp, err := svc.GetTransfer(ctx, &igatewayv1.GetTransferRequest{TransferRef: stored.TransferRef})
|
getResp, err := svc.GetTransfer(ctx, &ichainv1.GetTransferRequest{TransferRef: stored.TransferRef})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, stored.TransferRef, getResp.GetTransfer().GetTransferRef())
|
require.Equal(t, stored.TransferRef, getResp.GetTransfer().GetTransferRef())
|
||||||
|
|
||||||
// ListTransfers
|
// ListTransfers
|
||||||
listResp, err := svc.ListTransfers(ctx, &igatewayv1.ListTransfersRequest{
|
listResp, err := svc.ListTransfers(ctx, &ichainv1.ListTransfersRequest{
|
||||||
SourceWalletRef: srcRef,
|
SourceWalletRef: srcRef,
|
||||||
Page: &paginationv1.CursorPageRequest{Limit: 10},
|
Page: &paginationv1.CursorPageRequest{Limit: 10},
|
||||||
})
|
})
|
||||||
@@ -136,7 +137,7 @@ func TestGetWalletBalance_NotFound(t *testing.T) {
|
|||||||
svc, _ := newTestService(t)
|
svc, _ := newTestService(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
_, err := svc.GetWalletBalance(ctx, &igatewayv1.GetWalletBalanceRequest{WalletRef: "missing"})
|
_, err := svc.GetWalletBalance(ctx, &ichainv1.GetWalletBalanceRequest{WalletRef: "missing"})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
st, _ := status.FromError(err)
|
st, _ := status.FromError(err)
|
||||||
require.Equal(t, codes.NotFound, st.Code())
|
require.Equal(t, codes.NotFound, st.Code())
|
||||||
@@ -530,13 +531,13 @@ func newTestService(_ *testing.T) (*Service, *inMemoryRepository) {
|
|||||||
logger := zap.NewNop()
|
logger := zap.NewNop()
|
||||||
svc := NewService(logger, repo, nil,
|
svc := NewService(logger, repo, nil,
|
||||||
WithKeyManager(&fakeKeyManager{}),
|
WithKeyManager(&fakeKeyManager{}),
|
||||||
WithNetworks([]Network{{
|
WithNetworks([]shared.Network{{
|
||||||
Name: "ethereum_mainnet",
|
Name: "ethereum_mainnet",
|
||||||
TokenConfigs: []TokenContract{
|
TokenConfigs: []shared.TokenContract{
|
||||||
{Symbol: "USDC", ContractAddress: "0xusdc"},
|
{Symbol: "USDC", ContractAddress: "0xusdc"},
|
||||||
},
|
},
|
||||||
}}),
|
}}),
|
||||||
WithServiceWallet(ServiceWallet{Network: "ethereum_mainnet", Address: "0xservice"}),
|
WithServiceWallet(shared.ServiceWallet{Network: "ethereum_mainnet", Address: "0xservice"}),
|
||||||
)
|
)
|
||||||
return svc, repo
|
return svc, repo
|
||||||
}
|
}
|
||||||
142
api/gateway/chain/internal/service/gateway/shared/helpers.go
Normal file
142
api/gateway/chain/internal/service/gateway/shared/helpers.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CloneMoney defensively copies a Money proto.
|
||||||
|
func CloneMoney(m *moneyv1.Money) *moneyv1.Money {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &moneyv1.Money{Amount: m.GetAmount(), Currency: m.GetCurrency()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloneMetadata defensively copies metadata maps.
|
||||||
|
func CloneMetadata(input map[string]string) map[string]string {
|
||||||
|
if len(input) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
clone := make(map[string]string, len(input))
|
||||||
|
for k, v := range input {
|
||||||
|
clone[k] = v
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveContractAddress finds a token contract for a symbol in a case-insensitive manner.
|
||||||
|
func ResolveContractAddress(tokens []TokenContract, symbol string) string {
|
||||||
|
upper := strings.ToUpper(symbol)
|
||||||
|
for _, token := range tokens {
|
||||||
|
if strings.EqualFold(token.Symbol, upper) && token.ContractAddress != "" {
|
||||||
|
return strings.ToLower(token.ContractAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateWalletRef() string {
|
||||||
|
return primitive.NewObjectID().Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateTransferRef() string {
|
||||||
|
return primitive.NewObjectID().Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChainKeyFromEnum(chain chainv1.ChainNetwork) (string, chainv1.ChainNetwork) {
|
||||||
|
if name, ok := chainv1.ChainNetwork_name[int32(chain)]; ok {
|
||||||
|
key := strings.ToLower(strings.TrimPrefix(name, "CHAIN_NETWORK_"))
|
||||||
|
return key, chain
|
||||||
|
}
|
||||||
|
return "", chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChainEnumFromName(name string) chainv1.ChainNetwork {
|
||||||
|
if name == "" {
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
||||||
|
}
|
||||||
|
upper := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(name, " ", "_"), "-", "_"))
|
||||||
|
key := "CHAIN_NETWORK_" + upper
|
||||||
|
if val, ok := chainv1.ChainNetwork_value[key]; ok {
|
||||||
|
return chainv1.ChainNetwork(val)
|
||||||
|
}
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
func ManagedWalletStatusToProto(status model.ManagedWalletStatus) chainv1.ManagedWalletStatus {
|
||||||
|
switch status {
|
||||||
|
case model.ManagedWalletStatusActive:
|
||||||
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE
|
||||||
|
case model.ManagedWalletStatusSuspended:
|
||||||
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED
|
||||||
|
case model.ManagedWalletStatusClosed:
|
||||||
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED
|
||||||
|
default:
|
||||||
|
return chainv1.ManagedWalletStatus_MANAGED_WALLET_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TransferStatusToModel(status chainv1.TransferStatus) model.TransferStatus {
|
||||||
|
switch status {
|
||||||
|
case chainv1.TransferStatus_TRANSFER_PENDING:
|
||||||
|
return model.TransferStatusPending
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SIGNING:
|
||||||
|
return model.TransferStatusSigning
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
||||||
|
return model.TransferStatusSubmitted
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
||||||
|
return model.TransferStatusConfirmed
|
||||||
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
|
return model.TransferStatusFailed
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
|
return model.TransferStatusCancelled
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TransferStatusToProto(status model.TransferStatus) chainv1.TransferStatus {
|
||||||
|
switch status {
|
||||||
|
case model.TransferStatusPending:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_PENDING
|
||||||
|
case model.TransferStatusSigning:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_SIGNING
|
||||||
|
case model.TransferStatusSubmitted:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_SUBMITTED
|
||||||
|
case model.TransferStatusConfirmed:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
||||||
|
case model.TransferStatusFailed:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_FAILED
|
||||||
|
case model.TransferStatusCancelled:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||||
|
default:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network describes a supported blockchain network and known token contracts.
|
||||||
|
type Network struct {
|
||||||
|
Name string
|
||||||
|
RPCURL string
|
||||||
|
ChainID uint64
|
||||||
|
NativeToken string
|
||||||
|
TokenConfigs []TokenContract
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenContract captures the metadata needed to work with a specific on-chain token.
|
||||||
|
type TokenContract struct {
|
||||||
|
Symbol string
|
||||||
|
ContractAddress string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceWallet captures the managed service wallet configuration.
|
||||||
|
type ServiceWallet struct {
|
||||||
|
Network string
|
||||||
|
Address string
|
||||||
|
PrivateKey string
|
||||||
|
}
|
||||||
@@ -7,17 +7,19 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, network Network) {
|
func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, network shared.Network) {
|
||||||
if s.executor == nil {
|
if s.executor == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go func(ref, walletRef string, net Network) {
|
go func(ref, walletRef string, net shared.Network) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, n
|
|||||||
}(transferRef, sourceWalletRef, network)
|
}(transferRef, sourceWalletRef, network)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWalletRef string, network Network) error {
|
func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWalletRef string, network shared.Network) error {
|
||||||
transfer, err := s.storage.Transfers().Get(ctx, transferRef)
|
transfer, err := s.storage.Transfers().Get(ctx, transferRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/tech/sendico/chain/gateway/internal/appversion"
|
"github.com/tech/sendico/gateway/chain/internal/appversion"
|
||||||
si "github.com/tech/sendico/chain/gateway/internal/server"
|
si "github.com/tech/sendico/gateway/chain/internal/server"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/server"
|
"github.com/tech/sendico/pkg/server"
|
||||||
smain "github.com/tech/sendico/pkg/server/main"
|
smain "github.com/tech/sendico/pkg/server/main"
|
||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/mongo/store"
|
"github.com/tech/sendico/gateway/chain/storage/mongo/store"
|
||||||
"github.com/tech/sendico/pkg/db"
|
"github.com/tech/sendico/pkg/db"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/db/repository"
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/db/repository"
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/db/repository"
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
@@ -3,7 +3,7 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/tech/sendico/chain/gateway/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type storageError string
|
type storageError string
|
||||||
@@ -16,6 +16,8 @@ import (
|
|||||||
|
|
||||||
// Client exposes typed helpers around the ledger gRPC API.
|
// Client exposes typed helpers around the ledger gRPC API.
|
||||||
type Client interface {
|
type Client interface {
|
||||||
|
CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
|
||||||
|
ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
|
||||||
PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
|
PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
|
||||||
PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
|
PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
|
||||||
TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
|
TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
|
||||||
@@ -29,6 +31,8 @@ type Client interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type grpcLedgerClient interface {
|
type grpcLedgerClient interface {
|
||||||
|
CreateAccount(ctx context.Context, in *ledgerv1.CreateAccountRequest, opts ...grpc.CallOption) (*ledgerv1.CreateAccountResponse, error)
|
||||||
|
ListAccounts(ctx context.Context, in *ledgerv1.ListAccountsRequest, opts ...grpc.CallOption) (*ledgerv1.ListAccountsResponse, error)
|
||||||
PostCreditWithCharges(ctx context.Context, in *ledgerv1.PostCreditRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
PostCreditWithCharges(ctx context.Context, in *ledgerv1.PostCreditRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
||||||
PostDebitWithCharges(ctx context.Context, in *ledgerv1.PostDebitRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
PostDebitWithCharges(ctx context.Context, in *ledgerv1.PostDebitRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
||||||
TransferInternal(ctx context.Context, in *ledgerv1.TransferRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
TransferInternal(ctx context.Context, in *ledgerv1.TransferRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
||||||
@@ -91,6 +95,18 @@ func (c *ledgerClient) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
||||||
|
ctx, cancel := c.callContext(ctx)
|
||||||
|
defer cancel()
|
||||||
|
return c.client.CreateAccount(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ledgerClient) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
|
||||||
|
ctx, cancel := c.callContext(ctx)
|
||||||
|
defer cancel()
|
||||||
|
return c.client.ListAccounts(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ledgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
func (c *ledgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||||
ctx, cancel := c.callContext(ctx)
|
ctx, cancel := c.callContext(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ require (
|
|||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.47.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.4 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
@@ -51,5 +51,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-20251111163417-95abcf5c77ba // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -143,8 +143,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
|
|||||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
@@ -214,8 +214,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/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=
|
||||||
|
|||||||
47
api/ledger/internal/service/ledger/list_accounts.go
Normal file
47
api/ledger/internal/service/ledger/list_accounts.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package ledger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) listAccountsResponder(_ context.Context, req *ledgerv1.ListAccountsRequest) gsresponse.Responder[ledgerv1.ListAccountsResponse] {
|
||||||
|
return func(ctx context.Context) (*ledgerv1.ListAccountsResponse, error) {
|
||||||
|
if s.storage == nil {
|
||||||
|
return nil, errStorageNotInitialized
|
||||||
|
}
|
||||||
|
if req == nil {
|
||||||
|
return nil, merrors.InvalidArgument("request is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
orgRefStr := strings.TrimSpace(req.GetOrganizationRef())
|
||||||
|
if orgRefStr == "" {
|
||||||
|
return nil, merrors.InvalidArgument("organization_ref is required")
|
||||||
|
}
|
||||||
|
orgRef, err := parseObjectID(orgRefStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// No pagination requested; return all accounts for the organization.
|
||||||
|
accounts, err := s.storage.Accounts().ListByOrganization(ctx, orgRef, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("failed to list ledger accounts", zap.Error(err), zap.String("organizationRef", orgRef.Hex()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &ledgerv1.ListAccountsResponse{
|
||||||
|
Accounts: make([]*ledgerv1.LedgerAccount, 0, len(accounts)),
|
||||||
|
}
|
||||||
|
for _, acc := range accounts {
|
||||||
|
resp.Accounts = append(resp.Accounts, toProtoAccount(acc))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,12 @@ func (s *Service) Register(router routers.GRPC) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAccounts lists ledger accounts for an organization.
|
||||||
|
func (s *Service) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
|
||||||
|
responder := s.listAccountsResponder(ctx, req)
|
||||||
|
return responder(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// CreateAccount provisions a new ledger account scoped to an organization.
|
// CreateAccount provisions a new ledger account scoped to an organization.
|
||||||
func (s *Service) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
func (s *Service) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
||||||
responder := s.createAccountResponder(ctx, req)
|
responder := s.createAccountResponder(ctx, req)
|
||||||
|
|||||||
1
api/notification/.gitignore
vendored
1
api/notification/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
notification
|
notification
|
||||||
|
.gocache
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ api:
|
|||||||
settings:
|
settings:
|
||||||
username_env: MAIL_USER
|
username_env: MAIL_USER
|
||||||
password_env: MAIL_SECRET
|
password_env: MAIL_SECRET
|
||||||
host: "smtp.mail.ru"
|
host: "mail.sendico.io"
|
||||||
port: 465
|
port: 465
|
||||||
from: "Sendico Tech"
|
from: "Sendico Tech"
|
||||||
network_timeout: 10
|
network_timeout: 10
|
||||||
@@ -65,7 +65,7 @@ api:
|
|||||||
|
|
||||||
localizer:
|
localizer:
|
||||||
path: "./i18n"
|
path: "./i18n"
|
||||||
languages: ["en", "ru", "uk"]
|
languages: ["en", "ru"]
|
||||||
service_name: "Sendico"
|
service_name: "Sendico"
|
||||||
support: "support@sendico.io"
|
support: "support@sendico.io"
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ require (
|
|||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.47.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
@@ -43,7 +43,7 @@ require (
|
|||||||
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
@@ -52,7 +52,7 @@ require (
|
|||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
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
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
|
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
|
||||||
@@ -154,8 +154,8 @@ github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5
|
|||||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
||||||
@@ -227,8 +227,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/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=
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -46,7 +46,7 @@ func (m *EmailNotificationTemplate) prepareUnsubscribe(msg mmail.Message) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
localization.AddLocData(d, "UnsubscribeLink", unsLink)
|
localization.AddLocData(d, "UnsubscribeLink", unsLink)
|
||||||
if block, err = m.l.LocalizeTemplate("mail.template.unsubscribe.block", d, nil, msg.Locale()); err != nil {
|
if block, err = renderUnsubscribeBlock(d); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ func (m *EmailNotificationTemplate) prepareButton(msg mmail.Message) error {
|
|||||||
var block string
|
var block string
|
||||||
if m.hasButton {
|
if m.hasButton {
|
||||||
var err error
|
var err error
|
||||||
if block, err = m.l.LocalizeTemplate("mail.template.btn.block", m.data, nil, msg.Locale()); err != nil {
|
if block, err = renderButtonBlock(m.data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ func (m *EmailNotificationTemplate) SignatureData(msg mmail.Message, content, su
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.l.LocalizeTemplate("mail.template.one_button", m.data, nil, msg.Locale())
|
return renderOneButtonEmail(m.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *EmailNotificationTemplate) putOnHTMLTemplate(msg mmail.Message, content, subj string) (string, error) {
|
func (m *EmailNotificationTemplate) putOnHTMLTemplate(msg mmail.Message, content, subj string) (string, error) {
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
package mailimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
unsubscribeBlockTpl = template.Must(template.New("unsubscribeBlock").Parse(`
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
style="-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;text-decoration:none;color:#2D3142;font-size:13px"
|
||||||
|
href=""
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
•
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
style="-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;text-decoration:none;color:#2D3142;font-size:14px"
|
||||||
|
href="{{.UnsubscribeLink}}"
|
||||||
|
>
|
||||||
|
{{.Unsubscribe}}
|
||||||
|
</a>`))
|
||||||
|
|
||||||
|
buttonBlockTpl = template.Must(template.New("buttonBlock").Parse(`
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding:0;Margin:0">
|
||||||
|
<!--[if mso]>
|
||||||
|
<a href="" target="_blank" hidden>
|
||||||
|
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" esdevVmlButton href="{{.ButtonLink}}"
|
||||||
|
style="height:56px; v-text-anchor:middle; width:520px" arcsize="14%" stroke="f" fillcolor="#0b58ff">
|
||||||
|
<w:anchorlock></w:anchorlock>
|
||||||
|
<center style='color:#ffffff; font-family:montserrat, roboto; font-size:22px; font-weight:700; line-height:22px; mso-text-raise:1px'>{{.ButtonText}}</center>
|
||||||
|
</v:roundrect>
|
||||||
|
</a>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if !mso]><!-- -->
|
||||||
|
<span class="msohide es-button-border" style="border-style:solid;border-color:#0b58ff;background:#0b58ff;border-width:0px;display:block;border-radius:8px;width:auto;mso-border-alt:10px;mso-hide:all;width:520px">
|
||||||
|
<a
|
||||||
|
href="{{.ButtonLink}}"
|
||||||
|
class="es-button msohide"
|
||||||
|
target="_blank"
|
||||||
|
style="mso-style-priority:100 !important;text-decoration:none;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;color:#ffffff;font-size:22px;padding:15px 20px 15px 20px;display:block;background:#0b58ff;border-radius:8px;font-family:montserrat, roboto;font-weight:bold;font-style:normal;line-height:26px;width:auto;text-align:center;border-color:#0b58ff;mso-hide:all;padding-left:5px;padding-right:5px"
|
||||||
|
>
|
||||||
|
{{.ButtonText}}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<!--<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>`))
|
||||||
|
|
||||||
|
oneButtonEmailTpl = template.Must(template.New("oneButtonEmail").Parse(`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" style="font-family:montserrat, roboto">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||||
|
<meta name="x-apple-disable-message-reformatting">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta content="telephone=no" name="format-detection">
|
||||||
|
<title>{{.MessageTitle}}</title>
|
||||||
|
<!--[if (mso 16)]>
|
||||||
|
<style type="text/css">
|
||||||
|
a {text-decoration: none;}
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if gte mso 9]><style>sup { font-size: 100% !important; }</style><![endif]-->
|
||||||
|
<!--[if gte mso 9]>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG></o:AllowPNG>
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if !mso]><!-- -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Imprima&display=swap" rel="stylesheet">
|
||||||
|
<!--<![endif]-->
|
||||||
|
<!--[if !mso]><!-- -->
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat:100,300,400,500,700,900">
|
||||||
|
<!--<![endif]-->
|
||||||
|
<style type="text/css">
|
||||||
|
#outlook a { padding:0; }
|
||||||
|
.es-button { mso-style-priority:100!important; text-decoration:none!important; }
|
||||||
|
a[x-apple-data-detectors] {
|
||||||
|
color:inherit!important;
|
||||||
|
text-decoration:none!important;
|
||||||
|
font-size:inherit!important;
|
||||||
|
font-family:inherit!important;
|
||||||
|
font-weight:inherit!important;
|
||||||
|
line-height:inherit!important;
|
||||||
|
}
|
||||||
|
.es-desk-hidden { display:none; float:left; overflow:hidden; width:0; max-height:0; line-height:0; mso-hide:all; }
|
||||||
|
@media only screen and (max-width:600px) {
|
||||||
|
p, ul li, ol li, a { line-height:150%!important }
|
||||||
|
h1, h2, h3, h1 a, h2 a, h3 a { line-height:120% }
|
||||||
|
h1 { font-size:30px!important; text-align:left }
|
||||||
|
h2 { font-size:24px!important; text-align:left }
|
||||||
|
h3 { font-size:20px!important; text-align:left }
|
||||||
|
.es-header-body h1 a, .es-content-body h1 a, .es-footer-body h1 a { font-size:30px!important; text-align:left }
|
||||||
|
.es-header-body h2 a, .es-content-body h2 a, .es-footer-body h2 a { font-size:24px!important; text-align:left }
|
||||||
|
.es-header-body h3 a, .es-content-body h3 a, .es-footer-body h3 a { font-size:20px!important; text-align:left }
|
||||||
|
.es-menu td a { font-size:14px!important }
|
||||||
|
.es-header-body p, .es-header-body ul li, .es-header-body ol li, .es-header-body a { font-size:14px!important }
|
||||||
|
.es-content-body p, .es-content-body ul li, .es-content-body ol li, .es-content-body a { font-size:14px!important }
|
||||||
|
.es-footer-body p, .es-footer-body ul li, .es-footer-body ol li, .es-footer-body a { font-size:14px!important }
|
||||||
|
.es-infoblock p, .es-infoblock ul li, .es-infoblock ol li, .es-infoblock a { font-size:12px!important }
|
||||||
|
*[class="gmail-fix"] { display:none!important }
|
||||||
|
.es-m-txt-c, .es-m-txt-c h1, .es-m-txt-c h2, .es-m-txt-c h3 { text-align:center!important }
|
||||||
|
.es-m-txt-r, .es-m-txt-r h1, .es-m-txt-r h2, .es-m-txt-r h3 { text-align:right!important }
|
||||||
|
.es-m-txt-l, .es-m-txt-l h1, .es-m-txt-l h2, .es-m-txt-l h3 { text-align:left!important }
|
||||||
|
.es-m-txt-r img, .es-m-txt-c img, .es-m-txt-l img { display:inline!important }
|
||||||
|
.es-button-border { display:block!important }
|
||||||
|
a.es-button, button.es-button { font-size:18px!important; display:block!important; border-right-width:0px!important; border-left-width:0px!important; border-top-width:15px!important; border-bottom-width:15px!important; padding-left:0px!important; padding-right:0px!important }
|
||||||
|
.es-adaptive table, .es-left, .es-right { width:100%!important }
|
||||||
|
.es-content table, .es-header table, .es-footer table, .es-content, .es-footer, .es-header { width:100%!important; max-width:600px!important }
|
||||||
|
.es-adapt-td { display:block!important; width:100%!important }
|
||||||
|
.adapt-img { width:100%!important; height:auto!important }
|
||||||
|
.es-m-p0 { padding:0px!important }
|
||||||
|
.es-m-p0r { padding-right:0px!important }
|
||||||
|
.es-m-p0l { padding-left:0px!important }
|
||||||
|
.es-m-p0t { padding-top:0px!important }
|
||||||
|
.es-m-p0b { padding-bottom:0!important }
|
||||||
|
.es-m-p20b { padding-bottom:20px!important }
|
||||||
|
.es-mobile-hidden, .es-hidden { display:none!important }
|
||||||
|
tr.es-desk-hidden, td.es-desk-hidden, table.es-desk-hidden { width:auto!important; overflow:visible!important; float:none!important; max-height:inherit!important; line-height:inherit!important }
|
||||||
|
tr.es-desk-hidden { display:table-row!important }
|
||||||
|
table.es-desk-hidden { display:table!important }
|
||||||
|
td.es-desk-menu-hidden { display:table-cell!important }
|
||||||
|
.es-menu td { width:1%!important }
|
||||||
|
table.es-table-not-adapt, .esd-block-html table { width:auto!important }
|
||||||
|
table.es-social { display:inline-block!important }
|
||||||
|
table.es-social td { display:inline-block!important }
|
||||||
|
.es-desk-hidden { display:table-row!important; width:auto!important; overflow:visible!important; max-height:inherit!important }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="width:100%;font-family:montserrat, roboto;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;padding:0;Margin:0">
|
||||||
|
<div class="es-wrapper-color" style="background-color:#FFFFFF">
|
||||||
|
<!--[if gte mso 9]>
|
||||||
|
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||||
|
<v:fill type="tile" color="#ffffff"></v:fill>
|
||||||
|
</v:background>
|
||||||
|
<![endif]-->
|
||||||
|
<table class="es-wrapper" width="100%" cellspacing="0" cellpadding="0" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;padding:0;Margin:0;width:100%;height:100%;background-repeat:repeat;background-position:center top;background-color:#FFFFFF">
|
||||||
|
<tr>
|
||||||
|
<td valign="top" style="padding:0;Margin:0">
|
||||||
|
<table cellpadding="0" cellspacing="0" class="es-content" align="center" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;table-layout:fixed !important;width:100%">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding:0;Margin:0">
|
||||||
|
<table bgcolor="#ffffff" class="es-content-body" align="center" cellpadding="0" cellspacing="0" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;background-color:#FFFFFF;border-radius:20px 20px 0 0;width:600px">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="padding:0;Margin:0;padding-top:20px;padding-left:40px;padding-right:40px;border-radius:8px 8px 0px 0px">
|
||||||
|
<table cellpadding="0" cellspacing="0" width="100%" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" style="padding:0;Margin:0;width:520px">
|
||||||
|
<table cellpadding="0" cellspacing="0" width="100%" bgcolor="#fafafa" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:separate;border-spacing:0px;background-color:#fafafa;border-radius:10px" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="padding:20px;Margin:0">
|
||||||
|
<h3 style="Margin:0;line-height:34px;mso-line-height-rule:exactly;font-family:montserrat, roboto;font-size:28px;font-style:normal;font-weight:bold;color:#2D3142">
|
||||||
|
{{.Greeting}}
|
||||||
|
</h3>
|
||||||
|
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:27px;color:#2D3142;font-size:18px"><br></p>
|
||||||
|
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:27px;color:#2D3142;font-size:18px">
|
||||||
|
{{.Content}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table cellpadding="0" cellspacing="0" class="es-content" align="center" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;table-layout:fixed !important;width:100%">
|
||||||
|
{{.ButtonBlock}}
|
||||||
|
</table>
|
||||||
|
<table cellpadding="0" cellspacing="0" class="es-footer" align="center" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;table-layout:fixed !important;width:100%;background-color:transparent;background-repeat:repeat;background-position:center top">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding:0;Margin:0">
|
||||||
|
<table bgcolor="#bcb8b1" class="es-footer-body" align="center" cellpadding="0" cellspacing="0" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;background-color:#FFFFFF;width:600px">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="Margin:0;padding-left:20px;padding-right:20px;padding-bottom:30px;padding-top:40px">
|
||||||
|
<!--[if mso]><table style="width:560px" cellpadding="0" cellspacing="0"><tr><td style="width:82px" valign="top"><![endif]-->
|
||||||
|
<table cellpadding="0" cellspacing="0" align="left" class="es-left" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;float:left">
|
||||||
|
<tr>
|
||||||
|
<td align="left" class="es-m-p20b" style="padding:0;Margin:0;width:82px">
|
||||||
|
<table cellpadding="0" cellspacing="0" width="100%" role="presentation" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px">
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
style="padding:0;Margin:0;padding-left:20px;font-size:0px">
|
||||||
|
<img class="adapt-img"
|
||||||
|
src="{{.LogoLink}}"
|
||||||
|
alt
|
||||||
|
style="display:block;border:0;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic" width="62"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!--[if mso]></td><td style="width:20px"></td><td style="width:458px" valign="top"><![endif]-->
|
||||||
|
<table cellpadding="0" cellspacing="0" class="es-right" align="right" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px;float:right">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="padding:0;Margin:0;width:458px">
|
||||||
|
<table cellpadding="0" cellspacing="0" width="100%" role="presentation" style="mso-table-lspace:0pt;mso-table-rspace:0pt;border-collapse:collapse;border-spacing:0px">
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="padding:0;Margin:0">
|
||||||
|
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:20px;color:#2D3142;font-size:13px">
|
||||||
|
<a target="_blank" style="-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;text-decoration:none;color:#2D3142;font-size:14px" href=""></a>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
style="-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;text-decoration:none;color:#2D3142;font-size:14px"
|
||||||
|
href="{{.PolicyLink}}"
|
||||||
|
>
|
||||||
|
{{.Privacy}}
|
||||||
|
</a>
|
||||||
|
{{.UnsubscribeBlock}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="padding:0;Margin:0">
|
||||||
|
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:20px;color:#2D3142;font-size:13px">
|
||||||
|
{{.ServiceOwner}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="padding:0;Margin:0">
|
||||||
|
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:20px;color:#2D3142;font-size:13px">
|
||||||
|
{{.OwnerAddress}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="padding:0;Margin:0">
|
||||||
|
<p style="Margin:0;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;mso-line-height-rule:exactly;font-family:montserrat, roboto;line-height:20px;color:#2D3142;font-size:13px">
|
||||||
|
{{.OwnerPhone}}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!--[if mso]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`))
|
||||||
|
)
|
||||||
|
|
||||||
|
func renderTemplate(tpl *template.Template, data any) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tpl.Execute(&buf, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderUnsubscribeBlock(data any) (string, error) {
|
||||||
|
return renderTemplate(unsubscribeBlockTpl, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderButtonBlock(data any) (string, error) {
|
||||||
|
return renderTemplate(buttonBlockTpl, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderOneButtonEmail(data any) (string, error) {
|
||||||
|
return renderTemplate(oneButtonEmailTpl, data)
|
||||||
|
}
|
||||||
@@ -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,20 +36,20 @@ 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
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nats.go v1.47.0 // indirect
|
github.com/nats-io/nats.go v1.47.0 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.4 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.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-20251111163417-95abcf5c77ba // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -97,8 +97,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
|
||||||
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
@@ -144,8 +144,8 @@ github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9R
|
|||||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
@@ -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-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
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-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
chainclient "github.com/tech/sendico/chain/gateway/client"
|
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||||
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
||||||
ledgerclient "github.com/tech/sendico/ledger/client"
|
ledgerclient "github.com/tech/sendico/ledger/client"
|
||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
@@ -327,11 +327,11 @@ func protoFailureFromModel(code model.PaymentFailureCode) orchestratorv1.Payment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneAsset(asset *gatewayv1.Asset) *gatewayv1.Asset {
|
func cloneAsset(asset *chainv1.Asset) *chainv1.Asset {
|
||||||
if asset == nil {
|
if asset == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &gatewayv1.Asset{
|
return &chainv1.Asset{
|
||||||
Chain: asset.GetChain(),
|
Chain: asset.GetChain(),
|
||||||
TokenSymbol: asset.GetTokenSymbol(),
|
TokenSymbol: asset.GetTokenSymbol(),
|
||||||
ContractAddress: asset.GetContractAddress(),
|
ContractAddress: asset.GetContractAddress(),
|
||||||
@@ -358,11 +358,11 @@ func cloneFXQuote(quote *oraclev1.Quote) *oraclev1.Quote {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneNetworkEstimate(resp *gatewayv1.EstimateTransferFeeResponse) *gatewayv1.EstimateTransferFeeResponse {
|
func cloneNetworkEstimate(resp *chainv1.EstimateTransferFeeResponse) *chainv1.EstimateTransferFeeResponse {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if cloned, ok := proto.Clone(resp).(*gatewayv1.EstimateTransferFeeResponse); ok {
|
if cloned, ok := proto.Clone(resp).(*chainv1.EstimateTransferFeeResponse); ok {
|
||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
@@ -29,7 +29,7 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
|
|||||||
}
|
}
|
||||||
feeTotal := extractFeeTotal(feeQuote.GetLines(), amount.GetCurrency())
|
feeTotal := extractFeeTotal(feeQuote.GetLines(), amount.GetCurrency())
|
||||||
|
|
||||||
var networkFee *gatewayv1.EstimateTransferFeeResponse
|
var networkFee *chainv1.EstimateTransferFeeResponse
|
||||||
if shouldEstimateNetworkFee(intent) {
|
if shouldEstimateNetworkFee(intent) {
|
||||||
networkFee, err = s.estimateNetworkFee(ctx, intent)
|
networkFee, err = s.estimateNetworkFee(ctx, intent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -90,25 +90,25 @@ func (s *Service) quoteFees(ctx context.Context, orgRef string, req *orchestrato
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) estimateNetworkFee(ctx context.Context, intent *orchestratorv1.PaymentIntent) (*gatewayv1.EstimateTransferFeeResponse, error) {
|
func (s *Service) estimateNetworkFee(ctx context.Context, intent *orchestratorv1.PaymentIntent) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||||
if !s.gateway.available() {
|
if !s.gateway.available() {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &gatewayv1.EstimateTransferFeeRequest{
|
req := &chainv1.EstimateTransferFeeRequest{
|
||||||
Amount: cloneMoney(intent.GetAmount()),
|
Amount: cloneMoney(intent.GetAmount()),
|
||||||
}
|
}
|
||||||
if src := intent.GetSource().GetManagedWallet(); src != nil {
|
if src := intent.GetSource().GetManagedWallet(); src != nil {
|
||||||
req.SourceWalletRef = strings.TrimSpace(src.GetManagedWalletRef())
|
req.SourceWalletRef = strings.TrimSpace(src.GetManagedWalletRef())
|
||||||
}
|
}
|
||||||
if dst := intent.GetDestination().GetManagedWallet(); dst != nil {
|
if dst := intent.GetDestination().GetManagedWallet(); dst != nil {
|
||||||
req.Destination = &gatewayv1.TransferDestination{
|
req.Destination = &chainv1.TransferDestination{
|
||||||
Destination: &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(dst.GetManagedWalletRef())},
|
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(dst.GetManagedWalletRef())},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if dst := intent.GetDestination().GetExternalChain(); dst != nil {
|
if dst := intent.GetDestination().GetExternalChain(); dst != nil {
|
||||||
req.Destination = &gatewayv1.TransferDestination{
|
req.Destination = &chainv1.TransferDestination{
|
||||||
Destination: &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(dst.GetAddress())},
|
Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(dst.GetAddress())},
|
||||||
Memo: strings.TrimSpace(dst.GetMemo()),
|
Memo: strings.TrimSpace(dst.GetMemo()),
|
||||||
}
|
}
|
||||||
req.Asset = dst.GetAsset()
|
req.Asset = dst.GetAsset()
|
||||||
@@ -320,7 +320,7 @@ func (s *Service) applyFX(ctx context.Context, payment *model.Payment, quote *or
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) submitChainTransfer(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote) (*gatewayv1.SubmitTransferResponse, error) {
|
func (s *Service) submitChainTransfer(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote) (*chainv1.SubmitTransferResponse, error) {
|
||||||
intent := payment.Intent
|
intent := payment.Intent
|
||||||
source := intent.Source.ManagedWallet
|
source := intent.Source.ManagedWallet
|
||||||
destination := intent.Destination
|
destination := intent.Destination
|
||||||
@@ -336,7 +336,7 @@ func (s *Service) submitChainTransfer(ctx context.Context, payment *model.Paymen
|
|||||||
return nil, merrors.InvalidArgument("chain: amount is required")
|
return nil, merrors.InvalidArgument("chain: amount is required")
|
||||||
}
|
}
|
||||||
fees := feeBreakdownFromQuote(quote)
|
fees := feeBreakdownFromQuote(quote)
|
||||||
req := &gatewayv1.SubmitTransferRequest{
|
req := &chainv1.SubmitTransferRequest{
|
||||||
IdempotencyKey: payment.IdempotencyKey,
|
IdempotencyKey: payment.IdempotencyKey,
|
||||||
OrganizationRef: payment.OrganizationRef.Hex(),
|
OrganizationRef: payment.OrganizationRef.Hex(),
|
||||||
SourceWalletRef: strings.TrimSpace(source.ManagedWalletRef),
|
SourceWalletRef: strings.TrimSpace(source.ManagedWalletRef),
|
||||||
@@ -437,21 +437,21 @@ func hasManagedWallet(endpoint model.PaymentEndpoint) bool {
|
|||||||
return endpoint.Type == model.EndpointTypeManagedWallet && endpoint.ManagedWallet != nil && strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) != ""
|
return endpoint.Type == model.EndpointTypeManagedWallet && endpoint.ManagedWallet != nil && strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func toGatewayDestination(endpoint model.PaymentEndpoint) (*gatewayv1.TransferDestination, error) {
|
func toGatewayDestination(endpoint model.PaymentEndpoint) (*chainv1.TransferDestination, error) {
|
||||||
switch endpoint.Type {
|
switch endpoint.Type {
|
||||||
case model.EndpointTypeManagedWallet:
|
case model.EndpointTypeManagedWallet:
|
||||||
if endpoint.ManagedWallet == nil || strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) == "" {
|
if endpoint.ManagedWallet == nil || strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) == "" {
|
||||||
return nil, merrors.InvalidArgument("chain: destination managed wallet is required")
|
return nil, merrors.InvalidArgument("chain: destination managed wallet is required")
|
||||||
}
|
}
|
||||||
return &gatewayv1.TransferDestination{
|
return &chainv1.TransferDestination{
|
||||||
Destination: &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef)},
|
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef)},
|
||||||
}, nil
|
}, nil
|
||||||
case model.EndpointTypeExternalChain:
|
case model.EndpointTypeExternalChain:
|
||||||
if endpoint.ExternalChain == nil || strings.TrimSpace(endpoint.ExternalChain.Address) == "" {
|
if endpoint.ExternalChain == nil || strings.TrimSpace(endpoint.ExternalChain.Address) == "" {
|
||||||
return nil, merrors.InvalidArgument("chain: external address is required")
|
return nil, merrors.InvalidArgument("chain: external address is required")
|
||||||
}
|
}
|
||||||
return &gatewayv1.TransferDestination{
|
return &chainv1.TransferDestination{
|
||||||
Destination: &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(endpoint.ExternalChain.Address)},
|
Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(endpoint.ExternalChain.Address)},
|
||||||
Memo: strings.TrimSpace(endpoint.ExternalChain.Memo),
|
Memo: strings.TrimSpace(endpoint.ExternalChain.Memo),
|
||||||
}, nil
|
}, nil
|
||||||
default:
|
default:
|
||||||
@@ -459,7 +459,7 @@ func toGatewayDestination(endpoint model.PaymentEndpoint) (*gatewayv1.TransferDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyTransferStatus(event *gatewayv1.TransferStatusChangedEvent, payment *model.Payment) {
|
func applyTransferStatus(event *chainv1.TransferStatusChangedEvent, payment *model.Payment) {
|
||||||
if payment.Execution == nil {
|
if payment.Execution == nil {
|
||||||
payment.Execution = &model.ExecutionRefs{}
|
payment.Execution = &model.ExecutionRefs{}
|
||||||
}
|
}
|
||||||
@@ -473,21 +473,21 @@ func applyTransferStatus(event *gatewayv1.TransferStatusChangedEvent, payment *m
|
|||||||
reason = strings.TrimSpace(transfer.GetFailureReason())
|
reason = strings.TrimSpace(transfer.GetFailureReason())
|
||||||
}
|
}
|
||||||
switch transfer.GetStatus() {
|
switch transfer.GetStatus() {
|
||||||
case gatewayv1.TransferStatus_TRANSFER_CONFIRMED:
|
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
||||||
payment.State = model.PaymentStateSettled
|
payment.State = model.PaymentStateSettled
|
||||||
payment.FailureCode = model.PaymentFailureCodeUnspecified
|
payment.FailureCode = model.PaymentFailureCodeUnspecified
|
||||||
payment.FailureReason = ""
|
payment.FailureReason = ""
|
||||||
case gatewayv1.TransferStatus_TRANSFER_FAILED:
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
payment.State = model.PaymentStateFailed
|
payment.State = model.PaymentStateFailed
|
||||||
payment.FailureCode = model.PaymentFailureCodeChain
|
payment.FailureCode = model.PaymentFailureCodeChain
|
||||||
payment.FailureReason = reason
|
payment.FailureReason = reason
|
||||||
case gatewayv1.TransferStatus_TRANSFER_CANCELLED:
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
payment.State = model.PaymentStateCancelled
|
payment.State = model.PaymentStateCancelled
|
||||||
payment.FailureCode = model.PaymentFailureCodePolicy
|
payment.FailureCode = model.PaymentFailureCodePolicy
|
||||||
payment.FailureReason = reason
|
payment.FailureReason = reason
|
||||||
case gatewayv1.TransferStatus_TRANSFER_SIGNING,
|
case chainv1.TransferStatus_TRANSFER_SIGNING,
|
||||||
gatewayv1.TransferStatus_TRANSFER_PENDING,
|
chainv1.TransferStatus_TRANSFER_PENDING,
|
||||||
gatewayv1.TransferStatus_TRANSFER_SUBMITTED:
|
chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
||||||
payment.State = model.PaymentStateSubmitted
|
payment.State = model.PaymentStateSubmitted
|
||||||
default:
|
default:
|
||||||
// retain previous state
|
// retain previous state
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
|
accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func cloneMoney(input *moneyv1.Money) *moneyv1.Money {
|
func cloneMoney(input *moneyv1.Money) *moneyv1.Money {
|
||||||
@@ -108,7 +108,7 @@ func extractFeeTotal(lines []*feesv1.DerivedPostingLine, currency string) *money
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeAggregates(base, fee *moneyv1.Money, network *gatewayv1.EstimateTransferFeeResponse) (*moneyv1.Money, *moneyv1.Money) {
|
func computeAggregates(base, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse) (*moneyv1.Money, *moneyv1.Money) {
|
||||||
if base == nil {
|
if base == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -219,12 +219,12 @@ func ledgerLineTypeFromAccounting(lineType accountingv1.PostingLineType) ledgerv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*gatewayv1.ServiceFeeBreakdown {
|
func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*chainv1.ServiceFeeBreakdown {
|
||||||
if quote == nil {
|
if quote == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
lines := quote.GetFeeLines()
|
lines := quote.GetFeeLines()
|
||||||
breakdown := make([]*gatewayv1.ServiceFeeBreakdown, 0, len(lines)+1)
|
breakdown := make([]*chainv1.ServiceFeeBreakdown, 0, len(lines)+1)
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if line == nil {
|
if line == nil {
|
||||||
continue
|
continue
|
||||||
@@ -241,7 +241,7 @@ func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*gatewayv1.Serv
|
|||||||
code = line.GetLineType().String()
|
code = line.GetLineType().String()
|
||||||
}
|
}
|
||||||
desc := strings.TrimSpace(line.GetMeta()["description"])
|
desc := strings.TrimSpace(line.GetMeta()["description"])
|
||||||
breakdown = append(breakdown, &gatewayv1.ServiceFeeBreakdown{
|
breakdown = append(breakdown, &chainv1.ServiceFeeBreakdown{
|
||||||
FeeCode: code,
|
FeeCode: code,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
@@ -250,7 +250,7 @@ func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*gatewayv1.Serv
|
|||||||
if quote.GetNetworkFee() != nil && quote.GetNetworkFee().GetNetworkFee() != nil {
|
if quote.GetNetworkFee() != nil && quote.GetNetworkFee().GetNetworkFee() != nil {
|
||||||
networkAmount := cloneMoney(quote.GetNetworkFee().GetNetworkFee())
|
networkAmount := cloneMoney(quote.GetNetworkFee().GetNetworkFee())
|
||||||
if networkAmount != nil {
|
if networkAmount != nil {
|
||||||
breakdown = append(breakdown, &gatewayv1.ServiceFeeBreakdown{
|
breakdown = append(breakdown, &chainv1.ServiceFeeBreakdown{
|
||||||
FeeCode: "network_fee",
|
FeeCode: "network_fee",
|
||||||
Amount: networkAmount,
|
Amount: networkAmount,
|
||||||
Description: strings.TrimSpace(quote.GetNetworkFee().GetEstimationContext()),
|
Description: strings.TrimSpace(quote.GetNetworkFee().GetEstimationContext()),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package orchestrator
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
chainclient "github.com/tech/sendico/chain/gateway/client"
|
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||||
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
||||||
ledgerclient "github.com/tech/sendico/ledger/client"
|
ledgerclient "github.com/tech/sendico/ledger/client"
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
chainclient "github.com/tech/sendico/chain/gateway/client"
|
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||||
ledgerclient "github.com/tech/sendico/ledger/client"
|
ledgerclient "github.com/tech/sendico/ledger/client"
|
||||||
"github.com/tech/sendico/payments/orchestrator/storage"
|
"github.com/tech/sendico/payments/orchestrator/storage"
|
||||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
mo "github.com/tech/sendico/pkg/model"
|
mo "github.com/tech/sendico/pkg/model"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
@@ -88,7 +88,7 @@ func TestExecutePayment_ChainFailure(t *testing.T) {
|
|||||||
clock: testClock{now: time.Now()},
|
clock: testClock{now: time.Now()},
|
||||||
storage: repo,
|
storage: repo,
|
||||||
gateway: gatewayDependency{client: &chainclient.Fake{
|
gateway: gatewayDependency{client: &chainclient.Fake{
|
||||||
SubmitTransferFn: func(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) {
|
SubmitTransferFn: func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||||
return nil, errors.New("chain failure")
|
return nil, errors.New("chain failure")
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
@@ -147,10 +147,10 @@ func TestProcessTransferUpdateHandler_Settled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req := &orchestratorv1.ProcessTransferUpdateRequest{
|
req := &orchestratorv1.ProcessTransferUpdateRequest{
|
||||||
Event: &gatewayv1.TransferStatusChangedEvent{
|
Event: &chainv1.TransferStatusChangedEvent{
|
||||||
Transfer: &gatewayv1.Transfer{
|
Transfer: &chainv1.Transfer{
|
||||||
TransferRef: "transfer-1",
|
TransferRef: "transfer-1",
|
||||||
Status: gatewayv1.TransferStatus_TRANSFER_CONFIRMED,
|
Status: chainv1.TransferStatus_TRANSFER_CONFIRMED,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ func TestProcessDepositObservedHandler_MatchesPayment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req := &orchestratorv1.ProcessDepositObservedRequest{
|
req := &orchestratorv1.ProcessDepositObservedRequest{
|
||||||
Event: &gatewayv1.WalletDepositObservedEvent{
|
Event: &chainv1.WalletDepositObservedEvent{
|
||||||
WalletRef: "wallet-dst",
|
WalletRef: "wallet-dst",
|
||||||
Amount: &moneyv1.Money{Currency: "USD", Amount: "40"},
|
Amount: &moneyv1.Money{Currency: "USD", Amount: "40"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
|
||||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,12 +68,12 @@ 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"`
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ type PaymentQuoteSnapshot struct {
|
|||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
api/pkg/.gitignore
vendored
2
api/pkg/.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
proto/billing
|
proto/billing
|
||||||
proto/common
|
proto/common
|
||||||
proto/chain
|
proto/chain
|
||||||
|
proto/gateway
|
||||||
proto/ledger
|
proto/ledger
|
||||||
proto/oracle
|
proto/oracle
|
||||||
proto/payments
|
proto/payments
|
||||||
|
.gocache
|
||||||
@@ -47,6 +47,12 @@ func (n *Enforcer) Enforce(
|
|||||||
permissionRef, accountRef, organizationRef, objectRef primitive.ObjectID,
|
permissionRef, accountRef, organizationRef, objectRef primitive.ObjectID,
|
||||||
action model.Action,
|
action model.Action,
|
||||||
) (bool, error) {
|
) (bool, error) {
|
||||||
|
if organizationRef.IsZero() {
|
||||||
|
n.logger.Warn("Missing organization context", mzap.ObjRef("account_ref", accountRef),
|
||||||
|
mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef),
|
||||||
|
mzap.ObjRef("object", objectRef), zap.String("action", string(action)))
|
||||||
|
return false, merrors.InvalidArgument("organization context missing", "organizationRef")
|
||||||
|
}
|
||||||
roleAssignments, err := n.rdb.Roles(ctx, accountRef, organizationRef)
|
roleAssignments, err := n.rdb.Roles(ctx, accountRef, organizationRef)
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
n.logger.Debug("No roles defined for account", mzap.ObjRef("account_ref", accountRef))
|
n.logger.Debug("No roles defined for account", mzap.ObjRef("account_ref", accountRef))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package mongo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/tech/sendico/pkg/auth"
|
"github.com/tech/sendico/pkg/auth"
|
||||||
@@ -12,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"
|
||||||
@@ -28,7 +35,6 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
mutil "github.com/tech/sendico/pkg/mutil/config"
|
mutil "github.com/tech/sendico/pkg/mutil/config"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
|
||||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -44,6 +50,13 @@ type Config struct {
|
|||||||
DatabaseEnv *string `mapstructure:"database_env"`
|
DatabaseEnv *string `mapstructure:"database_env"`
|
||||||
Host *string `mapstructure:"host"`
|
Host *string `mapstructure:"host"`
|
||||||
HostEnv *string `mapstructure:"host_env"`
|
HostEnv *string `mapstructure:"host_env"`
|
||||||
|
Hosts []string `mapstructure:"hosts,omitempty"`
|
||||||
|
HostsEnvPrefix *string `mapstructure:"hosts_env_prefix,omitempty"`
|
||||||
|
HostsEnvPrefixEnv *string `mapstructure:"hosts_env_prefix_env,omitempty"`
|
||||||
|
PortsEnvPrefix *string `mapstructure:"ports_env_prefix,omitempty"`
|
||||||
|
PortsEnvPrefixEnv *string `mapstructure:"ports_env_prefix_env,omitempty"`
|
||||||
|
URI *string `mapstructure:"uri,omitempty"`
|
||||||
|
URIEnv *string `mapstructure:"uri_env,omitempty"`
|
||||||
AuthSource *string `mapstructure:"auth_source,omitempty"`
|
AuthSource *string `mapstructure:"auth_source,omitempty"`
|
||||||
AuthSourceEnv *string `mapstructure:"auth_source_env,omitempty"`
|
AuthSourceEnv *string `mapstructure:"auth_source_env,omitempty"`
|
||||||
AuthMechanism *string `mapstructure:"auth_mechanism,omitempty"`
|
AuthMechanism *string `mapstructure:"auth_mechanism,omitempty"`
|
||||||
@@ -55,6 +68,7 @@ type Config struct {
|
|||||||
|
|
||||||
type DBSettings struct {
|
type DBSettings struct {
|
||||||
Host string
|
Host string
|
||||||
|
Hosts []string
|
||||||
Port string
|
Port string
|
||||||
User string
|
User string
|
||||||
Password string
|
Password string
|
||||||
@@ -62,6 +76,7 @@ type DBSettings struct {
|
|||||||
AuthSource string
|
AuthSource string
|
||||||
AuthMechanism string
|
AuthMechanism string
|
||||||
ReplicaSet string
|
ReplicaSet string
|
||||||
|
URI string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProtectedDB[T any](
|
func newProtectedDB[T any](
|
||||||
@@ -87,6 +102,19 @@ func Config2DBSettings(logger mlogger.Logger, config *Config) *DBSettings {
|
|||||||
p.AuthSource = mutil.GetConfigValue(logger, "auth_source", "auth_source_env", config.AuthSource, config.AuthSourceEnv)
|
p.AuthSource = mutil.GetConfigValue(logger, "auth_source", "auth_source_env", config.AuthSource, config.AuthSourceEnv)
|
||||||
p.AuthMechanism = mutil.GetConfigValue(logger, "auth_mechanism", "auth_mechanism_env", config.AuthMechanism, config.AuthMechanismEnv)
|
p.AuthMechanism = mutil.GetConfigValue(logger, "auth_mechanism", "auth_mechanism_env", config.AuthMechanism, config.AuthMechanismEnv)
|
||||||
p.ReplicaSet = mutil.GetConfigValue(logger, "replica_set", "replica_set_env", config.ReplicaSet, config.ReplicaSetEnv)
|
p.ReplicaSet = mutil.GetConfigValue(logger, "replica_set", "replica_set_env", config.ReplicaSet, config.ReplicaSetEnv)
|
||||||
|
p.URI = mutil.GetConfigValue(logger, "uri", "uri_env", config.URI, config.URIEnv)
|
||||||
|
|
||||||
|
hostPrefix := mutil.GetConfigValue(logger, "hosts_env_prefix", "hosts_env_prefix_env", config.HostsEnvPrefix, config.HostsEnvPrefixEnv)
|
||||||
|
portPrefix := mutil.GetConfigValue(logger, "ports_env_prefix", "ports_env_prefix_env", config.PortsEnvPrefix, config.PortsEnvPrefixEnv)
|
||||||
|
|
||||||
|
if hostPrefix == "" && p.ReplicaSet != "" {
|
||||||
|
hostPrefix = "MONGO_HOSTS_"
|
||||||
|
}
|
||||||
|
if portPrefix == "" && p.ReplicaSet != "" {
|
||||||
|
portPrefix = "MONGO_PORTS_"
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Hosts = collectReplicaHosts(config.Hosts, p.ReplicaSet, p.Port, hostPrefix, portPrefix)
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,21 +129,19 @@ func decodeConfig(logger mlogger.Logger, settings model.SettingsT) (*Config, *DB
|
|||||||
}
|
}
|
||||||
|
|
||||||
func dialMongo(logger mlogger.Logger, dbSettings *DBSettings) (*mongo.Client, error) {
|
func dialMongo(logger mlogger.Logger, dbSettings *DBSettings) (*mongo.Client, error) {
|
||||||
cred := options.Credential{
|
opts := buildOptions(dbSettings)
|
||||||
AuthMechanism: dbSettings.AuthMechanism,
|
|
||||||
AuthSource: dbSettings.AuthSource,
|
|
||||||
Username: dbSettings.User,
|
|
||||||
Password: dbSettings.Password,
|
|
||||||
}
|
|
||||||
dbURI := buildURI(dbSettings)
|
|
||||||
|
|
||||||
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(dbURI).SetAuth(cred))
|
client, err := mongo.Connect(context.Background(), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Unable to connect to database", zap.Error(err))
|
logger.Error("Unable to connect to database", zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Connected successfully", zap.String("uri", dbURI))
|
if dbSettings.URI != "" {
|
||||||
|
logger.Info("Connected successfully", zap.Bool("uri_provided", true))
|
||||||
|
} else {
|
||||||
|
logger.Info("Connected successfully", zap.Strings("hosts", opts.Hosts), zap.String("replica_set", dbSettings.ReplicaSet))
|
||||||
|
}
|
||||||
|
|
||||||
if err := client.Ping(context.Background(), readpref.Primary()); err != nil {
|
if err := client.Ping(context.Background(), readpref.Primary()); err != nil {
|
||||||
logger.Error("Unable to ping database", zap.Error(err))
|
logger.Error("Unable to ping database", zap.Error(err))
|
||||||
@@ -179,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())
|
||||||
}
|
}
|
||||||
@@ -199,6 +248,70 @@ func (db *DB) TransactionFactory() transaction.Factory {
|
|||||||
return transactionimp.CreateFactory(db.client)
|
return transactionimp.CreateFactory(db.client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectReplicaHosts(configuredHosts []string, replicaSet, defaultPort, hostPrefix, portPrefix string) []string {
|
||||||
|
normalize := func(host, port string) string {
|
||||||
|
host = strings.TrimSpace(host)
|
||||||
|
port = strings.TrimSpace(port)
|
||||||
|
if host == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// If host already has a port, keep it; otherwise apply provided/default port.
|
||||||
|
if _, _, err := net.SplitHostPort(host); err == nil {
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
if port != "" {
|
||||||
|
return net.JoinHostPort(host, port)
|
||||||
|
}
|
||||||
|
if defaultPort != "" {
|
||||||
|
return net.JoinHostPort(host, defaultPort)
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
appendHost := func(list []string, host, port string) []string {
|
||||||
|
if normalized := normalize(host, port); normalized != "" {
|
||||||
|
return append(list, normalized)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
var hosts []string
|
||||||
|
for _, h := range configuredHosts {
|
||||||
|
hosts = appendHost(hosts, h, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if replicaSet == "" || hostPrefix == "" {
|
||||||
|
return hosts
|
||||||
|
}
|
||||||
|
|
||||||
|
index := 0
|
||||||
|
for {
|
||||||
|
hostEnv := os.Getenv(fmt.Sprintf("%s%d", hostPrefix, index))
|
||||||
|
portEnv := ""
|
||||||
|
if portPrefix != "" {
|
||||||
|
portEnv = os.Getenv(fmt.Sprintf("%s%d", portPrefix, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostEnv == "" && index == 0 {
|
||||||
|
hostEnv = os.Getenv(fmt.Sprintf("%s%d", hostPrefix, 1))
|
||||||
|
if portPrefix != "" {
|
||||||
|
portEnv = os.Getenv(fmt.Sprintf("%s%d", portPrefix, 1))
|
||||||
|
}
|
||||||
|
if hostEnv == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
index = 1
|
||||||
|
} else if hostEnv == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts = appendHost(hosts, hostEnv, portEnv)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
return hosts
|
||||||
|
}
|
||||||
|
|
||||||
func (db *DB) Permissions() auth.Provider {
|
func (db *DB) Permissions() auth.Provider {
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,49 @@
|
|||||||
package mongo
|
package mongo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildURI(s *DBSettings) string {
|
func buildOptions(s *DBSettings) *options.ClientOptions {
|
||||||
u := &url.URL{
|
opts := options.Client()
|
||||||
Scheme: "mongodb",
|
|
||||||
Host: s.Host,
|
if s.URI != "" {
|
||||||
Path: "/" + url.PathEscape(s.Database), // /my%20db
|
return opts.ApplyURI(s.URI)
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts := make([]string, 0, len(s.Hosts)+1)
|
||||||
|
for _, h := range s.Hosts {
|
||||||
|
if trimmed := strings.TrimSpace(h); trimmed != "" {
|
||||||
|
hosts = append(hosts, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hosts) == 0 && s.Host != "" {
|
||||||
|
host := s.Host
|
||||||
|
if _, _, err := net.SplitHostPort(host); err != nil && s.Port != "" {
|
||||||
|
host = net.JoinHostPort(host, s.Port)
|
||||||
|
}
|
||||||
|
hosts = append(hosts, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
opts.SetHosts(hosts)
|
||||||
}
|
}
|
||||||
|
|
||||||
q := url.Values{}
|
|
||||||
if s.ReplicaSet != "" {
|
if s.ReplicaSet != "" {
|
||||||
q.Set("replicaSet", s.ReplicaSet)
|
opts.SetReplicaSet(s.ReplicaSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.RawQuery = q.Encode()
|
cred := options.Credential{
|
||||||
|
AuthMechanism: s.AuthMechanism,
|
||||||
return u.String()
|
AuthSource: s.AuthSource,
|
||||||
|
Username: s.User,
|
||||||
|
Password: s.Password,
|
||||||
|
}
|
||||||
|
opts.SetAuth(cred)
|
||||||
|
|
||||||
|
return opts
|
||||||
}
|
}
|
||||||
|
|||||||
20
api/pkg/db/internal/mongo/paymethoddb/archived.go
Normal file
20
api/pkg/db/internal/mongo/paymethoddb/archived.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package paymethoddb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *PaymentMethodsDB) SetArchived(ctx context.Context, accountRef, organizationRef, objectRef primitive.ObjectID, isArchived, cascade bool) error {
|
||||||
|
// Use the ArchivableDB for the main archiving logic
|
||||||
|
if err := db.ArchivableDB.SetArchived(ctx, accountRef, objectRef, isArchived); err != nil {
|
||||||
|
db.DBImp.Logger.Warn("Failed to chnage object archive status", zap.Error(err),
|
||||||
|
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
|
||||||
|
mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
49
api/pkg/db/internal/mongo/paymethoddb/db.go
Normal file
49
api/pkg/db/internal/mongo/paymethoddb/db.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package paymethoddb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/auth"
|
||||||
|
"github.com/tech/sendico/pkg/db/policy"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaymentMethodsDB struct {
|
||||||
|
auth.ProtectedDBImp[*model.PaymentMethod]
|
||||||
|
auth.ArchivableDB[*model.PaymentMethod]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(ctx context.Context,
|
||||||
|
logger mlogger.Logger,
|
||||||
|
enforcer auth.Enforcer,
|
||||||
|
pdb policy.DB,
|
||||||
|
db *mongo.Database,
|
||||||
|
) (*PaymentMethodsDB, error) {
|
||||||
|
p, err := auth.CreateDBImp[*model.PaymentMethod](ctx, logger, pdb, enforcer, mservice.PaymentMethods, db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createEmpty := func() *model.PaymentMethod {
|
||||||
|
return &model.PaymentMethod{}
|
||||||
|
}
|
||||||
|
|
||||||
|
getArchivable := func(c *model.PaymentMethod) model.Archivable {
|
||||||
|
return &c.ArchivableBase
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &PaymentMethodsDB{
|
||||||
|
ProtectedDBImp: *p,
|
||||||
|
ArchivableDB: auth.NewArchivableDB(
|
||||||
|
p.DBImp,
|
||||||
|
logger,
|
||||||
|
p.Enforcer,
|
||||||
|
createEmpty,
|
||||||
|
getArchivable,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
28
api/pkg/db/internal/mongo/paymethoddb/list.go
Normal file
28
api/pkg/db/internal/mongo/paymethoddb/list.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package paymethoddb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
mauth "github.com/tech/sendico/pkg/mutil/db/auth"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *PaymentMethodsDB) List(ctx context.Context, accountRef, organizationRef, recipientRef primitive.ObjectID, cursor *model.ViewCursor) ([]model.PaymentMethod, error) {
|
||||||
|
res, err := mauth.GetProtectedObjects[model.PaymentMethod](
|
||||||
|
ctx,
|
||||||
|
db.DBImp.Logger,
|
||||||
|
accountRef, organizationRef, model.ActionRead,
|
||||||
|
repository.OrgFilter(organizationRef).And(repository.Filter("recipientRef", recipientRef)),
|
||||||
|
cursor,
|
||||||
|
db.Enforcer,
|
||||||
|
db.DBImp.Repository,
|
||||||
|
)
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
return []model.PaymentMethod{}, nil
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
57
api/pkg/db/internal/mongo/recipientdb/archived.go
Normal file
57
api/pkg/db/internal/mongo/recipientdb/archived.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package recipientdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *RecipientDB) SetArchived(ctx context.Context, accountRef, organizationRef, objectRef primitive.ObjectID, isArchived, cascade bool) error {
|
||||||
|
// Use the ArchivableDB for the main archiving logic
|
||||||
|
if err := db.ArchivableDB.SetArchived(ctx, accountRef, objectRef, isArchived); err != nil {
|
||||||
|
db.DBImp.Logger.Warn("Failed to change recipient archive status", zap.Error(err),
|
||||||
|
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
|
||||||
|
mzap.ObjRef("recipient_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cascade {
|
||||||
|
if err := db.setArchivedPaymentMethods(ctx, accountRef, organizationRef, objectRef, isArchived); err != nil {
|
||||||
|
db.DBImp.Logger.Warn("Failed to update payment methods archive status", zap.Error(err),
|
||||||
|
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef),
|
||||||
|
mzap.ObjRef("recipient_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *RecipientDB) setArchivedPaymentMethods(ctx context.Context, accountRef, organizationRef, recipientRef primitive.ObjectID, archived bool) error {
|
||||||
|
db.DBImp.Logger.Debug("Setting archived status for recipient payment methods", mzap.ObjRef("recipient_ref", recipientRef), zap.Bool("archived", archived))
|
||||||
|
|
||||||
|
db.DBImp.Logger.Debug("Applying archived status to payment methods for recipient", mzap.ObjRef("recipient_ref", recipientRef))
|
||||||
|
|
||||||
|
// Get all payMethods for the recipient
|
||||||
|
payMethods, err := db.pmdb.List(ctx, accountRef, organizationRef, recipientRef, nil)
|
||||||
|
if err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||||
|
db.DBImp.Logger.Warn("Failed to fetch payment methods for recipient", zap.Error(err), mzap.ObjRef("recipient_ref", recipientRef))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive each payment method
|
||||||
|
for _, pmethod := range payMethods {
|
||||||
|
if err := db.pmdb.SetArchived(ctx, accountRef, organizationRef, pmethod.ID, archived, true); err != nil {
|
||||||
|
db.DBImp.Logger.Warn("Failed to set archived status for payment method", zap.Error(err), mzap.ObjRef("payment_method_ref", pmethod.ID))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.DBImp.Logger.Debug("Successfully updated payment methods archived status", zap.Int("count", len(payMethods)), mzap.ObjRef("recipient_ref", recipientRef))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
56
api/pkg/db/internal/mongo/recipientdb/db.go
Normal file
56
api/pkg/db/internal/mongo/recipientdb/db.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package recipientdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/auth"
|
||||||
|
"github.com/tech/sendico/pkg/db/paymethod"
|
||||||
|
"github.com/tech/sendico/pkg/db/policy"
|
||||||
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecipientDB struct {
|
||||||
|
auth.ProtectedDBImp[*model.Recipient]
|
||||||
|
auth.ArchivableDB[*model.Recipient]
|
||||||
|
pmdb paymethod.DB
|
||||||
|
paymentMethodsRepo repository.Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(ctx context.Context,
|
||||||
|
logger mlogger.Logger,
|
||||||
|
enforcer auth.Enforcer,
|
||||||
|
pdb policy.DB,
|
||||||
|
pmdb paymethod.DB,
|
||||||
|
db *mongo.Database,
|
||||||
|
) (*RecipientDB, error) {
|
||||||
|
p, err := auth.CreateDBImp[*model.Recipient](ctx, logger, pdb, enforcer, mservice.Organizations, db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createEmpty := func() *model.Recipient {
|
||||||
|
return &model.Recipient{}
|
||||||
|
}
|
||||||
|
|
||||||
|
getArchivable := func(c *model.Recipient) model.Archivable {
|
||||||
|
return &c.ArchivableBase
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &RecipientDB{
|
||||||
|
ProtectedDBImp: *p,
|
||||||
|
ArchivableDB: auth.NewArchivableDB(
|
||||||
|
p.DBImp,
|
||||||
|
p.DBImp.Logger,
|
||||||
|
enforcer,
|
||||||
|
createEmpty,
|
||||||
|
getArchivable,
|
||||||
|
),
|
||||||
|
paymentMethodsRepo: repository.CreateMongoRepository(db, string(mservice.PaymentMethods)),
|
||||||
|
pmdb: pmdb,
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user