refactored payment orchestration #393
@@ -65,6 +65,6 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -258,8 +258,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-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -50,6 +50,6 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -208,8 +208,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-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -208,8 +208,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-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -208,8 +208,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-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -48,5 +48,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -208,8 +208,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-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/tech/sendico/fx/storage/model"
|
"github.com/tech/sendico/fx/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
smodel "github.com/tech/sendico/pkg/model"
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
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"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
@@ -138,11 +138,11 @@ func (qc *quoteComputation) buildModelQuote(firm bool, expiryMillis int64, req *
|
|||||||
Pair: qc.pair.Pair,
|
Pair: qc.pair.Pair,
|
||||||
Side: qc.sideModel,
|
Side: qc.sideModel,
|
||||||
Price: formatRat(qc.priceRounded, qc.priceScale),
|
Price: formatRat(qc.priceRounded, qc.priceScale),
|
||||||
BaseAmount: smodel.Money{
|
BaseAmount: paymenttypes.Money{
|
||||||
Currency: qc.pair.Pair.Base,
|
Currency: qc.pair.Pair.Base,
|
||||||
Amount: formatRat(qc.baseRounded, qc.baseScale),
|
Amount: formatRat(qc.baseRounded, qc.baseScale),
|
||||||
},
|
},
|
||||||
QuoteAmount: smodel.Money{
|
QuoteAmount: paymenttypes.Money{
|
||||||
Currency: qc.pair.Pair.Quote,
|
Currency: qc.pair.Pair.Quote,
|
||||||
Amount: formatRat(qc.quoteRounded, qc.quoteScale),
|
Amount: formatRat(qc.quoteRounded, qc.quoteScale),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/tech/sendico/fx/storage"
|
"github.com/tech/sendico/fx/storage"
|
||||||
"github.com/tech/sendico/fx/storage/model"
|
"github.com/tech/sendico/fx/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
smodel "github.com/tech/sendico/pkg/model"
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
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"
|
||||||
tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
|
tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
|
||||||
@@ -382,8 +382,8 @@ func TestServiceValidateQuote(t *testing.T) {
|
|||||||
Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"},
|
Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"},
|
||||||
Side: model.QuoteSideBuyBaseSellQuote,
|
Side: model.QuoteSideBuyBaseSellQuote,
|
||||||
Price: "1.10",
|
Price: "1.10",
|
||||||
BaseAmount: smodel.Money{Currency: "USD", Amount: "100"},
|
BaseAmount: paymenttypes.Money{Currency: "USD", Amount: "100"},
|
||||||
QuoteAmount: smodel.Money{Currency: "EUR", Amount: "110"},
|
QuoteAmount: paymenttypes.Money{Currency: "EUR", Amount: "110"},
|
||||||
ExpiresAtUnixMs: now.UnixMilli(),
|
ExpiresAtUnixMs: now.UnixMilli(),
|
||||||
Status: model.QuoteStatusIssued,
|
Status: model.QuoteStatusIssued,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/fx/storage/model"
|
"github.com/tech/sendico/fx/storage/model"
|
||||||
smodel "github.com/tech/sendico/pkg/model"
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
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"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
@@ -42,7 +42,7 @@ func quoteModelToProto(q *model.Quote) *oraclev1.Quote {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func moneyModelToProto(m *smodel.Money) *moneyv1.Money {
|
func moneyModelToProto(m *paymenttypes.Money) *moneyv1.Money {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,32 +4,32 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/db/storable"
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
"github.com/tech/sendico/pkg/model"
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Quote represents a firm or indicative quote persisted by the oracle.
|
// Quote represents a firm or indicative quote persisted by the oracle.
|
||||||
type Quote struct {
|
type Quote struct {
|
||||||
storable.Base `bson:",inline" json:",inline"`
|
storable.Base `bson:",inline" json:",inline"`
|
||||||
|
|
||||||
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
|
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
|
||||||
Firm bool `bson:"firm" json:"firm"`
|
Firm bool `bson:"firm" json:"firm"`
|
||||||
Status QuoteStatus `bson:"status" json:"status"`
|
Status QuoteStatus `bson:"status" json:"status"`
|
||||||
Pair CurrencyPair `bson:"pair" json:"pair"`
|
Pair CurrencyPair `bson:"pair" json:"pair"`
|
||||||
Side QuoteSide `bson:"side" json:"side"`
|
Side QuoteSide `bson:"side" json:"side"`
|
||||||
Price string `bson:"price" json:"price"`
|
Price string `bson:"price" json:"price"`
|
||||||
BaseAmount model.Money `bson:"baseAmount" json:"baseAmount"`
|
BaseAmount paymenttypes.Money `bson:"baseAmount" json:"baseAmount"`
|
||||||
QuoteAmount model.Money `bson:"quoteAmount" json:"quoteAmount"`
|
QuoteAmount paymenttypes.Money `bson:"quoteAmount" json:"quoteAmount"`
|
||||||
AmountType QuoteAmountType `bson:"amountType" json:"amountType"`
|
AmountType QuoteAmountType `bson:"amountType" json:"amountType"`
|
||||||
ExpiresAtUnixMs int64 `bson:"expiresAtUnixMs" json:"expiresAtUnixMs"`
|
ExpiresAtUnixMs int64 `bson:"expiresAtUnixMs" json:"expiresAtUnixMs"`
|
||||||
ExpiresAt *time.Time `bson:"expiresAt,omitempty" json:"expiresAt,omitempty"`
|
ExpiresAt *time.Time `bson:"expiresAt,omitempty" json:"expiresAt,omitempty"`
|
||||||
RateRef string `bson:"rateRef" json:"rateRef"`
|
RateRef string `bson:"rateRef" json:"rateRef"`
|
||||||
Provider string `bson:"provider" json:"provider"`
|
Provider string `bson:"provider" json:"provider"`
|
||||||
PreferredProvider string `bson:"preferredProvider,omitempty" json:"preferredProvider,omitempty"`
|
PreferredProvider string `bson:"preferredProvider,omitempty" json:"preferredProvider,omitempty"`
|
||||||
RequestedTTLMs int64 `bson:"requestedTtlMs,omitempty" json:"requestedTtlMs,omitempty"`
|
RequestedTTLMs int64 `bson:"requestedTtlMs,omitempty" json:"requestedTtlMs,omitempty"`
|
||||||
MaxAgeToleranceMs int64 `bson:"maxAgeToleranceMs,omitempty" json:"maxAgeToleranceMs,omitempty"`
|
MaxAgeToleranceMs int64 `bson:"maxAgeToleranceMs,omitempty" json:"maxAgeToleranceMs,omitempty"`
|
||||||
ConsumedByLedgerTxnRef string `bson:"consumedByLedgerTxnRef,omitempty" json:"consumedByLedgerTxnRef,omitempty"`
|
ConsumedByLedgerTxnRef string `bson:"consumedByLedgerTxnRef,omitempty" json:"consumedByLedgerTxnRef,omitempty"`
|
||||||
ConsumedAtUnixMs *int64 `bson:"consumedAtUnixMs,omitempty" json:"consumedAtUnixMs,omitempty"`
|
ConsumedAtUnixMs *int64 `bson:"consumedAtUnixMs,omitempty" json:"consumedAtUnixMs,omitempty"`
|
||||||
Meta *QuoteMeta `bson:"meta,omitempty" json:"meta,omitempty"`
|
Meta *QuoteMeta `bson:"meta,omitempty" json:"meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collection implements storable.Storable.
|
// Collection implements storable.Storable.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
chainasset "github.com/tech/sendico/pkg/chain"
|
chainasset "github.com/tech/sendico/pkg/chain"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
pmodel "github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model/account_role"
|
||||||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
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"
|
||||||
@@ -426,7 +426,7 @@ func operationFromTransfer(req *chainv1.SubmitTransferRequest) (*connectorv1.Ope
|
|||||||
|
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||||
"client_reference": strings.TrimSpace(req.GetClientReference()),
|
"payment_ref": strings.TrimSpace(req.GetPaymentRef()),
|
||||||
}
|
}
|
||||||
if memo := strings.TrimSpace(req.GetDestination().GetMemo()); memo != "" {
|
if memo := strings.TrimSpace(req.GetDestination().GetMemo()); memo != "" {
|
||||||
params["destination_memo"] = memo
|
params["destination_memo"] = memo
|
||||||
@@ -444,6 +444,8 @@ func operationFromTransfer(req *chainv1.SubmitTransferRequest) (*connectorv1.Ope
|
|||||||
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
|
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
|
||||||
Money: req.GetAmount(),
|
Money: req.GetAmount(),
|
||||||
Params: structFromMap(params),
|
Params: structFromMap(params),
|
||||||
|
IntentRef: strings.TrimSpace(req.GetIntentRef()),
|
||||||
|
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
||||||
}
|
}
|
||||||
to, err := destinationToParty(req.GetDestination())
|
to, err := destinationToParty(req.GetDestination())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -472,14 +474,14 @@ func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[strin
|
|||||||
if op == nil || len(metadata) == 0 {
|
if op == nil || len(metadata) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyFromRole]); raw != "" {
|
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyFromRole]); raw != "" {
|
||||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||||
op.FromRole = pmodel.ToProto(role)
|
op.FromRole = account_role.ToProto(role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyToRole]); raw != "" {
|
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyToRole]); raw != "" {
|
||||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||||
op.ToRole = pmodel.ToProto(role)
|
op.ToRole = account_role.ToProto(role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -619,7 +621,7 @@ func gasTopUpEnsureOperation(req *chainv1.EnsureGasTopUpRequest) (*connectorv1.O
|
|||||||
"mode": "ensure",
|
"mode": "ensure",
|
||||||
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||||
"target_wallet_ref": strings.TrimSpace(req.GetTargetWalletRef()),
|
"target_wallet_ref": strings.TrimSpace(req.GetTargetWalletRef()),
|
||||||
"client_reference": strings.TrimSpace(req.GetClientReference()),
|
"payment_ref": strings.TrimSpace(req.GetPaymentRef()),
|
||||||
"estimated_total_fee": map[string]interface{}{"amount": fee.GetAmount(), "currency": fee.GetCurrency()},
|
"estimated_total_fee": map[string]interface{}{"amount": fee.GetAmount(), "currency": fee.GetCurrency()},
|
||||||
}
|
}
|
||||||
if len(req.GetMetadata()) > 0 {
|
if len(req.GetMetadata()) > 0 {
|
||||||
@@ -765,28 +767,54 @@ func managedWalletStatusFromAccount(state connectorv1.AccountState) chainv1.Mana
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func transferStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
|
||||||
switch status {
|
|
||||||
case connectorv1.OperationStatus_CONFIRMED:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
|
||||||
case connectorv1.OperationStatus_FAILED:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_FAILED
|
|
||||||
case connectorv1.OperationStatus_CANCELED:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
|
||||||
default:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_PENDING
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func operationStatusFromTransfer(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
func operationStatusFromTransfer(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
|
||||||
return connectorv1.OperationStatus_CONFIRMED
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CREATED
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_PROCESSING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_WAITING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_SUCCESS
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
return connectorv1.OperationStatus_FAILED
|
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
return connectorv1.OperationStatus_CANCELED
|
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func transferStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
||||||
|
switch status {
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_CREATED:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_CREATED
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_PROCESSING:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_PROCESSING
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_WAITING:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_WAITING
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_SUCCESS:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_SUCCESS
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_FAILED:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_FAILED
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_CANCELLED:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||||
|
|
||||||
|
default:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
pmodel "github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model/account_role"
|
||||||
"github.com/tech/sendico/pkg/payments/rail"
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
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"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
@@ -102,13 +102,15 @@ func (g *chainRailGateway) Send(ctx context.Context, req rail.TransferRequest) (
|
|||||||
OrganizationRef: orgRef,
|
OrganizationRef: orgRef,
|
||||||
SourceWalletRef: source,
|
SourceWalletRef: source,
|
||||||
Destination: dest,
|
Destination: dest,
|
||||||
|
IntentRef: strings.TrimSpace(req.IntentRef),
|
||||||
|
OperationRef: strings.TrimSpace(req.OperationRef),
|
||||||
|
PaymentRef: strings.TrimSpace(req.PaymentRef),
|
||||||
Amount: &moneyv1.Money{
|
Amount: &moneyv1.Money{
|
||||||
Currency: currency,
|
Currency: currency,
|
||||||
Amount: amountValue,
|
Amount: amountValue,
|
||||||
},
|
},
|
||||||
Fees: fees,
|
Fees: fees,
|
||||||
Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
|
Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
|
||||||
ClientReference: strings.TrimSpace(req.ClientReference),
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rail.RailResult{}, err
|
return rail.RailResult{}, err
|
||||||
@@ -186,20 +188,29 @@ func (g *chainRailGateway) isManagedWallet(ctx context.Context, walletRef string
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusFromTransfer(status chainv1.TransferStatus) string {
|
func statusFromTransfer(status chainv1.TransferStatus) rail.TransferStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
|
return rail.TransferStatusCreated
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||||
|
return rail.TransferStatusProcessing
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return rail.TransferStatusProcessing
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
return rail.TransferStatusSuccess
|
return rail.TransferStatusSuccess
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
return rail.TransferStatusFailed
|
return rail.TransferStatusFailed
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
return rail.TransferStatusRejected
|
return rail.TransferStatusCancelled
|
||||||
case chainv1.TransferStatus_TRANSFER_SIGNING,
|
|
||||||
chainv1.TransferStatus_TRANSFER_PENDING,
|
|
||||||
chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
|
||||||
return rail.TransferStatusPending
|
|
||||||
default:
|
default:
|
||||||
return rail.TransferStatusPending
|
return rail.TransferStatusUnspecified
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,19 +266,19 @@ func railMoneyFromProto(m *moneyv1.Money) *rail.Money {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func transferMetadataWithRoles(metadata map[string]string, fromRole, toRole pmodel.AccountRole) map[string]string {
|
func transferMetadataWithRoles(metadata map[string]string, fromRole, toRole account_role.AccountRole) map[string]string {
|
||||||
result := cloneMetadata(metadata)
|
result := cloneMetadata(metadata)
|
||||||
if strings.TrimSpace(string(fromRole)) != "" {
|
if strings.TrimSpace(string(fromRole)) != "" {
|
||||||
if result == nil {
|
if result == nil {
|
||||||
result = map[string]string{}
|
result = map[string]string{}
|
||||||
}
|
}
|
||||||
result[pmodel.MetadataKeyFromRole] = strings.TrimSpace(string(fromRole))
|
result[account_role.MetadataKeyFromRole] = strings.TrimSpace(string(fromRole))
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(string(toRole)) != "" {
|
if strings.TrimSpace(string(toRole)) != "" {
|
||||||
if result == nil {
|
if result == nil {
|
||||||
result = map[string]string{}
|
result = map[string]string{}
|
||||||
}
|
}
|
||||||
result[pmodel.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
|
result[account_role.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
|
||||||
}
|
}
|
||||||
if len(result) == 0 {
|
if len(result) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260131145833-e3fabd62fc61 // indirect
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260201044653-ee82dce4af02 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||||
@@ -84,5 +84,5 @@ require (
|
|||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.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-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
|||||||
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260131145833-e3fabd62fc61 h1:iLc9NjmJ3AdAl5VoiRSDXzEmmW8kvHp3E2vJ2eKKc7s=
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260201044653-ee82dce4af02 h1:0uY5Ooun4eqGmP0IrQhiKVqeeEXoeEcL8KVRtug8+r8=
|
||||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260131145833-e3fabd62fc61/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260201044653-ee82dce4af02/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||||
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
|
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
|
||||||
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
|
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -360,8 +360,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-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ManagedWalletKey captures information returned after provisioning a managed wallet key.
|
// ManagedWalletKey captures information returned after provisioning a managed wallet key.
|
||||||
@@ -17,7 +18,7 @@ type ManagedWalletKey struct {
|
|||||||
// Manager defines the contract for managing managed wallet keys.
|
// Manager defines the contract for managing managed wallet keys.
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
// CreateManagedWalletKey provisions a new managed wallet key for the provided wallet reference and network.
|
// CreateManagedWalletKey provisions a new managed wallet key for the provided wallet reference and network.
|
||||||
CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*ManagedWalletKey, error)
|
CreateManagedWalletKey(ctx context.Context, walletRef string, network pmodel.ChainNetwork) (*ManagedWalletKey, error)
|
||||||
// SignTransaction signs the provided transaction using the identified key material.
|
// SignTransaction signs the provided transaction using the identified key material.
|
||||||
SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
|
SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/tech/sendico/gateway/chain/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"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config describes how to connect to Vault for managed wallet keys.
|
// Config describes how to connect to Vault for managed wallet keys.
|
||||||
@@ -92,19 +93,19 @@ func New(logger mlogger.Logger, cfg Config) (*Manager, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateManagedWalletKey creates a new managed wallet key and stores it in Vault.
|
// CreateManagedWalletKey creates a new managed wallet key and stores it in Vault.
|
||||||
func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) {
|
func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network pmodel.ChainNetwork) (*keymanager.ManagedWalletKey, error) {
|
||||||
if strings.TrimSpace(walletRef) == "" {
|
if strings.TrimSpace(walletRef) == "" {
|
||||||
m.logger.Warn("WalletRef missing for managed key creation", zap.String("network", network))
|
m.logger.Warn("WalletRef missing for managed key creation", zap.String("network", string(network)))
|
||||||
return nil, merrors.InvalidArgument("vault key manager: walletRef is required")
|
return nil, merrors.InvalidArgument("vault key manager: walletRef is required")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(network) == "" {
|
if network == pmodel.ChainNetworkUnspecified {
|
||||||
m.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
|
m.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
|
||||||
return nil, merrors.InvalidArgument("vault key manager: network is required")
|
return nil, merrors.InvalidArgument("vault key manager: network is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKey, err := ecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
|
privateKey, err := ecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("Failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
m.logger.Warn("Failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", string(network)), zap.Error(err))
|
||||||
return nil, merrors.Internal("vault key manager: failed to generate key: " + err.Error())
|
return nil, merrors.Internal("vault key manager: failed to generate key: " + err.Error())
|
||||||
}
|
}
|
||||||
privateKeyBytes := crypto.FromECDSA(privateKey)
|
privateKeyBytes := crypto.FromECDSA(privateKey)
|
||||||
@@ -113,9 +114,9 @@ func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string,
|
|||||||
publicKeyHex := hex.EncodeToString(publicKeyBytes)
|
publicKeyHex := hex.EncodeToString(publicKeyBytes)
|
||||||
address := crypto.PubkeyToAddress(publicKey).Hex()
|
address := crypto.PubkeyToAddress(publicKey).Hex()
|
||||||
|
|
||||||
err = m.persistKey(ctx, walletRef, network, privateKeyBytes, publicKeyBytes, address)
|
err = m.persistKey(ctx, walletRef, string(network), privateKeyBytes, publicKeyBytes, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.logger.Warn("Failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
|
m.logger.Warn("Failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", string(network)), zap.Error(err))
|
||||||
zeroBytes(privateKeyBytes)
|
zeroBytes(privateKeyBytes)
|
||||||
zeroBytes(publicKeyBytes)
|
zeroBytes(publicKeyBytes)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -125,12 +126,12 @@ func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string,
|
|||||||
|
|
||||||
m.logger.Info("Managed wallet key created",
|
m.logger.Info("Managed wallet key created",
|
||||||
zap.String("wallet_ref", walletRef),
|
zap.String("wallet_ref", walletRef),
|
||||||
zap.String("network", network),
|
zap.String("network", string(network)),
|
||||||
zap.String("address", strings.ToLower(address)),
|
zap.String("address", strings.ToLower(address)),
|
||||||
)
|
)
|
||||||
|
|
||||||
return &keymanager.ManagedWalletKey{
|
return &keymanager.ManagedWalletKey{
|
||||||
KeyID: m.buildKeyID(network, walletRef),
|
KeyID: m.buildKeyID(string(network), walletRef),
|
||||||
Address: strings.ToLower(address),
|
Address: strings.ToLower(address),
|
||||||
PublicKey: publicKeyHex,
|
PublicKey: publicKeyHex,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/server/grpcapp"
|
"github.com/tech/sendico/pkg/server/grpcapp"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -48,7 +49,7 @@ type config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type chainConfig struct {
|
type chainConfig struct {
|
||||||
Name string `yaml:"name"`
|
Name pmodel.ChainNetwork `yaml:"name"`
|
||||||
RPCURLEnv string `yaml:"rpc_url_env"`
|
RPCURLEnv string `yaml:"rpc_url_env"`
|
||||||
ChainID uint64 `yaml:"chain_id"`
|
ChainID uint64 `yaml:"chain_id"`
|
||||||
NativeToken string `yaml:"native_token"`
|
NativeToken string `yaml:"native_token"`
|
||||||
@@ -57,10 +58,10 @@ type chainConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type serviceWalletConfig struct {
|
type serviceWalletConfig struct {
|
||||||
Chain string `yaml:"chain"`
|
Chain pmodel.ChainNetwork `yaml:"chain"`
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address"`
|
||||||
AddressEnv string `yaml:"address_env"`
|
AddressEnv string `yaml:"address_env"`
|
||||||
PrivateKeyEnv string `yaml:"private_key_env"`
|
PrivateKeyEnv string `yaml:"private_key_env"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type tokenConfig struct {
|
type tokenConfig struct {
|
||||||
@@ -209,20 +210,16 @@ func (i *Imp) loadConfig() (*config, error) {
|
|||||||
func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatewayshared.Network, error) {
|
func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatewayshared.Network, error) {
|
||||||
result := make([]gatewayshared.Network, 0, len(chains))
|
result := make([]gatewayshared.Network, 0, len(chains))
|
||||||
for _, chain := range chains {
|
for _, chain := range chains {
|
||||||
if strings.TrimSpace(chain.Name) == "" {
|
|
||||||
logger.Warn("Skipping unnamed chain configuration")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rpcURL := strings.TrimSpace(os.Getenv(chain.RPCURLEnv))
|
rpcURL := strings.TrimSpace(os.Getenv(chain.RPCURLEnv))
|
||||||
if rpcURL == "" {
|
if rpcURL == "" {
|
||||||
logger.Error("RPC url not configured", zap.String("chain", chain.Name), zap.String("env", chain.RPCURLEnv))
|
logger.Error("RPC url not configured", zap.String("chain", string(chain.Name)), zap.String("env", chain.RPCURLEnv))
|
||||||
return nil, merrors.InvalidArgument(fmt.Sprintf("chain RPC endpoint not configured (chain=%s env=%s)", chain.Name, chain.RPCURLEnv))
|
return nil, merrors.InvalidArgument(fmt.Sprintf("chain RPC endpoint not configured (chain=%s env=%s)", chain.Name, chain.RPCURLEnv))
|
||||||
}
|
}
|
||||||
contracts := make([]gatewayshared.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 == "" {
|
||||||
logger.Warn("Skipping token with empty symbol", zap.String("chain", chain.Name))
|
logger.Warn("Skipping token with empty symbol", zap.String("chain", string(chain.Name)))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
addr := strings.TrimSpace(token.Contract)
|
addr := strings.TrimSpace(token.Contract)
|
||||||
@@ -232,9 +229,9 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
|
|||||||
}
|
}
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
if env != "" {
|
if env != "" {
|
||||||
logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("env", env), zap.String("chain", chain.Name))
|
logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("env", env), zap.String("chain", string(chain.Name)))
|
||||||
} else {
|
} else {
|
||||||
logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("chain", chain.Name))
|
logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("chain", string(chain.Name)))
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -246,7 +243,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
|
|||||||
|
|
||||||
gasPolicy, err := buildGasTopUpPolicy(chain.Name, chain.GasTopUpPolicy)
|
gasPolicy, err := buildGasTopUpPolicy(chain.Name, chain.GasTopUpPolicy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Invalid gas top-up policy", zap.String("chain", chain.Name), zap.Error(err))
|
logger.Error("Invalid gas top-up policy", zap.String("chain", string(chain.Name)), zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +259,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildGasTopUpPolicy(chainName string, cfg *gasTopUpPolicyConfig) (*gatewayshared.GasTopUpPolicy, error) {
|
func buildGasTopUpPolicy(chainName pmodel.ChainNetwork, cfg *gasTopUpPolicyConfig) (*gatewayshared.GasTopUpPolicy, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -300,7 +297,7 @@ func buildGasTopUpPolicy(chainName string, cfg *gasTopUpPolicyConfig) (*gateways
|
|||||||
return policy, nil
|
return policy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseGasTopUpRule(chainName, label string, cfg gasTopUpRuleConfig) (gatewayshared.GasTopUpRule, bool, error) {
|
func parseGasTopUpRule(chainName pmodel.ChainNetwork, label string, cfg gasTopUpRuleConfig) (gatewayshared.GasTopUpRule, bool, error) {
|
||||||
if cfg.BufferPercent == 0 && cfg.MinNativeBalanceTRX == 0 && cfg.RoundingUnitTRX == 0 && cfg.MaxTopUpTRX == 0 {
|
if cfg.BufferPercent == 0 && cfg.MinNativeBalanceTRX == 0 && cfg.RoundingUnitTRX == 0 && cfg.MaxTopUpTRX == 0 {
|
||||||
return gatewayshared.GasTopUpRule{}, false, nil
|
return gatewayshared.GasTopUpRule{}, false, nil
|
||||||
}
|
}
|
||||||
@@ -336,7 +333,7 @@ func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewa
|
|||||||
if cfg.AddressEnv != "" {
|
if cfg.AddressEnv != "" {
|
||||||
logger.Warn("Service wallet address not configured", zap.String("env", cfg.AddressEnv))
|
logger.Warn("Service wallet address not configured", zap.String("env", cfg.AddressEnv))
|
||||||
} else {
|
} else {
|
||||||
logger.Warn("Service wallet address not configured", zap.String("chain", cfg.Chain))
|
logger.Warn("Service wallet address not configured", zap.String("chain", string(cfg.Chain)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if privateKey == "" {
|
if privateKey == "" {
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ func resolveDestination(ctx context.Context, deps Deps, dest *chainv1.TransferDe
|
|||||||
deps.Logger.Warn("Destination wallet lookup failed", zap.Error(err), zap.String("managed_wallet_ref", managedRef))
|
deps.Logger.Warn("Destination wallet lookup failed", zap.Error(err), zap.String("managed_wallet_ref", managedRef))
|
||||||
return model.TransferDestination{}, err
|
return model.TransferDestination{}, err
|
||||||
}
|
}
|
||||||
if !strings.EqualFold(wallet.Network, source.Network) {
|
if wallet.Network != source.Network {
|
||||||
deps.Logger.Warn("Destination wallet network mismatch", zap.String("source_network", source.Network), zap.String("dest_network", wallet.Network))
|
deps.Logger.Warn("Destination wallet network mismatch", zap.String("source_network", string(source.Network)), zap.String("dest_network", string(wallet.Network)))
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet network mismatch")
|
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet network mismatch")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
||||||
@@ -44,12 +44,12 @@ func resolveDestination(ctx context.Context, deps Deps, dest *chainv1.TransferDe
|
|||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
||||||
}
|
}
|
||||||
if deps.Drivers == nil {
|
if deps.Drivers == nil {
|
||||||
deps.Logger.Warn("Chain drivers missing", zap.String("network", source.Network))
|
deps.Logger.Warn("Chain drivers missing", zap.String("network", string(source.Network)))
|
||||||
return model.TransferDestination{}, merrors.Internal("chain drivers not configured")
|
return model.TransferDestination{}, merrors.Internal("chain drivers not configured")
|
||||||
}
|
}
|
||||||
chainDriver, err := deps.Drivers.Driver(source.Network)
|
chainDriver, err := deps.Drivers.Driver(source.Network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
deps.Logger.Warn("Unsupported chain driver", zap.String("network", source.Network), zap.Error(err))
|
deps.Logger.Warn("Unsupported chain driver", zap.String("network", string(source.Network)), zap.Error(err))
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("unsupported chain for wallet")
|
return model.TransferDestination{}, merrors.InvalidArgument("unsupported chain for wallet")
|
||||||
}
|
}
|
||||||
normalized, err := chainDriver.NormalizeAddress(external)
|
normalized, err := chainDriver.NormalizeAddress(external)
|
||||||
|
|||||||
@@ -53,19 +53,18 @@ func (c *estimateTransferFeeCommand) Execute(ctx context.Context, req *chainv1.E
|
|||||||
return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
|
networkCfg, ok := c.deps.Networks.Network(sourceWallet.Network)
|
||||||
networkCfg, ok := c.deps.Networks.Network(networkKey)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
c.deps.Logger.Warn("Unsupported chain", zap.String("network", networkKey))
|
c.deps.Logger.Warn("Unsupported chain", zap.String("network", string(sourceWallet.Network)))
|
||||||
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
||||||
}
|
}
|
||||||
if c.deps.Drivers == nil {
|
if c.deps.Drivers == nil {
|
||||||
c.deps.Logger.Warn("Chain drivers missing", zap.String("network", networkKey))
|
c.deps.Logger.Warn("Chain drivers missing", zap.String("network", string(sourceWallet.Network)))
|
||||||
return gsresponse.Internal[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("chain drivers not configured"))
|
return gsresponse.Internal[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("chain drivers not configured"))
|
||||||
}
|
}
|
||||||
chainDriver, err := c.deps.Drivers.Driver(networkKey)
|
chainDriver, err := c.deps.Drivers.Driver(sourceWallet.Network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.deps.Logger.Warn("Unsupported chain driver", zap.String("network", networkKey), zap.Error(err))
|
c.deps.Logger.Warn("Unsupported chain driver", zap.String("network", string(sourceWallet.Network)), zap.Error(err))
|
||||||
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,9 +125,9 @@ func (c *ensureGasTopUpCommand) Execute(ctx context.Context, req *chainv1.Ensure
|
|||||||
Destination: &chainv1.TransferDestination{
|
Destination: &chainv1.TransferDestination{
|
||||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: targetWalletRef},
|
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: targetWalletRef},
|
||||||
},
|
},
|
||||||
Amount: topUp,
|
Amount: topUp,
|
||||||
Metadata: shared.CloneMetadata(req.GetMetadata()),
|
Metadata: shared.CloneMetadata(req.GetMetadata()),
|
||||||
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
|
||||||
}
|
}
|
||||||
|
|
||||||
submitResponder := NewSubmitTransfer(c.deps.WithLogger("transfer.submit")).Execute(ctx, submitReq)
|
submitResponder := NewSubmitTransfer(c.deps.WithLogger("transfer.submit")).Execute(ctx, submitReq)
|
||||||
@@ -152,12 +152,7 @@ func computeGasTopUp(ctx context.Context, deps Deps, walletRef string, estimated
|
|||||||
return nil, false, nil, nil, err
|
return nil, false, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
networkKey := strings.ToLower(strings.TrimSpace(walletModel.Network))
|
networkCfg, ok := deps.Networks.Network(walletModel.Network)
|
||||||
if strings.HasPrefix(networkKey, "tron") {
|
|
||||||
return nil, false, nil, nil, merrors.InvalidArgument("tron networks must use the tron gateway")
|
|
||||||
}
|
|
||||||
|
|
||||||
networkCfg, ok := deps.Networks.Network(networkKey)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false, nil, nil, merrors.InvalidArgument("unsupported chain for wallet")
|
return nil, false, nil, nil, merrors.InvalidArgument("unsupported chain for wallet")
|
||||||
}
|
}
|
||||||
@@ -248,7 +243,7 @@ func logDecision(logger mlogger.Logger, walletRef string, estimatedFee *moneyv1.
|
|||||||
zap.Bool("cap_hit", capHit),
|
zap.Bool("cap_hit", capHit),
|
||||||
}
|
}
|
||||||
if walletModel != nil {
|
if walletModel != nil {
|
||||||
fields = append(fields, zap.String("network", strings.TrimSpace(walletModel.Network)))
|
fields = append(fields, zap.String("network", string(walletModel.Network)))
|
||||||
}
|
}
|
||||||
logger.Info("Gas top-up decision", fields...)
|
logger.Info("Gas top-up decision", fields...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ func toProtoTransfer(transfer *model.Transfer) *chainv1.Transfer {
|
|||||||
SourceWalletRef: transfer.SourceWalletRef,
|
SourceWalletRef: transfer.SourceWalletRef,
|
||||||
Destination: destination,
|
Destination: destination,
|
||||||
Asset: asset,
|
Asset: asset,
|
||||||
RequestedAmount: shared.CloneMoney(transfer.RequestedAmount),
|
RequestedAmount: shared.MonenyToProto(transfer.RequestedAmount),
|
||||||
NetAmount: shared.CloneMoney(transfer.NetAmount),
|
NetAmount: shared.MonenyToProto(transfer.NetAmount),
|
||||||
Fees: protoFees,
|
Fees: protoFees,
|
||||||
Status: shared.TransferStatusToProto(transfer.Status),
|
Status: shared.TransferStatusToProto(transfer.Status),
|
||||||
TransactionHash: transfer.TxHash,
|
TransactionHash: transfer.TxHash,
|
||||||
|
|||||||
@@ -77,10 +77,9 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
|||||||
c.deps.Logger.Warn("Organization mismatch", zap.String("wallet_org", sourceWallet.OrganizationRef), zap.String("req_org", 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"))
|
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.Network(sourceWallet.Network)
|
||||||
networkCfg, ok := c.deps.Networks.Network(networkKey)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
c.deps.Logger.Warn("Unsupported chain", zap.String("network", networkKey))
|
c.deps.Logger.Warn("Unsupported chain", zap.String("network", string(sourceWallet.Network)))
|
||||||
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,17 +123,19 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
|||||||
transfer := &model.Transfer{
|
transfer := &model.Transfer{
|
||||||
IdempotencyKey: idempotencyKey,
|
IdempotencyKey: idempotencyKey,
|
||||||
TransferRef: shared.GenerateTransferRef(),
|
TransferRef: shared.GenerateTransferRef(),
|
||||||
|
IntentRef: req.IntentRef,
|
||||||
|
OperationRef: req.OperationRef,
|
||||||
OrganizationRef: organizationRef,
|
OrganizationRef: organizationRef,
|
||||||
SourceWalletRef: sourceWalletRef,
|
SourceWalletRef: sourceWalletRef,
|
||||||
Destination: destination,
|
Destination: destination,
|
||||||
Network: sourceWallet.Network,
|
Network: sourceWallet.Network,
|
||||||
TokenSymbol: effectiveTokenSymbol,
|
TokenSymbol: effectiveTokenSymbol,
|
||||||
ContractAddress: effectiveContractAddress,
|
ContractAddress: effectiveContractAddress,
|
||||||
RequestedAmount: shared.CloneMoney(amount),
|
RequestedAmount: shared.ProtoToMoney(amount),
|
||||||
NetAmount: netAmount,
|
NetAmount: shared.ProtoToMoney(netAmount),
|
||||||
Fees: fees,
|
Fees: fees,
|
||||||
Status: model.TransferStatusPending,
|
Status: model.TransferStatusCreated,
|
||||||
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
|
||||||
LastStatusAt: c.deps.Clock.Now().UTC(),
|
LastStatusAt: c.deps.Clock.Now().UTC(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,23 +51,19 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C
|
|||||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required"))
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
chainKey, _ := shared.ChainKeyFromEnum(asset.GetChain())
|
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.Network(chainKey)
|
networkCfg, ok := c.deps.Networks.Network(chainKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
c.deps.Logger.Warn("Unsupported chain in config", zap.String("chain", chainKey))
|
c.deps.Logger.Warn("Unsupported chain in config", zap.String("chain", string(chainKey)))
|
||||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
||||||
}
|
}
|
||||||
if c.deps.Drivers == nil {
|
if c.deps.Drivers == nil {
|
||||||
c.deps.Logger.Warn("Chain drivers missing", zap.String("chain", chainKey))
|
c.deps.Logger.Warn("Chain drivers missing", zap.String("chain", string(chainKey)))
|
||||||
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("chain drivers not configured"))
|
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("chain drivers not configured"))
|
||||||
}
|
}
|
||||||
chainDriver, err := c.deps.Drivers.Driver(chainKey)
|
chainDriver, err := c.deps.Drivers.Driver(chainKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.deps.Logger.Warn("Unsupported chain driver", zap.String("chain", chainKey), zap.Error(err))
|
c.deps.Logger.Warn("Unsupported chain driver", zap.String("chain", string(chainKey)), zap.Error(err))
|
||||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +77,7 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C
|
|||||||
if !strings.EqualFold(tokenSymbol, networkCfg.NativeToken) {
|
if !strings.EqualFold(tokenSymbol, networkCfg.NativeToken) {
|
||||||
contractAddress = shared.ResolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
|
contractAddress = shared.ResolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
|
||||||
if contractAddress == "" {
|
if contractAddress == "" {
|
||||||
c.deps.Logger.Warn("Unsupported token", zap.String("token", tokenSymbol), zap.String("chain", chainKey))
|
c.deps.Logger.Warn("Unsupported token", zap.String("token", tokenSymbol), zap.String("chain", string(chainKey)))
|
||||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain"))
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (c *listManagedWalletsCommand) Execute(ctx context.Context, req *chainv1.Li
|
|||||||
filter.OwnerRefFilter = &ownerRef
|
filter.OwnerRefFilter = &ownerRef
|
||||||
}
|
}
|
||||||
if asset := req.GetAsset(); asset != nil {
|
if asset := req.GetAsset(); asset != nil {
|
||||||
filter.Network, _ = shared.ChainKeyFromEnum(asset.GetChain())
|
filter.Network = shared.ChainKeyFromEnum(asset.GetChain())
|
||||||
filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol())
|
filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol())
|
||||||
}
|
}
|
||||||
if page := req.GetPage(); page != nil {
|
if page := req.GetPage(); page != nil {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package wallet
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||||
"github.com/tech/sendico/gateway/chain/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
@@ -24,21 +23,20 @@ func OnChainWalletBalances(ctx context.Context, deps Deps, wallet *model.Managed
|
|||||||
return nil, nil, merrors.Internal("chain drivers not configured")
|
return nil, nil, merrors.Internal("chain drivers not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
networkKey := strings.ToLower(strings.TrimSpace(wallet.Network))
|
network, ok := deps.Networks.Network(wallet.Network)
|
||||||
network, ok := deps.Networks.Network(networkKey)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Warn("Requested network is not configured",
|
logger.Warn("Requested network is not configured",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", networkKey),
|
zap.String("network", string(wallet.Network)),
|
||||||
)
|
)
|
||||||
return nil, nil, merrors.Internal(fmt.Sprintf("Requested network '%s' is not configured", networkKey))
|
return nil, nil, merrors.Internal(fmt.Sprintf("Requested network '%s' is not configured", wallet.Network))
|
||||||
}
|
}
|
||||||
|
|
||||||
chainDriver, err := deps.Drivers.Driver(networkKey)
|
chainDriver, err := deps.Drivers.Driver(wallet.Network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Chain driver not configured",
|
logger.Warn("Chain driver not configured",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", networkKey),
|
zap.String("network", string(wallet.Network)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, nil, merrors.InvalidArgument("unsupported chain")
|
return nil, nil, merrors.InvalidArgument("unsupported chain")
|
||||||
|
|||||||
@@ -165,11 +165,13 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
OrganizationRef: orgRef,
|
OrganizationRef: orgRef,
|
||||||
SourceWalletRef: source,
|
SourceWalletRef: source,
|
||||||
|
IntentRef: strings.TrimSpace(op.GetIntentRef()),
|
||||||
|
OperationRef: op.GetOperationRef(),
|
||||||
Destination: dest,
|
Destination: dest,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
Fees: parseChainFees(reader),
|
Fees: parseChainFees(reader),
|
||||||
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||||||
ClientReference: strings.TrimSpace(reader.String("client_reference")),
|
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
@@ -208,7 +210,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
return &connectorv1.SubmitOperationResponse{
|
return &connectorv1.SubmitOperationResponse{
|
||||||
Receipt: &connectorv1.OperationReceipt{
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
OperationId: opID,
|
OperationId: opID,
|
||||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
|
||||||
Result: result,
|
Result: result,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -238,7 +240,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
return &connectorv1.SubmitOperationResponse{
|
return &connectorv1.SubmitOperationResponse{
|
||||||
Receipt: &connectorv1.OperationReceipt{
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
OperationId: opID,
|
OperationId: opID,
|
||||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
|
||||||
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), ""),
|
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), ""),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -256,12 +258,14 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
}
|
}
|
||||||
resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
|
resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
|
||||||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
|
IntentRef: strings.TrimSpace(op.GetIntentRef()),
|
||||||
|
OperationRef: strings.TrimSpace(op.GetOperationRef()),
|
||||||
OrganizationRef: orgRef,
|
OrganizationRef: orgRef,
|
||||||
SourceWalletRef: source,
|
SourceWalletRef: source,
|
||||||
TargetWalletRef: target,
|
TargetWalletRef: target,
|
||||||
EstimatedTotalFee: fee,
|
EstimatedTotalFee: fee,
|
||||||
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||||||
ClientReference: strings.TrimSpace(reader.String("client_reference")),
|
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
@@ -273,7 +277,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
return &connectorv1.SubmitOperationResponse{
|
return &connectorv1.SubmitOperationResponse{
|
||||||
Receipt: &connectorv1.OperationReceipt{
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
OperationId: opID,
|
OperationId: opID,
|
||||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
Status: shared.СhainTransferStatusToOperation(resp.GetTransfer().GetStatus()),
|
||||||
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), transferRef),
|
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), transferRef),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -544,25 +548,51 @@ func chainTransferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation
|
|||||||
|
|
||||||
func chainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
func chainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
|
||||||
return connectorv1.OperationStatus_CONFIRMED
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CREATED
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_PROCESSING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_WAITING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_SUCCESS
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
return connectorv1.OperationStatus_FAILED
|
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
return connectorv1.OperationStatus_CANCELED
|
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return connectorv1.OperationStatus_PENDING
|
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chainStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
func chainStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case connectorv1.OperationStatus_CONFIRMED:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
case connectorv1.OperationStatus_OPERATION_CREATED:
|
||||||
case connectorv1.OperationStatus_FAILED:
|
return chainv1.TransferStatus_TRANSFER_CREATED
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_PROCESSING:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_PROCESSING
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_WAITING:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_WAITING
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_SUCCESS:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_SUCCESS
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_FAILED:
|
||||||
return chainv1.TransferStatus_TRANSFER_FAILED
|
return chainv1.TransferStatus_TRANSFER_FAILED
|
||||||
case connectorv1.OperationStatus_CANCELED:
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_CANCELLED:
|
||||||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func (d *Driver) NormalizeAddress(address string) (string, error) {
|
|||||||
func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||||
d.logger.Debug("Balance request",
|
d.logger.Debug("Balance request",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
driverDeps := deps
|
driverDeps := deps
|
||||||
driverDeps.Logger = d.logger
|
driverDeps.Logger = d.logger
|
||||||
@@ -55,13 +55,13 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Warn("Balance failed",
|
d.logger.Warn("Balance failed",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
} else if result != nil {
|
} else if result != nil {
|
||||||
d.logger.Debug("Balance result",
|
d.logger.Debug("Balance result",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("amount", result.Amount),
|
zap.String("amount", result.Amount),
|
||||||
zap.String("currency", result.Currency),
|
zap.String("currency", result.Currency),
|
||||||
)
|
)
|
||||||
@@ -72,7 +72,7 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
|
|||||||
func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||||
d.logger.Debug("Native balance request",
|
d.logger.Debug("Native balance request",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
driverDeps := deps
|
driverDeps := deps
|
||||||
driverDeps.Logger = d.logger
|
driverDeps.Logger = d.logger
|
||||||
@@ -80,13 +80,13 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Warn("Native balance failed",
|
d.logger.Warn("Native balance failed",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
} else if result != nil {
|
} else if result != nil {
|
||||||
d.logger.Debug("Native balance result",
|
d.logger.Debug("Native balance result",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("amount", result.Amount),
|
zap.String("amount", result.Amount),
|
||||||
zap.String("currency", result.Currency),
|
zap.String("currency", result.Currency),
|
||||||
)
|
)
|
||||||
@@ -97,7 +97,7 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
|
|||||||
func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
|
func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
|
||||||
d.logger.Debug("Estimate fee request",
|
d.logger.Debug("Estimate fee request",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("destination", destination),
|
zap.String("destination", destination),
|
||||||
)
|
)
|
||||||
driverDeps := deps
|
driverDeps := deps
|
||||||
@@ -106,13 +106,13 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Warn("Estimate fee failed",
|
d.logger.Warn("Estimate fee failed",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
} else if result != nil {
|
} else if result != nil {
|
||||||
d.logger.Debug("Estimate fee result",
|
d.logger.Debug("Estimate fee result",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("amount", result.Amount),
|
zap.String("amount", result.Amount),
|
||||||
zap.String("currency", result.Currency),
|
zap.String("currency", result.Currency),
|
||||||
)
|
)
|
||||||
@@ -123,7 +123,7 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
|
|||||||
func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, destination string) (string, error) {
|
func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, destination string) (string, error) {
|
||||||
d.logger.Debug("Submit transfer request",
|
d.logger.Debug("Submit transfer request",
|
||||||
zap.String("transfer_ref", transfer.TransferRef),
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("destination", destination),
|
zap.String("destination", destination),
|
||||||
)
|
)
|
||||||
driverDeps := deps
|
driverDeps := deps
|
||||||
@@ -132,13 +132,13 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Warn("Submit transfer failed",
|
d.logger.Warn("Submit transfer failed",
|
||||||
zap.String("transfer_ref", transfer.TransferRef),
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
d.logger.Debug("Submit transfer result",
|
d.logger.Debug("Submit transfer result",
|
||||||
zap.String("transfer_ref", transfer.TransferRef),
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
|
|||||||
func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Network, txHash string) (*types.Receipt, error) {
|
func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Network, txHash string) (*types.Receipt, error) {
|
||||||
d.logger.Debug("Await confirmation",
|
d.logger.Debug("Await confirmation",
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
driverDeps := deps
|
driverDeps := deps
|
||||||
driverDeps.Logger = d.logger
|
driverDeps.Logger = d.logger
|
||||||
@@ -156,13 +156,13 @@ func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, networ
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Warn("Await confirmation failed",
|
d.logger.Warn("Await confirmation failed",
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
} else if receipt != nil {
|
} else if receipt != nil {
|
||||||
d.logger.Debug("Await confirmation result",
|
d.logger.Debug("Await confirmation result",
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
||||||
zap.Uint64("status", receipt.Status),
|
zap.Uint64("status", receipt.Status),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func (d *Driver) NormalizeAddress(address string) (string, error) {
|
|||||||
func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||||
d.logger.Debug("Balance request",
|
d.logger.Debug("Balance request",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
driverDeps := deps
|
driverDeps := deps
|
||||||
driverDeps.Logger = d.logger
|
driverDeps.Logger = d.logger
|
||||||
@@ -55,13 +55,13 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Warn("Balance failed",
|
d.logger.Warn("Balance failed",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
} else if result != nil {
|
} else if result != nil {
|
||||||
d.logger.Debug("Balance result",
|
d.logger.Debug("Balance result",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("amount", result.Amount),
|
zap.String("amount", result.Amount),
|
||||||
zap.String("currency", result.Currency),
|
zap.String("currency", result.Currency),
|
||||||
)
|
)
|
||||||
@@ -72,7 +72,7 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
|
|||||||
func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||||
d.logger.Debug("Native balance request",
|
d.logger.Debug("Native balance request",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
driverDeps := deps
|
driverDeps := deps
|
||||||
driverDeps.Logger = d.logger
|
driverDeps.Logger = d.logger
|
||||||
@@ -80,13 +80,13 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Warn("Native balance failed",
|
d.logger.Warn("Native balance failed",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
} else if result != nil {
|
} else if result != nil {
|
||||||
d.logger.Debug("Native balance result",
|
d.logger.Debug("Native balance result",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("amount", result.Amount),
|
zap.String("amount", result.Amount),
|
||||||
zap.String("currency", result.Currency),
|
zap.String("currency", result.Currency),
|
||||||
)
|
)
|
||||||
@@ -97,7 +97,7 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
|
|||||||
func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
|
func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
|
||||||
d.logger.Debug("Estimate fee request",
|
d.logger.Debug("Estimate fee request",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("destination", destination),
|
zap.String("destination", destination),
|
||||||
)
|
)
|
||||||
driverDeps := deps
|
driverDeps := deps
|
||||||
@@ -106,13 +106,13 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Warn("Estimate fee failed",
|
d.logger.Warn("Estimate fee failed",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
} else if result != nil {
|
} else if result != nil {
|
||||||
d.logger.Debug("Estimate fee result",
|
d.logger.Debug("Estimate fee result",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("amount", result.Amount),
|
zap.String("amount", result.Amount),
|
||||||
zap.String("currency", result.Currency),
|
zap.String("currency", result.Currency),
|
||||||
)
|
)
|
||||||
@@ -123,7 +123,7 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
|
|||||||
func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, destination string) (string, error) {
|
func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, destination string) (string, error) {
|
||||||
d.logger.Debug("Submit transfer request",
|
d.logger.Debug("Submit transfer request",
|
||||||
zap.String("transfer_ref", transfer.TransferRef),
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("destination", destination),
|
zap.String("destination", destination),
|
||||||
)
|
)
|
||||||
driverDeps := deps
|
driverDeps := deps
|
||||||
@@ -132,13 +132,13 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Warn("Submit transfer failed",
|
d.logger.Warn("Submit transfer failed",
|
||||||
zap.String("transfer_ref", transfer.TransferRef),
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
d.logger.Debug("Submit transfer result",
|
d.logger.Debug("Submit transfer result",
|
||||||
zap.String("transfer_ref", transfer.TransferRef),
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
|
|||||||
func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Network, txHash string) (*types.Receipt, error) {
|
func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Network, txHash string) (*types.Receipt, error) {
|
||||||
d.logger.Debug("Await confirmation",
|
d.logger.Debug("Await confirmation",
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
driverDeps := deps
|
driverDeps := deps
|
||||||
driverDeps.Logger = d.logger
|
driverDeps.Logger = d.logger
|
||||||
@@ -156,13 +156,13 @@ func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, networ
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
d.logger.Warn("Await confirmation failed",
|
d.logger.Warn("Await confirmation failed",
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
} else if receipt != nil {
|
} else if receipt != nil {
|
||||||
d.logger.Debug("Await confirmation result",
|
d.logger.Debug("Await confirmation result",
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
||||||
zap.Uint64("status", receipt.Status),
|
zap.Uint64("status", receipt.Status),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func NormalizeAddress(address string) (string, error) {
|
|||||||
func nativeCurrency(network shared.Network) string {
|
func nativeCurrency(network shared.Network) string {
|
||||||
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
||||||
if currency == "" {
|
if currency == "" {
|
||||||
currency = strings.ToUpper(network.Name)
|
currency = strings.ToUpper(string(network.Name))
|
||||||
}
|
}
|
||||||
return currency
|
return currency
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ func Balance(ctx context.Context, deps driver.Deps, network shared.Network, wall
|
|||||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
logFields := []zap.Field{
|
logFields := []zap.Field{
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", strings.ToLower(strings.TrimSpace(network.Name))),
|
zap.String("network", strings.ToLower(strings.TrimSpace(string(network.Name)))),
|
||||||
zap.String("token_symbol", strings.ToUpper(strings.TrimSpace(wallet.TokenSymbol))),
|
zap.String("token_symbol", strings.ToUpper(strings.TrimSpace(wallet.TokenSymbol))),
|
||||||
zap.String("contract", strings.ToLower(strings.TrimSpace(wallet.ContractAddress))),
|
zap.String("contract", strings.ToLower(strings.TrimSpace(wallet.ContractAddress))),
|
||||||
zap.String("wallet_address", normalizedAddress),
|
zap.String("wallet_address", normalizedAddress),
|
||||||
@@ -194,7 +194,7 @@ func NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network
|
|||||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
logFields := []zap.Field{
|
logFields := []zap.Field{
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", strings.ToLower(strings.TrimSpace(network.Name))),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("wallet_address", normalizedAddress),
|
zap.String("wallet_address", normalizedAddress),
|
||||||
}
|
}
|
||||||
if rpcURL == "" {
|
if rpcURL == "" {
|
||||||
@@ -260,12 +260,12 @@ func EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network,
|
|||||||
|
|
||||||
client, err := registry.Client(network.Name)
|
client, err := registry.Client(network.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Failed to resolve client", zap.Error(err), zap.String("network_name", network.Name))
|
logger.Warn("Failed to resolve client", zap.Error(err), zap.String("network_name", string(network.Name)))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rpcClient, err := registry.RPCClient(network.Name)
|
rpcClient, err := registry.RPCClient(network.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Failed to resolve RPC client", zap.Error(err), zap.String("network_name", network.Name))
|
logger.Warn("Failed to resolve RPC client", zap.Error(err), zap.String("network_name", string(network.Name)))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +374,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
|
|||||||
}
|
}
|
||||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
if rpcURL == "" {
|
if rpcURL == "" {
|
||||||
logger.Warn("Network rpc url missing", zap.String("network", network.Name))
|
logger.Warn("Network rpc url missing", zap.String("network", string(network.Name)))
|
||||||
return "", executorInvalid("network rpc url is not configured")
|
return "", executorInvalid("network rpc url is not configured")
|
||||||
}
|
}
|
||||||
if source == nil || transfer == nil {
|
if source == nil || transfer == nil {
|
||||||
@@ -397,18 +397,18 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
|
|||||||
logger.Info("Submitting transfer",
|
logger.Info("Submitting transfer",
|
||||||
zap.String("transfer_ref", transfer.TransferRef),
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
zap.String("source_wallet_ref", source.WalletRef),
|
zap.String("source_wallet_ref", source.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("destination", strings.ToLower(destination)),
|
zap.String("destination", strings.ToLower(destination)),
|
||||||
)
|
)
|
||||||
|
|
||||||
client, err := registry.Client(network.Name)
|
client, err := registry.Client(network.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Failed to initialise RPC client", zap.Error(err), zap.String("network", network.Name))
|
logger.Warn("Failed to initialise RPC client", zap.Error(err), zap.String("network", string(network.Name)))
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
rpcClient, err := registry.RPCClient(network.Name)
|
rpcClient, err := registry.RPCClient(network.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Failed to initialise RPC client", zap.String("network", network.Name))
|
logger.Warn("Failed to initialise RPC client", zap.String("network", string(network.Name)))
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +429,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
|
|||||||
gasPrice, err := client.SuggestGasPrice(ctx)
|
gasPrice, err := client.SuggestGasPrice(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("Failed to suggest gas price", zap.Error(err),
|
logger.Warn("Failed to suggest gas price", zap.Error(err),
|
||||||
zap.String("transfer_ref", transfer.TransferRef), zap.String("network", network.Name),
|
zap.String("transfer_ref", transfer.TransferRef), zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
return "", executorInternal("failed to suggest gas price", err)
|
return "", executorInternal("failed to suggest gas price", err)
|
||||||
}
|
}
|
||||||
@@ -532,7 +532,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
|
|||||||
|
|
||||||
txHash := signedTx.Hash().Hex()
|
txHash := signedTx.Hash().Hex()
|
||||||
logger.Info("Transaction submitted", zap.String("transfer_ref", transfer.TransferRef),
|
logger.Info("Transaction submitted", zap.String("transfer_ref", transfer.TransferRef),
|
||||||
zap.String("tx_hash", txHash), zap.String("network", network.Name),
|
zap.String("tx_hash", txHash), zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
|
|
||||||
return txHash, nil
|
return txHash, nil
|
||||||
@@ -544,7 +544,7 @@ func AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Net
|
|||||||
registry := deps.Registry
|
registry := deps.Registry
|
||||||
|
|
||||||
if strings.TrimSpace(txHash) == "" {
|
if strings.TrimSpace(txHash) == "" {
|
||||||
logger.Warn("Missing transaction hash for confirmation", zap.String("network", network.Name))
|
logger.Warn("Missing transaction hash for confirmation", zap.String("network", string(network.Name)))
|
||||||
return nil, executorInvalid("tx hash is required")
|
return nil, executorInvalid("tx hash is required")
|
||||||
}
|
}
|
||||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
@@ -572,23 +572,23 @@ func AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Net
|
|||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
logger.Debug("Transaction not yet mined", zap.String("tx_hash", txHash),
|
logger.Debug("Transaction not yet mined", zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
logger.Warn("Context cancelled while awaiting confirmation", zap.String("tx_hash", txHash),
|
logger.Warn("Context cancelled while awaiting confirmation", zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.Warn("Failed to fetch transaction receipt", zap.Error(err),
|
logger.Warn("Failed to fetch transaction receipt", zap.Error(err),
|
||||||
zap.String("tx_hash", txHash), zap.String("network", network.Name),
|
zap.String("tx_hash", txHash), zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
return nil, executorInternal("failed to fetch transaction receipt", err)
|
return nil, executorInternal("failed to fetch transaction receipt", err)
|
||||||
}
|
}
|
||||||
logger.Info("Transaction confirmed", zap.String("tx_hash", txHash),
|
logger.Info("Transaction confirmed", zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name), zap.Uint64("status", receipt.Status),
|
zap.String("network", string(network.Name)), zap.Uint64("status", receipt.Status),
|
||||||
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
||||||
)
|
)
|
||||||
return receipt, nil
|
return receipt, nil
|
||||||
@@ -654,12 +654,6 @@ type gasEstimator interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func estimateGas(ctx context.Context, network shared.Network, client gasEstimator, rpcClient *rpc.Client, callMsg ethereum.CallMsg) (uint64, error) {
|
func estimateGas(ctx context.Context, network shared.Network, client gasEstimator, rpcClient *rpc.Client, callMsg ethereum.CallMsg) (uint64, error) {
|
||||||
if isTronNetwork(network) {
|
|
||||||
if rpcClient == nil {
|
|
||||||
return 0, merrors.Internal("rpc client not initialised")
|
|
||||||
}
|
|
||||||
return estimateGasTron(ctx, rpcClient, callMsg)
|
|
||||||
}
|
|
||||||
return client.EstimateGas(ctx, callMsg)
|
return client.EstimateGas(ctx, callMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,10 +696,6 @@ func tronEstimateCall(callMsg ethereum.CallMsg) map[string]string {
|
|||||||
return call
|
return call
|
||||||
}
|
}
|
||||||
|
|
||||||
func isTronNetwork(network shared.Network) bool {
|
|
||||||
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(network.Name)), "tron")
|
|
||||||
}
|
|
||||||
|
|
||||||
func toBaseUnits(amount string, decimals uint8) (*big.Int, error) {
|
func toBaseUnits(amount string, decimals uint8) (*big.Int, error) {
|
||||||
value, err := decimal.NewFromString(strings.TrimSpace(amount))
|
value, err := decimal.NewFromString(strings.TrimSpace(amount))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ func ComputeGasTopUp(network shared.Network, wallet *model.ManagedWallet, estima
|
|||||||
}
|
}
|
||||||
|
|
||||||
nativeCurrency := strings.TrimSpace(network.NativeToken)
|
nativeCurrency := strings.TrimSpace(network.NativeToken)
|
||||||
if nativeCurrency == "" {
|
|
||||||
nativeCurrency = strings.ToUpper(strings.TrimSpace(network.Name))
|
|
||||||
}
|
|
||||||
if !strings.EqualFold(nativeCurrency, estimatedFee.GetCurrency()) {
|
if !strings.EqualFold(nativeCurrency, estimatedFee.GetCurrency()) {
|
||||||
return nil, false, merrors.InvalidArgument(fmt.Sprintf("estimated fee currency mismatch (expected %s)", nativeCurrency))
|
return nil, false, merrors.InvalidArgument(fmt.Sprintf("estimated fee currency mismatch (expected %s)", nativeCurrency))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package drivers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/arbitrum"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/arbitrum"
|
||||||
@@ -10,12 +9,13 @@ import (
|
|||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Registry maps configured network keys to chain drivers.
|
// Registry maps configured network keys to chain drivers.
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
byNetwork map[string]driver.Driver
|
byNetwork map[pmodel.ChainNetwork]driver.Driver
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRegistry selects drivers for the configured networks.
|
// NewRegistry selects drivers for the configured networks.
|
||||||
@@ -23,18 +23,14 @@ func NewRegistry(logger mlogger.Logger, networks []shared.Network) (*Registry, e
|
|||||||
if logger == nil {
|
if logger == nil {
|
||||||
return nil, merrors.InvalidArgument("driver registry: logger is required")
|
return nil, merrors.InvalidArgument("driver registry: logger is required")
|
||||||
}
|
}
|
||||||
result := &Registry{byNetwork: map[string]driver.Driver{}}
|
result := &Registry{byNetwork: map[pmodel.ChainNetwork]driver.Driver{}}
|
||||||
for _, network := range networks {
|
for _, network := range networks {
|
||||||
name := strings.ToLower(strings.TrimSpace(network.Name))
|
chainDriver, err := resolveDriver(logger, network.Name)
|
||||||
if name == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
chainDriver, err := resolveDriver(logger, name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Unsupported chain driver", zap.String("network", name), zap.Error(err))
|
logger.Error("Unsupported chain driver", zap.String("network", string(network.Name)), zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result.byNetwork[name] = chainDriver
|
result.byNetwork[network.Name] = chainDriver
|
||||||
}
|
}
|
||||||
if len(result.byNetwork) == 0 {
|
if len(result.byNetwork) == 0 {
|
||||||
return nil, merrors.InvalidArgument("driver registry: no supported networks configured")
|
return nil, merrors.InvalidArgument("driver registry: no supported networks configured")
|
||||||
@@ -44,30 +40,25 @@ func NewRegistry(logger mlogger.Logger, networks []shared.Network) (*Registry, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Driver resolves a driver for the provided network key.
|
// Driver resolves a driver for the provided network key.
|
||||||
func (r *Registry) Driver(network string) (driver.Driver, error) {
|
func (r *Registry) Driver(network pmodel.ChainNetwork) (driver.Driver, error) {
|
||||||
if r == nil || len(r.byNetwork) == 0 {
|
if r == nil || len(r.byNetwork) == 0 {
|
||||||
return nil, merrors.Internal("driver registry is not configured")
|
return nil, merrors.Internal("driver registry is not configured")
|
||||||
}
|
}
|
||||||
key := strings.ToLower(strings.TrimSpace(network))
|
chainDriver, ok := r.byNetwork[network]
|
||||||
if key == "" {
|
|
||||||
return nil, merrors.InvalidArgument("network is required")
|
|
||||||
}
|
|
||||||
chainDriver, ok := r.byNetwork[key]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, merrors.InvalidArgument(fmt.Sprintf("unsupported chain network %s", key))
|
return nil, merrors.InvalidArgument(fmt.Sprintf("unsupported chain network %s", network))
|
||||||
}
|
}
|
||||||
return chainDriver, nil
|
return chainDriver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveDriver(logger mlogger.Logger, network string) (driver.Driver, error) {
|
func resolveDriver(logger mlogger.Logger, network pmodel.ChainNetwork) (driver.Driver, error) {
|
||||||
switch {
|
switch network {
|
||||||
case strings.HasPrefix(network, "tron"):
|
case pmodel.ChainNetworkArbitrumOne:
|
||||||
return nil, merrors.InvalidArgument("tron networks must use the tron gateway, not chain gateway")
|
case pmodel.ChainNetworkArbitrumSepolia:
|
||||||
case strings.HasPrefix(network, "arbitrum"):
|
|
||||||
return arbitrum.New(logger), nil
|
return arbitrum.New(logger), nil
|
||||||
case strings.HasPrefix(network, "ethereum"):
|
case pmodel.ChainNetworkEthereumMainnet:
|
||||||
return ethereum.New(logger), nil
|
return ethereum.New(logger), nil
|
||||||
default:
|
default:
|
||||||
return nil, merrors.InvalidArgument("unsupported chain network " + network)
|
|
||||||
}
|
}
|
||||||
|
return nil, merrors.InvalidArgument("unsupported chain network " + string(network))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
|
|||||||
}
|
}
|
||||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
if rpcURL == "" {
|
if rpcURL == "" {
|
||||||
o.logger.Warn("Network rpc url missing", zap.String("network", network.Name))
|
o.logger.Warn("Network rpc url missing", zap.String("network", string(network.Name)))
|
||||||
return "", executorInvalid("network rpc url is not configured")
|
return "", executorInvalid("network rpc url is not configured")
|
||||||
}
|
}
|
||||||
if source == nil || transfer == nil {
|
if source == nil || transfer == nil {
|
||||||
@@ -75,19 +75,19 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
|
|||||||
o.logger.Info("Submitting transfer",
|
o.logger.Info("Submitting transfer",
|
||||||
zap.String("transfer_ref", transfer.TransferRef),
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
zap.String("source_wallet_ref", source.WalletRef),
|
zap.String("source_wallet_ref", source.WalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.String("destination", strings.ToLower(destinationAddress)),
|
zap.String("destination", strings.ToLower(destinationAddress)),
|
||||||
)
|
)
|
||||||
|
|
||||||
client, err := o.clients.Client(network.Name)
|
client, err := o.clients.Client(network.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.logger.Warn("Failed to initialise RPC client", zap.Error(err), zap.String("network", network.Name))
|
o.logger.Warn("Failed to initialise RPC client", zap.Error(err), zap.String("network", string(network.Name)))
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
rpcClient, err := o.clients.RPCClient(network.Name)
|
rpcClient, err := o.clients.RPCClient(network.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.logger.Warn("Failed to initialise RPC client",
|
o.logger.Warn("Failed to initialise RPC client",
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return "", err
|
return "", err
|
||||||
@@ -112,7 +112,7 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
o.logger.Warn("Failed to suggest gas price",
|
o.logger.Warn("Failed to suggest gas price",
|
||||||
zap.String("transfer_ref", transfer.TransferRef),
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return "", executorInternal("failed to suggest gas price", err)
|
return "", executorInternal("failed to suggest gas price", err)
|
||||||
@@ -206,7 +206,7 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
|
|||||||
o.logger.Info("Transaction submitted",
|
o.logger.Info("Transaction submitted",
|
||||||
zap.String("transfer_ref", transfer.TransferRef),
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
|
|
||||||
return txHash, nil
|
return txHash, nil
|
||||||
@@ -214,7 +214,7 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
|
|||||||
|
|
||||||
func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network shared.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", string(network.Name)))
|
||||||
return nil, executorInvalid("tx hash is required")
|
return nil, executorInvalid("tx hash is required")
|
||||||
}
|
}
|
||||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
@@ -240,27 +240,27 @@ func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network shared.
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
o.logger.Debug("Transaction not yet mined",
|
o.logger.Debug("Transaction not yet mined",
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
o.logger.Warn("Context cancelled while awaiting confirmation",
|
o.logger.Warn("Context cancelled while awaiting confirmation",
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
o.logger.Warn("Failed to fetch transaction receipt",
|
o.logger.Warn("Failed to fetch transaction receipt",
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
return nil, executorInternal("failed to fetch transaction receipt", err)
|
return nil, executorInternal("failed to fetch transaction receipt", err)
|
||||||
}
|
}
|
||||||
o.logger.Info("Transaction confirmed",
|
o.logger.Info("Transaction confirmed",
|
||||||
zap.String("tx_hash", txHash),
|
zap.String("tx_hash", txHash),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
||||||
zap.Uint64("status", receipt.Status),
|
zap.Uint64("status", receipt.Status),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option configures the Service.
|
// Option configures the Service.
|
||||||
@@ -34,7 +35,7 @@ func WithNetworks(networks []shared.Network) Option {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.networks == nil {
|
if s.networks == nil {
|
||||||
s.networks = make(map[string]shared.Network, len(networks))
|
s.networks = make(map[pmodel.ChainNetwork]shared.Network, len(networks))
|
||||||
}
|
}
|
||||||
for _, network := range networks {
|
for _, network := range networks {
|
||||||
if network.Name == "" {
|
if network.Name == "" {
|
||||||
@@ -48,7 +49,7 @@ func WithNetworks(networks []shared.Network) Option {
|
|||||||
clone.TokenConfigs[i].Symbol = strings.ToUpper(strings.TrimSpace(clone.TokenConfigs[i].Symbol))
|
clone.TokenConfigs[i].Symbol = strings.ToUpper(strings.TrimSpace(clone.TokenConfigs[i].Symbol))
|
||||||
clone.TokenConfigs[i].ContractAddress = strings.ToLower(strings.TrimSpace(clone.TokenConfigs[i].ContractAddress))
|
clone.TokenConfigs[i].ContractAddress = strings.ToLower(strings.TrimSpace(clone.TokenConfigs[i].ContractAddress))
|
||||||
}
|
}
|
||||||
clone.Name = strings.ToLower(strings.TrimSpace(clone.Name))
|
clone.Name = clone.Name
|
||||||
s.networks[clone.Name] = clone
|
s.networks[clone.Name] = clone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ import (
|
|||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Clients holds pre-initialised RPC clients keyed by network name.
|
// Clients holds pre-initialised RPC clients keyed by network name.
|
||||||
type Clients struct {
|
type Clients struct {
|
||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
clients map[string]clientEntry
|
clients map[pmodel.ChainNetwork]clientEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientEntry struct {
|
type clientEntry struct {
|
||||||
@@ -36,25 +37,20 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
|
|||||||
clientLogger := logger.Named("rpc_client")
|
clientLogger := logger.Named("rpc_client")
|
||||||
result := &Clients{
|
result := &Clients{
|
||||||
logger: clientLogger,
|
logger: clientLogger,
|
||||||
clients: make(map[string]clientEntry),
|
clients: make(map[pmodel.ChainNetwork]clientEntry),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, network := range networks {
|
for _, network := range networks {
|
||||||
name := strings.ToLower(strings.TrimSpace(network.Name))
|
|
||||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
if name == "" {
|
|
||||||
clientLogger.Warn("Skipping network with empty name during rpc client preparation")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if rpcURL == "" {
|
if rpcURL == "" {
|
||||||
result.Close()
|
result.Close()
|
||||||
err := merrors.InvalidArgument(fmt.Sprintf("rpc url not configured for network %s", name))
|
err := merrors.InvalidArgument(fmt.Sprintf("rpc url not configured for network %s", network.Name))
|
||||||
clientLogger.Warn("Rpc url missing", zap.String("network", name))
|
clientLogger.Warn("Rpc url missing", zap.String("network", string(network.Name)))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := []zap.Field{
|
fields := []zap.Field{
|
||||||
zap.String("network", name),
|
zap.String("network", string(network.Name)),
|
||||||
}
|
}
|
||||||
clientLogger.Info("Initialising rpc client", fields...)
|
clientLogger.Info("Initialising rpc client", fields...)
|
||||||
|
|
||||||
@@ -62,7 +58,7 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
|
|||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
Transport: &loggingRoundTripper{
|
Transport: &loggingRoundTripper{
|
||||||
logger: clientLogger,
|
logger: clientLogger,
|
||||||
network: name,
|
network: network.Name,
|
||||||
endpoint: rpcURL,
|
endpoint: rpcURL,
|
||||||
base: http.DefaultTransport,
|
base: http.DefaultTransport,
|
||||||
},
|
},
|
||||||
@@ -72,10 +68,10 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
result.Close()
|
result.Close()
|
||||||
clientLogger.Warn("Failed to dial rpc endpoint", append(fields, zap.Error(err))...)
|
clientLogger.Warn("Failed to dial rpc endpoint", append(fields, zap.Error(err))...)
|
||||||
return nil, merrors.Internal(fmt.Sprintf("rpc dial failed for %s: %s", name, err.Error()))
|
return nil, merrors.Internal(fmt.Sprintf("rpc dial failed for %s: %s", network.Name, err.Error()))
|
||||||
}
|
}
|
||||||
client := ethclient.NewClient(rpcCli)
|
client := ethclient.NewClient(rpcCli)
|
||||||
result.clients[name] = clientEntry{
|
result.clients[network.Name] = clientEntry{
|
||||||
eth: client,
|
eth: client,
|
||||||
rpc: rpcCli,
|
rpc: rpcCli,
|
||||||
}
|
}
|
||||||
@@ -93,27 +89,25 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Client returns a prepared client for the given network name.
|
// Client returns a prepared client for the given network name.
|
||||||
func (c *Clients) Client(network string) (*ethclient.Client, error) {
|
func (c *Clients) Client(network pmodel.ChainNetwork) (*ethclient.Client, error) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return nil, merrors.Internal("RPC clients not initialised")
|
return nil, merrors.Internal("RPC clients not initialised")
|
||||||
}
|
}
|
||||||
name := strings.ToLower(strings.TrimSpace(network))
|
entry, ok := c.clients[network]
|
||||||
entry, ok := c.clients[name]
|
|
||||||
if !ok || entry.eth == nil {
|
if !ok || entry.eth == nil {
|
||||||
return nil, merrors.InvalidArgument(fmt.Sprintf("RPC client not configured for network %s", name))
|
return nil, merrors.InvalidArgument(fmt.Sprintf("RPC client not configured for network %s", network))
|
||||||
}
|
}
|
||||||
return entry.eth, nil
|
return entry.eth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RPCClient returns the raw RPC client for low-level calls.
|
// RPCClient returns the raw RPC client for low-level calls.
|
||||||
func (c *Clients) RPCClient(network string) (*rpc.Client, error) {
|
func (c *Clients) RPCClient(network pmodel.ChainNetwork) (*rpc.Client, error) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return nil, merrors.Internal("rpc clients not initialised")
|
return nil, merrors.Internal("rpc clients not initialised")
|
||||||
}
|
}
|
||||||
name := strings.ToLower(strings.TrimSpace(network))
|
entry, ok := c.clients[network]
|
||||||
entry, ok := c.clients[name]
|
|
||||||
if !ok || entry.rpc == nil {
|
if !ok || entry.rpc == nil {
|
||||||
return nil, merrors.InvalidArgument(fmt.Sprintf("rpc client not configured for network %s", name))
|
return nil, merrors.InvalidArgument(fmt.Sprintf("rpc client not configured for network %s", network))
|
||||||
}
|
}
|
||||||
return entry.rpc, nil
|
return entry.rpc, nil
|
||||||
}
|
}
|
||||||
@@ -130,14 +124,14 @@ func (c *Clients) Close() {
|
|||||||
entry.eth.Close()
|
entry.eth.Close()
|
||||||
}
|
}
|
||||||
if c.logger != nil {
|
if c.logger != nil {
|
||||||
c.logger.Info("RPC client closed", zap.String("network", name))
|
c.logger.Info("RPC client closed", zap.String("network", string(name)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type loggingRoundTripper struct {
|
type loggingRoundTripper struct {
|
||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
network string
|
network pmodel.ChainNetwork
|
||||||
endpoint string
|
endpoint string
|
||||||
base http.RoundTripper
|
base http.RoundTripper
|
||||||
}
|
}
|
||||||
@@ -155,7 +149,7 @@ func (l *loggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
fields := []zap.Field{
|
fields := []zap.Field{
|
||||||
zap.String("network", l.network),
|
zap.String("network", string(l.network)),
|
||||||
}
|
}
|
||||||
if len(reqBody) > 0 {
|
if len(reqBody) > 0 {
|
||||||
fields = append(fields, zap.String("rpc_request", truncate(string(reqBody), 2048)))
|
fields = append(fields, zap.String("rpc_request", truncate(string(reqBody), 2048)))
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
package rpcclient
|
package rpcclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/ethclient"
|
"github.com/ethereum/go-ethereum/ethclient"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Registry binds static network metadata with prepared RPC clients.
|
// Registry binds static network metadata with prepared RPC clients.
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
networks map[string]shared.Network
|
networks map[pmodel.ChainNetwork]shared.Network
|
||||||
clients *Clients
|
clients *Clients
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRegistry constructs a registry keyed by lower-cased network name.
|
// NewRegistry constructs a registry keyed by lower-cased network name.
|
||||||
func NewRegistry(networks map[string]shared.Network, clients *Clients) *Registry {
|
func NewRegistry(networks map[pmodel.ChainNetwork]shared.Network, clients *Clients) *Registry {
|
||||||
return &Registry{
|
return &Registry{
|
||||||
networks: networks,
|
networks: networks,
|
||||||
clients: clients,
|
clients: clients,
|
||||||
@@ -24,31 +23,31 @@ func NewRegistry(networks map[string]shared.Network, clients *Clients) *Registry
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Network fetches network metadata by key (case-insensitive).
|
// Network fetches network metadata by key (case-insensitive).
|
||||||
func (r *Registry) Network(key string) (shared.Network, bool) {
|
func (r *Registry) Network(key pmodel.ChainNetwork) (shared.Network, bool) {
|
||||||
if r == nil || len(r.networks) == 0 {
|
if r == nil || len(r.networks) == 0 {
|
||||||
return shared.Network{}, false
|
return shared.Network{}, false
|
||||||
}
|
}
|
||||||
n, ok := r.networks[strings.ToLower(strings.TrimSpace(key))]
|
n, ok := r.networks[key]
|
||||||
return n, ok
|
return n, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client returns the prepared RPC client for the given network name.
|
// Client returns the prepared RPC client for the given network name.
|
||||||
func (r *Registry) Client(key string) (*ethclient.Client, error) {
|
func (r *Registry) Client(key pmodel.ChainNetwork) (*ethclient.Client, error) {
|
||||||
if r == nil || r.clients == nil {
|
if r == nil || r.clients == nil {
|
||||||
return nil, merrors.Internal("rpc clients not initialised")
|
return nil, merrors.Internal("rpc clients not initialised")
|
||||||
}
|
}
|
||||||
return r.clients.Client(strings.ToLower(strings.TrimSpace(key)))
|
return r.clients.Client(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RPCClient returns the raw RPC client for low-level calls.
|
// RPCClient returns the raw RPC client for low-level calls.
|
||||||
func (r *Registry) RPCClient(key string) (*rpc.Client, error) {
|
func (r *Registry) RPCClient(key pmodel.ChainNetwork) (*rpc.Client, error) {
|
||||||
if r == nil || r.clients == nil {
|
if r == nil || r.clients == nil {
|
||||||
return nil, merrors.Internal("rpc clients not initialised")
|
return nil, merrors.Internal("rpc clients not initialised")
|
||||||
}
|
}
|
||||||
return r.clients.RPCClient(strings.ToLower(strings.TrimSpace(key)))
|
return r.clients.RPCClient(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Networks exposes the registry map for iteration when needed.
|
// Networks exposes the registry map for iteration when needed.
|
||||||
func (r *Registry) Networks() map[string]shared.Network {
|
func (r *Registry) Networks() map[pmodel.ChainNetwork]shared.Network {
|
||||||
return r.networks
|
return r.networks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/discovery"
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
@@ -43,7 +44,7 @@ type Service struct {
|
|||||||
|
|
||||||
settings CacheSettings
|
settings CacheSettings
|
||||||
|
|
||||||
networks map[string]shared.Network
|
networks map[pmodel.ChainNetwork]shared.Network
|
||||||
serviceWallet shared.ServiceWallet
|
serviceWallet shared.ServiceWallet
|
||||||
keyManager keymanager.Manager
|
keyManager keymanager.Manager
|
||||||
rpcClients *rpcclient.Clients
|
rpcClients *rpcclient.Clients
|
||||||
@@ -64,7 +65,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
|||||||
producer: producer,
|
producer: producer,
|
||||||
clock: clockpkg.System{},
|
clock: clockpkg.System{},
|
||||||
settings: defaultSettings(),
|
settings: defaultSettings(),
|
||||||
networks: map[string]shared.Network{},
|
networks: map[pmodel.ChainNetwork]shared.Network{},
|
||||||
}
|
}
|
||||||
|
|
||||||
initMetrics()
|
initMetrics()
|
||||||
@@ -79,7 +80,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
|||||||
svc.clock = clockpkg.System{}
|
svc.clock = clockpkg.System{}
|
||||||
}
|
}
|
||||||
if svc.networks == nil {
|
if svc.networks == nil {
|
||||||
svc.networks = map[string]shared.Network{}
|
svc.networks = map[pmodel.ChainNetwork]shared.Network{}
|
||||||
}
|
}
|
||||||
svc.settings = svc.settings.withDefaults()
|
svc.settings = svc.settings.withDefaults()
|
||||||
svc.networkRegistry = rpcclient.NewRegistry(svc.networks, svc.rpcClients)
|
svc.networkRegistry = rpcclient.NewRegistry(svc.networks, svc.rpcClients)
|
||||||
@@ -207,7 +208,7 @@ func (s *Service) startDiscoveryAnnouncers() {
|
|||||||
announce := discovery.Announcement{
|
announce := discovery.Announcement{
|
||||||
Service: "CRYPTO_RAIL_GATEWAY",
|
Service: "CRYPTO_RAIL_GATEWAY",
|
||||||
Rail: "CRYPTO",
|
Rail: "CRYPTO",
|
||||||
Network: network.Name,
|
Network: string(network.Name),
|
||||||
Operations: []string{"balance.read", "payin.crypto", "payout.crypto", "fee.send", "observe.confirm"},
|
Operations: []string{"balance.read", "payin.crypto", "payout.crypto", "fee.send", "observe.confirm"},
|
||||||
Currencies: currencies,
|
Currencies: currencies,
|
||||||
InvokeURI: s.invokeURI,
|
InvokeURI: s.invokeURI,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
ichainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
ichainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
@@ -170,6 +171,7 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|||||||
Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"},
|
Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
IntentRef: "intent-1",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, transferResp.GetTransfer())
|
require.NotNil(t, transferResp.GetTransfer())
|
||||||
@@ -177,7 +179,7 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|||||||
|
|
||||||
stored := repo.transfers.get(transferResp.GetTransfer().GetTransferRef())
|
stored := repo.transfers.get(transferResp.GetTransfer().GetTransferRef())
|
||||||
require.NotNil(t, stored)
|
require.NotNil(t, stored)
|
||||||
require.Equal(t, model.TransferStatusPending, stored.Status)
|
require.Equal(t, model.TransferStatusCreated, stored.Status)
|
||||||
|
|
||||||
// GetTransfer
|
// GetTransfer
|
||||||
getResp, err := svc.GetTransfer(ctx, &ichainv1.GetTransferRequest{TransferRef: stored.TransferRef})
|
getResp, err := svc.GetTransfer(ctx, &ichainv1.GetTransferRequest{TransferRef: stored.TransferRef})
|
||||||
@@ -335,7 +337,7 @@ func (w *inMemoryWallets) List(ctx context.Context, filter model.ManagedWalletFi
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if filter.Network != "" && !strings.EqualFold(wallet.Network, filter.Network) {
|
if wallet.Network != filter.Network {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if filter.TokenSymbol != "" && !strings.EqualFold(wallet.TokenSymbol, filter.TokenSymbol) {
|
if filter.TokenSymbol != "" && !strings.EqualFold(wallet.TokenSymbol, filter.TokenSymbol) {
|
||||||
@@ -644,9 +646,9 @@ func newTestService(t *testing.T) (*Service, *inMemoryRepository) {
|
|||||||
|
|
||||||
type fakeKeyManager struct{}
|
type fakeKeyManager struct{}
|
||||||
|
|
||||||
func (f *fakeKeyManager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) {
|
func (f *fakeKeyManager) CreateManagedWalletKey(ctx context.Context, walletRef string, network pmodel.ChainNetwork) (*keymanager.ManagedWalletKey, error) {
|
||||||
return &keymanager.ManagedWalletKey{
|
return &keymanager.ManagedWalletKey{
|
||||||
KeyID: fmt.Sprintf("%s/%s", strings.ToLower(network), walletRef),
|
KeyID: fmt.Sprintf("%s/%s", network, walletRef),
|
||||||
Address: "0x" + strings.Repeat("a", 40),
|
Address: "0x" + strings.Repeat("a", 40),
|
||||||
PublicKey: strings.Repeat("b", 128),
|
PublicKey: strings.Repeat("b", 128),
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package shared
|
package shared
|
||||||
|
|
||||||
import "github.com/shopspring/decimal"
|
import (
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
)
|
||||||
|
|
||||||
// GasTopUpRule defines buffer, minimum, rounding, and cap behavior for native gas top-ups.
|
// GasTopUpRule defines buffer, minimum, rounding, and cap behavior for native gas top-ups.
|
||||||
type GasTopUpRule struct {
|
type GasTopUpRule struct {
|
||||||
@@ -30,3 +34,20 @@ func (p *GasTopUpPolicy) Rule(contractTransfer bool) (GasTopUpRule, bool) {
|
|||||||
}
|
}
|
||||||
return p.Default, true
|
return p.Default, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func СhainTransferStatusToOperation(ts chainv1.TransferStatus) connectorv1.OperationStatus {
|
||||||
|
switch ts {
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CREATED
|
||||||
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_SUCCESS
|
||||||
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_WAITING
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/chain/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
chainasset "github.com/tech/sendico/pkg/chain"
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
)
|
)
|
||||||
@@ -49,16 +51,38 @@ func GenerateTransferRef() string {
|
|||||||
return bson.NewObjectID().Hex()
|
return bson.NewObjectID().Hex()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ChainKeyFromEnum(chain chainv1.ChainNetwork) (string, chainv1.ChainNetwork) {
|
func ChainKeyFromEnum(chain chainv1.ChainNetwork) pmodel.ChainNetwork {
|
||||||
if name, ok := chainv1.ChainNetwork_name[int32(chain)]; ok {
|
switch chain {
|
||||||
key := strings.ToLower(strings.TrimPrefix(name, "CHAIN_NETWORK_"))
|
case chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE:
|
||||||
return key, chain
|
return pmodel.ChainNetworkArbitrumOne
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_SEPOLIA:
|
||||||
|
return pmodel.ChainNetworkArbitrumSepolia
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET:
|
||||||
|
return pmodel.ChainNetworkEthereumMainnet
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET:
|
||||||
|
return pmodel.ChainNetworkTronMainnet
|
||||||
|
case chainv1.ChainNetwork_CHAIN_NETWORK_TRON_NILE:
|
||||||
|
return pmodel.ChainNetworkTronNile
|
||||||
|
default:
|
||||||
|
return pmodel.ChainNetworkUnspecified
|
||||||
}
|
}
|
||||||
return "", chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ChainEnumFromName(name string) chainv1.ChainNetwork {
|
func ChainEnumFromName(name pmodel.ChainNetwork) chainv1.ChainNetwork {
|
||||||
return chainasset.NetworkFromString(name)
|
switch name {
|
||||||
|
case pmodel.ChainNetworkArbitrumOne:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE
|
||||||
|
case pmodel.ChainNetworkArbitrumSepolia:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_SEPOLIA
|
||||||
|
case pmodel.ChainNetworkEthereumMainnet:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET
|
||||||
|
case pmodel.ChainNetworkTronMainnet:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET
|
||||||
|
case pmodel.ChainNetworkTronNile:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_NILE
|
||||||
|
default:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ManagedWalletStatusToProto(status model.ManagedWalletStatus) chainv1.ManagedWalletStatus {
|
func ManagedWalletStatusToProto(status model.ManagedWalletStatus) chainv1.ManagedWalletStatus {
|
||||||
@@ -76,54 +100,94 @@ func ManagedWalletStatusToProto(status model.ManagedWalletStatus) chainv1.Manage
|
|||||||
|
|
||||||
func TransferStatusToModel(status chainv1.TransferStatus) model.TransferStatus {
|
func TransferStatusToModel(status chainv1.TransferStatus) model.TransferStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case chainv1.TransferStatus_TRANSFER_PENDING:
|
|
||||||
return model.TransferStatusPending
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
case chainv1.TransferStatus_TRANSFER_SIGNING:
|
return model.TransferStatusCreated
|
||||||
return model.TransferStatusSigning
|
|
||||||
case chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||||
return model.TransferStatusSubmitted
|
return model.TransferStatusProcessing
|
||||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
|
||||||
return model.TransferStatusConfirmed
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return model.TransferStatusWaiting
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
|
return model.TransferStatusSuccess
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
return model.TransferStatusFailed
|
return model.TransferStatusFailed
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
return model.TransferStatusCancelled
|
return model.TransferStatusCancelled
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return ""
|
return model.TransferStatus("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TransferStatusToProto(status model.TransferStatus) chainv1.TransferStatus {
|
func TransferStatusToProto(status model.TransferStatus) chainv1.TransferStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case model.TransferStatusPending:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_PENDING
|
case model.TransferStatusCreated:
|
||||||
case model.TransferStatusSigning:
|
return chainv1.TransferStatus_TRANSFER_CREATED
|
||||||
return chainv1.TransferStatus_TRANSFER_SIGNING
|
|
||||||
case model.TransferStatusSubmitted:
|
case model.TransferStatusProcessing:
|
||||||
return chainv1.TransferStatus_TRANSFER_SUBMITTED
|
return chainv1.TransferStatus_TRANSFER_PROCESSING
|
||||||
case model.TransferStatusConfirmed:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
case model.TransferStatusWaiting:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_WAITING
|
||||||
|
|
||||||
|
case model.TransferStatusSuccess:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_SUCCESS
|
||||||
|
|
||||||
case model.TransferStatusFailed:
|
case model.TransferStatusFailed:
|
||||||
return chainv1.TransferStatus_TRANSFER_FAILED
|
return chainv1.TransferStatus_TRANSFER_FAILED
|
||||||
|
|
||||||
case model.TransferStatusCancelled:
|
case model.TransferStatusCancelled:
|
||||||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ChainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
||||||
|
switch status {
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CREATED
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_PROCESSING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_WAITING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_SUCCESS
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||||
|
|
||||||
|
default:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NativeCurrency returns the canonical native token symbol for a network.
|
// NativeCurrency returns the canonical native token symbol for a network.
|
||||||
func NativeCurrency(network Network) string {
|
func NativeCurrency(network Network) string {
|
||||||
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
||||||
if currency == "" {
|
if currency == "" {
|
||||||
currency = strings.ToUpper(strings.TrimSpace(network.Name))
|
currency = strings.ToUpper(network.Name.String())
|
||||||
}
|
}
|
||||||
return currency
|
return currency
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network describes a supported blockchain network and known token contracts.
|
// Network describes a supported blockchain network and known token contracts.
|
||||||
type Network struct {
|
type Network struct {
|
||||||
Name string
|
Name pmodel.ChainNetwork
|
||||||
RPCURL string
|
RPCURL string
|
||||||
ChainID uint64
|
ChainID uint64
|
||||||
NativeToken string
|
NativeToken string
|
||||||
@@ -139,7 +203,27 @@ type TokenContract struct {
|
|||||||
|
|
||||||
// ServiceWallet captures the managed service wallet configuration.
|
// ServiceWallet captures the managed service wallet configuration.
|
||||||
type ServiceWallet struct {
|
type ServiceWallet struct {
|
||||||
Network string
|
Network pmodel.ChainNetwork
|
||||||
Address string
|
Address string
|
||||||
PrivateKey string
|
PrivateKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ProtoToMoney(money *moneyv1.Money) *paymenttypes.Money {
|
||||||
|
if money == nil {
|
||||||
|
return &paymenttypes.Money{}
|
||||||
|
}
|
||||||
|
return &paymenttypes.Money{
|
||||||
|
Amount: money.GetAmount(),
|
||||||
|
Currency: money.GetCurrency(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MonenyToProto(money *paymenttypes.Money) *moneyv1.Money {
|
||||||
|
if money == nil {
|
||||||
|
return &moneyv1.Money{}
|
||||||
|
}
|
||||||
|
return &moneyv1.Money{
|
||||||
|
Amount: money.Amount,
|
||||||
|
Currency: money.Currency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/gateway/chain/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,35 +41,35 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSigning, "", ""); err != nil {
|
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusProcessing, "", ""); err != nil {
|
||||||
s.logger.Warn("Failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
|
s.logger.Warn("Failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
driverDeps := s.driverDeps()
|
driverDeps := s.driverDeps()
|
||||||
chainDriver, err := s.driverForNetwork(network.Name)
|
chainDriver, err := s.driverForNetwork(network.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
destinationAddress, err := s.destinationAddress(ctx, chainDriver, transfer.Destination)
|
destinationAddress, err := s.destinationAddress(ctx, chainDriver, transfer.Destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceAddress, err := chainDriver.NormalizeAddress(sourceWallet.DepositAddress)
|
sourceAddress, err := chainDriver.NormalizeAddress(sourceWallet.DepositAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if chainDriver.Name() == "tron" && sourceAddress == destinationAddress {
|
if chainDriver.Name() == "tron" && sourceAddress == destinationAddress {
|
||||||
s.logger.Info("Self transfer detected; skipping submission",
|
s.logger.Info("Self transfer detected; skipping submission",
|
||||||
zap.String("transfer_ref", transferRef),
|
zap.String("transfer_ref", transferRef),
|
||||||
zap.String("wallet_ref", sourceWalletRef),
|
zap.String("wallet_ref", sourceWalletRef),
|
||||||
zap.String("network", network.Name),
|
zap.String("network", string(network.Name)),
|
||||||
)
|
)
|
||||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", ""); err != nil {
|
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusSuccess, "", ""); err != nil {
|
||||||
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -76,11 +77,14 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
|||||||
|
|
||||||
txHash, err := chainDriver.SubmitTransfer(ctx, driverDeps, network, transfer, sourceWallet, destinationAddress)
|
txHash, err := chainDriver.SubmitTransfer(ctx, driverDeps, network, transfer, sourceWallet, destinationAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
s.logger.Warn("Failed to submit transfer", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||||
|
if _, e := s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), ""); e != nil {
|
||||||
|
s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(e))
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSubmitted, "", txHash); err != nil {
|
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusWaiting, "", txHash); err != nil {
|
||||||
s.logger.Warn("Failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err))
|
s.logger.Warn("Failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,15 +98,15 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if receipt != nil && receipt.Status == types.ReceiptStatusSuccessful {
|
failureReason := ""
|
||||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", txHash); err != nil {
|
pStatus := model.TransferStatusSuccess
|
||||||
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
if receipt != nil && receipt.Status != types.ReceiptStatusSuccessful {
|
||||||
}
|
failureReason = "transaction reverted"
|
||||||
return nil
|
pStatus = model.TransferStatusFailed
|
||||||
}
|
}
|
||||||
|
if _, err := s.updateTransferStatus(ctx, transferRef, pStatus, failureReason, txHash); err != nil {
|
||||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, "transaction reverted", txHash); err != nil {
|
s.logger.Warn("Failed to update transfer status", zap.Error(err),
|
||||||
s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
zap.String("transfer_ref", transferRef), zap.String("status", string(pStatus)))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -133,7 +137,7 @@ func (s *Service) driverDeps() driver.Deps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) driverForNetwork(network string) (driver.Driver, error) {
|
func (s *Service) driverForNetwork(network pmodel.ChainNetwork) (driver.Driver, error) {
|
||||||
if s.drivers == nil {
|
if s.drivers == nil {
|
||||||
return nil, merrors.Internal("chain drivers not configured")
|
return nil, merrors.Internal("chain drivers not configured")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isFinalStatus(t *model.Transfer) bool {
|
||||||
|
switch t.Status {
|
||||||
|
case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toOpStatus(t *model.Transfer) rail.OperationResult {
|
||||||
|
switch t.Status {
|
||||||
|
case model.TransferStatusFailed:
|
||||||
|
return rail.OperationResultFailed
|
||||||
|
case model.TransferStatusSuccess:
|
||||||
|
return rail.OperationResultSuccess
|
||||||
|
case model.TransferStatusCancelled:
|
||||||
|
return rail.OperationResultCancelled
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("toOpStatus: unexpected transfer status: %s", t.Status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toError(t *model.Transfer) string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if t.Status == model.TransferStatusSuccess {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.FailureReason
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) (*model.Transfer, error) {
|
||||||
|
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
|
||||||
|
}
|
||||||
|
if isFinalStatus(transfer) {
|
||||||
|
s.emitTransferStatusEvent(transfer)
|
||||||
|
}
|
||||||
|
return transfer, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) emitTransferStatusEvent(transfer *model.Transfer) {
|
||||||
|
if s == nil || s.producer == nil || transfer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exec := pmodel.PaymentGatewayExecution{
|
||||||
|
PaymentIntentID: transfer.IntentRef,
|
||||||
|
IdempotencyKey: transfer.IdempotencyKey,
|
||||||
|
ExecutedMoney: transfer.NetAmount,
|
||||||
|
PaymentRef: transfer.PaymentRef,
|
||||||
|
Status: toOpStatus(transfer),
|
||||||
|
OperationRef: transfer.OperationRef,
|
||||||
|
Error: toError(transfer),
|
||||||
|
TransferRef: transfer.TransferRef,
|
||||||
|
}
|
||||||
|
env := paymentgateway.PaymentGatewayExecution(mservice.ChainGateway, &exec)
|
||||||
|
if err := s.producer.SendMessage(env); err != nil {
|
||||||
|
s.logger.Warn("Failed to publish transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,19 +5,22 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/db/storable"
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
paytypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TransferStatus string
|
type TransferStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TransferStatusPending TransferStatus = "pending"
|
TransferStatusCreated TransferStatus = "created" // record exists, not started
|
||||||
TransferStatusSigning TransferStatus = "signing"
|
TransferStatusProcessing TransferStatus = "processing" // we are working on it
|
||||||
TransferStatusSubmitted TransferStatus = "submitted"
|
TransferStatusWaiting TransferStatus = "waiting" // waiting external world
|
||||||
TransferStatusConfirmed TransferStatus = "confirmed"
|
|
||||||
TransferStatusFailed TransferStatus = "failed"
|
TransferStatusSuccess TransferStatus = "success" // final success
|
||||||
TransferStatusCancelled TransferStatus = "cancelled"
|
TransferStatusFailed TransferStatus = "failed" // final failure
|
||||||
|
TransferStatusCancelled TransferStatus = "cancelled" // final cancelled
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceFee represents a fee component applied to a transfer.
|
// ServiceFee represents a fee component applied to a transfer.
|
||||||
@@ -38,21 +41,23 @@ type TransferDestination struct {
|
|||||||
type Transfer struct {
|
type Transfer struct {
|
||||||
storable.Base `bson:",inline" json:",inline"`
|
storable.Base `bson:",inline" json:",inline"`
|
||||||
|
|
||||||
|
OperationRef string `bson:"operationRef" json:"operationRef"`
|
||||||
TransferRef string `bson:"transferRef" json:"transferRef"`
|
TransferRef string `bson:"transferRef" json:"transferRef"`
|
||||||
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
|
IdempotencyKey string `bson:"intentRef" json:"intentRef"`
|
||||||
|
IntentRef string `bson:"idempotencyKey" json:"idempotencyKey"`
|
||||||
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
|
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
|
||||||
SourceWalletRef string `bson:"sourceWalletRef" json:"sourceWalletRef"`
|
SourceWalletRef string `bson:"sourceWalletRef" json:"sourceWalletRef"`
|
||||||
Destination TransferDestination `bson:"destination" json:"destination"`
|
Destination TransferDestination `bson:"destination" json:"destination"`
|
||||||
Network string `bson:"network" json:"network"`
|
Network pmodel.ChainNetwork `bson:"network" json:"network"`
|
||||||
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
|
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
|
||||||
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
|
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
|
||||||
RequestedAmount *moneyv1.Money `bson:"requestedAmount" json:"requestedAmount"`
|
RequestedAmount *paytypes.Money `bson:"requestedAmount" json:"requestedAmount"`
|
||||||
NetAmount *moneyv1.Money `bson:"netAmount" json:"netAmount"`
|
NetAmount *paytypes.Money `bson:"netAmount" json:"netAmount"`
|
||||||
Fees []ServiceFee `bson:"fees,omitempty" json:"fees,omitempty"`
|
Fees []ServiceFee `bson:"fees,omitempty" json:"fees,omitempty"`
|
||||||
Status TransferStatus `bson:"status" json:"status"`
|
Status TransferStatus `bson:"status" json:"status"`
|
||||||
TxHash string `bson:"txHash,omitempty" json:"txHash,omitempty"`
|
TxHash string `bson:"txHash,omitempty" json:"txHash,omitempty"`
|
||||||
FailureReason string `bson:"failureReason,omitempty" json:"failureReason,omitempty"`
|
FailureReason string `bson:"failureReason,omitempty" json:"failureReason,omitempty"`
|
||||||
ClientReference string `bson:"clientReference,omitempty" json:"clientReference,omitempty"`
|
PaymentRef string `bson:"paymentRef,omitempty" json:"paymentRef,omitempty"`
|
||||||
LastStatusAt time.Time `bson:"lastStatusAt" json:"lastStatusAt"`
|
LastStatusAt time.Time `bson:"lastStatusAt" json:"lastStatusAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,12 +87,11 @@ func (t *Transfer) Normalize() {
|
|||||||
t.IdempotencyKey = strings.TrimSpace(t.IdempotencyKey)
|
t.IdempotencyKey = strings.TrimSpace(t.IdempotencyKey)
|
||||||
t.OrganizationRef = strings.TrimSpace(t.OrganizationRef)
|
t.OrganizationRef = strings.TrimSpace(t.OrganizationRef)
|
||||||
t.SourceWalletRef = strings.TrimSpace(t.SourceWalletRef)
|
t.SourceWalletRef = strings.TrimSpace(t.SourceWalletRef)
|
||||||
t.Network = strings.TrimSpace(strings.ToLower(t.Network))
|
|
||||||
t.TokenSymbol = strings.TrimSpace(strings.ToUpper(t.TokenSymbol))
|
t.TokenSymbol = strings.TrimSpace(strings.ToUpper(t.TokenSymbol))
|
||||||
t.ContractAddress = strings.TrimSpace(strings.ToLower(t.ContractAddress))
|
t.ContractAddress = strings.TrimSpace(strings.ToLower(t.ContractAddress))
|
||||||
t.Destination.ManagedWalletRef = strings.TrimSpace(t.Destination.ManagedWalletRef)
|
t.Destination.ManagedWalletRef = strings.TrimSpace(t.Destination.ManagedWalletRef)
|
||||||
t.Destination.ExternalAddress = normalizeWalletAddress(t.Destination.ExternalAddress)
|
t.Destination.ExternalAddress = normalizeWalletAddress(t.Destination.ExternalAddress)
|
||||||
t.Destination.ExternalAddressOriginal = strings.TrimSpace(t.Destination.ExternalAddressOriginal)
|
t.Destination.ExternalAddressOriginal = strings.TrimSpace(t.Destination.ExternalAddressOriginal)
|
||||||
t.Destination.Memo = strings.TrimSpace(t.Destination.Memo)
|
t.Destination.Memo = strings.TrimSpace(t.Destination.Memo)
|
||||||
t.ClientReference = strings.TrimSpace(t.ClientReference)
|
t.PaymentRef = strings.TrimSpace(t.PaymentRef)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/pkg/db/storable"
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
pkgmodel "github.com/tech/sendico/pkg/model"
|
pkgmodel "github.com/tech/sendico/pkg/model"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
)
|
)
|
||||||
@@ -23,17 +24,17 @@ type ManagedWallet struct {
|
|||||||
storable.Base `bson:",inline" json:",inline"`
|
storable.Base `bson:",inline" json:",inline"`
|
||||||
pkgmodel.Describable `bson:",inline" json:",inline"`
|
pkgmodel.Describable `bson:",inline" json:",inline"`
|
||||||
|
|
||||||
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
|
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
|
||||||
WalletRef string `bson:"walletRef" json:"walletRef"`
|
WalletRef string `bson:"walletRef" json:"walletRef"`
|
||||||
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
|
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
|
||||||
OwnerRef string `bson:"ownerRef" json:"ownerRef"`
|
OwnerRef string `bson:"ownerRef" json:"ownerRef"`
|
||||||
Network string `bson:"network" json:"network"`
|
Network pkgmodel.ChainNetwork `bson:"network" json:"network"`
|
||||||
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
|
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
|
||||||
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
|
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
|
||||||
DepositAddress string `bson:"depositAddress" json:"depositAddress"`
|
DepositAddress string `bson:"depositAddress" json:"depositAddress"`
|
||||||
KeyReference string `bson:"keyReference,omitempty" json:"keyReference,omitempty"`
|
KeyReference string `bson:"keyReference,omitempty" json:"keyReference,omitempty"`
|
||||||
Status ManagedWalletStatus `bson:"status" json:"status"`
|
Status ManagedWalletStatus `bson:"status" json:"status"`
|
||||||
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
|
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collection implements storable.Storable.
|
// Collection implements storable.Storable.
|
||||||
@@ -66,7 +67,7 @@ type ManagedWalletFilter struct {
|
|||||||
// - pointer to empty string: filter for wallets where owner_ref is empty
|
// - pointer to empty string: filter for wallets where owner_ref is empty
|
||||||
// - pointer to a value: filter for wallets where owner_ref matches
|
// - pointer to a value: filter for wallets where owner_ref matches
|
||||||
OwnerRefFilter *string
|
OwnerRefFilter *string
|
||||||
Network string
|
Network pmodel.ChainNetwork
|
||||||
TokenSymbol string
|
TokenSymbol string
|
||||||
Cursor string
|
Cursor string
|
||||||
Limit int32
|
Limit int32
|
||||||
@@ -93,7 +94,6 @@ func (m *ManagedWallet) Normalize() {
|
|||||||
m.Description = &desc
|
m.Description = &desc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.Network = strings.TrimSpace(strings.ToLower(m.Network))
|
|
||||||
m.TokenSymbol = strings.TrimSpace(strings.ToUpper(m.TokenSymbol))
|
m.TokenSymbol = strings.TrimSpace(strings.ToUpper(m.TokenSymbol))
|
||||||
m.ContractAddress = strings.TrimSpace(strings.ToLower(m.ContractAddress))
|
m.ContractAddress = strings.TrimSpace(strings.ToLower(m.ContractAddress))
|
||||||
m.DepositAddress = normalizeWalletAddress(m.DepositAddress)
|
m.DepositAddress = normalizeWalletAddress(m.DepositAddress)
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func (t *Transfers) Create(ctx context.Context, transfer *model.Transfer) (*mode
|
|||||||
return nil, merrors.InvalidArgument("transfersStore: empty idempotencyKey")
|
return nil, merrors.InvalidArgument("transfersStore: empty idempotencyKey")
|
||||||
}
|
}
|
||||||
if transfer.Status == "" {
|
if transfer.Status == "" {
|
||||||
transfer.Status = model.TransferStatusPending
|
transfer.Status = model.TransferStatusCreated
|
||||||
}
|
}
|
||||||
if transfer.LastStatusAt.IsZero() {
|
if transfer.LastStatusAt.IsZero() {
|
||||||
transfer.LastStatusAt = time.Now().UTC()
|
transfer.LastStatusAt = time.Now().UTC()
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func (w *Wallets) Create(ctx context.Context, wallet *model.ManagedWallet) (*mod
|
|||||||
fields = append(fields, zap.String("owner_ref", wallet.OwnerRef))
|
fields = append(fields, zap.String("owner_ref", wallet.OwnerRef))
|
||||||
}
|
}
|
||||||
if wallet.Network != "" {
|
if wallet.Network != "" {
|
||||||
fields = append(fields, zap.String("network", wallet.Network))
|
fields = append(fields, zap.String("network", string(wallet.Network)))
|
||||||
}
|
}
|
||||||
if wallet.TokenSymbol != "" {
|
if wallet.TokenSymbol != "" {
|
||||||
fields = append(fields, zap.String("token_symbol", wallet.TokenSymbol))
|
fields = append(fields, zap.String("token_symbol", wallet.TokenSymbol))
|
||||||
@@ -161,11 +161,7 @@ func (w *Wallets) List(ctx context.Context, filter model.ManagedWalletFilter) (*
|
|||||||
query = query.Filter(repository.Field("ownerRef"), ownerRef)
|
query = query.Filter(repository.Field("ownerRef"), ownerRef)
|
||||||
fields = append(fields, zap.String("owner_ref_filter", ownerRef))
|
fields = append(fields, zap.String("owner_ref_filter", ownerRef))
|
||||||
}
|
}
|
||||||
if network := strings.TrimSpace(filter.Network); network != "" {
|
fields = append(fields, zap.String("network", string(filter.Network)))
|
||||||
normalized := strings.ToLower(network)
|
|
||||||
query = query.Filter(repository.Field("network"), normalized)
|
|
||||||
fields = append(fields, zap.String("network", normalized))
|
|
||||||
}
|
|
||||||
if token := strings.TrimSpace(filter.TokenSymbol); token != "" {
|
if token := strings.TrimSpace(filter.TokenSymbol); token != "" {
|
||||||
normalized := strings.ToUpper(token)
|
normalized := strings.ToUpper(token)
|
||||||
query = query.Filter(repository.Field("tokenSymbol"), normalized)
|
query = query.Filter(repository.Field("tokenSymbol"), normalized)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
pmodel "github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model/account_role"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||||
@@ -179,14 +179,14 @@ func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[strin
|
|||||||
if op == nil || len(metadata) == 0 {
|
if op == nil || len(metadata) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyFromRole]); raw != "" {
|
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyFromRole]); raw != "" {
|
||||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||||
op.FromRole = pmodel.ToProto(role)
|
op.FromRole = account_role.ToProto(role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyToRole]); raw != "" {
|
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyToRole]); raw != "" {
|
||||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||||
op.ToRole = pmodel.ToProto(role)
|
op.ToRole = account_role.ToProto(role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,12 +298,22 @@ func minorFromMoney(m *moneyv1.Money) int64 {
|
|||||||
|
|
||||||
func payoutStatusFromOperation(status connectorv1.OperationStatus) mntxv1.PayoutStatus {
|
func payoutStatusFromOperation(status connectorv1.OperationStatus) mntxv1.PayoutStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case connectorv1.OperationStatus_CONFIRMED:
|
|
||||||
return mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED
|
case connectorv1.OperationStatus_OPERATION_CREATED:
|
||||||
case connectorv1.OperationStatus_FAILED:
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_CREATED
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_WAITING:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_SUCCESS:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_FAILED:
|
||||||
return mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||||
case connectorv1.OperationStatus_PENDING, connectorv1.OperationStatus_SUBMITTED:
|
|
||||||
return mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
case connectorv1.OperationStatus_OPERATION_CANCELLED:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_CANCELLED
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return mntxv1.PayoutStatus_PAYOUT_STATUS_UNSPECIFIED
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_UNSPECIFIED
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ grpc:
|
|||||||
metrics:
|
metrics:
|
||||||
address: ":9404"
|
address: ":9404"
|
||||||
|
|
||||||
|
database:
|
||||||
|
driver: mongodb
|
||||||
|
settings:
|
||||||
|
host_env: MNTX_GATEWAY_MONGO_HOST
|
||||||
|
port_env: MNTX_GATEWAY_MONGO_PORT
|
||||||
|
database_env: MNTX_GATEWAY_MONGO_DATABASE
|
||||||
|
user_env: MNTX_GATEWAY_MONGO_USER
|
||||||
|
password_env: MNTX_GATEWAY_MONGO_PASSWORD
|
||||||
|
auth_source_env: MNTX_GATEWAY_MONGO_AUTH_SOURCE
|
||||||
|
replica_set_env: MNTX_GATEWAY_MONGO_REPLICA_SET
|
||||||
|
|
||||||
messaging:
|
messaging:
|
||||||
driver: NATS
|
driver: NATS
|
||||||
settings:
|
settings:
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ grpc:
|
|||||||
metrics:
|
metrics:
|
||||||
address: ":9404"
|
address: ":9404"
|
||||||
|
|
||||||
|
database:
|
||||||
|
driver: mongodb
|
||||||
|
settings:
|
||||||
|
host_env: MNTX_GATEWAY_MONGO_HOST
|
||||||
|
port_env: MNTX_GATEWAY_MONGO_PORT
|
||||||
|
database_env: MNTX_GATEWAY_MONGO_DATABASE
|
||||||
|
user_env: MNTX_GATEWAY_MONGO_USER
|
||||||
|
password_env: MNTX_GATEWAY_MONGO_PASSWORD
|
||||||
|
auth_source_env: MNTX_GATEWAY_MONGO_AUTH_SOURCE
|
||||||
|
replica_set_env: MNTX_GATEWAY_MONGO_REPLICA_SET
|
||||||
|
|
||||||
messaging:
|
messaging:
|
||||||
driver: NATS
|
driver: NATS
|
||||||
settings:
|
settings:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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/pkg v0.1.0
|
github.com/tech/sendico/pkg v0.1.0
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
google.golang.org/grpc v1.78.0
|
google.golang.org/grpc v1.78.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
@@ -40,7 +41,6 @@ require (
|
|||||||
github.com/xdg-go/scram v1.2.0 // 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/v2 v2.5.0 // indirect
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/crypto v0.47.0 // indirect
|
golang.org/x/crypto v0.47.0 // indirect
|
||||||
@@ -48,5 +48,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -210,8 +210,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-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import (
|
|||||||
"github.com/tech/sendico/gateway/mntx/internal/appversion"
|
"github.com/tech/sendico/gateway/mntx/internal/appversion"
|
||||||
mntxservice "github.com/tech/sendico/gateway/mntx/internal/service/gateway"
|
mntxservice "github.com/tech/sendico/gateway/mntx/internal/service/gateway"
|
||||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage"
|
||||||
|
gatewaymongo "github.com/tech/sendico/gateway/mntx/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/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
@@ -31,7 +34,7 @@ type Imp struct {
|
|||||||
debug bool
|
debug bool
|
||||||
|
|
||||||
config *config
|
config *config
|
||||||
app *grpcapp.App[struct{}]
|
app *grpcapp.App[storage.Repository]
|
||||||
http *http.Server
|
http *http.Server
|
||||||
service *mntxservice.Service
|
service *mntxservice.Service
|
||||||
}
|
}
|
||||||
@@ -183,7 +186,7 @@ func (i *Imp) Start() error {
|
|||||||
zap.Int64("max_body_bytes", callbackCfg.MaxBodyBytes),
|
zap.Int64("max_body_bytes", callbackCfg.MaxBodyBytes),
|
||||||
)
|
)
|
||||||
|
|
||||||
serviceFactory := func(logger mlogger.Logger, _ struct{}, producer msg.Producer) (grpcapp.Service, error) {
|
serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) {
|
||||||
invokeURI := ""
|
invokeURI := ""
|
||||||
if cfg.GRPC != nil {
|
if cfg.GRPC != nil {
|
||||||
invokeURI = cfg.GRPC.DiscoveryInvokeURI()
|
invokeURI = cfg.GRPC.DiscoveryInvokeURI()
|
||||||
@@ -194,6 +197,7 @@ func (i *Imp) Start() error {
|
|||||||
mntxservice.WithMonetixConfig(monetixCfg),
|
mntxservice.WithMonetixConfig(monetixCfg),
|
||||||
mntxservice.WithGatewayDescriptor(gatewayDescriptor),
|
mntxservice.WithGatewayDescriptor(gatewayDescriptor),
|
||||||
mntxservice.WithHTTPClient(&http.Client{Timeout: monetixCfg.Timeout()}),
|
mntxservice.WithHTTPClient(&http.Client{Timeout: monetixCfg.Timeout()}),
|
||||||
|
mntxservice.WithStorage(repo),
|
||||||
)
|
)
|
||||||
i.service = svc
|
i.service = svc
|
||||||
|
|
||||||
@@ -204,7 +208,11 @@ func (i *Imp) Start() error {
|
|||||||
return svc, nil
|
return svc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := grpcapp.NewApp(i.logger, "monetix", cfg.Config, i.debug, nil, serviceFactory)
|
repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) {
|
||||||
|
return gatewaymongo.New(logger, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := grpcapp.NewApp(i.logger, "monetix", cfg.Config, i.debug, repoFactory, serviceFactory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,10 +92,10 @@ func mapCallbackToState(clock clockpkg.Clock, cfg monetix.Config, cb monetixCall
|
|||||||
internalStatus := mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
internalStatus := mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||||
|
|
||||||
if status == cfg.SuccessStatus() && opStatus == cfg.SuccessStatus() && (code == "" || code == "0") {
|
if status == cfg.SuccessStatus() && opStatus == cfg.SuccessStatus() && (code == "" || code == "0") {
|
||||||
internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED
|
internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS
|
||||||
outcome = monetix.OutcomeSuccess
|
outcome = monetix.OutcomeSuccess
|
||||||
} else if status == cfg.ProcessingStatus() || opStatus == cfg.ProcessingStatus() {
|
} else if status == cfg.ProcessingStatus() || opStatus == cfg.ProcessingStatus() {
|
||||||
internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING
|
||||||
outcome = monetix.OutcomeProcessing
|
outcome = monetix.OutcomeProcessing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func TestMapCallbackToState_StatusMapping(t *testing.T) {
|
|||||||
paymentStatus: "success",
|
paymentStatus: "success",
|
||||||
operationStatus: "success",
|
operationStatus: "success",
|
||||||
code: "0",
|
code: "0",
|
||||||
expectedStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED,
|
expectedStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS,
|
||||||
expectedOutcome: monetix.OutcomeSuccess,
|
expectedOutcome: monetix.OutcomeSuccess,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -59,7 +59,7 @@ func TestMapCallbackToState_StatusMapping(t *testing.T) {
|
|||||||
paymentStatus: "processing",
|
paymentStatus: "processing",
|
||||||
operationStatus: "success",
|
operationStatus: "success",
|
||||||
code: "",
|
code: "",
|
||||||
expectedStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
|
expectedStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING,
|
||||||
expectedOutcome: monetix.OutcomeProcessing,
|
expectedOutcome: monetix.OutcomeProcessing,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
package gateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
type cardPayoutStore struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
payouts map[string]*mntxv1.CardPayoutState
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCardPayoutStore() *cardPayoutStore {
|
|
||||||
return &cardPayoutStore{
|
|
||||||
payouts: make(map[string]*mntxv1.CardPayoutState),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *cardPayoutStore) Save(p *mntxv1.CardPayoutState) {
|
|
||||||
if p == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
key := strings.TrimSpace(p.GetPayoutId())
|
|
||||||
if key == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
s.payouts[key] = cloneCardPayoutState(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *cardPayoutStore) Get(payoutID string) (*mntxv1.CardPayoutState, bool) {
|
|
||||||
id := strings.TrimSpace(payoutID)
|
|
||||||
if id == "" {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
val, ok := s.payouts[id]
|
|
||||||
return cloneCardPayoutState(val), ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneCardPayoutState(p *mntxv1.CardPayoutState) *mntxv1.CardPayoutState {
|
|
||||||
if p == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cloned := proto.Clone(p)
|
|
||||||
if cp, ok := cloned.(*mntxv1.CardPayoutState); ok {
|
|
||||||
return cp
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage"
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockRepository implements storage.Repository for tests.
|
||||||
|
type mockRepository struct {
|
||||||
|
payouts *cardPayoutStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockRepository() *mockRepository {
|
||||||
|
return &mockRepository{
|
||||||
|
payouts: newCardPayoutStore(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockRepository) Payouts() storage.PayoutsStore {
|
||||||
|
return r.payouts
|
||||||
|
}
|
||||||
|
|
||||||
|
// cardPayoutStore implements storage.PayoutsStore for tests.
|
||||||
|
type cardPayoutStore struct {
|
||||||
|
data map[string]*model.CardPayout
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCardPayoutStore() *cardPayoutStore {
|
||||||
|
return &cardPayoutStore{
|
||||||
|
data: make(map[string]*model.CardPayout),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cardPayoutStore) FindByIdempotencyKey(_ context.Context, key string) (*model.CardPayout, error) {
|
||||||
|
for _, v := range s.data {
|
||||||
|
if v.IdempotencyKey == key {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cardPayoutStore) FindByPaymentID(_ context.Context, id string) (*model.CardPayout, error) {
|
||||||
|
v, ok := s.data[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cardPayoutStore) Upsert(_ context.Context, record *model.CardPayout) error {
|
||||||
|
s.data[record.PayoutID] = record
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save is a helper for tests to pre-populate data.
|
||||||
|
func (s *cardPayoutStore) Save(state *model.CardPayout) {
|
||||||
|
s.data[state.PayoutID] = state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get is a helper for tests to retrieve data.
|
||||||
|
func (s *cardPayoutStore) Get(id string) (*model.CardPayout, bool) {
|
||||||
|
v, ok := s.data[id]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
@@ -8,30 +8,33 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage"
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
messaging "github.com/tech/sendico/pkg/messaging/envelope"
|
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/model"
|
|
||||||
nm "github.com/tech/sendico/pkg/model/notification"
|
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
|
||||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type cardPayoutProcessor struct {
|
type cardPayoutProcessor struct {
|
||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
config monetix.Config
|
config monetix.Config
|
||||||
clock clockpkg.Clock
|
clock clockpkg.Clock
|
||||||
store *cardPayoutStore
|
store storage.Repository
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
producer msg.Producer
|
producer msg.Producer
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCardPayoutProcessor(logger mlogger.Logger, cfg monetix.Config, clock clockpkg.Clock, store *cardPayoutStore, client *http.Client, producer msg.Producer) *cardPayoutProcessor {
|
func newCardPayoutProcessor(
|
||||||
|
logger mlogger.Logger,
|
||||||
|
cfg monetix.Config,
|
||||||
|
clock clockpkg.Clock,
|
||||||
|
store storage.Repository,
|
||||||
|
client *http.Client,
|
||||||
|
producer msg.Producer,
|
||||||
|
) *cardPayoutProcessor {
|
||||||
return &cardPayoutProcessor{
|
return &cardPayoutProcessor{
|
||||||
logger: logger.Named("card_payout_processor"),
|
logger: logger.Named("card_payout_processor"),
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@@ -46,18 +49,23 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
|||||||
if p == nil {
|
if p == nil {
|
||||||
return nil, merrors.Internal("card payout processor not initialised")
|
return nil, merrors.Internal("card payout processor not initialised")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req = sanitizeCardPayoutRequest(req)
|
||||||
|
|
||||||
p.logger.Info("Submitting card payout",
|
p.logger.Info("Submitting card payout",
|
||||||
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
||||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||||
|
zap.String("operation_ref", strings.TrimSpace(req.GetOperationRef())),
|
||||||
|
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
||||||
)
|
)
|
||||||
|
|
||||||
if strings.TrimSpace(p.config.BaseURL) == "" || strings.TrimSpace(p.config.SecretKey) == "" {
|
if strings.TrimSpace(p.config.BaseURL) == "" || strings.TrimSpace(p.config.SecretKey) == "" {
|
||||||
p.logger.Warn("Monetix configuration is incomplete for payout submission")
|
p.logger.Warn("Monetix configuration is incomplete for payout submission")
|
||||||
return nil, merrors.Internal("monetix configuration is incomplete")
|
return nil, merrors.Internal("monetix configuration is incomplete")
|
||||||
}
|
}
|
||||||
|
|
||||||
req = sanitizeCardPayoutRequest(req)
|
|
||||||
if err := validateCardPayoutRequest(req, p.config); err != nil {
|
if err := validateCardPayoutRequest(req, p.config); err != nil {
|
||||||
p.logger.Warn("Card payout validation failed",
|
p.logger.Warn("Card payout validation failed",
|
||||||
zap.String("payout_id", req.GetPayoutId()),
|
zap.String("payout_id", req.GetPayoutId()),
|
||||||
@@ -76,53 +84,88 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
|||||||
return nil, merrors.Internal("monetix project_id is not configured")
|
return nil, merrors.Internal("monetix project_id is not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
now := timestamppb.New(p.clock.Now())
|
now := p.clock.Now()
|
||||||
state := &mntxv1.CardPayoutState{
|
|
||||||
PayoutId: req.GetPayoutId(),
|
state := &model.CardPayout{
|
||||||
ProjectId: projectID,
|
PayoutID: strings.TrimSpace(req.GetPayoutId()),
|
||||||
CustomerId: req.GetCustomerId(),
|
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
||||||
AmountMinor: req.GetAmountMinor(),
|
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||||
Currency: strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
|
ProjectID: projectID,
|
||||||
Status: mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
|
CustomerID: strings.TrimSpace(req.GetCustomerId()),
|
||||||
CreatedAt: now,
|
AmountMinor: req.GetAmountMinor(),
|
||||||
UpdatedAt: now,
|
Currency: strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
|
||||||
|
Status: model.PayoutStatusWaiting,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if existing, ok := p.store.Get(state.GetPayoutId()); ok && existing != nil {
|
// Keep CreatedAt/refs if record already exists.
|
||||||
if existing.GetCreatedAt() != nil {
|
if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PayoutID); err == nil && existing != nil {
|
||||||
state.CreatedAt = existing.GetCreatedAt()
|
if !existing.CreatedAt.IsZero() {
|
||||||
|
state.CreatedAt = existing.CreatedAt
|
||||||
|
}
|
||||||
|
if state.OperationRef == "" {
|
||||||
|
state.OperationRef = existing.OperationRef
|
||||||
|
}
|
||||||
|
if state.IdempotencyKey == "" {
|
||||||
|
state.IdempotencyKey = existing.IdempotencyKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client := monetix.NewClient(p.config, p.httpClient, p.logger)
|
client := monetix.NewClient(p.config, p.httpClient, p.logger)
|
||||||
apiReq := buildCardPayoutRequest(projectID, req)
|
apiReq := buildCardPayoutRequest(projectID, req)
|
||||||
|
|
||||||
result, err := client.CreateCardPayout(ctx, apiReq)
|
result, err := client.CreateCardPayout(ctx, apiReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
state.Status = model.PayoutStatusFailed
|
||||||
state.ProviderMessage = err.Error()
|
state.ProviderMessage = err.Error()
|
||||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
state.UpdatedAt = p.clock.Now()
|
||||||
p.store.Save(state)
|
|
||||||
|
if e := p.updatePayoutStatus(ctx, state); e != nil {
|
||||||
|
p.logger.Warn("Failed to update payout status",
|
||||||
|
zap.Error(e),
|
||||||
|
zap.String("payout_id", state.PayoutID),
|
||||||
|
zap.String("customer_id", state.CustomerID),
|
||||||
|
zap.String("operation_ref", state.OperationRef),
|
||||||
|
zap.String("idempotency_key", state.IdempotencyKey),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
p.logger.Warn("Monetix payout submission failed",
|
p.logger.Warn("Monetix payout submission failed",
|
||||||
zap.String("payout_id", req.GetPayoutId()),
|
|
||||||
zap.String("customer_id", req.GetCustomerId()),
|
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
zap.String("payout_id", state.PayoutID),
|
||||||
|
zap.String("customer_id", state.CustomerID),
|
||||||
|
zap.String("operation_ref", state.OperationRef),
|
||||||
|
zap.String("idempotency_key", state.IdempotencyKey),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
state.ProviderPaymentId = result.ProviderRequestID
|
// Provider request id is the provider-side payment id in your model.
|
||||||
|
state.ProviderPaymentID = strings.TrimSpace(result.ProviderRequestID)
|
||||||
if result.Accepted {
|
if result.Accepted {
|
||||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
state.Status = model.PayoutStatusWaiting
|
||||||
} else {
|
} else {
|
||||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
state.Status = model.PayoutStatusFailed
|
||||||
state.ProviderCode = result.ErrorCode
|
state.ProviderCode = strings.TrimSpace(result.ErrorCode)
|
||||||
state.ProviderMessage = result.ErrorMessage
|
state.ProviderMessage = strings.TrimSpace(result.ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.UpdatedAt = p.clock.Now()
|
||||||
|
if err := p.updatePayoutStatus(ctx, state); err != nil {
|
||||||
|
p.logger.Warn("Failed to store payout",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("payout_id", state.PayoutID),
|
||||||
|
zap.String("customer_id", state.CustomerID),
|
||||||
|
zap.String("operation_ref", state.OperationRef),
|
||||||
|
zap.String("idempotency_key", state.IdempotencyKey),
|
||||||
|
)
|
||||||
|
// do not fail request here: provider already answered and client expects response
|
||||||
}
|
}
|
||||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
|
||||||
p.store.Save(state)
|
|
||||||
|
|
||||||
resp := &mntxv1.CardPayoutResponse{
|
resp := &mntxv1.CardPayoutResponse{
|
||||||
Payout: state,
|
Payout: StateToProto(state),
|
||||||
Accepted: result.Accepted,
|
Accepted: result.Accepted,
|
||||||
ProviderRequestId: result.ProviderRequestID,
|
ProviderRequestId: result.ProviderRequestID,
|
||||||
ErrorCode: result.ErrorCode,
|
ErrorCode: result.ErrorCode,
|
||||||
@@ -130,8 +173,8 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.logger.Info("Card payout submission stored",
|
p.logger.Info("Card payout submission stored",
|
||||||
zap.String("payout_id", state.GetPayoutId()),
|
zap.String("payout_id", state.PayoutID),
|
||||||
zap.String("status", state.GetStatus().String()),
|
zap.String("status", string(state.Status)),
|
||||||
zap.Bool("accepted", result.Accepted),
|
zap.Bool("accepted", result.Accepted),
|
||||||
zap.String("provider_request_id", result.ProviderRequestID),
|
zap.String("provider_request_id", result.ProviderRequestID),
|
||||||
)
|
)
|
||||||
@@ -143,18 +186,23 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
|||||||
if p == nil {
|
if p == nil {
|
||||||
return nil, merrors.Internal("card payout processor not initialised")
|
return nil, merrors.Internal("card payout processor not initialised")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req = sanitizeCardTokenPayoutRequest(req)
|
||||||
|
|
||||||
p.logger.Info("Submitting card token payout",
|
p.logger.Info("Submitting card token payout",
|
||||||
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
||||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||||
|
zap.String("operation_ref", strings.TrimSpace(req.GetOperationRef())),
|
||||||
|
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
||||||
)
|
)
|
||||||
|
|
||||||
if strings.TrimSpace(p.config.BaseURL) == "" || strings.TrimSpace(p.config.SecretKey) == "" {
|
if strings.TrimSpace(p.config.BaseURL) == "" || strings.TrimSpace(p.config.SecretKey) == "" {
|
||||||
p.logger.Warn("Monetix configuration is incomplete for token payout submission")
|
p.logger.Warn("Monetix configuration is incomplete for token payout submission")
|
||||||
return nil, merrors.Internal("monetix configuration is incomplete")
|
return nil, merrors.Internal("monetix configuration is incomplete")
|
||||||
}
|
}
|
||||||
|
|
||||||
req = sanitizeCardTokenPayoutRequest(req)
|
|
||||||
if err := validateCardTokenPayoutRequest(req, p.config); err != nil {
|
if err := validateCardTokenPayoutRequest(req, p.config); err != nil {
|
||||||
p.logger.Warn("Card token payout validation failed",
|
p.logger.Warn("Card token payout validation failed",
|
||||||
zap.String("payout_id", req.GetPayoutId()),
|
zap.String("payout_id", req.GetPayoutId()),
|
||||||
@@ -173,53 +221,69 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
|||||||
return nil, merrors.Internal("monetix project_id is not configured")
|
return nil, merrors.Internal("monetix project_id is not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
now := timestamppb.New(p.clock.Now())
|
now := p.clock.Now()
|
||||||
state := &mntxv1.CardPayoutState{
|
state := &model.CardPayout{
|
||||||
PayoutId: req.GetPayoutId(),
|
PayoutID: strings.TrimSpace(req.GetPayoutId()),
|
||||||
ProjectId: projectID,
|
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
||||||
CustomerId: req.GetCustomerId(),
|
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||||
AmountMinor: req.GetAmountMinor(),
|
ProjectID: projectID,
|
||||||
Currency: strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
|
CustomerID: strings.TrimSpace(req.GetCustomerId()),
|
||||||
Status: mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
|
AmountMinor: req.GetAmountMinor(),
|
||||||
CreatedAt: now,
|
Currency: strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
|
||||||
UpdatedAt: now,
|
Status: model.PayoutStatusWaiting,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if existing, ok := p.store.Get(state.GetPayoutId()); ok && existing != nil {
|
if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PayoutID); err == nil && existing != nil {
|
||||||
if existing.GetCreatedAt() != nil {
|
if !existing.CreatedAt.IsZero() {
|
||||||
state.CreatedAt = existing.GetCreatedAt()
|
state.CreatedAt = existing.CreatedAt
|
||||||
|
}
|
||||||
|
if state.OperationRef == "" {
|
||||||
|
state.OperationRef = existing.OperationRef
|
||||||
|
}
|
||||||
|
if state.IdempotencyKey == "" {
|
||||||
|
state.IdempotencyKey = existing.IdempotencyKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client := monetix.NewClient(p.config, p.httpClient, p.logger)
|
client := monetix.NewClient(p.config, p.httpClient, p.logger)
|
||||||
apiReq := buildCardTokenPayoutRequest(projectID, req)
|
apiReq := buildCardTokenPayoutRequest(projectID, req)
|
||||||
|
|
||||||
result, err := client.CreateCardTokenPayout(ctx, apiReq)
|
result, err := client.CreateCardTokenPayout(ctx, apiReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
state.Status = model.PayoutStatusFailed
|
||||||
state.ProviderMessage = err.Error()
|
state.ProviderMessage = err.Error()
|
||||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
state.UpdatedAt = p.clock.Now()
|
||||||
p.store.Save(state)
|
|
||||||
|
_ = p.updatePayoutStatus(ctx, state)
|
||||||
|
|
||||||
p.logger.Warn("Monetix token payout submission failed",
|
p.logger.Warn("Monetix token payout submission failed",
|
||||||
zap.String("payout_id", req.GetPayoutId()),
|
zap.String("payout_id", state.PayoutID),
|
||||||
zap.String("customer_id", req.GetCustomerId()),
|
zap.String("customer_id", state.CustomerID),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
state.ProviderPaymentId = result.ProviderRequestID
|
state.ProviderPaymentID = strings.TrimSpace(result.ProviderRequestID)
|
||||||
if result.Accepted {
|
if result.Accepted {
|
||||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
state.Status = model.PayoutStatusWaiting
|
||||||
} else {
|
} else {
|
||||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
state.Status = model.PayoutStatusFailed
|
||||||
state.ProviderCode = result.ErrorCode
|
state.ProviderCode = strings.TrimSpace(result.ErrorCode)
|
||||||
state.ProviderMessage = result.ErrorMessage
|
state.ProviderMessage = strings.TrimSpace(result.ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.UpdatedAt = p.clock.Now()
|
||||||
|
if err := p.updatePayoutStatus(ctx, state); err != nil {
|
||||||
|
p.logger.Warn("Failed to update payout status", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
|
||||||
p.store.Save(state)
|
|
||||||
|
|
||||||
resp := &mntxv1.CardTokenPayoutResponse{
|
resp := &mntxv1.CardTokenPayoutResponse{
|
||||||
Payout: state,
|
Payout: StateToProto(state),
|
||||||
Accepted: result.Accepted,
|
Accepted: result.Accepted,
|
||||||
ProviderRequestId: result.ProviderRequestID,
|
ProviderRequestId: result.ProviderRequestID,
|
||||||
ErrorCode: result.ErrorCode,
|
ErrorCode: result.ErrorCode,
|
||||||
@@ -227,8 +291,8 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.logger.Info("Card token payout submission stored",
|
p.logger.Info("Card token payout submission stored",
|
||||||
zap.String("payout_id", state.GetPayoutId()),
|
zap.String("payout_id", state.PayoutID),
|
||||||
zap.String("status", state.GetStatus().String()),
|
zap.String("status", string(state.Status)),
|
||||||
zap.Bool("accepted", result.Accepted),
|
zap.Bool("accepted", result.Accepted),
|
||||||
zap.String("provider_request_id", result.ProviderRequestID),
|
zap.String("provider_request_id", result.ProviderRequestID),
|
||||||
)
|
)
|
||||||
@@ -240,10 +304,12 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
|
|||||||
if p == nil {
|
if p == nil {
|
||||||
return nil, merrors.Internal("card payout processor not initialised")
|
return nil, merrors.Internal("card payout processor not initialised")
|
||||||
}
|
}
|
||||||
|
|
||||||
p.logger.Info("Submitting card tokenization",
|
p.logger.Info("Submitting card tokenization",
|
||||||
zap.String("request_id", strings.TrimSpace(req.GetRequestId())),
|
zap.String("request_id", strings.TrimSpace(req.GetRequestId())),
|
||||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||||
)
|
)
|
||||||
|
|
||||||
cardInput, err := validateCardTokenizeRequest(req, p.config)
|
cardInput, err := validateCardTokenizeRequest(req, p.config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logger.Warn("Card tokenization validation failed",
|
p.logger.Warn("Card tokenization validation failed",
|
||||||
@@ -265,8 +331,10 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
|
|||||||
|
|
||||||
req = sanitizeCardTokenizeRequest(req)
|
req = sanitizeCardTokenizeRequest(req)
|
||||||
cardInput = extractTokenizeCard(req)
|
cardInput = extractTokenizeCard(req)
|
||||||
|
|
||||||
client := monetix.NewClient(p.config, p.httpClient, p.logger)
|
client := monetix.NewClient(p.config, p.httpClient, p.logger)
|
||||||
apiReq := buildCardTokenizeRequest(projectID, req, cardInput)
|
apiReq := buildCardTokenizeRequest(projectID, req, cardInput)
|
||||||
|
|
||||||
result, err := client.CreateCardTokenization(ctx, apiReq)
|
result, err := client.CreateCardTokenization(ctx, apiReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logger.Warn("Monetix tokenization request failed",
|
p.logger.Warn("Monetix tokenization request failed",
|
||||||
@@ -298,36 +366,45 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *cardPayoutProcessor) Status(_ context.Context, payoutID string) (*mntxv1.CardPayoutState, error) {
|
func (p *cardPayoutProcessor) Status(ctx context.Context, payoutID string) (*mntxv1.CardPayoutState, error) {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil, merrors.Internal("card payout processor not initialised")
|
return nil, merrors.Internal("card payout processor not initialised")
|
||||||
}
|
}
|
||||||
|
|
||||||
id := strings.TrimSpace(payoutID)
|
id := strings.TrimSpace(payoutID)
|
||||||
p.logger.Info("Card payout status requested", zap.String("payout_id", id))
|
p.logger.Info("Card payout status requested", zap.String("payout_id", id))
|
||||||
|
|
||||||
if id == "" {
|
if id == "" {
|
||||||
p.logger.Warn("Payout status requested with empty payout_id")
|
p.logger.Warn("Payout status requested with empty payout_id")
|
||||||
return nil, merrors.InvalidArgument("payout_id is required", "payout_id")
|
return nil, merrors.InvalidArgument("payout_id is required", "payout_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
state, ok := p.store.Get(id)
|
state, err := p.store.Payouts().FindByPaymentID(ctx, id)
|
||||||
if !ok || state == nil {
|
if err != nil || state == nil {
|
||||||
p.logger.Warn("Payout status not found", zap.String("payout_id", id))
|
p.logger.Warn("Payout status not found", zap.String("payout_id", id), zap.Error(err))
|
||||||
return nil, merrors.NoData("payout not found")
|
return nil, merrors.NoData("payout not found")
|
||||||
}
|
}
|
||||||
p.logger.Info("Card payout status resolved", zap.String("payout_id", state.GetPayoutId()), zap.String("status", state.GetStatus().String()))
|
|
||||||
return state, nil
|
p.logger.Info("Card payout status resolved",
|
||||||
|
zap.String("payout_id", state.PayoutID),
|
||||||
|
zap.String("status", string(state.Status)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return StateToProto(state), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byte) (int, error) {
|
func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byte) (int, error) {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return http.StatusInternalServerError, merrors.Internal("card payout processor not initialised")
|
return http.StatusInternalServerError, merrors.Internal("card payout processor not initialised")
|
||||||
}
|
}
|
||||||
|
|
||||||
p.logger.Debug("Processing Monetix callback", zap.Int("payload_bytes", len(payload)))
|
p.logger.Debug("Processing Monetix callback", zap.Int("payload_bytes", len(payload)))
|
||||||
|
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
p.logger.Warn("Received empty Monetix callback payload")
|
p.logger.Warn("Received empty Monetix callback payload")
|
||||||
return http.StatusBadRequest, merrors.InvalidArgument("callback body is empty")
|
return http.StatusBadRequest, merrors.InvalidArgument("callback body is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(p.config.SecretKey) == "" {
|
if strings.TrimSpace(p.config.SecretKey) == "" {
|
||||||
p.logger.Warn("Monetix secret key is not configured; cannot verify callback")
|
p.logger.Warn("Monetix secret key is not configured; cannot verify callback")
|
||||||
return http.StatusInternalServerError, merrors.Internal("monetix secret key is not configured")
|
return http.StatusInternalServerError, merrors.Internal("monetix secret key is not configured")
|
||||||
@@ -354,45 +431,48 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt
|
|||||||
return status, err
|
return status, err
|
||||||
}
|
}
|
||||||
|
|
||||||
state, statusLabel := mapCallbackToState(p.clock, p.config, cb)
|
// mapCallbackToState currently returns proto-state in your code.
|
||||||
if existing, ok := p.store.Get(state.GetPayoutId()); ok && existing != nil {
|
// Convert it to mongo model and preserve internal refs if record exists.
|
||||||
if existing.GetCreatedAt() != nil {
|
pbState, statusLabel := mapCallbackToState(p.clock, p.config, cb)
|
||||||
state.CreatedAt = existing.GetCreatedAt()
|
|
||||||
|
// Convert proto -> mongo (operationRef/idempotencyKey are internal; keep empty for now)
|
||||||
|
state := CardPayoutStateFromProto(p.clock, pbState)
|
||||||
|
|
||||||
|
// Preserve CreatedAt + internal keys from existing record if present.
|
||||||
|
if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PayoutID); err != nil {
|
||||||
|
p.logger.Warn("Failed to fetch payout state while processing callback",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("payout_id", state.PayoutID),
|
||||||
|
)
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
} else if existing != nil {
|
||||||
|
if !existing.CreatedAt.IsZero() {
|
||||||
|
state.CreatedAt = existing.CreatedAt
|
||||||
|
}
|
||||||
|
if state.OperationRef == "" {
|
||||||
|
state.OperationRef = existing.OperationRef
|
||||||
|
}
|
||||||
|
if state.IdempotencyKey == "" {
|
||||||
|
state.IdempotencyKey = existing.IdempotencyKey
|
||||||
|
}
|
||||||
|
// keep failure reason if you want, or override depending on callback semantics
|
||||||
|
if state.FailureReason == "" {
|
||||||
|
state.FailureReason = existing.FailureReason
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.store.Save(state)
|
|
||||||
p.emitCardPayoutEvent(state)
|
if err := p.updatePayoutStatus(ctx, state); err != nil {
|
||||||
|
p.logger.Warn("Failed to update payout state while processing callback", zap.Error(err))
|
||||||
|
}
|
||||||
monetix.ObserveCallback(statusLabel)
|
monetix.ObserveCallback(statusLabel)
|
||||||
|
|
||||||
p.logger.Info("Monetix payout callback processed",
|
p.logger.Info("Monetix payout callback processed",
|
||||||
zap.String("payout_id", state.GetPayoutId()),
|
zap.String("payout_id", state.PayoutID),
|
||||||
zap.String("status", statusLabel),
|
zap.String("status", statusLabel),
|
||||||
zap.String("provider_code", state.GetProviderCode()),
|
zap.String("provider_code", state.ProviderCode),
|
||||||
zap.String("provider_message", state.GetProviderMessage()),
|
zap.String("provider_message", state.ProviderMessage),
|
||||||
zap.String("masked_account", cb.Account.Number),
|
zap.String("masked_account", cb.Account.Number),
|
||||||
)
|
)
|
||||||
|
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *cardPayoutProcessor) emitCardPayoutEvent(state *mntxv1.CardPayoutState) {
|
|
||||||
if state == nil || p.producer == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
event := &mntxv1.CardPayoutStatusChangedEvent{Payout: state}
|
|
||||||
payload, err := protojson.Marshal(event)
|
|
||||||
if err != nil {
|
|
||||||
p.logger.Warn("Failed to marshal payout callback event", zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
env := messaging.CreateEnvelope(string(mservice.MntxGateway), model.NewNotification(mservice.MntxGateway, nm.NAUpdated))
|
|
||||||
if _, err := env.Wrap(payload); err != nil {
|
|
||||||
p.logger.Warn("Failed to wrap payout callback event payload", zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := p.producer.SendMessage(env); err != nil {
|
|
||||||
p.logger.Warn("Failed to publish payout callback event", zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||||
@@ -40,10 +40,11 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
|
|||||||
AllowedCurrencies: []string{"RUB"},
|
AllowedCurrencies: []string{"RUB"},
|
||||||
}
|
}
|
||||||
|
|
||||||
existingCreated := timestamppb.New(time.Date(2020, 2, 3, 0, 0, 0, 0, time.UTC))
|
existingCreated := time.Date(2020, 2, 3, 0, 0, 0, 0, time.UTC)
|
||||||
store := newCardPayoutStore()
|
|
||||||
store.Save(&mntxv1.CardPayoutState{
|
repo := newMockRepository()
|
||||||
PayoutId: "payout-1",
|
repo.payouts.Save(&model.CardPayout{
|
||||||
|
PayoutID: "payout-1",
|
||||||
CreatedAt: existingCreated,
|
CreatedAt: existingCreated,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
||||||
processor := newCardPayoutProcessor(zap.NewNop(), cfg, staticClock{now: now}, store, httpClient, nil)
|
processor := newCardPayoutProcessor(zap.NewNop(), cfg, staticClock{now: now}, repo, httpClient, nil)
|
||||||
|
|
||||||
req := validCardPayoutRequest()
|
req := validCardPayoutRequest()
|
||||||
req.ProjectId = 0
|
req.ProjectId = 0
|
||||||
@@ -76,27 +77,38 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
|
|||||||
if resp.GetPayout().GetProjectId() != cfg.ProjectID {
|
if resp.GetPayout().GetProjectId() != cfg.ProjectID {
|
||||||
t.Fatalf("expected project id %d, got %d", cfg.ProjectID, resp.GetPayout().GetProjectId())
|
t.Fatalf("expected project id %d, got %d", cfg.ProjectID, resp.GetPayout().GetProjectId())
|
||||||
}
|
}
|
||||||
if resp.GetPayout().GetStatus() != mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING {
|
if resp.GetPayout().GetStatus() != mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING {
|
||||||
t.Fatalf("expected pending status, got %v", resp.GetPayout().GetStatus())
|
t.Fatalf("expected waiting status, got %v", resp.GetPayout().GetStatus())
|
||||||
}
|
}
|
||||||
if !resp.GetPayout().GetCreatedAt().AsTime().Equal(existingCreated.AsTime()) {
|
if !resp.GetPayout().GetCreatedAt().AsTime().Equal(existingCreated) {
|
||||||
t.Fatalf("expected created_at preserved, got %v", resp.GetPayout().GetCreatedAt().AsTime())
|
t.Fatalf("expected created_at preserved, got %v", resp.GetPayout().GetCreatedAt().AsTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
stored, ok := store.Get(req.GetPayoutId())
|
stored, ok := repo.payouts.Get(req.GetPayoutId())
|
||||||
if !ok || stored == nil {
|
if !ok || stored == nil {
|
||||||
t.Fatalf("expected payout state stored")
|
t.Fatalf("expected payout state stored")
|
||||||
}
|
}
|
||||||
if stored.GetProviderPaymentId() == "" {
|
if stored.ProviderPaymentID == "" {
|
||||||
t.Fatalf("expected provider payment id")
|
t.Fatalf("expected provider payment id")
|
||||||
}
|
}
|
||||||
|
if !stored.CreatedAt.Equal(existingCreated) {
|
||||||
|
t.Fatalf("expected created_at preserved in model, got %v", stored.CreatedAt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCardPayoutProcessor_Submit_MissingConfig(t *testing.T) {
|
func TestCardPayoutProcessor_Submit_MissingConfig(t *testing.T) {
|
||||||
cfg := monetix.Config{
|
cfg := monetix.Config{
|
||||||
AllowedCurrencies: []string{"RUB"},
|
AllowedCurrencies: []string{"RUB"},
|
||||||
}
|
}
|
||||||
processor := newCardPayoutProcessor(zap.NewNop(), cfg, clockpkg.NewSystem(), newCardPayoutStore(), &http.Client{}, nil)
|
|
||||||
|
processor := newCardPayoutProcessor(
|
||||||
|
zap.NewNop(),
|
||||||
|
cfg,
|
||||||
|
clockpkg.NewSystem(),
|
||||||
|
newMockRepository(),
|
||||||
|
&http.Client{},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
_, err := processor.Submit(context.Background(), validCardPayoutRequest())
|
_, err := processor.Submit(context.Background(), validCardPayoutRequest())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -114,12 +126,21 @@ func TestCardPayoutProcessor_ProcessCallback(t *testing.T) {
|
|||||||
StatusProcessing: "processing",
|
StatusProcessing: "processing",
|
||||||
AllowedCurrencies: []string{"RUB"},
|
AllowedCurrencies: []string{"RUB"},
|
||||||
}
|
}
|
||||||
store := newCardPayoutStore()
|
|
||||||
processor := newCardPayoutProcessor(zap.NewNop(), cfg, staticClock{now: time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)}, store, &http.Client{}, nil)
|
repo := newMockRepository()
|
||||||
|
|
||||||
|
processor := newCardPayoutProcessor(
|
||||||
|
zap.NewNop(),
|
||||||
|
cfg,
|
||||||
|
staticClock{now: time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)},
|
||||||
|
repo,
|
||||||
|
&http.Client{},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
cb := baseCallback()
|
cb := baseCallback()
|
||||||
cb.Payment.Sum.Currency = "RUB"
|
cb.Payment.Sum.Currency = "RUB"
|
||||||
cb.Signature = ""
|
|
||||||
sig, err := monetix.SignPayload(cb, cfg.SecretKey)
|
sig, err := monetix.SignPayload(cb, cfg.SecretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to sign callback: %v", err)
|
t.Fatalf("failed to sign callback: %v", err)
|
||||||
@@ -139,11 +160,12 @@ func TestCardPayoutProcessor_ProcessCallback(t *testing.T) {
|
|||||||
t.Fatalf("expected status ok, got %d", status)
|
t.Fatalf("expected status ok, got %d", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
state, ok := store.Get(cb.Payment.ID)
|
state, ok := repo.payouts.Get(cb.Payment.ID)
|
||||||
if !ok || state == nil {
|
if !ok || state == nil {
|
||||||
t.Fatalf("expected payout state stored")
|
t.Fatalf("expected payout state stored")
|
||||||
}
|
}
|
||||||
if state.GetStatus() != mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED {
|
|
||||||
t.Fatalf("expected processed status, got %v", state.GetStatus())
|
if state.Status != model.PayoutStatusSuccess {
|
||||||
|
t.Fatalf("expected success status in model, got %v", state.Status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/tech/sendico/gateway/mntx/internal/appversion"
|
"github.com/tech/sendico/gateway/mntx/internal/appversion"
|
||||||
"github.com/tech/sendico/pkg/connector/params"
|
"github.com/tech/sendico/pkg/connector/params"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,9 +19,9 @@ const mntxConnectorID = "mntx"
|
|||||||
func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
||||||
return &connectorv1.GetCapabilitiesResponse{
|
return &connectorv1.GetCapabilitiesResponse{
|
||||||
Capabilities: &connectorv1.ConnectorCapabilities{
|
Capabilities: &connectorv1.ConnectorCapabilities{
|
||||||
ConnectorType: mntxConnectorID,
|
ConnectorType: mntxConnectorID,
|
||||||
Version: appversion.Create().Short(),
|
Version: appversion.Create().Short(),
|
||||||
SupportedAccountKinds: nil,
|
SupportedAccountKinds: nil,
|
||||||
SupportedOperationTypes: []connectorv1.OperationType{connectorv1.OperationType_PAYOUT},
|
SupportedOperationTypes: []connectorv1.OperationType{connectorv1.OperationType_PAYOUT},
|
||||||
OperationParams: mntxOperationParams(),
|
OperationParams: mntxOperationParams(),
|
||||||
},
|
},
|
||||||
@@ -161,49 +161,49 @@ func currencyFromOperation(op *connectorv1.Operation) string {
|
|||||||
|
|
||||||
func buildCardTokenPayoutRequestFromParams(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardTokenPayoutRequest {
|
func buildCardTokenPayoutRequestFromParams(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardTokenPayoutRequest {
|
||||||
req := &mntxv1.CardTokenPayoutRequest{
|
req := &mntxv1.CardTokenPayoutRequest{
|
||||||
PayoutId: payoutID,
|
PayoutId: payoutID,
|
||||||
ProjectId: readerInt64(reader, "project_id"),
|
ProjectId: readerInt64(reader, "project_id"),
|
||||||
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
||||||
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
||||||
CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
|
CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
|
||||||
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
||||||
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
||||||
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
||||||
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
||||||
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
||||||
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
||||||
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
||||||
AmountMinor: amountMinor,
|
AmountMinor: amountMinor,
|
||||||
Currency: currency,
|
Currency: currency,
|
||||||
CardToken: strings.TrimSpace(reader.String("card_token")),
|
CardToken: strings.TrimSpace(reader.String("card_token")),
|
||||||
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
||||||
MaskedPan: strings.TrimSpace(reader.String("masked_pan")),
|
MaskedPan: strings.TrimSpace(reader.String("masked_pan")),
|
||||||
Metadata: reader.StringMap("metadata"),
|
Metadata: reader.StringMap("metadata"),
|
||||||
}
|
}
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCardPayoutRequestFromParams(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardPayoutRequest {
|
func buildCardPayoutRequestFromParams(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardPayoutRequest {
|
||||||
return &mntxv1.CardPayoutRequest{
|
return &mntxv1.CardPayoutRequest{
|
||||||
PayoutId: payoutID,
|
PayoutId: payoutID,
|
||||||
ProjectId: readerInt64(reader, "project_id"),
|
ProjectId: readerInt64(reader, "project_id"),
|
||||||
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
||||||
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
||||||
CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
|
CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
|
||||||
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
||||||
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
||||||
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
||||||
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
||||||
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
||||||
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
||||||
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
||||||
AmountMinor: amountMinor,
|
AmountMinor: amountMinor,
|
||||||
Currency: currency,
|
Currency: currency,
|
||||||
CardPan: strings.TrimSpace(reader.String("card_pan")),
|
CardPan: strings.TrimSpace(reader.String("card_pan")),
|
||||||
CardExpYear: uint32(readerInt64(reader, "card_exp_year")),
|
CardExpYear: uint32(readerInt64(reader, "card_exp_year")),
|
||||||
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")),
|
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")),
|
||||||
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
||||||
Metadata: reader.StringMap("metadata"),
|
Metadata: reader.StringMap("metadata"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ func readerInt64(reader params.Reader, key string) int64 {
|
|||||||
func payoutReceipt(state *mntxv1.CardPayoutState) *connectorv1.OperationReceipt {
|
func payoutReceipt(state *mntxv1.CardPayoutState) *connectorv1.OperationReceipt {
|
||||||
if state == nil {
|
if state == nil {
|
||||||
return &connectorv1.OperationReceipt{
|
return &connectorv1.OperationReceipt{
|
||||||
Status: connectorv1.OperationStatus_PENDING,
|
Status: connectorv1.OperationStatus_OPERATION_PROCESSING,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &connectorv1.OperationReceipt{
|
return &connectorv1.OperationReceipt{
|
||||||
@@ -252,14 +252,24 @@ func minorToDecimal(amount int64) string {
|
|||||||
|
|
||||||
func payoutStatusToOperation(status mntxv1.PayoutStatus) connectorv1.OperationStatus {
|
func payoutStatusToOperation(status mntxv1.PayoutStatus) connectorv1.OperationStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED:
|
|
||||||
return connectorv1.OperationStatus_CONFIRMED
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_CREATED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CREATED
|
||||||
|
|
||||||
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_WAITING
|
||||||
|
|
||||||
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_SUCCESS
|
||||||
|
|
||||||
case mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED:
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED:
|
||||||
return connectorv1.OperationStatus_FAILED
|
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||||
case mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING:
|
|
||||||
return connectorv1.OperationStatus_PENDING
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_CANCELLED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return connectorv1.OperationStatus_PENDING
|
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
97
api/gateway/mntx/internal/service/gateway/helpers.go
Normal file
97
api/gateway/mntx/internal/service/gateway/helpers.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||||
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
|
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func tsOrNow(clock clockpkg.Clock, ts *timestamppb.Timestamp) time.Time {
|
||||||
|
if ts == nil {
|
||||||
|
return clock.Now()
|
||||||
|
}
|
||||||
|
return ts.AsTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CardPayoutStateFromProto(clock clockpkg.Clock, p *mntxv1.CardPayoutState) *model.CardPayout {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.CardPayout{
|
||||||
|
PayoutID: p.PayoutId,
|
||||||
|
OperationRef: p.GetOperationRef(),
|
||||||
|
IntentRef: p.GetIntentRef(),
|
||||||
|
IdempotencyKey: p.GetIdempotencyKey(),
|
||||||
|
ProjectID: p.ProjectId,
|
||||||
|
CustomerID: p.CustomerId,
|
||||||
|
AmountMinor: p.AmountMinor,
|
||||||
|
Currency: p.Currency,
|
||||||
|
Status: payoutStatusFromProto(p.Status),
|
||||||
|
ProviderCode: p.ProviderCode,
|
||||||
|
ProviderMessage: p.ProviderMessage,
|
||||||
|
ProviderPaymentID: p.ProviderPaymentId,
|
||||||
|
CreatedAt: tsOrNow(clock, p.CreatedAt),
|
||||||
|
UpdatedAt: tsOrNow(clock, p.UpdatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StateToProto(m *model.CardPayout) *mntxv1.CardPayoutState {
|
||||||
|
return &mntxv1.CardPayoutState{
|
||||||
|
PayoutId: m.PayoutID,
|
||||||
|
ProjectId: m.ProjectID,
|
||||||
|
CustomerId: m.CustomerID,
|
||||||
|
AmountMinor: m.AmountMinor,
|
||||||
|
Currency: m.Currency,
|
||||||
|
Status: payoutStatusToProto(m.Status),
|
||||||
|
ProviderCode: m.ProviderCode,
|
||||||
|
ProviderMessage: m.ProviderMessage,
|
||||||
|
ProviderPaymentId: m.ProviderPaymentID,
|
||||||
|
CreatedAt: timestamppb.New(m.CreatedAt),
|
||||||
|
UpdatedAt: timestamppb.New(m.UpdatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func payoutStatusToProto(s model.PayoutStatus) mntxv1.PayoutStatus {
|
||||||
|
switch s {
|
||||||
|
case model.PayoutStatusCreated:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_CREATED
|
||||||
|
case model.PayoutStatusProcessing:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING
|
||||||
|
case model.PayoutStatusWaiting:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING
|
||||||
|
case model.PayoutStatusSuccess:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS
|
||||||
|
case model.PayoutStatusFailed:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||||
|
case model.PayoutStatusCancelled:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_CANCELLED
|
||||||
|
default:
|
||||||
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_CREATED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func payoutStatusFromProto(s mntxv1.PayoutStatus) model.PayoutStatus {
|
||||||
|
switch s {
|
||||||
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_CREATED:
|
||||||
|
return model.PayoutStatusCreated
|
||||||
|
|
||||||
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING:
|
||||||
|
return model.PayoutStatusWaiting
|
||||||
|
|
||||||
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS:
|
||||||
|
return model.PayoutStatusSuccess
|
||||||
|
|
||||||
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED:
|
||||||
|
return model.PayoutStatusFailed
|
||||||
|
|
||||||
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_CANCELLED:
|
||||||
|
return model.PayoutStatusCancelled
|
||||||
|
|
||||||
|
default:
|
||||||
|
return model.PayoutStatusCreated
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage"
|
||||||
"github.com/tech/sendico/pkg/clock"
|
"github.com/tech/sendico/pkg/clock"
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||||
@@ -29,6 +30,12 @@ func WithProducer(p msg.Producer) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithStorage(storage storage.Repository) Option {
|
||||||
|
return func(s *Service) {
|
||||||
|
s.storage = storage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithHTTPClient injects a custom HTTP client (useful for tests).
|
// WithHTTPClient injects a custom HTTP client (useful for tests).
|
||||||
func WithHTTPClient(client *http.Client) Option {
|
func WithHTTPClient(client *http.Client) Option {
|
||||||
return func(s *Service) {
|
return func(s *Service) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/gateway/mntx/internal/appversion"
|
"github.com/tech/sendico/gateway/mntx/internal/appversion"
|
||||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage"
|
||||||
"github.com/tech/sendico/pkg/api/routers"
|
"github.com/tech/sendico/pkg/api/routers"
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
@@ -24,7 +25,7 @@ type Service struct {
|
|||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
clock clockpkg.Clock
|
clock clockpkg.Clock
|
||||||
producer msg.Producer
|
producer msg.Producer
|
||||||
cardStore *cardPayoutStore
|
storage storage.Repository
|
||||||
config monetix.Config
|
config monetix.Config
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
card *cardPayoutProcessor
|
card *cardPayoutProcessor
|
||||||
@@ -60,10 +61,9 @@ func (r reasonedError) Reason() string {
|
|||||||
// NewService constructs the Monetix gateway service skeleton.
|
// NewService constructs the Monetix gateway service skeleton.
|
||||||
func NewService(logger mlogger.Logger, opts ...Option) *Service {
|
func NewService(logger mlogger.Logger, opts ...Option) *Service {
|
||||||
svc := &Service{
|
svc := &Service{
|
||||||
logger: logger.Named("service"),
|
logger: logger.Named("service"),
|
||||||
clock: clockpkg.NewSystem(),
|
clock: clockpkg.NewSystem(),
|
||||||
cardStore: newCardPayoutStore(),
|
config: monetix.DefaultConfig(),
|
||||||
config: monetix.DefaultConfig(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initMetrics()
|
initMetrics()
|
||||||
@@ -84,11 +84,7 @@ func NewService(logger mlogger.Logger, opts ...Option) *Service {
|
|||||||
svc.httpClient.Timeout = svc.config.Timeout()
|
svc.httpClient.Timeout = svc.config.Timeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
if svc.cardStore == nil {
|
svc.card = newCardPayoutProcessor(svc.logger, svc.config, svc.clock, svc.storage, svc.httpClient, svc.producer)
|
||||||
svc.cardStore = newCardPayoutStore()
|
|
||||||
}
|
|
||||||
|
|
||||||
svc.card = newCardPayoutProcessor(svc.logger, svc.config, svc.clock, svc.cardStore, svc.httpClient, svc.producer)
|
|
||||||
svc.startDiscoveryAnnouncer()
|
svc.startDiscoveryAnnouncer()
|
||||||
|
|
||||||
return svc
|
return svc
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||||
|
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
|
paytypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
|
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isFinalStatus(t *model.CardPayout) bool {
|
||||||
|
switch t.Status {
|
||||||
|
case model.PayoutStatusFailed, model.PayoutStatusSuccess, model.PayoutStatusCancelled:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toOpStatus(t *model.CardPayout) rail.OperationResult {
|
||||||
|
switch t.Status {
|
||||||
|
case model.PayoutStatusFailed:
|
||||||
|
return rail.OperationResultFailed
|
||||||
|
case model.PayoutStatusSuccess:
|
||||||
|
return rail.OperationResultSuccess
|
||||||
|
case model.PayoutStatusCancelled:
|
||||||
|
return rail.OperationResultCancelled
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unexpected transfer status, %s", t.Status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toError(t *model.CardPayout) *gatewayv1.OperationError {
|
||||||
|
if t.Status == model.PayoutStatusSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &gatewayv1.OperationError{
|
||||||
|
Message: t.FailureReason,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cardPayoutProcessor) updatePayoutStatus(ctx context.Context, state *model.CardPayout) error {
|
||||||
|
if err := p.store.Payouts().Upsert(ctx, state); err != nil {
|
||||||
|
p.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", state.PayoutID), zap.String("status", string(state.Status)), zap.Error(err))
|
||||||
|
}
|
||||||
|
if isFinalStatus(state) {
|
||||||
|
p.emitTransferStatusEvent(state)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cardPayoutProcessor) emitTransferStatusEvent(payout *model.CardPayout) {
|
||||||
|
if p == nil || p.producer == nil || payout == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exec := pmodel.PaymentGatewayExecution{
|
||||||
|
PaymentIntentID: payout.IntentRef,
|
||||||
|
IdempotencyKey: payout.IdempotencyKey,
|
||||||
|
ExecutedMoney: &paytypes.Money{
|
||||||
|
Amount: fmt.Sprintf("%d", payout.AmountMinor),
|
||||||
|
Currency: payout.Currency,
|
||||||
|
},
|
||||||
|
PaymentRef: payout.PaymentRef,
|
||||||
|
Status: toOpStatus(payout),
|
||||||
|
OperationRef: payout.OperationRef,
|
||||||
|
Error: payout.FailureReason,
|
||||||
|
TransferRef: payout.GetID().Hex(),
|
||||||
|
}
|
||||||
|
env := paymentgateway.PaymentGatewayExecution(mservice.MntxGateway, &exec)
|
||||||
|
if err := p.producer.SendMessage(env); err != nil {
|
||||||
|
p.logger.Warn("Failed to publish transfer status event", zap.Error(err), mzap.ObjRef("transfer_ref", payout.ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
29
api/gateway/mntx/storage/model/state.go
Normal file
29
api/gateway/mntx/storage/model/state.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CardPayout is a Mongo/JSON representation of proto CardPayout
|
||||||
|
type CardPayout struct {
|
||||||
|
storable.Base `bson:",inline" json:",inline"`
|
||||||
|
PayoutID string `bson:"payoutId" json:"payout_id"`
|
||||||
|
PaymentRef string `bson:"paymentRef" json:"payment_ref"`
|
||||||
|
OperationRef string `bson:"operationRef" json:"operation_ref"`
|
||||||
|
IdempotencyKey string `bson:"idempotencyKey" json:"idempotency_key"`
|
||||||
|
IntentRef string `bson:"intentRef" json:"intentRef"`
|
||||||
|
ProjectID int64 `bson:"projectId" json:"project_id"`
|
||||||
|
CustomerID string `bson:"customerId" json:"customer_id"`
|
||||||
|
AmountMinor int64 `bson:"amountMinor" json:"amount_minor"`
|
||||||
|
Currency string `bson:"currency" json:"currency"`
|
||||||
|
Status PayoutStatus `bson:"status" json:"status"`
|
||||||
|
ProviderCode string `bson:"providerCode,omitempty" json:"provider_code,omitempty"`
|
||||||
|
ProviderMessage string `bson:"providerMessage,omitempty" json:"provider_message,omitempty"`
|
||||||
|
ProviderPaymentID string `bson:"providerPaymentId,omitempty" json:"provider_payment_id,omitempty"`
|
||||||
|
FailureReason string `bson:"failureReason,omitempty" json:"failure_reason,omitempty"`
|
||||||
|
|
||||||
|
CreatedAt time.Time `bson:"createdAt" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `bson:"updatedAt" json:"updated_at"`
|
||||||
|
}
|
||||||
13
api/gateway/mntx/storage/model/status.go
Normal file
13
api/gateway/mntx/storage/model/status.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type PayoutStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PayoutStatusCreated PayoutStatus = "created" // record exists, not started
|
||||||
|
PayoutStatusProcessing PayoutStatus = "processing" // we are working on it
|
||||||
|
PayoutStatusWaiting PayoutStatus = "waiting" // waiting external world
|
||||||
|
|
||||||
|
PayoutStatusSuccess PayoutStatus = "success" // final success
|
||||||
|
PayoutStatusFailed PayoutStatus = "failed" // final failure
|
||||||
|
PayoutStatusCancelled PayoutStatus = "cancelled" // final cancelled
|
||||||
|
)
|
||||||
69
api/gateway/mntx/storage/mongo/repository.go
Normal file
69
api/gateway/mntx/storage/mongo/repository.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage"
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage/mongo/store"
|
||||||
|
"github.com/tech/sendico/pkg/db"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
conn *db.MongoConnection
|
||||||
|
db *mongo.Database
|
||||||
|
|
||||||
|
payouts storage.PayoutsStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) {
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
if conn == nil {
|
||||||
|
return nil, merrors.InvalidArgument("mongo connection is nil")
|
||||||
|
}
|
||||||
|
client := conn.Client()
|
||||||
|
if client == nil {
|
||||||
|
return nil, merrors.Internal("mongo client is not initialised")
|
||||||
|
}
|
||||||
|
db := conn.Database()
|
||||||
|
if db == nil {
|
||||||
|
return nil, merrors.Internal("mongo database is not initialised")
|
||||||
|
}
|
||||||
|
dbName := db.Name()
|
||||||
|
logger = logger.Named("storage").Named("mongo")
|
||||||
|
if dbName != "" {
|
||||||
|
logger = logger.With(zap.String("database", dbName))
|
||||||
|
}
|
||||||
|
result := &Repository{
|
||||||
|
logger: logger,
|
||||||
|
conn: conn,
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := result.conn.Ping(ctx); err != nil {
|
||||||
|
result.logger.Error("Mongo ping failed during repository initialisation", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
payoutsStore, err := store.NewPayouts(result.logger, result.db)
|
||||||
|
if err != nil {
|
||||||
|
result.logger.Error("Failed to initialise payouts store", zap.Error(err), zap.String("store", "payments"))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.payouts = payoutsStore
|
||||||
|
result.logger.Info("Payouts gateway MongoDB storage initialised")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Payouts() storage.PayoutsStore {
|
||||||
|
return r.payouts
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ storage.Repository = (*Repository)(nil)
|
||||||
137
api/gateway/mntx/storage/mongo/store/payouts.go
Normal file
137
api/gateway/mntx/storage/mongo/store/payouts.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
storage "github.com/tech/sendico/gateway/mntx/storage"
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
payoutsCollection = "card_payouts"
|
||||||
|
payoutIdemField = "idempotencyKey"
|
||||||
|
payoutIdField = "payoutId"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Payouts struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
coll *mongo.Collection
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPayouts(logger mlogger.Logger, db *mongo.Database) (*Payouts, error) {
|
||||||
|
if db == nil {
|
||||||
|
return nil, merrors.InvalidArgument("mongo database is nil")
|
||||||
|
}
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
logger = logger.Named("payouts").With(zap.String("collection", payoutsCollection))
|
||||||
|
|
||||||
|
repo := repository.CreateMongoRepository(db, payoutsCollection)
|
||||||
|
if err := repo.CreateIndex(&ri.Definition{
|
||||||
|
Keys: []ri.Key{{Field: payoutIdemField, Sort: ri.Asc}},
|
||||||
|
Unique: true,
|
||||||
|
}); err != nil {
|
||||||
|
logger.Error("Failed to create payouts idempotency index",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("index_field", payoutIdemField))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &Payouts{
|
||||||
|
logger: logger,
|
||||||
|
coll: db.Collection(payoutsCollection),
|
||||||
|
}
|
||||||
|
p.logger.Debug("Payouts store initialised")
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payouts) findOneByField(ctx context.Context, field, value string) (*model.CardPayout, error) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return nil, merrors.InvalidArgument("lookup key is required", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result model.CardPayout
|
||||||
|
err := p.coll.FindOne(ctx, bson.M{field: value}).Decode(&result)
|
||||||
|
if err == mongo.ErrNoDocuments {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
p.logger.Warn("Payout record lookup failed",
|
||||||
|
zap.String("field", field),
|
||||||
|
zap.String("value", value),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payouts) FindByIdempotencyKey(ctx context.Context, key string) (*model.CardPayout, error) {
|
||||||
|
return p.findOneByField(ctx, payoutIdemField, key) // operationRef
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payouts) FindByPaymentID(ctx context.Context, paymentID string) (*model.CardPayout, error) {
|
||||||
|
return p.findOneByField(ctx, payoutIdField, paymentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payouts) Upsert(ctx context.Context, record *model.CardPayout) error {
|
||||||
|
if record == nil {
|
||||||
|
return merrors.InvalidArgument("payout record is nil", "record")
|
||||||
|
}
|
||||||
|
|
||||||
|
record.OperationRef = strings.TrimSpace(record.OperationRef)
|
||||||
|
record.PayoutID = strings.TrimSpace(record.PayoutID)
|
||||||
|
record.CustomerID = strings.TrimSpace(record.CustomerID)
|
||||||
|
record.ProviderCode = strings.TrimSpace(record.ProviderCode)
|
||||||
|
record.ProviderPaymentID = strings.TrimSpace(record.ProviderPaymentID)
|
||||||
|
|
||||||
|
if record.OperationRef == "" {
|
||||||
|
return merrors.InvalidArgument("operation ref is required", "operation_ref")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if record.CreatedAt.IsZero() {
|
||||||
|
record.CreatedAt = now
|
||||||
|
}
|
||||||
|
record.UpdatedAt = now
|
||||||
|
|
||||||
|
// critical: never let caller override _id
|
||||||
|
record.ID = bson.NilObjectID
|
||||||
|
|
||||||
|
update := bson.M{
|
||||||
|
"$set": record,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.coll.UpdateOne(
|
||||||
|
ctx,
|
||||||
|
bson.M{payoutIdemField: record.OperationRef},
|
||||||
|
update,
|
||||||
|
options.UpdateOne().SetUpsert(true),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
p.logger.Warn("Failed to upsert payout record",
|
||||||
|
zap.String("operation_ref", record.OperationRef),
|
||||||
|
zap.String("payout_id", record.PayoutID),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ storage.PayoutsStore = (*Payouts)(nil)
|
||||||
20
api/gateway/mntx/storage/storage.go
Normal file
20
api/gateway/mntx/storage/storage.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrDuplicate = merrors.DataConflict("payment gateway storage: duplicate record")
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
Payouts() PayoutsStore
|
||||||
|
}
|
||||||
|
|
||||||
|
type PayoutsStore interface {
|
||||||
|
FindByIdempotencyKey(ctx context.Context, key string) (*model.CardPayout, error)
|
||||||
|
FindByPaymentID(ctx context.Context, key string) (*model.CardPayout, error)
|
||||||
|
Upsert(ctx context.Context, record *model.CardPayout) error
|
||||||
|
}
|
||||||
@@ -45,5 +45,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -208,8 +208,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-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/pkg/connector/params"
|
"github.com/tech/sendico/pkg/connector/params"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -18,9 +18,9 @@ const tgsettleConnectorID = "tgsettle"
|
|||||||
func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
||||||
return &connectorv1.GetCapabilitiesResponse{
|
return &connectorv1.GetCapabilitiesResponse{
|
||||||
Capabilities: &connectorv1.ConnectorCapabilities{
|
Capabilities: &connectorv1.ConnectorCapabilities{
|
||||||
ConnectorType: tgsettleConnectorID,
|
ConnectorType: tgsettleConnectorID,
|
||||||
Version: "",
|
Version: "",
|
||||||
SupportedAccountKinds: nil,
|
SupportedAccountKinds: nil,
|
||||||
SupportedOperationTypes: []connectorv1.OperationType{connectorv1.OperationType_TRANSFER},
|
SupportedOperationTypes: []connectorv1.OperationType{connectorv1.OperationType_TRANSFER},
|
||||||
OperationParams: tgsettleOperationParams(),
|
OperationParams: tgsettleOperationParams(),
|
||||||
},
|
},
|
||||||
@@ -64,7 +64,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
}
|
}
|
||||||
paymentIntentID := strings.TrimSpace(reader.String("payment_intent_id"))
|
paymentIntentID := strings.TrimSpace(reader.String("payment_intent_id"))
|
||||||
if paymentIntentID == "" {
|
if paymentIntentID == "" {
|
||||||
paymentIntentID = strings.TrimSpace(reader.String("client_reference"))
|
paymentIntentID = strings.TrimSpace(reader.String("payment_ref"))
|
||||||
}
|
}
|
||||||
if paymentIntentID == "" {
|
if paymentIntentID == "" {
|
||||||
paymentIntentID = strings.TrimSpace(metadata[metadataPaymentIntentID])
|
paymentIntentID = strings.TrimSpace(metadata[metadataPaymentIntentID])
|
||||||
@@ -122,7 +122,9 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
Destination: dest,
|
Destination: dest,
|
||||||
Amount: normalizedAmount,
|
Amount: normalizedAmount,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
ClientReference: paymentIntentID,
|
PaymentRef: paymentIntentID,
|
||||||
|
IntentRef: strings.TrimSpace(op.GetIntentRef()),
|
||||||
|
OperationRef: strings.TrimSpace(op.GetOperationRef()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("Submit operation transfer failed", append(logFields, zap.Error(err))...)
|
s.logger.Warn("Submit operation transfer failed", append(logFields, zap.Error(err))...)
|
||||||
@@ -239,14 +241,29 @@ func transferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation {
|
|||||||
|
|
||||||
func transferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
func transferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
|
||||||
return connectorv1.OperationStatus_CONFIRMED
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CREATED
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_PROCESSING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_WAITING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_SUCCESS
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
return connectorv1.OperationStatus_FAILED
|
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
return connectorv1.OperationStatus_CANCELED
|
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED:
|
||||||
|
fallthrough
|
||||||
default:
|
default:
|
||||||
return connectorv1.OperationStatus_PENDING
|
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
mb "github.com/tech/sendico/pkg/messaging/broker"
|
mb "github.com/tech/sendico/pkg/messaging/broker"
|
||||||
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
||||||
confirmations "github.com/tech/sendico/pkg/messaging/notifications/confirmations"
|
confirmations "github.com/tech/sendico/pkg/messaging/notifications/confirmations"
|
||||||
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
|
|
||||||
np "github.com/tech/sendico/pkg/messaging/notifications/processor"
|
np "github.com/tech/sendico/pkg/messaging/notifications/processor"
|
||||||
tnotifications "github.com/tech/sendico/pkg/messaging/notifications/telegram"
|
tnotifications "github.com/tech/sendico/pkg/messaging/notifications/telegram"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
@@ -188,12 +187,7 @@ func (s *Service) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransfe
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
existing, err = s.expirePaymentIfNeeded(ctx, existing)
|
s.logger.Info("Submit transfer idempotent hit", append(logFields, zap.String("status", string(existing.Status)))...)
|
||||||
if err != nil {
|
|
||||||
s.logger.Warn("Submit transfer status refresh failed", append(logFields, zap.Error(err))...)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.logger.Info("Submit transfer idempotent hit", append(logFields, zap.String("status", string(paymentStatus(existing))))...)
|
|
||||||
return &chainv1.SubmitTransferResponse{Transfer: transferFromPayment(existing, req)}, nil
|
return &chainv1.SubmitTransferResponse{Transfer: transferFromPayment(existing, req)}, nil
|
||||||
}
|
}
|
||||||
if err := s.onIntent(ctx, intent); err != nil {
|
if err := s.onIntent(ctx, intent); err != nil {
|
||||||
@@ -225,14 +219,9 @@ func (s *Service) GetTransfer(ctx context.Context, req *chainv1.GetTransferReque
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
existing, err = s.expirePaymentIfNeeded(ctx, existing)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Warn("Get transfer status refresh failed", append(logFields, zap.Error(err))...)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.logger.Info("Get transfer resolved from execution", append(logFields,
|
s.logger.Info("Get transfer resolved from execution", append(logFields,
|
||||||
zap.String("payment_intent_id", strings.TrimSpace(existing.PaymentIntentID)),
|
zap.String("payment_intent_id", strings.TrimSpace(existing.PaymentIntentID)),
|
||||||
zap.String("status", string(paymentStatus(existing))),
|
zap.String("status", string(existing.Status)),
|
||||||
)...)
|
)...)
|
||||||
return &chainv1.GetTransferResponse{Transfer: transferFromPayment(existing, nil)}, nil
|
return &chainv1.GetTransferResponse{Transfer: transferFromPayment(existing, nil)}, nil
|
||||||
}
|
}
|
||||||
@@ -274,27 +263,30 @@ func (s *Service) onIntent(ctx context.Context, intent *model.PaymentGatewayInte
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
existing, err = s.expirePaymentIfNeeded(ctx, existing)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.logger.Info("Payment gateway intent already recorded",
|
s.logger.Info("Payment gateway intent already recorded",
|
||||||
zap.String("idempotency_key", confirmReq.RequestID),
|
zap.String("idempotency_key", confirmReq.RequestID),
|
||||||
zap.String("payment_intent_id", confirmReq.PaymentIntentID),
|
zap.String("payment_intent_id", confirmReq.PaymentIntentID),
|
||||||
zap.String("quote_ref", confirmReq.QuoteRef),
|
zap.String("quote_ref", confirmReq.QuoteRef),
|
||||||
zap.String("rail", confirmReq.Rail),
|
zap.String("rail", confirmReq.Rail),
|
||||||
zap.String("status", string(paymentStatus(existing))))
|
zap.String("status", string(existing.Status)))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
record := paymentRecordFromIntent(intent, confirmReq)
|
record := paymentRecordFromIntent(intent, confirmReq)
|
||||||
if err := s.repo.Payments().Upsert(ctx, record); err != nil {
|
if err := s.updateTransferStatus(ctx, record); err != nil {
|
||||||
s.logger.Warn("Failed to persist pending payment", zap.Error(err), zap.String("idempotency_key", confirmReq.RequestID))
|
s.logger.Warn("Failed to persist payment record", zap.Error(err), zap.String("idempotency_key", confirmReq.RequestID))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.sendConfirmationRequest(confirmReq); err != nil {
|
if err := s.sendConfirmationRequest(confirmReq); err != nil {
|
||||||
s.logger.Warn("Failed to publish confirmation request", zap.Error(err), zap.String("idempotency_key", confirmReq.RequestID))
|
s.logger.Warn("Failed to publish confirmation request", zap.Error(err), zap.String("idempotency_key", confirmReq.RequestID))
|
||||||
s.markPaymentExpired(ctx, record, time.Now())
|
// If the confirmation request was not sent, we keep the record in waiting
|
||||||
|
// (or it can be marked as failed — depending on your semantics).
|
||||||
|
// Here, failed is chosen to avoid it hanging indefinitely.
|
||||||
|
record.Status = storagemodel.PaymentStatusFailed
|
||||||
|
record.UpdatedAt = time.Now()
|
||||||
|
if e := s.updateTransferStatus(ctx, record); e != nil {
|
||||||
|
s.logger.Warn("Failed to update payment status change", zap.Error(e), zap.String("idempotency_key", confirmReq.RequestID))
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -302,65 +294,88 @@ func (s *Service) onIntent(ctx context.Context, intent *model.PaymentGatewayInte
|
|||||||
|
|
||||||
func (s *Service) onConfirmationResult(ctx context.Context, result *model.ConfirmationResult) error {
|
func (s *Service) onConfirmationResult(ctx context.Context, result *model.ConfirmationResult) error {
|
||||||
if result == nil {
|
if result == nil {
|
||||||
s.logger.Warn("Confirmation result rejected", zap.String("reason", "result is nil"))
|
|
||||||
return merrors.InvalidArgument("confirmation result is nil", "result")
|
return merrors.InvalidArgument("confirmation result is nil", "result")
|
||||||
}
|
}
|
||||||
|
|
||||||
requestID := strings.TrimSpace(result.RequestID)
|
requestID := strings.TrimSpace(result.RequestID)
|
||||||
if requestID == "" {
|
if requestID == "" {
|
||||||
s.logger.Warn("Confirmation result rejected", zap.String("reason", "request_id is required"))
|
|
||||||
return merrors.InvalidArgument("confirmation request_id is required", "request_id")
|
return merrors.InvalidArgument("confirmation request_id is required", "request_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
record, err := s.loadPayment(ctx, requestID)
|
record, err := s.loadPayment(ctx, requestID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("Confirmation result lookup failed", zap.Error(err), zap.String("request_id", requestID))
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if record == nil {
|
if record == nil {
|
||||||
s.logger.Warn("Confirmation result ignored: payment not found", zap.String("request_id", requestID))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store raw reply for audit/debug purposes. This does NOT affect payment state.
|
||||||
if result.RawReply != nil && s.repo != nil && s.repo.TelegramConfirmations() != nil {
|
if result.RawReply != nil && s.repo != nil && s.repo.TelegramConfirmations() != nil {
|
||||||
if err := s.repo.TelegramConfirmations().Upsert(ctx, &storagemodel.TelegramConfirmation{
|
if e := s.repo.TelegramConfirmations().Upsert(ctx, &storagemodel.TelegramConfirmation{
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
PaymentIntentID: record.PaymentIntentID,
|
PaymentIntentID: record.PaymentIntentID,
|
||||||
QuoteRef: record.QuoteRef,
|
QuoteRef: record.QuoteRef,
|
||||||
RawReply: result.RawReply,
|
RawReply: result.RawReply,
|
||||||
}); err != nil {
|
}); e != nil {
|
||||||
s.logger.Warn("Failed to store telegram confirmation", zap.Error(err), zap.String("request_id", requestID))
|
s.logger.Warn("Failed to store confirmation error", zap.Error(e),
|
||||||
} else {
|
zap.String("request_id", requestID),
|
||||||
s.logger.Info("Stored telegram confirmation", zap.String("request_id", requestID),
|
zap.String("status", string(result.Status)))
|
||||||
zap.String("payment_intent_id", record.PaymentIntentID),
|
|
||||||
zap.String("reply_text", result.RawReply.Text), zap.String("reply_user_id", result.RawReply.FromUserID),
|
|
||||||
zap.String("reply_user", result.RawReply.FromUsername))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextStatus := paymentStatusFromResult(result)
|
// If the payment is already finalized — ignore the result.
|
||||||
currentStatus := paymentStatus(record)
|
switch record.Status {
|
||||||
if currentStatus == storagemodel.PaymentStatusExecuted || currentStatus == storagemodel.PaymentStatusExpired {
|
case storagemodel.PaymentStatusSuccess,
|
||||||
s.logger.Info("Confirmation result ignored: payment already finalized",
|
storagemodel.PaymentStatusFailed,
|
||||||
zap.String("request_id", requestID),
|
storagemodel.PaymentStatusCancelled:
|
||||||
zap.String("status", string(currentStatus)))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
s.applyPaymentResult(record, nextStatus, result)
|
|
||||||
if err := s.repo.Payments().Upsert(ctx, record); err != nil {
|
now := time.Now()
|
||||||
s.logger.Warn("Failed to persist payment status", zap.Error(err), zap.String("request_id", requestID))
|
|
||||||
|
switch result.Status {
|
||||||
|
|
||||||
|
// FINAL: confirmation succeeded
|
||||||
|
case model.ConfirmationStatusConfirmed:
|
||||||
|
record.Status = storagemodel.PaymentStatusSuccess
|
||||||
|
record.ExecutedMoney = result.Money
|
||||||
|
if record.ExecutedAt.IsZero() {
|
||||||
|
record.ExecutedAt = now
|
||||||
|
}
|
||||||
|
record.UpdatedAt = now
|
||||||
|
|
||||||
|
// FINAL: confirmation rejected or timed out
|
||||||
|
case model.ConfirmationStatusRejected,
|
||||||
|
model.ConfirmationStatusTimeout:
|
||||||
|
record.Status = storagemodel.PaymentStatusFailed
|
||||||
|
record.UpdatedAt = now
|
||||||
|
|
||||||
|
// NOT FINAL: do absolutely nothing
|
||||||
|
case model.ConfirmationStatusClarified:
|
||||||
|
s.logger.Debug("Confirmation clarified — no state change",
|
||||||
|
zap.String("request_id", requestID))
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
s.logger.Debug("Non-final confirmation status — ignored",
|
||||||
|
zap.String("request_id", requestID),
|
||||||
|
zap.String("status", string(result.Status)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ONLY place where state is persisted and rail event may be emitted
|
||||||
|
if err := s.updateTransferStatus(ctx, record); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
intent := intentFromPayment(record)
|
|
||||||
s.publishExecution(intent, result)
|
|
||||||
s.publishTelegramReaction(result)
|
s.publishTelegramReaction(result)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) buildConfirmationRequest(intent *model.PaymentGatewayIntent) (*model.ConfirmationRequest, error) {
|
func (s *Service) buildConfirmationRequest(intent *model.PaymentGatewayIntent) (*model.ConfirmationRequest, error) {
|
||||||
targetChatID := strings.TrimSpace(intent.TargetChatID)
|
targetChatID := s.chatID
|
||||||
if targetChatID == "" {
|
|
||||||
targetChatID = s.chatID
|
|
||||||
}
|
|
||||||
if targetChatID == "" {
|
if targetChatID == "" {
|
||||||
return nil, merrors.InvalidArgument("target_chat_id is required", "target_chat_id")
|
return nil, merrors.InvalidArgument("target_chat_id is required", "target_chat_id")
|
||||||
}
|
}
|
||||||
@@ -414,38 +429,6 @@ func (s *Service) sendConfirmationRequest(request *model.ConfirmationRequest) er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) publishExecution(intent *model.PaymentGatewayIntent, result *model.ConfirmationResult) {
|
|
||||||
if s == nil || intent == nil || result == nil || s.producer == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
exec := &model.PaymentGatewayExecution{
|
|
||||||
PaymentIntentID: intent.PaymentIntentID,
|
|
||||||
IdempotencyKey: intent.IdempotencyKey,
|
|
||||||
QuoteRef: intent.QuoteRef,
|
|
||||||
ExecutedMoney: result.Money,
|
|
||||||
Status: result.Status,
|
|
||||||
RequestID: result.RequestID,
|
|
||||||
RawReply: result.RawReply,
|
|
||||||
}
|
|
||||||
env := paymentgateway.PaymentGatewayExecution(string(mservice.PaymentGateway), exec)
|
|
||||||
if err := s.producer.SendMessage(env); err != nil {
|
|
||||||
s.logger.Warn("Failed to publish gateway execution result",
|
|
||||||
zap.Error(err),
|
|
||||||
zap.String("request_id", result.RequestID),
|
|
||||||
zap.String("idempotency_key", intent.IdempotencyKey),
|
|
||||||
zap.String("payment_intent_id", intent.PaymentIntentID),
|
|
||||||
zap.String("quote_ref", intent.QuoteRef),
|
|
||||||
zap.String("status", string(result.Status)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.logger.Info("Published gateway execution result",
|
|
||||||
zap.String("request_id", result.RequestID),
|
|
||||||
zap.String("idempotency_key", intent.IdempotencyKey),
|
|
||||||
zap.String("payment_intent_id", intent.PaymentIntentID),
|
|
||||||
zap.String("quote_ref", intent.QuoteRef),
|
|
||||||
zap.String("status", string(result.Status)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) publishTelegramReaction(result *model.ConfirmationResult) {
|
func (s *Service) publishTelegramReaction(result *model.ConfirmationResult) {
|
||||||
if s == nil || s.producer == nil || result == nil || result.RawReply == nil {
|
if s == nil || s.producer == nil || result == nil || result.RawReply == nil {
|
||||||
return
|
return
|
||||||
@@ -490,46 +473,6 @@ func (s *Service) loadPayment(ctx context.Context, requestID string) (*storagemo
|
|||||||
return s.repo.Payments().FindByIdempotencyKey(ctx, requestID)
|
return s.repo.Payments().FindByIdempotencyKey(ctx, requestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) expirePaymentIfNeeded(ctx context.Context, record *storagemodel.PaymentRecord) (*storagemodel.PaymentRecord, error) {
|
|
||||||
if record == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
status := paymentStatus(record)
|
|
||||||
if status != storagemodel.PaymentStatusPending {
|
|
||||||
return record, nil
|
|
||||||
}
|
|
||||||
if record.ExpiresAt.IsZero() {
|
|
||||||
return record, nil
|
|
||||||
}
|
|
||||||
if time.Now().Before(record.ExpiresAt) {
|
|
||||||
return record, nil
|
|
||||||
}
|
|
||||||
record.Status = storagemodel.PaymentStatusExpired
|
|
||||||
if record.ExpiredAt.IsZero() {
|
|
||||||
record.ExpiredAt = time.Now()
|
|
||||||
}
|
|
||||||
if s != nil && s.repo != nil && s.repo.Payments() != nil {
|
|
||||||
if err := s.repo.Payments().Upsert(ctx, record); err != nil {
|
|
||||||
return record, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return record, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) markPaymentExpired(ctx context.Context, record *storagemodel.PaymentRecord, when time.Time) {
|
|
||||||
if record == nil || s == nil || s.repo == nil || s.repo.Payments() == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if when.IsZero() {
|
|
||||||
when = time.Now()
|
|
||||||
}
|
|
||||||
record.Status = storagemodel.PaymentStatusExpired
|
|
||||||
record.ExpiredAt = when
|
|
||||||
if err := s.repo.Payments().Upsert(ctx, record); err != nil {
|
|
||||||
s.logger.Warn("Failed to mark payment as expired", zap.Error(err), zap.String("request_id", record.IdempotencyKey))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) startAnnouncer() {
|
func (s *Service) startAnnouncer() {
|
||||||
if s == nil || s.producer == nil {
|
if s == nil || s.producer == nil {
|
||||||
return
|
return
|
||||||
@@ -557,78 +500,37 @@ func normalizeIntent(intent *model.PaymentGatewayIntent) *model.PaymentGatewayIn
|
|||||||
cp.IdempotencyKey = strings.TrimSpace(cp.IdempotencyKey)
|
cp.IdempotencyKey = strings.TrimSpace(cp.IdempotencyKey)
|
||||||
cp.OutgoingLeg = strings.TrimSpace(cp.OutgoingLeg)
|
cp.OutgoingLeg = strings.TrimSpace(cp.OutgoingLeg)
|
||||||
cp.QuoteRef = strings.TrimSpace(cp.QuoteRef)
|
cp.QuoteRef = strings.TrimSpace(cp.QuoteRef)
|
||||||
cp.TargetChatID = strings.TrimSpace(cp.TargetChatID)
|
|
||||||
if cp.RequestedMoney != nil {
|
if cp.RequestedMoney != nil {
|
||||||
cp.RequestedMoney.Amount = strings.TrimSpace(cp.RequestedMoney.Amount)
|
cp.RequestedMoney.Amount = strings.TrimSpace(cp.RequestedMoney.Amount)
|
||||||
cp.RequestedMoney.Currency = strings.TrimSpace(cp.RequestedMoney.Currency)
|
cp.RequestedMoney.Currency = strings.TrimSpace(cp.RequestedMoney.Currency)
|
||||||
}
|
}
|
||||||
|
cp.IntentRef = strings.TrimSpace(cp.IntentRef)
|
||||||
|
cp.OperationRef = strings.TrimSpace(cp.OperationRef)
|
||||||
return &cp
|
return &cp
|
||||||
}
|
}
|
||||||
|
|
||||||
func paymentStatus(record *storagemodel.PaymentRecord) storagemodel.PaymentStatus {
|
|
||||||
if record == nil {
|
|
||||||
return storagemodel.PaymentStatusPending
|
|
||||||
}
|
|
||||||
if record.Status != "" {
|
|
||||||
return record.Status
|
|
||||||
}
|
|
||||||
if record.ExecutedMoney != nil || !record.ExecutedAt.IsZero() {
|
|
||||||
return storagemodel.PaymentStatusExecuted
|
|
||||||
}
|
|
||||||
return storagemodel.PaymentStatusPending
|
|
||||||
}
|
|
||||||
|
|
||||||
func paymentStatusFromResult(result *model.ConfirmationResult) storagemodel.PaymentStatus {
|
|
||||||
if result == nil {
|
|
||||||
return storagemodel.PaymentStatusPending
|
|
||||||
}
|
|
||||||
switch result.Status {
|
|
||||||
case model.ConfirmationStatusConfirmed, model.ConfirmationStatusClarified:
|
|
||||||
return storagemodel.PaymentStatusExecuted
|
|
||||||
case model.ConfirmationStatusTimeout, model.ConfirmationStatusRejected:
|
|
||||||
return storagemodel.PaymentStatusExpired
|
|
||||||
default:
|
|
||||||
return storagemodel.PaymentStatusPending
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) applyPaymentResult(record *storagemodel.PaymentRecord, status storagemodel.PaymentStatus, result *model.ConfirmationResult) {
|
|
||||||
if record == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
record.Status = status
|
|
||||||
switch status {
|
|
||||||
case storagemodel.PaymentStatusExecuted:
|
|
||||||
record.ExecutedMoney = result.Money
|
|
||||||
if record.ExecutedAt.IsZero() {
|
|
||||||
record.ExecutedAt = time.Now()
|
|
||||||
}
|
|
||||||
case storagemodel.PaymentStatusExpired:
|
|
||||||
if record.ExpiredAt.IsZero() {
|
|
||||||
record.ExpiredAt = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func paymentRecordFromIntent(intent *model.PaymentGatewayIntent, confirmReq *model.ConfirmationRequest) *storagemodel.PaymentRecord {
|
func paymentRecordFromIntent(intent *model.PaymentGatewayIntent, confirmReq *model.ConfirmationRequest) *storagemodel.PaymentRecord {
|
||||||
record := &storagemodel.PaymentRecord{
|
record := &storagemodel.PaymentRecord{
|
||||||
Status: storagemodel.PaymentStatusPending,
|
Status: storagemodel.PaymentStatusWaiting,
|
||||||
}
|
}
|
||||||
if intent != nil {
|
if intent != nil {
|
||||||
record.IdempotencyKey = strings.TrimSpace(intent.IdempotencyKey)
|
record.IdempotencyKey = strings.TrimSpace(intent.IdempotencyKey)
|
||||||
record.PaymentIntentID = strings.TrimSpace(intent.PaymentIntentID)
|
record.PaymentIntentID = strings.TrimSpace(intent.PaymentIntentID)
|
||||||
record.QuoteRef = strings.TrimSpace(intent.QuoteRef)
|
record.QuoteRef = strings.TrimSpace(intent.QuoteRef)
|
||||||
record.OutgoingLeg = strings.TrimSpace(intent.OutgoingLeg)
|
record.OutgoingLeg = strings.TrimSpace(intent.OutgoingLeg)
|
||||||
record.TargetChatID = strings.TrimSpace(intent.TargetChatID)
|
|
||||||
record.RequestedMoney = intent.RequestedMoney
|
record.RequestedMoney = intent.RequestedMoney
|
||||||
|
record.IntentRef = intent.IntentRef
|
||||||
|
record.OperationRef = intent.OperationRef
|
||||||
}
|
}
|
||||||
if confirmReq != nil {
|
if confirmReq != nil {
|
||||||
record.IdempotencyKey = strings.TrimSpace(confirmReq.RequestID)
|
record.IdempotencyKey = strings.TrimSpace(confirmReq.RequestID)
|
||||||
record.PaymentIntentID = strings.TrimSpace(confirmReq.PaymentIntentID)
|
record.PaymentIntentID = strings.TrimSpace(confirmReq.PaymentIntentID)
|
||||||
record.QuoteRef = strings.TrimSpace(confirmReq.QuoteRef)
|
record.QuoteRef = strings.TrimSpace(confirmReq.QuoteRef)
|
||||||
record.OutgoingLeg = strings.TrimSpace(confirmReq.Rail)
|
record.OutgoingLeg = strings.TrimSpace(confirmReq.Rail)
|
||||||
record.TargetChatID = strings.TrimSpace(confirmReq.TargetChatID)
|
|
||||||
record.RequestedMoney = confirmReq.RequestedMoney
|
record.RequestedMoney = confirmReq.RequestedMoney
|
||||||
|
record.IntentRef = strings.TrimSpace(confirmReq.IntentRef)
|
||||||
|
record.OperationRef = strings.TrimSpace(confirmReq.OperationRef)
|
||||||
|
// ExpiresAt is not used to derive an "expired" status — it can be kept for informational purposes only.
|
||||||
if confirmReq.TimeoutSeconds > 0 {
|
if confirmReq.TimeoutSeconds > 0 {
|
||||||
record.ExpiresAt = time.Now().Add(time.Duration(confirmReq.TimeoutSeconds) * time.Second)
|
record.ExpiresAt = time.Now().Add(time.Duration(confirmReq.TimeoutSeconds) * time.Second)
|
||||||
}
|
}
|
||||||
@@ -641,12 +543,14 @@ func intentFromPayment(record *storagemodel.PaymentRecord) *model.PaymentGateway
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &model.PaymentGatewayIntent{
|
return &model.PaymentGatewayIntent{
|
||||||
|
PaymentRef: strings.TrimSpace(record.PaymentRef),
|
||||||
PaymentIntentID: strings.TrimSpace(record.PaymentIntentID),
|
PaymentIntentID: strings.TrimSpace(record.PaymentIntentID),
|
||||||
IdempotencyKey: strings.TrimSpace(record.IdempotencyKey),
|
IdempotencyKey: strings.TrimSpace(record.IdempotencyKey),
|
||||||
OutgoingLeg: strings.TrimSpace(record.OutgoingLeg),
|
OutgoingLeg: strings.TrimSpace(record.OutgoingLeg),
|
||||||
QuoteRef: strings.TrimSpace(record.QuoteRef),
|
QuoteRef: strings.TrimSpace(record.QuoteRef),
|
||||||
|
IntentRef: strings.TrimSpace(record.IntentRef),
|
||||||
|
OperationRef: strings.TrimSpace(record.OperationRef),
|
||||||
RequestedMoney: record.RequestedMoney,
|
RequestedMoney: record.RequestedMoney,
|
||||||
TargetChatID: strings.TrimSpace(record.TargetChatID),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,13 +582,17 @@ func intentFromSubmitTransfer(req *chainv1.SubmitTransferRequest, defaultRail, d
|
|||||||
Currency: sourceCurrency,
|
Currency: sourceCurrency,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
paymentIntentID := strings.TrimSpace(req.GetClientReference())
|
paymentIntentID := strings.TrimSpace(req.GetIntentRef())
|
||||||
if paymentIntentID == "" {
|
if paymentIntentID == "" {
|
||||||
paymentIntentID = strings.TrimSpace(metadata[metadataPaymentIntentID])
|
paymentIntentID = strings.TrimSpace(metadata[metadataPaymentIntentID])
|
||||||
}
|
}
|
||||||
if paymentIntentID == "" {
|
if paymentIntentID == "" {
|
||||||
return nil, merrors.InvalidArgument("submit_transfer: payment_intent_id is required")
|
return nil, merrors.InvalidArgument("submit_transfer: payment_intent_id is required")
|
||||||
}
|
}
|
||||||
|
paymentRef := strings.TrimSpace(req.PaymentRef)
|
||||||
|
if paymentRef == "" {
|
||||||
|
return nil, merrors.InvalidArgument("submit_transfer: payment_ref is required")
|
||||||
|
}
|
||||||
quoteRef := strings.TrimSpace(metadata[metadataQuoteRef])
|
quoteRef := strings.TrimSpace(metadata[metadataQuoteRef])
|
||||||
targetChatID := strings.TrimSpace(metadata[metadataTargetChatID])
|
targetChatID := strings.TrimSpace(metadata[metadataTargetChatID])
|
||||||
outgoingLeg := strings.TrimSpace(metadata[metadataOutgoingLeg])
|
outgoingLeg := strings.TrimSpace(metadata[metadataOutgoingLeg])
|
||||||
@@ -695,12 +603,12 @@ func intentFromSubmitTransfer(req *chainv1.SubmitTransferRequest, defaultRail, d
|
|||||||
targetChatID = strings.TrimSpace(defaultChatID)
|
targetChatID = strings.TrimSpace(defaultChatID)
|
||||||
}
|
}
|
||||||
return &model.PaymentGatewayIntent{
|
return &model.PaymentGatewayIntent{
|
||||||
|
PaymentRef: paymentRef,
|
||||||
PaymentIntentID: paymentIntentID,
|
PaymentIntentID: paymentIntentID,
|
||||||
IdempotencyKey: idempotencyKey,
|
IdempotencyKey: idempotencyKey,
|
||||||
OutgoingLeg: outgoingLeg,
|
OutgoingLeg: outgoingLeg,
|
||||||
QuoteRef: quoteRef,
|
QuoteRef: quoteRef,
|
||||||
RequestedMoney: requestedMoney,
|
RequestedMoney: requestedMoney,
|
||||||
TargetChatID: targetChatID,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,15 +616,14 @@ func transferFromRequest(req *chainv1.SubmitTransferRequest) *chainv1.Transfer {
|
|||||||
if req == nil {
|
if req == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
amount := req.GetAmount()
|
|
||||||
return &chainv1.Transfer{
|
return &chainv1.Transfer{
|
||||||
TransferRef: strings.TrimSpace(req.GetIdempotencyKey()),
|
TransferRef: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||||
OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
|
OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
|
||||||
SourceWalletRef: strings.TrimSpace(req.GetSourceWalletRef()),
|
SourceWalletRef: strings.TrimSpace(req.GetSourceWalletRef()),
|
||||||
Destination: req.GetDestination(),
|
Destination: req.GetDestination(),
|
||||||
RequestedAmount: amount,
|
RequestedAmount: req.GetAmount(),
|
||||||
Status: chainv1.TransferStatus_TRANSFER_SUBMITTED,
|
Status: chainv1.TransferStatus_TRANSFER_CREATED,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,20 +631,32 @@ func transferFromPayment(record *storagemodel.PaymentRecord, req *chainv1.Submit
|
|||||||
if record == nil {
|
if record == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var requested *moneyv1.Money
|
var requested *moneyv1.Money
|
||||||
if req != nil && req.GetAmount() != nil {
|
if req != nil && req.GetAmount() != nil {
|
||||||
requested = req.GetAmount()
|
requested = req.GetAmount()
|
||||||
} else {
|
} else {
|
||||||
requested = moneyFromPayment(record.RequestedMoney)
|
requested = moneyFromPayment(record.RequestedMoney)
|
||||||
}
|
}
|
||||||
net := moneyFromPayment(record.ExecutedMoney)
|
net := moneyFromPayment(record.RequestedMoney)
|
||||||
status := chainv1.TransferStatus_TRANSFER_SUBMITTED
|
|
||||||
switch paymentStatus(record) {
|
var status chainv1.TransferStatus
|
||||||
case storagemodel.PaymentStatusExecuted:
|
|
||||||
status = chainv1.TransferStatus_TRANSFER_CONFIRMED
|
switch record.Status {
|
||||||
case storagemodel.PaymentStatusExpired:
|
case storagemodel.PaymentStatusSuccess:
|
||||||
|
status = chainv1.TransferStatus_TRANSFER_SUCCESS
|
||||||
|
case storagemodel.PaymentStatusCancelled:
|
||||||
status = chainv1.TransferStatus_TRANSFER_CANCELLED
|
status = chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||||
|
case storagemodel.PaymentStatusFailed:
|
||||||
|
status = chainv1.TransferStatus_TRANSFER_FAILED
|
||||||
|
case storagemodel.PaymentStatusProcessing:
|
||||||
|
status = chainv1.TransferStatus_TRANSFER_PROCESSING
|
||||||
|
case storagemodel.PaymentStatusWaiting:
|
||||||
|
status = chainv1.TransferStatus_TRANSFER_WAITING
|
||||||
|
default:
|
||||||
|
status = chainv1.TransferStatus_TRANSFER_CREATED
|
||||||
}
|
}
|
||||||
|
|
||||||
transfer := &chainv1.Transfer{
|
transfer := &chainv1.Transfer{
|
||||||
TransferRef: strings.TrimSpace(record.IdempotencyKey),
|
TransferRef: strings.TrimSpace(record.IdempotencyKey),
|
||||||
IdempotencyKey: strings.TrimSpace(record.IdempotencyKey),
|
IdempotencyKey: strings.TrimSpace(record.IdempotencyKey),
|
||||||
@@ -745,11 +664,13 @@ func transferFromPayment(record *storagemodel.PaymentRecord, req *chainv1.Submit
|
|||||||
NetAmount: net,
|
NetAmount: net,
|
||||||
Status: status,
|
Status: status,
|
||||||
}
|
}
|
||||||
|
|
||||||
if req != nil {
|
if req != nil {
|
||||||
transfer.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
transfer.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
||||||
transfer.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
|
transfer.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
|
||||||
transfer.Destination = req.GetDestination()
|
transfer.Destination = req.GetDestination()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !record.ExecutedAt.IsZero() {
|
if !record.ExecutedAt.IsZero() {
|
||||||
ts := timestamppb.New(record.ExecutedAt)
|
ts := timestamppb.New(record.ExecutedAt)
|
||||||
transfer.CreatedAt = ts
|
transfer.CreatedAt = ts
|
||||||
@@ -761,6 +682,7 @@ func transferFromPayment(record *storagemodel.PaymentRecord, req *chainv1.Submit
|
|||||||
transfer.CreatedAt = timestamppb.New(record.CreatedAt)
|
transfer.CreatedAt = timestamppb.New(record.CreatedAt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return transfer
|
return transfer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,3 +709,27 @@ func readEnv(env string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _ grpcapp.Service = (*Service)(nil)
|
var _ grpcapp.Service = (*Service)(nil)
|
||||||
|
|
||||||
|
func statusFromConfirmationResult(r *model.ConfirmationResult) storagemodel.PaymentStatus {
|
||||||
|
if r == nil {
|
||||||
|
return storagemodel.PaymentStatusWaiting
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Status {
|
||||||
|
|
||||||
|
case model.ConfirmationStatusConfirmed:
|
||||||
|
return storagemodel.PaymentStatusProcessing
|
||||||
|
|
||||||
|
case model.ConfirmationStatusClarified:
|
||||||
|
return storagemodel.PaymentStatusWaiting
|
||||||
|
|
||||||
|
case model.ConfirmationStatusRejected:
|
||||||
|
return storagemodel.PaymentStatusFailed
|
||||||
|
|
||||||
|
case model.ConfirmationStatusTimeout:
|
||||||
|
return storagemodel.PaymentStatusFailed
|
||||||
|
|
||||||
|
default:
|
||||||
|
return storagemodel.PaymentStatusFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,22 +2,23 @@ package gateway
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/tgsettle/storage"
|
"github.com/tech/sendico/gateway/tgsettle/storage"
|
||||||
storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model"
|
storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model"
|
||||||
envelope "github.com/tech/sendico/pkg/messaging/envelope"
|
envelope "github.com/tech/sendico/pkg/messaging/envelope"
|
||||||
|
tnotifications "github.com/tech/sendico/pkg/messaging/notifications/telegram"
|
||||||
mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory"
|
mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
notification "github.com/tech/sendico/pkg/model/notification"
|
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
// FAKE STORES
|
||||||
|
//
|
||||||
|
|
||||||
type fakePaymentsStore struct {
|
type fakePaymentsStore struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
records map[string]*storagemodel.PaymentRecord
|
records map[string]*storagemodel.PaymentRecord
|
||||||
@@ -26,6 +27,9 @@ type fakePaymentsStore struct {
|
|||||||
func (f *fakePaymentsStore) FindByIdempotencyKey(_ context.Context, key string) (*storagemodel.PaymentRecord, error) {
|
func (f *fakePaymentsStore) FindByIdempotencyKey(_ context.Context, key string) (*storagemodel.PaymentRecord, error) {
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
defer f.mu.Unlock()
|
defer f.mu.Unlock()
|
||||||
|
if f.records == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
return f.records[key], nil
|
return f.records[key], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,286 +71,212 @@ func (f *fakeRepo) TelegramConfirmations() storage.TelegramConfirmationsStore {
|
|||||||
return f.tg
|
return f.tg
|
||||||
}
|
}
|
||||||
|
|
||||||
type captureProducer struct {
|
//
|
||||||
mu sync.Mutex
|
// FAKE BROKER (ОБЯЗАТЕЛЕН ДЛЯ СЕРВИСА)
|
||||||
confirmationRequests []*model.ConfirmationRequest
|
//
|
||||||
executions []*model.PaymentGatewayExecution
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *captureProducer) SendMessage(env envelope.Envelope) error {
|
type fakeBroker struct{}
|
||||||
_, _ = env.Serialize()
|
|
||||||
switch env.GetSignature().ToString() {
|
func (f *fakeBroker) Publish(_ envelope.Envelope) error {
|
||||||
case model.NewNotification(mservice.Notifications, notification.NAConfirmationRequest).ToString():
|
|
||||||
var req model.ConfirmationRequest
|
|
||||||
if err := json.Unmarshal(env.GetData(), &req); err == nil {
|
|
||||||
c.mu.Lock()
|
|
||||||
c.confirmationRequests = append(c.confirmationRequests, &req)
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
case model.NewNotification(mservice.PaymentGateway, notification.NAPaymentGatewayExecution).ToString():
|
|
||||||
var exec model.PaymentGatewayExecution
|
|
||||||
if err := json.Unmarshal(env.GetData(), &exec); err == nil {
|
|
||||||
c.mu.Lock()
|
|
||||||
c.executions = append(c.executions, &exec)
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *captureProducer) Reset() {
|
func (f *fakeBroker) Subscribe(event model.NotificationEvent) (<-chan envelope.Envelope, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeBroker) Unsubscribe(event model.NotificationEvent, subChan <-chan envelope.Envelope) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// CAPTURE ONLY TELEGRAM REACTIONS
|
||||||
|
//
|
||||||
|
|
||||||
|
type captureProducer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
reactions []envelope.Envelope
|
||||||
|
sig string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *captureProducer) SendMessage(env envelope.Envelope) error {
|
||||||
|
if env.GetSignature().ToString() != c.sig {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
c.reactions = append(c.reactions, env)
|
||||||
c.confirmationRequests = nil
|
c.mu.Unlock()
|
||||||
c.executions = nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOnIntentCreatesConfirmationRequest(t *testing.T) {
|
//
|
||||||
|
// TESTS
|
||||||
|
//
|
||||||
|
|
||||||
|
func newTestService(_ *testing.T) (*Service, *fakeRepo, *captureProducer) {
|
||||||
logger := mloggerfactory.NewLogger(false)
|
logger := mloggerfactory.NewLogger(false)
|
||||||
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
|
||||||
prod := &captureProducer{}
|
repo := &fakeRepo{
|
||||||
t.Setenv("PGS_CHAT_ID", "-100")
|
payments: &fakePaymentsStore{},
|
||||||
svc := NewService(logger, repo, prod, nil, Config{
|
tg: &fakeTelegramStore{},
|
||||||
Rail: "card",
|
}
|
||||||
TargetChatIDEnv: "PGS_CHAT_ID",
|
|
||||||
TimeoutSeconds: 90,
|
sigEnv := tnotifications.TelegramReaction(string(mservice.PaymentGateway), &model.TelegramReactionRequest{
|
||||||
AcceptedUserIDs: []string{"42"},
|
RequestID: "x",
|
||||||
|
ChatID: "1",
|
||||||
|
MessageID: "2",
|
||||||
|
Emoji: "ok",
|
||||||
})
|
})
|
||||||
prod.Reset()
|
|
||||||
|
|
||||||
intent := &model.PaymentGatewayIntent{
|
prod := &captureProducer{
|
||||||
|
sig: sigEnv.GetSignature().ToString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := NewService(logger, repo, prod, &fakeBroker{}, Config{
|
||||||
|
Rail: "card",
|
||||||
|
SuccessReaction: "👍",
|
||||||
|
})
|
||||||
|
|
||||||
|
return svc, repo, prod
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfirmed(t *testing.T) {
|
||||||
|
svc, repo, prod := newTestService(t)
|
||||||
|
|
||||||
|
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
||||||
|
IdempotencyKey: "idem-1",
|
||||||
PaymentIntentID: "pi-1",
|
PaymentIntentID: "pi-1",
|
||||||
IdempotencyKey: "idem-1",
|
|
||||||
OutgoingLeg: "card",
|
|
||||||
QuoteRef: "quote-1",
|
QuoteRef: "quote-1",
|
||||||
RequestedMoney: &paymenttypes.Money{Amount: "10.50", Currency: "USD"},
|
|
||||||
TargetChatID: "",
|
|
||||||
}
|
|
||||||
if err := svc.onIntent(context.Background(), intent); err != nil {
|
|
||||||
t.Fatalf("onIntent error: %v", err)
|
|
||||||
}
|
|
||||||
if len(prod.confirmationRequests) != 1 {
|
|
||||||
t.Fatalf("expected 1 confirmation request, got %d", len(prod.confirmationRequests))
|
|
||||||
}
|
|
||||||
req := prod.confirmationRequests[0]
|
|
||||||
if req.RequestID != "idem-1" || req.PaymentIntentID != "pi-1" || req.QuoteRef != "quote-1" {
|
|
||||||
t.Fatalf("unexpected confirmation request fields: %#v", req)
|
|
||||||
}
|
|
||||||
if req.TargetChatID != "-100" {
|
|
||||||
t.Fatalf("expected target chat id -100, got %q", req.TargetChatID)
|
|
||||||
}
|
|
||||||
if req.RequestedMoney == nil || req.RequestedMoney.Amount != "10.50" || req.RequestedMoney.Currency != "USD" {
|
|
||||||
t.Fatalf("requested money mismatch: %#v", req.RequestedMoney)
|
|
||||||
}
|
|
||||||
if req.TimeoutSeconds != 90 {
|
|
||||||
t.Fatalf("expected timeout 90, got %d", req.TimeoutSeconds)
|
|
||||||
}
|
|
||||||
if req.SourceService != string(mservice.PaymentGateway) || req.Rail != "card" {
|
|
||||||
t.Fatalf("unexpected source/rail: %#v", req)
|
|
||||||
}
|
|
||||||
record := repo.payments.records["idem-1"]
|
|
||||||
if record == nil {
|
|
||||||
t.Fatalf("expected pending payment to be stored")
|
|
||||||
}
|
|
||||||
if record.Status != storagemodel.PaymentStatusPending {
|
|
||||||
t.Fatalf("expected pending status, got %q", record.Status)
|
|
||||||
}
|
|
||||||
if record.RequestedMoney == nil || record.RequestedMoney.Amount != "10.50" {
|
|
||||||
t.Fatalf("requested money not stored correctly: %#v", record.RequestedMoney)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIntentFromSubmitTransferUsesSourceMoney(t *testing.T) {
|
|
||||||
req := &chainv1.SubmitTransferRequest{
|
|
||||||
IdempotencyKey: "idem-1",
|
|
||||||
ClientReference: "pi-1",
|
|
||||||
Amount: &moneyv1.Money{Amount: "10.00", Currency: "EUR"},
|
|
||||||
Metadata: map[string]string{
|
|
||||||
metadataSourceAmount: "12.34",
|
|
||||||
metadataSourceCurrency: "USD",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
intent, err := intentFromSubmitTransfer(req, "provider_settlement", "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("intentFromSubmitTransfer error: %v", err)
|
|
||||||
}
|
|
||||||
if intent.RequestedMoney == nil || intent.RequestedMoney.Amount != "12.34" || intent.RequestedMoney.Currency != "USD" {
|
|
||||||
t.Fatalf("expected source money override, got: %#v", intent.RequestedMoney)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfirmationResultPersistsExecutionAndReply(t *testing.T) {
|
|
||||||
logger := mloggerfactory.NewLogger(false)
|
|
||||||
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
|
||||||
prod := &captureProducer{}
|
|
||||||
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
|
||||||
intent := &model.PaymentGatewayIntent{
|
|
||||||
PaymentIntentID: "pi-2",
|
|
||||||
IdempotencyKey: "idem-2",
|
|
||||||
QuoteRef: "quote-2",
|
|
||||||
OutgoingLeg: "card",
|
OutgoingLeg: "card",
|
||||||
RequestedMoney: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
RequestedMoney: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
||||||
|
Status: storagemodel.PaymentStatusWaiting,
|
||||||
|
})
|
||||||
|
|
||||||
|
result := &model.ConfirmationResult{
|
||||||
|
RequestID: "idem-1",
|
||||||
|
Money: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
||||||
|
Status: model.ConfirmationStatusConfirmed,
|
||||||
|
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = svc.onConfirmationResult(context.Background(), result)
|
||||||
|
|
||||||
|
rec := repo.payments.records["idem-1"]
|
||||||
|
|
||||||
|
if rec.Status != storagemodel.PaymentStatusSuccess {
|
||||||
|
t.Fatalf("expected success, got %s", rec.Status)
|
||||||
|
}
|
||||||
|
if rec.RequestedMoney == nil {
|
||||||
|
t.Fatalf("requested money not set")
|
||||||
|
}
|
||||||
|
if rec.ExecutedAt.IsZero() {
|
||||||
|
t.Fatalf("executedAt not set")
|
||||||
|
}
|
||||||
|
if repo.tg.records["idem-1"] == nil {
|
||||||
|
t.Fatalf("telegram confirmation not stored")
|
||||||
|
}
|
||||||
|
if len(prod.reactions) != 1 {
|
||||||
|
t.Fatalf("reaction must be published")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClarified(t *testing.T) {
|
||||||
|
svc, repo, prod := newTestService(t)
|
||||||
|
|
||||||
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
||||||
IdempotencyKey: "idem-2",
|
IdempotencyKey: "idem-2",
|
||||||
PaymentIntentID: intent.PaymentIntentID,
|
Status: storagemodel.PaymentStatusWaiting,
|
||||||
QuoteRef: intent.QuoteRef,
|
|
||||||
OutgoingLeg: intent.OutgoingLeg,
|
|
||||||
RequestedMoney: intent.RequestedMoney,
|
|
||||||
Status: storagemodel.PaymentStatusPending,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
result := &model.ConfirmationResult{
|
result := &model.ConfirmationResult{
|
||||||
RequestID: "idem-2",
|
RequestID: "idem-2",
|
||||||
Money: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
Status: model.ConfirmationStatusClarified,
|
||||||
Status: model.ConfirmationStatusConfirmed,
|
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2"},
|
||||||
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2", Text: "5 EUR"},
|
|
||||||
}
|
}
|
||||||
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
|
||||||
t.Fatalf("onConfirmationResult error: %v", err)
|
_ = svc.onConfirmationResult(context.Background(), result)
|
||||||
|
|
||||||
|
rec := repo.payments.records["idem-2"]
|
||||||
|
|
||||||
|
if rec.Status != storagemodel.PaymentStatusWaiting {
|
||||||
|
t.Fatalf("clarified must not change status")
|
||||||
}
|
}
|
||||||
record := repo.payments.records["idem-2"]
|
if repo.tg.records["idem-2"] == nil {
|
||||||
if record == nil {
|
t.Fatalf("telegram confirmation must be stored")
|
||||||
t.Fatalf("expected payment record to be stored")
|
|
||||||
}
|
}
|
||||||
if record.Status != storagemodel.PaymentStatusExecuted {
|
if len(prod.reactions) != 0 {
|
||||||
t.Fatalf("expected executed status, got %q", record.Status)
|
t.Fatalf("clarified must not publish reaction")
|
||||||
}
|
|
||||||
if record.ExecutedMoney == nil || record.ExecutedMoney.Amount != "5" {
|
|
||||||
t.Fatalf("executed money not stored correctly: %#v", record.ExecutedMoney)
|
|
||||||
}
|
|
||||||
if repo.tg.records["idem-2"] == nil || repo.tg.records["idem-2"].RawReply.Text != "5 EUR" {
|
|
||||||
t.Fatalf("telegram reply not stored correctly")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClarifiedResultPersistsExecution(t *testing.T) {
|
func TestRejected(t *testing.T) {
|
||||||
logger := mloggerfactory.NewLogger(false)
|
svc, repo, prod := newTestService(t)
|
||||||
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
|
||||||
prod := &captureProducer{}
|
// ВАЖНО: чтобы текущий emitTransferStatusEvent не падал на nil,
|
||||||
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
// даем минимально ожидаемые поля + non-nil ExecutedMoney.
|
||||||
intent := &model.PaymentGatewayIntent{
|
|
||||||
PaymentIntentID: "pi-clarified",
|
|
||||||
IdempotencyKey: "idem-clarified",
|
|
||||||
QuoteRef: "quote-clarified",
|
|
||||||
OutgoingLeg: "card",
|
|
||||||
RequestedMoney: &paymenttypes.Money{Amount: "12", Currency: "USD"},
|
|
||||||
}
|
|
||||||
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
||||||
IdempotencyKey: "idem-clarified",
|
IdempotencyKey: "idem-3",
|
||||||
PaymentIntentID: intent.PaymentIntentID,
|
PaymentIntentID: "pi-3",
|
||||||
QuoteRef: intent.QuoteRef,
|
QuoteRef: "quote-3",
|
||||||
OutgoingLeg: intent.OutgoingLeg,
|
OutgoingLeg: "card",
|
||||||
RequestedMoney: intent.RequestedMoney,
|
RequestedMoney: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
||||||
Status: storagemodel.PaymentStatusPending,
|
ExecutedMoney: &paymenttypes.Money{Amount: "0", Currency: "EUR"},
|
||||||
|
Status: storagemodel.PaymentStatusWaiting,
|
||||||
})
|
})
|
||||||
|
|
||||||
result := &model.ConfirmationResult{
|
result := &model.ConfirmationResult{
|
||||||
RequestID: "idem-clarified",
|
RequestID: "idem-3",
|
||||||
Money: &paymenttypes.Money{Amount: "12", Currency: "USD"},
|
Status: model.ConfirmationStatusRejected,
|
||||||
Status: model.ConfirmationStatusClarified,
|
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2"},
|
||||||
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "3", Text: "12 USD"},
|
|
||||||
}
|
}
|
||||||
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
|
||||||
t.Fatalf("onConfirmationResult error: %v", err)
|
_ = svc.onConfirmationResult(context.Background(), result)
|
||||||
|
|
||||||
|
rec := repo.payments.records["idem-3"]
|
||||||
|
|
||||||
|
if rec.Status != storagemodel.PaymentStatusFailed {
|
||||||
|
t.Fatalf("expected failed")
|
||||||
}
|
}
|
||||||
record := repo.payments.records["idem-clarified"]
|
if repo.tg.records["idem-3"] == nil {
|
||||||
if record == nil || record.Status != storagemodel.PaymentStatusExecuted {
|
t.Fatalf("telegram confirmation must be stored")
|
||||||
t.Fatalf("expected payment executed status, got %#v", record)
|
}
|
||||||
|
if len(prod.reactions) != 0 {
|
||||||
|
t.Fatalf("rejected must not publish reaction")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIdempotencyPreventsDuplicateWrites(t *testing.T) {
|
func TestTimeout(t *testing.T) {
|
||||||
logger := mloggerfactory.NewLogger(false)
|
svc, repo, prod := newTestService(t)
|
||||||
repo := &fakeRepo{payments: &fakePaymentsStore{records: map[string]*storagemodel.PaymentRecord{
|
|
||||||
"idem-3": {IdempotencyKey: "idem-3", Status: storagemodel.PaymentStatusPending},
|
|
||||||
}}, tg: &fakeTelegramStore{}}
|
|
||||||
prod := &captureProducer{}
|
|
||||||
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
|
||||||
intent := &model.PaymentGatewayIntent{
|
|
||||||
PaymentIntentID: "pi-3",
|
|
||||||
IdempotencyKey: "idem-3",
|
|
||||||
OutgoingLeg: "card",
|
|
||||||
QuoteRef: "quote-3",
|
|
||||||
RequestedMoney: &paymenttypes.Money{Amount: "1", Currency: "USD"},
|
|
||||||
TargetChatID: "chat",
|
|
||||||
}
|
|
||||||
if err := svc.onIntent(context.Background(), intent); err != nil {
|
|
||||||
t.Fatalf("onIntent error: %v", err)
|
|
||||||
}
|
|
||||||
if len(prod.confirmationRequests) != 0 {
|
|
||||||
t.Fatalf("expected no confirmation request for duplicate intent")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTimeoutDoesNotPersistExecution(t *testing.T) {
|
// ВАЖНО: чтобы текущий emitTransferStatusEvent не падал на nil,
|
||||||
logger := mloggerfactory.NewLogger(false)
|
// даем минимально ожидаемые поля + non-nil ExecutedMoney.
|
||||||
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
|
||||||
prod := &captureProducer{}
|
|
||||||
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
|
||||||
intent := &model.PaymentGatewayIntent{
|
|
||||||
PaymentIntentID: "pi-4",
|
|
||||||
IdempotencyKey: "idem-4",
|
|
||||||
QuoteRef: "quote-4",
|
|
||||||
OutgoingLeg: "card",
|
|
||||||
RequestedMoney: &paymenttypes.Money{Amount: "8", Currency: "USD"},
|
|
||||||
}
|
|
||||||
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
||||||
IdempotencyKey: "idem-4",
|
IdempotencyKey: "idem-4",
|
||||||
PaymentIntentID: intent.PaymentIntentID,
|
PaymentIntentID: "pi-4",
|
||||||
QuoteRef: intent.QuoteRef,
|
QuoteRef: "quote-4",
|
||||||
OutgoingLeg: intent.OutgoingLeg,
|
OutgoingLeg: "card",
|
||||||
RequestedMoney: intent.RequestedMoney,
|
RequestedMoney: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
||||||
Status: storagemodel.PaymentStatusPending,
|
ExecutedMoney: &paymenttypes.Money{Amount: "0", Currency: "EUR"},
|
||||||
|
Status: storagemodel.PaymentStatusWaiting,
|
||||||
})
|
})
|
||||||
|
|
||||||
result := &model.ConfirmationResult{
|
result := &model.ConfirmationResult{
|
||||||
RequestID: "idem-4",
|
RequestID: "idem-4",
|
||||||
Status: model.ConfirmationStatusTimeout,
|
Status: model.ConfirmationStatusTimeout,
|
||||||
|
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2"},
|
||||||
}
|
}
|
||||||
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
|
||||||
t.Fatalf("onConfirmationResult error: %v", err)
|
_ = svc.onConfirmationResult(context.Background(), result)
|
||||||
|
|
||||||
|
rec := repo.payments.records["idem-4"]
|
||||||
|
|
||||||
|
if rec.Status != storagemodel.PaymentStatusFailed {
|
||||||
|
t.Fatalf("timeout must be failed")
|
||||||
}
|
}
|
||||||
record := repo.payments.records["idem-4"]
|
if repo.tg.records["idem-4"] == nil {
|
||||||
if record == nil || record.Status != storagemodel.PaymentStatusExpired {
|
t.Fatalf("telegram confirmation must be stored")
|
||||||
t.Fatalf("expected expired status for timeout, got %#v", record)
|
}
|
||||||
}
|
if len(prod.reactions) != 0 {
|
||||||
}
|
t.Fatalf("timeout must not publish reaction")
|
||||||
|
|
||||||
func TestRejectedDoesNotPersistExecution(t *testing.T) {
|
|
||||||
logger := mloggerfactory.NewLogger(false)
|
|
||||||
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
|
||||||
prod := &captureProducer{}
|
|
||||||
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
|
||||||
intent := &model.PaymentGatewayIntent{
|
|
||||||
PaymentIntentID: "pi-reject",
|
|
||||||
IdempotencyKey: "idem-reject",
|
|
||||||
QuoteRef: "quote-reject",
|
|
||||||
OutgoingLeg: "card",
|
|
||||||
RequestedMoney: &paymenttypes.Money{Amount: "3", Currency: "USD"},
|
|
||||||
}
|
|
||||||
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
|
||||||
IdempotencyKey: "idem-reject",
|
|
||||||
PaymentIntentID: intent.PaymentIntentID,
|
|
||||||
QuoteRef: intent.QuoteRef,
|
|
||||||
OutgoingLeg: intent.OutgoingLeg,
|
|
||||||
RequestedMoney: intent.RequestedMoney,
|
|
||||||
Status: storagemodel.PaymentStatusPending,
|
|
||||||
})
|
|
||||||
|
|
||||||
result := &model.ConfirmationResult{
|
|
||||||
RequestID: "idem-reject",
|
|
||||||
Status: model.ConfirmationStatusRejected,
|
|
||||||
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "4", Text: "no"},
|
|
||||||
}
|
|
||||||
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
|
||||||
t.Fatalf("onConfirmationResult error: %v", err)
|
|
||||||
}
|
|
||||||
record := repo.payments.records["idem-reject"]
|
|
||||||
if record == nil || record.Status != storagemodel.PaymentStatusExpired {
|
|
||||||
t.Fatalf("expected expired status for rejection, got %#v", record)
|
|
||||||
}
|
|
||||||
if repo.tg.records["idem-reject"] == nil {
|
|
||||||
t.Fatalf("expected raw reply to be stored for rejection")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/tgsettle/storage/model"
|
||||||
|
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
|
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isFinalStatus(t *model.PaymentRecord) bool {
|
||||||
|
switch t.Status {
|
||||||
|
case model.PaymentStatusFailed, model.PaymentStatusSuccess, model.PaymentStatusCancelled:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toOpStatus(t *model.PaymentRecord) rail.OperationResult {
|
||||||
|
switch t.Status {
|
||||||
|
case model.PaymentStatusFailed:
|
||||||
|
return rail.OperationResultFailed
|
||||||
|
case model.PaymentStatusSuccess:
|
||||||
|
return rail.OperationResultSuccess
|
||||||
|
case model.PaymentStatusCancelled:
|
||||||
|
return rail.OperationResultCancelled
|
||||||
|
default:
|
||||||
|
panic("unexpected transfer status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toError(t *model.PaymentRecord) *gatewayv1.OperationError {
|
||||||
|
if t.Status == model.PaymentStatusSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &gatewayv1.OperationError{
|
||||||
|
Message: t.FailureReason,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) updateTransferStatus(ctx context.Context, record *model.PaymentRecord) error {
|
||||||
|
if err := s.repo.Payments().Upsert(ctx, record); err != nil {
|
||||||
|
s.logger.Warn("Failed to update transfer status", zap.String("payment_ref", record.PaymentIntentID), zap.String("status", string(record.Status)), zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isFinalStatus(record) {
|
||||||
|
s.emitTransferStatusEvent(ctx, record)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) emitTransferStatusEvent(_ context.Context, record *model.PaymentRecord) {
|
||||||
|
if s == nil || s.producer == nil || record == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exec := pmodel.PaymentGatewayExecution{
|
||||||
|
PaymentIntentID: record.PaymentIntentID,
|
||||||
|
IdempotencyKey: record.IdempotencyKey,
|
||||||
|
ExecutedMoney: record.ExecutedMoney,
|
||||||
|
PaymentRef: record.PaymentRef,
|
||||||
|
Status: toOpStatus(record),
|
||||||
|
OperationRef: record.OperationRef,
|
||||||
|
Error: record.FailureReason,
|
||||||
|
TransferRef: record.ID.Hex(),
|
||||||
|
}
|
||||||
|
env := paymentgateway.PaymentGatewayExecution(mservice.MntxGateway, &exec)
|
||||||
|
if err := s.producer.SendMessage(env); err != nil {
|
||||||
|
s.logger.Warn("Failed to publish transfer status event", zap.Error(err), mzap.ObjRef("transfer_ref", record.ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,21 +11,28 @@ import (
|
|||||||
type PaymentStatus string
|
type PaymentStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PaymentStatusPending PaymentStatus = "pending"
|
PaymentStatusCreated PaymentStatus = "created" // created
|
||||||
PaymentStatusExpired PaymentStatus = "expired"
|
PaymentStatusProcessing PaymentStatus = "processing" // processing
|
||||||
PaymentStatusExecuted PaymentStatus = "executed"
|
PaymentStatusWaiting PaymentStatus = "waiting" // waiting external action
|
||||||
|
PaymentStatusSuccess PaymentStatus = "success" // final success
|
||||||
|
PaymentStatusFailed PaymentStatus = "failed" // final failure
|
||||||
|
PaymentStatusCancelled PaymentStatus = "cancelled" // cancelled final
|
||||||
)
|
)
|
||||||
|
|
||||||
type PaymentRecord struct {
|
type PaymentRecord struct {
|
||||||
ID bson.ObjectID `bson:"_id,omitempty" json:"id"`
|
ID bson.ObjectID `bson:"_id,omitempty" json:"id"`
|
||||||
|
OperationRef string `bson:"operationRef,omitempty" json:"operation_ref,omitempty"`
|
||||||
IdempotencyKey string `bson:"idempotencyKey,omitempty" json:"idempotency_key,omitempty"`
|
IdempotencyKey string `bson:"idempotencyKey,omitempty" json:"idempotency_key,omitempty"`
|
||||||
PaymentIntentID string `bson:"paymentIntentId,omitempty" json:"payment_intent_id,omitempty"`
|
PaymentIntentID string `bson:"paymentIntentId,omitempty" json:"payment_intent_id,omitempty"`
|
||||||
QuoteRef string `bson:"quoteRef,omitempty" json:"quote_ref,omitempty"`
|
QuoteRef string `bson:"quoteRef,omitempty" json:"quote_ref,omitempty"`
|
||||||
|
IntentRef string `bson:"intentRef,omitempty" json:"intent_ref,omitempty"`
|
||||||
|
PaymentRef string `bson:"paymentRef,omitempty" json:"payment_ref,omitempty"`
|
||||||
OutgoingLeg string `bson:"outgoingLeg,omitempty" json:"outgoing_leg,omitempty"`
|
OutgoingLeg string `bson:"outgoingLeg,omitempty" json:"outgoing_leg,omitempty"`
|
||||||
TargetChatID string `bson:"targetChatId,omitempty" json:"target_chat_id,omitempty"`
|
TargetChatID string `bson:"targetChatId,omitempty" json:"target_chat_id,omitempty"`
|
||||||
RequestedMoney *paymenttypes.Money `bson:"requestedMoney,omitempty" json:"requested_money,omitempty"`
|
RequestedMoney *paymenttypes.Money `bson:"requestedMoney,omitempty" json:"requested_money,omitempty"`
|
||||||
ExecutedMoney *paymenttypes.Money `bson:"executedMoney,omitempty" json:"executed_money,omitempty"`
|
ExecutedMoney *paymenttypes.Money `bson:"executedMoney,omitempty" json:"executed_money,omitempty"`
|
||||||
Status PaymentStatus `bson:"status,omitempty" json:"status,omitempty"`
|
Status PaymentStatus `bson:"status,omitempty" json:"status,omitempty"`
|
||||||
|
FailureReason string `bson:"failureReason,omitempty" json:"Failure_reason,omitempty"`
|
||||||
CreatedAt time.Time `bson:"createdAt,omitempty" json:"created_at,omitempty"`
|
CreatedAt time.Time `bson:"createdAt,omitempty" json:"created_at,omitempty"`
|
||||||
UpdatedAt time.Time `bson:"updatedAt,omitempty" json:"updated_at,omitempty"`
|
UpdatedAt time.Time `bson:"updatedAt,omitempty" json:"updated_at,omitempty"`
|
||||||
ExecutedAt time.Time `bson:"executedAt,omitempty" json:"executed_at,omitempty"`
|
ExecutedAt time.Time `bson:"executedAt,omitempty" json:"executed_at,omitempty"`
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ func (p *Payments) Upsert(ctx context.Context, record *model.PaymentRecord) erro
|
|||||||
if record.IdempotencyKey == "" {
|
if record.IdempotencyKey == "" {
|
||||||
return merrors.InvalidArgument("idempotency key is required", "idempotency_key")
|
return merrors.InvalidArgument("idempotency key is required", "idempotency_key")
|
||||||
}
|
}
|
||||||
|
if record.IntentRef == "" {
|
||||||
|
return merrors.InvalidArgument("intention reference key is required", "intent_ref")
|
||||||
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if record.CreatedAt.IsZero() {
|
if record.CreatedAt.IsZero() {
|
||||||
record.CreatedAt = now
|
record.CreatedAt = now
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
chainasset "github.com/tech/sendico/pkg/chain"
|
chainasset "github.com/tech/sendico/pkg/chain"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
pmodel "github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model/account_role"
|
||||||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
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"
|
||||||
@@ -426,7 +426,7 @@ func operationFromTransfer(req *chainv1.SubmitTransferRequest) (*connectorv1.Ope
|
|||||||
|
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||||
"client_reference": strings.TrimSpace(req.GetClientReference()),
|
"payment_ref": strings.TrimSpace(req.GetPaymentRef()),
|
||||||
}
|
}
|
||||||
if memo := strings.TrimSpace(req.GetDestination().GetMemo()); memo != "" {
|
if memo := strings.TrimSpace(req.GetDestination().GetMemo()); memo != "" {
|
||||||
params["destination_memo"] = memo
|
params["destination_memo"] = memo
|
||||||
@@ -472,14 +472,14 @@ func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[strin
|
|||||||
if op == nil || len(metadata) == 0 {
|
if op == nil || len(metadata) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyFromRole]); raw != "" {
|
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyFromRole]); raw != "" {
|
||||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||||
op.FromRole = pmodel.ToProto(role)
|
op.FromRole = account_role.ToProto(role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyToRole]); raw != "" {
|
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyToRole]); raw != "" {
|
||||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||||
op.ToRole = pmodel.ToProto(role)
|
op.ToRole = account_role.ToProto(role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -619,7 +619,7 @@ func gasTopUpEnsureOperation(req *chainv1.EnsureGasTopUpRequest) (*connectorv1.O
|
|||||||
"mode": "ensure",
|
"mode": "ensure",
|
||||||
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||||
"target_wallet_ref": strings.TrimSpace(req.GetTargetWalletRef()),
|
"target_wallet_ref": strings.TrimSpace(req.GetTargetWalletRef()),
|
||||||
"client_reference": strings.TrimSpace(req.GetClientReference()),
|
"payment_ref": strings.TrimSpace(req.GetPaymentRef()),
|
||||||
"estimated_total_fee": map[string]interface{}{"amount": fee.GetAmount(), "currency": fee.GetCurrency()},
|
"estimated_total_fee": map[string]interface{}{"amount": fee.GetAmount(), "currency": fee.GetCurrency()},
|
||||||
}
|
}
|
||||||
if len(req.GetMetadata()) > 0 {
|
if len(req.GetMetadata()) > 0 {
|
||||||
@@ -765,28 +765,54 @@ func managedWalletStatusFromAccount(state connectorv1.AccountState) chainv1.Mana
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func transferStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
|
||||||
switch status {
|
|
||||||
case connectorv1.OperationStatus_CONFIRMED:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
|
||||||
case connectorv1.OperationStatus_FAILED:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_FAILED
|
|
||||||
case connectorv1.OperationStatus_CANCELED:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
|
||||||
default:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_PENDING
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func operationStatusFromTransfer(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
func operationStatusFromTransfer(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
|
||||||
return connectorv1.OperationStatus_CONFIRMED
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CREATED
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_PROCESSING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_WAITING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_SUCCESS
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
return connectorv1.OperationStatus_FAILED
|
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
return connectorv1.OperationStatus_CANCELED
|
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func transferStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
||||||
|
switch status {
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_CREATED:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_CREATED
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_PROCESSING:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_PROCESSING
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_WAITING:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_WAITING
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_SUCCESS:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_SUCCESS
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_FAILED:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_FAILED
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_CANCELLED:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||||
|
|
||||||
|
default:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
pmodel "github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model/account_role"
|
||||||
"github.com/tech/sendico/pkg/payments/rail"
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
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"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
@@ -106,9 +106,11 @@ func (g *chainRailGateway) Send(ctx context.Context, req rail.TransferRequest) (
|
|||||||
Currency: currency,
|
Currency: currency,
|
||||||
Amount: amountValue,
|
Amount: amountValue,
|
||||||
},
|
},
|
||||||
Fees: fees,
|
Fees: fees,
|
||||||
Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
|
Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
|
||||||
ClientReference: strings.TrimSpace(req.ClientReference),
|
PaymentRef: strings.TrimSpace(req.PaymentRef),
|
||||||
|
OperationRef: strings.TrimSpace(req.OperationRef),
|
||||||
|
IntentRef: strings.TrimSpace(req.OperationRef),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rail.RailResult{}, err
|
return rail.RailResult{}, err
|
||||||
@@ -186,20 +188,29 @@ func (g *chainRailGateway) isManagedWallet(ctx context.Context, walletRef string
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusFromTransfer(status chainv1.TransferStatus) string {
|
func statusFromTransfer(status chainv1.TransferStatus) rail.TransferStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
|
return rail.TransferStatusCreated
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||||
|
return rail.TransferStatusProcessing
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return rail.TransferStatusProcessing
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
return rail.TransferStatusSuccess
|
return rail.TransferStatusSuccess
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
return rail.TransferStatusFailed
|
return rail.TransferStatusFailed
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
return rail.TransferStatusRejected
|
return rail.TransferStatusCancelled
|
||||||
case chainv1.TransferStatus_TRANSFER_SIGNING,
|
|
||||||
chainv1.TransferStatus_TRANSFER_PENDING,
|
|
||||||
chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
|
||||||
return rail.TransferStatusPending
|
|
||||||
default:
|
default:
|
||||||
return rail.TransferStatusPending
|
return rail.TransferStatusUnspecified
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,19 +266,19 @@ func railMoneyFromProto(m *moneyv1.Money) *rail.Money {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func transferMetadataWithRoles(metadata map[string]string, fromRole, toRole pmodel.AccountRole) map[string]string {
|
func transferMetadataWithRoles(metadata map[string]string, fromRole, toRole account_role.AccountRole) map[string]string {
|
||||||
result := cloneMetadata(metadata)
|
result := cloneMetadata(metadata)
|
||||||
if strings.TrimSpace(string(fromRole)) != "" {
|
if strings.TrimSpace(string(fromRole)) != "" {
|
||||||
if result == nil {
|
if result == nil {
|
||||||
result = map[string]string{}
|
result = map[string]string{}
|
||||||
}
|
}
|
||||||
result[pmodel.MetadataKeyFromRole] = strings.TrimSpace(string(fromRole))
|
result[account_role.MetadataKeyFromRole] = strings.TrimSpace(string(fromRole))
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(string(toRole)) != "" {
|
if strings.TrimSpace(string(toRole)) != "" {
|
||||||
if result == nil {
|
if result == nil {
|
||||||
result = map[string]string{}
|
result = map[string]string{}
|
||||||
}
|
}
|
||||||
result[pmodel.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
|
result[account_role.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
|
||||||
}
|
}
|
||||||
if len(result) == 0 {
|
if len(result) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260131145833-e3fabd62fc61 // indirect
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260201044653-ee82dce4af02 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||||
@@ -92,6 +92,6 @@ require (
|
|||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260202165425-ce8ad4cf556b // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
|
|||||||
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260131145833-e3fabd62fc61 h1:iLc9NjmJ3AdAl5VoiRSDXzEmmW8kvHp3E2vJ2eKKc7s=
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260201044653-ee82dce4af02 h1:0uY5Ooun4eqGmP0IrQhiKVqeeEXoeEcL8KVRtug8+r8=
|
||||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260131145833-e3fabd62fc61/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260201044653-ee82dce4af02/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||||
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
|
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
|
||||||
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
|
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -379,10 +379,10 @@ 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/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260202165425-ce8ad4cf556b h1:SGYyueaEovpqmWmtTvwtVgo638V/QFE2zlTCnRrR3jg=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -126,9 +126,11 @@ func (c *ensureGasTopUpCommand) Execute(ctx context.Context, req *chainv1.Ensure
|
|||||||
Destination: &chainv1.TransferDestination{
|
Destination: &chainv1.TransferDestination{
|
||||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: targetWalletRef},
|
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: targetWalletRef},
|
||||||
},
|
},
|
||||||
Amount: topUp,
|
Amount: topUp,
|
||||||
Metadata: shared.CloneMetadata(req.GetMetadata()),
|
Metadata: shared.CloneMetadata(req.GetMetadata()),
|
||||||
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
|
||||||
|
IntentRef: strings.TrimSpace(req.GetIntentRef()),
|
||||||
|
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
||||||
}
|
}
|
||||||
|
|
||||||
submitResponder := NewSubmitTransfer(c.deps.WithLogger("transfer.submit")).Execute(ctx, submitReq)
|
submitResponder := NewSubmitTransfer(c.deps.WithLogger("transfer.submit")).Execute(ctx, submitReq)
|
||||||
|
|||||||
@@ -38,11 +38,12 @@ func toProtoTransfer(transfer *model.Transfer) *chainv1.Transfer {
|
|||||||
TransferRef: transfer.TransferRef,
|
TransferRef: transfer.TransferRef,
|
||||||
IdempotencyKey: transfer.IdempotencyKey,
|
IdempotencyKey: transfer.IdempotencyKey,
|
||||||
OrganizationRef: transfer.OrganizationRef,
|
OrganizationRef: transfer.OrganizationRef,
|
||||||
|
IntentRef: transfer.IntentRef,
|
||||||
SourceWalletRef: transfer.SourceWalletRef,
|
SourceWalletRef: transfer.SourceWalletRef,
|
||||||
Destination: destination,
|
Destination: destination,
|
||||||
Asset: asset,
|
Asset: asset,
|
||||||
RequestedAmount: shared.CloneMoney(transfer.RequestedAmount),
|
RequestedAmount: shared.MonenyToProto(transfer.RequestedAmount),
|
||||||
NetAmount: shared.CloneMoney(transfer.NetAmount),
|
NetAmount: shared.MonenyToProto(transfer.NetAmount),
|
||||||
Fees: protoFees,
|
Fees: protoFees,
|
||||||
Status: shared.TransferStatusToProto(transfer.Status),
|
Status: shared.TransferStatusToProto(transfer.Status),
|
||||||
TransactionHash: transfer.TxHash,
|
TransactionHash: transfer.TxHash,
|
||||||
|
|||||||
@@ -38,6 +38,17 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
|||||||
c.deps.Logger.Warn("Missing idempotency key")
|
c.deps.Logger.Warn("Missing idempotency key")
|
||||||
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required"))
|
||||||
}
|
}
|
||||||
|
intentRef := strings.TrimSpace(req.GetIntentRef())
|
||||||
|
if intentRef == "" {
|
||||||
|
c.deps.Logger.Warn("Missing intent reference")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("intentRef is required"))
|
||||||
|
}
|
||||||
|
operationRef := strings.TrimSpace(req.GetOperationRef())
|
||||||
|
if operationRef == "" {
|
||||||
|
c.deps.Logger.Warn("Missing operation reference")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("operationRef is required"))
|
||||||
|
}
|
||||||
|
|
||||||
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
organizationRef := strings.TrimSpace(req.GetOrganizationRef())
|
||||||
if organizationRef == "" {
|
if organizationRef == "" {
|
||||||
c.deps.Logger.Warn("Missing organization ref")
|
c.deps.Logger.Warn("Missing organization ref")
|
||||||
@@ -63,6 +74,11 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
|||||||
c.deps.Logger.Warn("Missing amount value")
|
c.deps.Logger.Warn("Missing amount value")
|
||||||
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required"))
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required"))
|
||||||
}
|
}
|
||||||
|
paymentRef := strings.TrimSpace(req.GetPaymentRef())
|
||||||
|
if paymentRef == "" {
|
||||||
|
c.deps.Logger.Warn("Missing payment reference")
|
||||||
|
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("payment reference is required", "paymentRef"))
|
||||||
|
}
|
||||||
|
|
||||||
sourceWallet, err := c.deps.Storage.Wallets().Get(ctx, sourceWalletRef)
|
sourceWallet, err := c.deps.Storage.Wallets().Get(ctx, sourceWalletRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -123,6 +139,8 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
|||||||
|
|
||||||
transfer := &model.Transfer{
|
transfer := &model.Transfer{
|
||||||
IdempotencyKey: idempotencyKey,
|
IdempotencyKey: idempotencyKey,
|
||||||
|
OperationRef: operationRef,
|
||||||
|
IntentRef: intentRef,
|
||||||
TransferRef: shared.GenerateTransferRef(),
|
TransferRef: shared.GenerateTransferRef(),
|
||||||
OrganizationRef: organizationRef,
|
OrganizationRef: organizationRef,
|
||||||
SourceWalletRef: sourceWalletRef,
|
SourceWalletRef: sourceWalletRef,
|
||||||
@@ -130,11 +148,11 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
|||||||
Network: sourceWallet.Network,
|
Network: sourceWallet.Network,
|
||||||
TokenSymbol: effectiveTokenSymbol,
|
TokenSymbol: effectiveTokenSymbol,
|
||||||
ContractAddress: effectiveContractAddress,
|
ContractAddress: effectiveContractAddress,
|
||||||
RequestedAmount: shared.CloneMoney(amount),
|
RequestedAmount: shared.ProtoToMoney(amount),
|
||||||
NetAmount: netAmount,
|
NetAmount: shared.ProtoToMoney(netAmount),
|
||||||
|
PaymentRef: paymentRef,
|
||||||
Fees: fees,
|
Fees: fees,
|
||||||
Status: model.TransferStatusPending,
|
Status: model.TransferStatusCreated,
|
||||||
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
|
||||||
LastStatusAt: c.deps.Clock.Now().UTC(),
|
LastStatusAt: c.deps.Clock.Now().UTC(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,7 +169,9 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
Amount: amount,
|
Amount: amount,
|
||||||
Fees: parseChainFees(reader),
|
Fees: parseChainFees(reader),
|
||||||
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||||||
ClientReference: strings.TrimSpace(reader.String("client_reference")),
|
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
|
||||||
|
IntentRef: strings.TrimSpace(op.GetIntentRef()),
|
||||||
|
OperationRef: strings.TrimSpace(op.GetOperationRef()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
@@ -208,7 +210,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
return &connectorv1.SubmitOperationResponse{
|
return &connectorv1.SubmitOperationResponse{
|
||||||
Receipt: &connectorv1.OperationReceipt{
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
OperationId: opID,
|
OperationId: opID,
|
||||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
|
||||||
Result: result,
|
Result: result,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -238,7 +240,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
return &connectorv1.SubmitOperationResponse{
|
return &connectorv1.SubmitOperationResponse{
|
||||||
Receipt: &connectorv1.OperationReceipt{
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
OperationId: opID,
|
OperationId: opID,
|
||||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
|
||||||
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), ""),
|
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), ""),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -256,12 +258,14 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
}
|
}
|
||||||
resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
|
resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
|
||||||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
|
IntentRef: strings.TrimSpace(op.GetIntentRef()),
|
||||||
|
OperationRef: strings.TrimSpace(op.GetOperationRef()),
|
||||||
OrganizationRef: orgRef,
|
OrganizationRef: orgRef,
|
||||||
SourceWalletRef: source,
|
SourceWalletRef: source,
|
||||||
TargetWalletRef: target,
|
TargetWalletRef: target,
|
||||||
EstimatedTotalFee: fee,
|
EstimatedTotalFee: fee,
|
||||||
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||||||
ClientReference: strings.TrimSpace(reader.String("client_reference")),
|
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
@@ -273,7 +277,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
return &connectorv1.SubmitOperationResponse{
|
return &connectorv1.SubmitOperationResponse{
|
||||||
Receipt: &connectorv1.OperationReceipt{
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
OperationId: opID,
|
OperationId: opID,
|
||||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
Status: shared.ChainTransferStatusToOperation(resp.GetTransfer().GetStatus()),
|
||||||
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), transferRef),
|
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), transferRef),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -544,25 +548,51 @@ func chainTransferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation
|
|||||||
|
|
||||||
func chainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
func chainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
|
||||||
return connectorv1.OperationStatus_CONFIRMED
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CREATED
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_PROCESSING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_WAITING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_SUCCESS
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
return connectorv1.OperationStatus_FAILED
|
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
return connectorv1.OperationStatus_CANCELED
|
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return connectorv1.OperationStatus_PENDING
|
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func chainStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
func chainStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case connectorv1.OperationStatus_CONFIRMED:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
case connectorv1.OperationStatus_OPERATION_CREATED:
|
||||||
case connectorv1.OperationStatus_FAILED:
|
return chainv1.TransferStatus_TRANSFER_CREATED
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_PROCESSING:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_PROCESSING
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_WAITING:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_WAITING
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_SUCCESS:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_SUCCESS
|
||||||
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_FAILED:
|
||||||
return chainv1.TransferStatus_TRANSFER_FAILED
|
return chainv1.TransferStatus_TRANSFER_FAILED
|
||||||
case connectorv1.OperationStatus_CANCELED:
|
|
||||||
|
case connectorv1.OperationStatus_OPERATION_CANCELLED:
|
||||||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package tron
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
@@ -157,9 +156,3 @@ func GetTransactionStatus(
|
|||||||
}
|
}
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isTronNetwork checks if the network name indicates a TRON network.
|
|
||||||
func isTronNetwork(networkName string) bool {
|
|
||||||
name := strings.ToLower(strings.TrimSpace(networkName))
|
|
||||||
return strings.HasPrefix(name, "tron")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|||||||
IdempotencyKey: "transfer-1",
|
IdempotencyKey: "transfer-1",
|
||||||
OrganizationRef: "org-1",
|
OrganizationRef: "org-1",
|
||||||
SourceWalletRef: srcRef,
|
SourceWalletRef: srcRef,
|
||||||
|
PaymentRef: "ref-1",
|
||||||
Destination: &ichainv1.TransferDestination{
|
Destination: &ichainv1.TransferDestination{
|
||||||
Destination: &ichainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef},
|
Destination: &ichainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef},
|
||||||
},
|
},
|
||||||
@@ -172,6 +173,8 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|||||||
Amount: &moneyv1.Money{Currency: "USDT", Amount: "5"},
|
Amount: &moneyv1.Money{Currency: "USDT", Amount: "5"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
OperationRef: "oper-1",
|
||||||
|
IntentRef: "intent-1",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, transferResp.GetTransfer())
|
require.NotNil(t, transferResp.GetTransfer())
|
||||||
@@ -179,7 +182,7 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
|||||||
|
|
||||||
stored := repo.transfers.get(transferResp.GetTransfer().GetTransferRef())
|
stored := repo.transfers.get(transferResp.GetTransfer().GetTransferRef())
|
||||||
require.NotNil(t, stored)
|
require.NotNil(t, stored)
|
||||||
require.Equal(t, model.TransferStatusPending, stored.Status)
|
require.Equal(t, model.TransferStatusCreated, stored.Status)
|
||||||
|
|
||||||
// GetTransfer
|
// GetTransfer
|
||||||
getResp, err := svc.GetTransfer(ctx, &ichainv1.GetTransferRequest{TransferRef: stored.TransferRef})
|
getResp, err := svc.GetTransfer(ctx, &ichainv1.GetTransferRequest{TransferRef: stored.TransferRef})
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import (
|
|||||||
"github.com/tech/sendico/gateway/tron/storage/model"
|
"github.com/tech/sendico/gateway/tron/storage/model"
|
||||||
chainasset "github.com/tech/sendico/pkg/chain"
|
chainasset "github.com/tech/sendico/pkg/chain"
|
||||||
pmodel "github.com/tech/sendico/pkg/model"
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
)
|
)
|
||||||
@@ -77,42 +79,82 @@ func ManagedWalletStatusToProto(status model.ManagedWalletStatus) chainv1.Manage
|
|||||||
|
|
||||||
func TransferStatusToModel(status chainv1.TransferStatus) model.TransferStatus {
|
func TransferStatusToModel(status chainv1.TransferStatus) model.TransferStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case chainv1.TransferStatus_TRANSFER_PENDING:
|
|
||||||
return model.TransferStatusPending
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
case chainv1.TransferStatus_TRANSFER_SIGNING:
|
return model.TransferStatusCreated
|
||||||
return model.TransferStatusSigning
|
|
||||||
case chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||||
return model.TransferStatusSubmitted
|
return model.TransferStatusProcessing
|
||||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
|
||||||
return model.TransferStatusConfirmed
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return model.TransferStatusWaiting
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
|
return model.TransferStatusSuccess
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
return model.TransferStatusFailed
|
return model.TransferStatusFailed
|
||||||
|
|
||||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
return model.TransferStatusCancelled
|
return model.TransferStatusCancelled
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return ""
|
return model.TransferStatus("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TransferStatusToProto(status model.TransferStatus) chainv1.TransferStatus {
|
func TransferStatusToProto(status model.TransferStatus) chainv1.TransferStatus {
|
||||||
switch status {
|
switch status {
|
||||||
case model.TransferStatusPending:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_PENDING
|
case model.TransferStatusCreated:
|
||||||
case model.TransferStatusSigning:
|
return chainv1.TransferStatus_TRANSFER_CREATED
|
||||||
return chainv1.TransferStatus_TRANSFER_SIGNING
|
|
||||||
case model.TransferStatusSubmitted:
|
case model.TransferStatusProcessing:
|
||||||
return chainv1.TransferStatus_TRANSFER_SUBMITTED
|
return chainv1.TransferStatus_TRANSFER_PROCESSING
|
||||||
case model.TransferStatusConfirmed:
|
|
||||||
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
case model.TransferStatusWaiting:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_WAITING
|
||||||
|
|
||||||
|
case model.TransferStatusSuccess:
|
||||||
|
return chainv1.TransferStatus_TRANSFER_SUCCESS
|
||||||
|
|
||||||
case model.TransferStatusFailed:
|
case model.TransferStatusFailed:
|
||||||
return chainv1.TransferStatus_TRANSFER_FAILED
|
return chainv1.TransferStatus_TRANSFER_FAILED
|
||||||
|
|
||||||
case model.TransferStatusCancelled:
|
case model.TransferStatusCancelled:
|
||||||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ChainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
||||||
|
switch status {
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CREATED
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_PROCESSING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_WAITING
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_SUCCESS
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||||
|
|
||||||
|
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||||
|
|
||||||
|
default:
|
||||||
|
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NativeCurrency returns the canonical native token symbol for a network.
|
// NativeCurrency returns the canonical native token symbol for a network.
|
||||||
func NativeCurrency(network Network) string {
|
func NativeCurrency(network Network) string {
|
||||||
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
||||||
@@ -146,3 +188,23 @@ type ServiceWallet struct {
|
|||||||
Address string
|
Address string
|
||||||
PrivateKey string
|
PrivateKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ProtoToMoney(money *moneyv1.Money) *paymenttypes.Money {
|
||||||
|
if money == nil {
|
||||||
|
return &paymenttypes.Money{}
|
||||||
|
}
|
||||||
|
return &paymenttypes.Money{
|
||||||
|
Amount: money.GetAmount(),
|
||||||
|
Currency: money.GetCurrency(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MonenyToProto(money *paymenttypes.Money) *moneyv1.Money {
|
||||||
|
if money == nil {
|
||||||
|
return &moneyv1.Money{}
|
||||||
|
}
|
||||||
|
return &moneyv1.Money{
|
||||||
|
Amount: money.Amount,
|
||||||
|
Currency: money.Currency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,26 +40,26 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSigning, "", ""); err != nil {
|
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusProcessing, "", ""); err != nil {
|
||||||
s.logger.Warn("Failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
|
s.logger.Warn("Failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
driverDeps := s.driverDeps()
|
driverDeps := s.driverDeps()
|
||||||
chainDriver, err := s.driverForNetwork(network.Name.String())
|
chainDriver, err := s.driverForNetwork(network.Name.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
destinationAddress, err := s.destinationAddress(ctx, chainDriver, transfer.Destination)
|
destinationAddress, err := s.destinationAddress(ctx, chainDriver, transfer.Destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceAddress, err := chainDriver.NormalizeAddress(sourceWallet.DepositAddress)
|
sourceAddress, err := chainDriver.NormalizeAddress(sourceWallet.DepositAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if chainDriver.Name() == "tron" && sourceAddress == destinationAddress {
|
if chainDriver.Name() == "tron" && sourceAddress == destinationAddress {
|
||||||
@@ -68,7 +68,7 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
|||||||
zap.String("wallet_ref", sourceWalletRef),
|
zap.String("wallet_ref", sourceWalletRef),
|
||||||
zap.String("network", network.Name.String()),
|
zap.String("network", network.Name.String()),
|
||||||
)
|
)
|
||||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", ""); err != nil {
|
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusSuccess, "", ""); err != nil {
|
||||||
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -76,11 +76,14 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
|||||||
|
|
||||||
txHash, err := chainDriver.SubmitTransfer(ctx, driverDeps, network, transfer, sourceWallet, destinationAddress)
|
txHash, err := chainDriver.SubmitTransfer(ctx, driverDeps, network, transfer, sourceWallet, destinationAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
s.logger.Warn("Failed to submit transfer", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||||
|
if _, e := s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), ""); e != nil {
|
||||||
|
s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(e))
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSubmitted, "", txHash); err != nil {
|
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusWaiting, "", txHash); err != nil {
|
||||||
s.logger.Warn("Failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err))
|
s.logger.Warn("Failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,15 +97,15 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if receipt != nil && receipt.Status == types.ReceiptStatusSuccessful {
|
failureReason := ""
|
||||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", txHash); err != nil {
|
pStatus := model.TransferStatusSuccess
|
||||||
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
if receipt != nil && receipt.Status != types.ReceiptStatusSuccessful {
|
||||||
}
|
failureReason = "transaction reverted"
|
||||||
return nil
|
pStatus = model.TransferStatusFailed
|
||||||
}
|
}
|
||||||
|
if _, err := s.updateTransferStatus(ctx, transferRef, pStatus, failureReason, txHash); err != nil {
|
||||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, "transaction reverted", txHash); err != nil {
|
s.logger.Warn("Failed to update transfer status", zap.Error(err),
|
||||||
s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
zap.String("transfer_ref", transferRef), zap.String("status", string(pStatus)))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/tron/storage/model"
|
||||||
|
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isFinalStatus(t *model.Transfer) bool {
|
||||||
|
switch t.Status {
|
||||||
|
case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toOpStatus(t *model.Transfer) rail.OperationResult {
|
||||||
|
switch t.Status {
|
||||||
|
case model.TransferStatusFailed:
|
||||||
|
return rail.OperationResultFailed
|
||||||
|
case model.TransferStatusSuccess:
|
||||||
|
return rail.OperationResultSuccess
|
||||||
|
case model.TransferStatusCancelled:
|
||||||
|
return rail.OperationResultCancelled
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("toOpStatus: unexpected transfer status: %s", t.Status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toError(t *model.Transfer) string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if t.Status == model.TransferStatusSuccess {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.FailureReason
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) (*model.Transfer, error) {
|
||||||
|
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
|
||||||
|
}
|
||||||
|
if isFinalStatus(transfer) {
|
||||||
|
s.emitTransferStatusEvent(transfer)
|
||||||
|
}
|
||||||
|
return transfer, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) emitTransferStatusEvent(transfer *model.Transfer) {
|
||||||
|
if s == nil || s.producer == nil || transfer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exec := pmodel.PaymentGatewayExecution{
|
||||||
|
PaymentIntentID: transfer.IntentRef,
|
||||||
|
IdempotencyKey: transfer.IdempotencyKey,
|
||||||
|
ExecutedMoney: transfer.NetAmount,
|
||||||
|
PaymentRef: transfer.PaymentRef,
|
||||||
|
Status: toOpStatus(transfer),
|
||||||
|
OperationRef: transfer.OperationRef,
|
||||||
|
Error: toError(transfer),
|
||||||
|
TransferRef: transfer.TransferRef,
|
||||||
|
}
|
||||||
|
env := paymentgateway.PaymentGatewayExecution(mservice.ChainGateway, &exec)
|
||||||
|
if err := s.producer.SendMessage(env); err != nil {
|
||||||
|
s.logger.Warn("Failed to publish transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,18 +6,20 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/pkg/db/storable"
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TransferStatus string
|
type TransferStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TransferStatusPending TransferStatus = "pending"
|
TransferStatusCreated TransferStatus = "created" // record exists, not started
|
||||||
TransferStatusSigning TransferStatus = "signing"
|
TransferStatusProcessing TransferStatus = "processing" // we are working on it
|
||||||
TransferStatusSubmitted TransferStatus = "submitted"
|
TransferStatusWaiting TransferStatus = "waiting" // waiting external world
|
||||||
TransferStatusConfirmed TransferStatus = "confirmed"
|
|
||||||
TransferStatusFailed TransferStatus = "failed"
|
TransferStatusSuccess TransferStatus = "success" // final success
|
||||||
TransferStatusCancelled TransferStatus = "cancelled"
|
TransferStatusFailed TransferStatus = "failed" // final failure
|
||||||
|
TransferStatusCancelled TransferStatus = "cancelled" // final cancelled
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceFee represents a fee component applied to a transfer.
|
// ServiceFee represents a fee component applied to a transfer.
|
||||||
@@ -38,21 +40,23 @@ type TransferDestination struct {
|
|||||||
type Transfer struct {
|
type Transfer struct {
|
||||||
storable.Base `bson:",inline" json:",inline"`
|
storable.Base `bson:",inline" json:",inline"`
|
||||||
|
|
||||||
|
OperationRef string `bson:"operationRef" json:"operationRef"`
|
||||||
TransferRef string `bson:"transferRef" json:"transferRef"`
|
TransferRef string `bson:"transferRef" json:"transferRef"`
|
||||||
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
|
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
|
||||||
|
IntentRef string `bson:"intentRef" json:"intentRef"`
|
||||||
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
|
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
|
||||||
SourceWalletRef string `bson:"sourceWalletRef" json:"sourceWalletRef"`
|
SourceWalletRef string `bson:"sourceWalletRef" json:"sourceWalletRef"`
|
||||||
Destination TransferDestination `bson:"destination" json:"destination"`
|
Destination TransferDestination `bson:"destination" json:"destination"`
|
||||||
Network string `bson:"network" json:"network"`
|
Network string `bson:"network" json:"network"`
|
||||||
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
|
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
|
||||||
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
|
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
|
||||||
RequestedAmount *moneyv1.Money `bson:"requestedAmount" json:"requestedAmount"`
|
RequestedAmount *paymenttypes.Money `bson:"requestedAmount" json:"requestedAmount"`
|
||||||
NetAmount *moneyv1.Money `bson:"netAmount" json:"netAmount"`
|
NetAmount *paymenttypes.Money `bson:"netAmount" json:"netAmount"`
|
||||||
Fees []ServiceFee `bson:"fees,omitempty" json:"fees,omitempty"`
|
Fees []ServiceFee `bson:"fees,omitempty" json:"fees,omitempty"`
|
||||||
Status TransferStatus `bson:"status" json:"status"`
|
Status TransferStatus `bson:"status" json:"status"`
|
||||||
TxHash string `bson:"txHash,omitempty" json:"txHash,omitempty"`
|
TxHash string `bson:"txHash,omitempty" json:"txHash,omitempty"`
|
||||||
FailureReason string `bson:"failureReason,omitempty" json:"failureReason,omitempty"`
|
FailureReason string `bson:"failureReason,omitempty" json:"failureReason,omitempty"`
|
||||||
ClientReference string `bson:"clientReference,omitempty" json:"clientReference,omitempty"`
|
PaymentRef string `bson:"paymentRef,omitempty" json:"paymentRef,omitempty"`
|
||||||
LastStatusAt time.Time `bson:"lastStatusAt" json:"lastStatusAt"`
|
LastStatusAt time.Time `bson:"lastStatusAt" json:"lastStatusAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +85,7 @@ func (t *Transfer) Normalize() {
|
|||||||
t.TransferRef = strings.TrimSpace(t.TransferRef)
|
t.TransferRef = strings.TrimSpace(t.TransferRef)
|
||||||
t.IdempotencyKey = strings.TrimSpace(t.IdempotencyKey)
|
t.IdempotencyKey = strings.TrimSpace(t.IdempotencyKey)
|
||||||
t.OrganizationRef = strings.TrimSpace(t.OrganizationRef)
|
t.OrganizationRef = strings.TrimSpace(t.OrganizationRef)
|
||||||
|
t.IntentRef = strings.TrimSpace(t.IntentRef)
|
||||||
t.SourceWalletRef = strings.TrimSpace(t.SourceWalletRef)
|
t.SourceWalletRef = strings.TrimSpace(t.SourceWalletRef)
|
||||||
t.Network = strings.TrimSpace(strings.ToLower(t.Network))
|
t.Network = strings.TrimSpace(strings.ToLower(t.Network))
|
||||||
t.TokenSymbol = strings.TrimSpace(strings.ToUpper(t.TokenSymbol))
|
t.TokenSymbol = strings.TrimSpace(strings.ToUpper(t.TokenSymbol))
|
||||||
@@ -89,5 +94,5 @@ func (t *Transfer) Normalize() {
|
|||||||
t.Destination.ExternalAddress = normalizeWalletAddress(t.Destination.ExternalAddress)
|
t.Destination.ExternalAddress = normalizeWalletAddress(t.Destination.ExternalAddress)
|
||||||
t.Destination.ExternalAddressOriginal = strings.TrimSpace(t.Destination.ExternalAddressOriginal)
|
t.Destination.ExternalAddressOriginal = strings.TrimSpace(t.Destination.ExternalAddressOriginal)
|
||||||
t.Destination.Memo = strings.TrimSpace(t.Destination.Memo)
|
t.Destination.Memo = strings.TrimSpace(t.Destination.Memo)
|
||||||
t.ClientReference = strings.TrimSpace(t.ClientReference)
|
t.PaymentRef = strings.TrimSpace(t.PaymentRef)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func (t *Transfers) Create(ctx context.Context, transfer *model.Transfer) (*mode
|
|||||||
return nil, merrors.InvalidArgument("transfersStore: empty idempotencyKey")
|
return nil, merrors.InvalidArgument("transfersStore: empty idempotencyKey")
|
||||||
}
|
}
|
||||||
if transfer.Status == "" {
|
if transfer.Status == "" {
|
||||||
transfer.Status = model.TransferStatusPending
|
transfer.Status = model.TransferStatusCreated
|
||||||
}
|
}
|
||||||
if transfer.LastStatusAt.IsZero() {
|
if transfer.LastStatusAt.IsZero() {
|
||||||
transfer.LastStatusAt = time.Now().UTC()
|
transfer.LastStatusAt = time.Now().UTC()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/pkg/ledgerconv"
|
"github.com/tech/sendico/pkg/ledgerconv"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model/account_role"
|
||||||
"github.com/tech/sendico/pkg/payments/rail"
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
@@ -317,7 +317,7 @@ func (c *ledgerClient) BlockAccount(ctx context.Context, req *ledgerv1.BlockAcco
|
|||||||
if req == nil || strings.TrimSpace(req.GetLedgerAccountRef()) == "" {
|
if req == nil || strings.TrimSpace(req.GetLedgerAccountRef()) == "" {
|
||||||
return nil, merrors.InvalidArgument("ledger: ledger_account_ref is required")
|
return nil, merrors.InvalidArgument("ledger: ledger_account_ref is required")
|
||||||
}
|
}
|
||||||
sourceRole := model.ToProto(accountRoleFromLedgerProto(req.GetRole()))
|
sourceRole := account_role.ToProto(accountRoleFromLedgerProto(req.GetRole()))
|
||||||
resp, err := c.client.UpdateAccountState(ctx, &connectorv1.UpdateAccountStateRequest{
|
resp, err := c.client.UpdateAccountState(ctx, &connectorv1.UpdateAccountStateRequest{
|
||||||
AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())},
|
AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())},
|
||||||
TargetState: connectorv1.AccountState_ACCOUNT_SUSPENDED,
|
TargetState: connectorv1.AccountState_ACCOUNT_SUSPENDED,
|
||||||
@@ -338,7 +338,7 @@ func (c *ledgerClient) UnblockAccount(ctx context.Context, req *ledgerv1.Unblock
|
|||||||
if req == nil || strings.TrimSpace(req.GetLedgerAccountRef()) == "" {
|
if req == nil || strings.TrimSpace(req.GetLedgerAccountRef()) == "" {
|
||||||
return nil, merrors.InvalidArgument("ledger: ledger_account_ref is required")
|
return nil, merrors.InvalidArgument("ledger: ledger_account_ref is required")
|
||||||
}
|
}
|
||||||
sourceRole := model.ToProto(accountRoleFromLedgerProto(req.GetRole()))
|
sourceRole := account_role.ToProto(accountRoleFromLedgerProto(req.GetRole()))
|
||||||
resp, err := c.client.UpdateAccountState(ctx, &connectorv1.UpdateAccountStateRequest{
|
resp, err := c.client.UpdateAccountState(ctx, &connectorv1.UpdateAccountStateRequest{
|
||||||
AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())},
|
AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())},
|
||||||
TargetState: connectorv1.AccountState_ACCOUNT_ACTIVE,
|
TargetState: connectorv1.AccountState_ACCOUNT_ACTIVE,
|
||||||
@@ -430,8 +430,8 @@ func (c *ledgerClient) submitLedgerOperationWithExtras(ctx context.Context, opTy
|
|||||||
charges []*ledgerv1.PostingLine
|
charges []*ledgerv1.PostingLine
|
||||||
eventTime *timestamppb.Timestamp
|
eventTime *timestamppb.Timestamp
|
||||||
contraRef string
|
contraRef string
|
||||||
fromRole model.AccountRole
|
fromRole account_role.AccountRole
|
||||||
toRole model.AccountRole
|
toRole account_role.AccountRole
|
||||||
)
|
)
|
||||||
|
|
||||||
switch r := req.(type) {
|
switch r := req.(type) {
|
||||||
@@ -487,10 +487,10 @@ func (c *ledgerClient) submitLedgerOperationWithExtras(ctx context.Context, opTy
|
|||||||
op.To = accountParty(toRef)
|
op.To = accountParty(toRef)
|
||||||
}
|
}
|
||||||
if fromRole != "" {
|
if fromRole != "" {
|
||||||
op.FromRole = model.ToProto(fromRole)
|
op.FromRole = account_role.ToProto(fromRole)
|
||||||
}
|
}
|
||||||
if toRole != "" {
|
if toRole != "" {
|
||||||
op.ToRole = model.ToProto(toRole)
|
op.ToRole = account_role.ToProto(toRole)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: op})
|
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: op})
|
||||||
@@ -503,30 +503,30 @@ func (c *ledgerClient) submitLedgerOperationWithExtras(ctx context.Context, opTy
|
|||||||
return &ledgerv1.PostResponse{JournalEntryRef: resp.GetReceipt().GetOperationId(), EntryType: entryTypeFromOperation(opType)}, nil
|
return &ledgerv1.PostResponse{JournalEntryRef: resp.GetReceipt().GetOperationId(), EntryType: entryTypeFromOperation(opType)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountRoleFromLedgerProto(role ledgerv1.AccountRole) model.AccountRole {
|
func accountRoleFromLedgerProto(role ledgerv1.AccountRole) account_role.AccountRole {
|
||||||
switch role {
|
switch role {
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING:
|
||||||
return model.AccountRoleOperating
|
return account_role.AccountRoleOperating
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD:
|
||||||
return model.AccountRoleHold
|
return account_role.AccountRoleHold
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT:
|
||||||
return model.AccountRoleTransit
|
return account_role.AccountRoleTransit
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT:
|
||||||
return model.AccountRoleSettlement
|
return account_role.AccountRoleSettlement
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING:
|
||||||
return model.AccountRoleClearing
|
return account_role.AccountRoleClearing
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING:
|
||||||
return model.AccountRolePending
|
return account_role.AccountRolePending
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE:
|
||||||
return model.AccountRoleReserve
|
return account_role.AccountRoleReserve
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY:
|
||||||
return model.AccountRoleLiquidity
|
return account_role.AccountRoleLiquidity
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_FEE:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_FEE:
|
||||||
return model.AccountRoleFee
|
return account_role.AccountRoleFee
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK:
|
||||||
return model.AccountRoleChargeback
|
return account_role.AccountRoleChargeback
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT:
|
||||||
return model.AccountRoleAdjustment
|
return account_role.AccountRoleAdjustment
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -210,8 +210,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-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/model/account_role"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ type Account struct {
|
|||||||
// Posting policy & lifecycle
|
// Posting policy & lifecycle
|
||||||
AllowNegative bool `bson:"allowNegative" json:"allowNegative"`
|
AllowNegative bool `bson:"allowNegative" json:"allowNegative"`
|
||||||
Status model.LedgerAccountStatus `bson:"status" json:"status"`
|
Status model.LedgerAccountStatus `bson:"status" json:"status"`
|
||||||
Role model.AccountRole `bson:"role,omitempty" json:"role,omitempty"`
|
Role account_role.AccountRole `bson:"role,omitempty" json:"role,omitempty"`
|
||||||
|
|
||||||
// Legal ownership history
|
// Legal ownership history
|
||||||
Ownerships []Ownership `bson:"ownerships,omitempty" json:"ownerships,omitempty"`
|
Ownerships []Ownership `bson:"ownerships,omitempty" json:"ownerships,omitempty"`
|
||||||
@@ -78,17 +79,17 @@ func (a *Account) Validate() error {
|
|||||||
|
|
||||||
if role := strings.TrimSpace(string(a.Role)); role != "" {
|
if role := strings.TrimSpace(string(a.Role)); role != "" {
|
||||||
switch a.Role {
|
switch a.Role {
|
||||||
case model.AccountRoleOperating,
|
case account_role.AccountRoleOperating,
|
||||||
model.AccountRoleHold,
|
account_role.AccountRoleHold,
|
||||||
model.AccountRoleTransit,
|
account_role.AccountRoleTransit,
|
||||||
model.AccountRoleSettlement,
|
account_role.AccountRoleSettlement,
|
||||||
model.AccountRoleClearing,
|
account_role.AccountRoleClearing,
|
||||||
model.AccountRolePending,
|
account_role.AccountRolePending,
|
||||||
model.AccountRoleReserve,
|
account_role.AccountRoleReserve,
|
||||||
model.AccountRoleLiquidity,
|
account_role.AccountRoleLiquidity,
|
||||||
model.AccountRoleFee,
|
account_role.AccountRoleFee,
|
||||||
model.AccountRoleChargeback,
|
account_role.AccountRoleChargeback,
|
||||||
model.AccountRoleAdjustment:
|
account_role.AccountRoleAdjustment:
|
||||||
default:
|
default:
|
||||||
veAdd(&verr, "role", "invalid", "unknown account role")
|
veAdd(&verr, "role", "invalid", "unknown account role")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
pmodel "github.com/tech/sendico/pkg/model"
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/model/account_role"
|
||||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
@@ -25,7 +26,7 @@ type createAccountParams struct {
|
|||||||
currency string
|
currency string
|
||||||
modelType pmodel.LedgerAccountType
|
modelType pmodel.LedgerAccountType
|
||||||
modelStatus pmodel.LedgerAccountStatus
|
modelStatus pmodel.LedgerAccountStatus
|
||||||
modelRole pmodel.AccountRole
|
modelRole account_role.AccountRole
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateCreateAccountInput validates and normalizes all fields from the request.
|
// validateCreateAccountInput validates and normalizes all fields from the request.
|
||||||
@@ -93,7 +94,7 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Non-settlement accounts require a settlement account to exist first.
|
// Non-settlement accounts require a settlement account to exist first.
|
||||||
if p.modelRole != pmodel.AccountRoleSettlement {
|
if p.modelRole != account_role.AccountRoleSettlement {
|
||||||
if _, err := s.ensureSettlementAccount(ctx, p.orgRef, p.currency); err != nil {
|
if _, err := s.ensureSettlementAccount(ctx, p.orgRef, p.currency); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -104,7 +105,7 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolveTopologyAccount ensures ledger topology is initialized and returns the system account for the given role.
|
// resolveTopologyAccount ensures ledger topology is initialized and returns the system account for the given role.
|
||||||
func (s *Service) resolveTopologyAccount(ctx context.Context, orgRef bson.ObjectID, currency string, role pmodel.AccountRole) (*ledgerv1.CreateAccountResponse, error) {
|
func (s *Service) resolveTopologyAccount(ctx context.Context, orgRef bson.ObjectID, currency string, role account_role.AccountRole) (*ledgerv1.CreateAccountResponse, error) {
|
||||||
if err := s.ensureLedgerTopology(ctx, orgRef, currency); err != nil {
|
if err := s.ensureLedgerTopology(ctx, orgRef, currency); err != nil {
|
||||||
recordAccountOperation("create", "error")
|
recordAccountOperation("create", "error")
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -244,58 +245,58 @@ func modelAccountTypeToProto(t pmodel.LedgerAccountType) ledgerv1.AccountType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func protoAccountRoleToModel(r ledgerv1.AccountRole) (pmodel.AccountRole, error) {
|
func protoAccountRoleToModel(r ledgerv1.AccountRole) (account_role.AccountRole, error) {
|
||||||
switch r {
|
switch r {
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED:
|
||||||
return pmodel.AccountRoleOperating, nil
|
return account_role.AccountRoleOperating, nil
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD:
|
||||||
return pmodel.AccountRoleHold, nil
|
return account_role.AccountRoleHold, nil
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT:
|
||||||
return pmodel.AccountRoleTransit, nil
|
return account_role.AccountRoleTransit, nil
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT:
|
||||||
return pmodel.AccountRoleSettlement, nil
|
return account_role.AccountRoleSettlement, nil
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING:
|
||||||
return pmodel.AccountRoleClearing, nil
|
return account_role.AccountRoleClearing, nil
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING:
|
||||||
return pmodel.AccountRolePending, nil
|
return account_role.AccountRolePending, nil
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE:
|
||||||
return pmodel.AccountRoleReserve, nil
|
return account_role.AccountRoleReserve, nil
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY:
|
||||||
return pmodel.AccountRoleLiquidity, nil
|
return account_role.AccountRoleLiquidity, nil
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_FEE:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_FEE:
|
||||||
return pmodel.AccountRoleFee, nil
|
return account_role.AccountRoleFee, nil
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK:
|
||||||
return pmodel.AccountRoleChargeback, nil
|
return account_role.AccountRoleChargeback, nil
|
||||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT:
|
case ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT:
|
||||||
return pmodel.AccountRoleAdjustment, nil
|
return account_role.AccountRoleAdjustment, nil
|
||||||
default:
|
default:
|
||||||
return "", merrors.InvalidArgument("invalid account role")
|
return "", merrors.InvalidArgument("invalid account role")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func modelAccountRoleToProto(r pmodel.AccountRole) ledgerv1.AccountRole {
|
func modelAccountRoleToProto(r account_role.AccountRole) ledgerv1.AccountRole {
|
||||||
switch r {
|
switch r {
|
||||||
case pmodel.AccountRoleOperating, "":
|
case account_role.AccountRoleOperating, "":
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING
|
||||||
case pmodel.AccountRoleHold:
|
case account_role.AccountRoleHold:
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD
|
||||||
case pmodel.AccountRoleTransit:
|
case account_role.AccountRoleTransit:
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT
|
||||||
case pmodel.AccountRoleSettlement:
|
case account_role.AccountRoleSettlement:
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT
|
||||||
case pmodel.AccountRoleClearing:
|
case account_role.AccountRoleClearing:
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING
|
||||||
case pmodel.AccountRolePending:
|
case account_role.AccountRolePending:
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING
|
||||||
case pmodel.AccountRoleReserve:
|
case account_role.AccountRoleReserve:
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE
|
||||||
case pmodel.AccountRoleLiquidity:
|
case account_role.AccountRoleLiquidity:
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY
|
||||||
case pmodel.AccountRoleFee:
|
case account_role.AccountRoleFee:
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_FEE
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_FEE
|
||||||
case pmodel.AccountRoleChargeback:
|
case account_role.AccountRoleChargeback:
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK
|
||||||
case pmodel.AccountRoleAdjustment:
|
case account_role.AccountRoleAdjustment:
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT
|
||||||
default:
|
default:
|
||||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED
|
return ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED
|
||||||
@@ -414,7 +415,7 @@ func describableToProto(desc pmodel.Describable) *describablev1.Describable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ensureSettlementAccount(ctx context.Context, orgRef bson.ObjectID, currency string) (*pmodel.LedgerAccount, error) {
|
func (s *Service) ensureSettlementAccount(ctx context.Context, orgRef bson.ObjectID, currency string) (*pmodel.LedgerAccount, error) {
|
||||||
return s.ensureRoleAccount(ctx, orgRef, currency, pmodel.AccountRoleSettlement)
|
return s.ensureRoleAccount(ctx, orgRef, currency, account_role.AccountRoleSettlement)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateAccountCode(accountType pmodel.LedgerAccountType, currency string, id bson.ObjectID) string {
|
func generateAccountCode(accountType pmodel.LedgerAccountType, currency string, id bson.ObjectID) string {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/tech/sendico/ledger/storage"
|
"github.com/tech/sendico/ledger/storage"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
pmodel "github.com/tech/sendico/pkg/model"
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/model/account_role"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,14 +22,14 @@ type accountStoreStub struct {
|
|||||||
created []*pmodel.LedgerAccount
|
created []*pmodel.LedgerAccount
|
||||||
existing *pmodel.LedgerAccount
|
existing *pmodel.LedgerAccount
|
||||||
existingErr error
|
existingErr error
|
||||||
existingByRole map[pmodel.AccountRole]*pmodel.LedgerAccount
|
existingByRole map[account_role.AccountRole]*pmodel.LedgerAccount
|
||||||
defaultSettlement *pmodel.LedgerAccount
|
defaultSettlement *pmodel.LedgerAccount
|
||||||
defaultErr error
|
defaultErr error
|
||||||
createErrs []error
|
createErrs []error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *accountStoreStub) Create(_ context.Context, account *pmodel.LedgerAccount) error {
|
func (s *accountStoreStub) Create(_ context.Context, account *pmodel.LedgerAccount) error {
|
||||||
if account.Role == pmodel.AccountRoleSettlement {
|
if account.Role == account_role.AccountRoleSettlement {
|
||||||
if s.createErrSettlement != nil {
|
if s.createErrSettlement != nil {
|
||||||
return s.createErrSettlement
|
return s.createErrSettlement
|
||||||
}
|
}
|
||||||
@@ -66,7 +67,7 @@ func (s *accountStoreStub) Get(context.Context, bson.ObjectID) (*pmodel.LedgerAc
|
|||||||
return nil, storage.ErrAccountNotFound
|
return nil, storage.ErrAccountNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *accountStoreStub) GetByRole(_ context.Context, orgRef bson.ObjectID, currency string, role pmodel.AccountRole) (*pmodel.LedgerAccount, error) {
|
func (s *accountStoreStub) GetByRole(_ context.Context, orgRef bson.ObjectID, currency string, role account_role.AccountRole) (*pmodel.LedgerAccount, error) {
|
||||||
if s.existingByRole != nil {
|
if s.existingByRole != nil {
|
||||||
if acc, ok := s.existingByRole[role]; ok {
|
if acc, ok := s.existingByRole[role]; ok {
|
||||||
return acc, nil
|
return acc, nil
|
||||||
@@ -190,14 +191,14 @@ func TestCreateAccountResponder_AutoCreatesSettlementAccount(t *testing.T) {
|
|||||||
var settlement *pmodel.LedgerAccount
|
var settlement *pmodel.LedgerAccount
|
||||||
var operating *pmodel.LedgerAccount
|
var operating *pmodel.LedgerAccount
|
||||||
|
|
||||||
roles := make(map[pmodel.AccountRole]bool)
|
roles := make(map[account_role.AccountRole]bool)
|
||||||
for _, acc := range accountStore.created {
|
for _, acc := range accountStore.created {
|
||||||
roles[acc.Role] = true
|
roles[acc.Role] = true
|
||||||
|
|
||||||
if acc.Role == pmodel.AccountRoleSettlement {
|
if acc.Role == account_role.AccountRoleSettlement {
|
||||||
settlement = acc
|
settlement = acc
|
||||||
}
|
}
|
||||||
if acc.Role == pmodel.AccountRoleOperating {
|
if acc.Role == account_role.AccountRoleOperating {
|
||||||
operating = acc
|
operating = acc
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +231,7 @@ func TestCreateAccountResponder_AutoCreatesSettlementAccount(t *testing.T) {
|
|||||||
require.Equal(t, pmodel.LedgerAccountTypeAsset, settlement.AccountType)
|
require.Equal(t, pmodel.LedgerAccountTypeAsset, settlement.AccountType)
|
||||||
require.Equal(t, "USD", settlement.Currency)
|
require.Equal(t, "USD", settlement.Currency)
|
||||||
require.False(t, settlement.AllowNegative)
|
require.False(t, settlement.AllowNegative)
|
||||||
require.Equal(t, pmodel.AccountRoleSettlement, settlement.Role)
|
require.Equal(t, account_role.AccountRoleSettlement, settlement.Role)
|
||||||
require.Equal(t, "true", settlement.Metadata["system"])
|
require.Equal(t, "true", settlement.Metadata["system"])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +266,7 @@ func TestCreateAccountResponder_RetriesOnConflict(t *testing.T) {
|
|||||||
|
|
||||||
var createdFee *pmodel.LedgerAccount
|
var createdFee *pmodel.LedgerAccount
|
||||||
for _, acc := range accountStore.created {
|
for _, acc := range accountStore.created {
|
||||||
if acc.Role == pmodel.AccountRoleFee {
|
if acc.Role == account_role.AccountRoleFee {
|
||||||
createdFee = acc
|
createdFee = acc
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user