refactored payment orchestration #393
@@ -65,6 +65,6 @@ require (
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
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 h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||
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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
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/sys v0.40.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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
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 h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||
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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
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/sys v0.40.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/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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
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 h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||
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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
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/sys v0.40.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/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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
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 h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||
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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
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/sys v0.40.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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
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 h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||
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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/tech/sendico/fx/storage/model"
|
||||
"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"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/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,
|
||||
Side: qc.sideModel,
|
||||
Price: formatRat(qc.priceRounded, qc.priceScale),
|
||||
BaseAmount: smodel.Money{
|
||||
BaseAmount: paymenttypes.Money{
|
||||
Currency: qc.pair.Pair.Base,
|
||||
Amount: formatRat(qc.baseRounded, qc.baseScale),
|
||||
},
|
||||
QuoteAmount: smodel.Money{
|
||||
QuoteAmount: paymenttypes.Money{
|
||||
Currency: qc.pair.Pair.Quote,
|
||||
Amount: formatRat(qc.quoteRounded, qc.quoteScale),
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/tech/sendico/fx/storage"
|
||||
"github.com/tech/sendico/fx/storage/model"
|
||||
"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"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/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"},
|
||||
Side: model.QuoteSideBuyBaseSellQuote,
|
||||
Price: "1.10",
|
||||
BaseAmount: smodel.Money{Currency: "USD", Amount: "100"},
|
||||
QuoteAmount: smodel.Money{Currency: "EUR", Amount: "110"},
|
||||
BaseAmount: paymenttypes.Money{Currency: "USD", Amount: "100"},
|
||||
QuoteAmount: paymenttypes.Money{Currency: "EUR", Amount: "110"},
|
||||
ExpiresAtUnixMs: now.UnixMilli(),
|
||||
Status: model.QuoteStatusIssued,
|
||||
}, nil
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,32 +4,32 @@ import (
|
||||
"time"
|
||||
|
||||
"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.
|
||||
type Quote struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
|
||||
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
|
||||
Firm bool `bson:"firm" json:"firm"`
|
||||
Status QuoteStatus `bson:"status" json:"status"`
|
||||
Pair CurrencyPair `bson:"pair" json:"pair"`
|
||||
Side QuoteSide `bson:"side" json:"side"`
|
||||
Price string `bson:"price" json:"price"`
|
||||
BaseAmount model.Money `bson:"baseAmount" json:"baseAmount"`
|
||||
QuoteAmount model.Money `bson:"quoteAmount" json:"quoteAmount"`
|
||||
AmountType QuoteAmountType `bson:"amountType" json:"amountType"`
|
||||
ExpiresAtUnixMs int64 `bson:"expiresAtUnixMs" json:"expiresAtUnixMs"`
|
||||
ExpiresAt *time.Time `bson:"expiresAt,omitempty" json:"expiresAt,omitempty"`
|
||||
RateRef string `bson:"rateRef" json:"rateRef"`
|
||||
Provider string `bson:"provider" json:"provider"`
|
||||
PreferredProvider string `bson:"preferredProvider,omitempty" json:"preferredProvider,omitempty"`
|
||||
RequestedTTLMs int64 `bson:"requestedTtlMs,omitempty" json:"requestedTtlMs,omitempty"`
|
||||
MaxAgeToleranceMs int64 `bson:"maxAgeToleranceMs,omitempty" json:"maxAgeToleranceMs,omitempty"`
|
||||
ConsumedByLedgerTxnRef string `bson:"consumedByLedgerTxnRef,omitempty" json:"consumedByLedgerTxnRef,omitempty"`
|
||||
ConsumedAtUnixMs *int64 `bson:"consumedAtUnixMs,omitempty" json:"consumedAtUnixMs,omitempty"`
|
||||
Meta *QuoteMeta `bson:"meta,omitempty" json:"meta,omitempty"`
|
||||
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
|
||||
Firm bool `bson:"firm" json:"firm"`
|
||||
Status QuoteStatus `bson:"status" json:"status"`
|
||||
Pair CurrencyPair `bson:"pair" json:"pair"`
|
||||
Side QuoteSide `bson:"side" json:"side"`
|
||||
Price string `bson:"price" json:"price"`
|
||||
BaseAmount paymenttypes.Money `bson:"baseAmount" json:"baseAmount"`
|
||||
QuoteAmount paymenttypes.Money `bson:"quoteAmount" json:"quoteAmount"`
|
||||
AmountType QuoteAmountType `bson:"amountType" json:"amountType"`
|
||||
ExpiresAtUnixMs int64 `bson:"expiresAtUnixMs" json:"expiresAtUnixMs"`
|
||||
ExpiresAt *time.Time `bson:"expiresAt,omitempty" json:"expiresAt,omitempty"`
|
||||
RateRef string `bson:"rateRef" json:"rateRef"`
|
||||
Provider string `bson:"provider" json:"provider"`
|
||||
PreferredProvider string `bson:"preferredProvider,omitempty" json:"preferredProvider,omitempty"`
|
||||
RequestedTTLMs int64 `bson:"requestedTtlMs,omitempty" json:"requestedTtlMs,omitempty"`
|
||||
MaxAgeToleranceMs int64 `bson:"maxAgeToleranceMs,omitempty" json:"maxAgeToleranceMs,omitempty"`
|
||||
ConsumedByLedgerTxnRef string `bson:"consumedByLedgerTxnRef,omitempty" json:"consumedByLedgerTxnRef,omitempty"`
|
||||
ConsumedAtUnixMs *int64 `bson:"consumedAtUnixMs,omitempty" json:"consumedAtUnixMs,omitempty"`
|
||||
Meta *QuoteMeta `bson:"meta,omitempty" json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// Collection implements storable.Storable.
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
chainasset "github.com/tech/sendico/pkg/chain"
|
||||
"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"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/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{}{
|
||||
"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 != "" {
|
||||
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())}}},
|
||||
Money: req.GetAmount(),
|
||||
Params: structFromMap(params),
|
||||
IntentRef: strings.TrimSpace(req.GetIntentRef()),
|
||||
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
||||
}
|
||||
to, err := destinationToParty(req.GetDestination())
|
||||
if err != nil {
|
||||
@@ -472,14 +474,14 @@ func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[strin
|
||||
if op == nil || len(metadata) == 0 {
|
||||
return
|
||||
}
|
||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyFromRole]); raw != "" {
|
||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
||||
op.FromRole = pmodel.ToProto(role)
|
||||
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyFromRole]); raw != "" {
|
||||
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||
op.FromRole = account_role.ToProto(role)
|
||||
}
|
||||
}
|
||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyToRole]); raw != "" {
|
||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
||||
op.ToRole = pmodel.ToProto(role)
|
||||
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyToRole]); raw != "" {
|
||||
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||
op.ToRole = account_role.ToProto(role)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -619,7 +621,7 @@ func gasTopUpEnsureOperation(req *chainv1.EnsureGasTopUpRequest) (*connectorv1.O
|
||||
"mode": "ensure",
|
||||
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||
"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()},
|
||||
}
|
||||
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 {
|
||||
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:
|
||||
return connectorv1.OperationStatus_FAILED
|
||||
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return connectorv1.OperationStatus_CANCELED
|
||||
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||
|
||||
default:
|
||||
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"
|
||||
|
||||
"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"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/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,
|
||||
SourceWalletRef: source,
|
||||
Destination: dest,
|
||||
IntentRef: strings.TrimSpace(req.IntentRef),
|
||||
OperationRef: strings.TrimSpace(req.OperationRef),
|
||||
PaymentRef: strings.TrimSpace(req.PaymentRef),
|
||||
Amount: &moneyv1.Money{
|
||||
Currency: currency,
|
||||
Amount: amountValue,
|
||||
},
|
||||
Fees: fees,
|
||||
Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
|
||||
ClientReference: strings.TrimSpace(req.ClientReference),
|
||||
Fees: fees,
|
||||
Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
|
||||
})
|
||||
if err != nil {
|
||||
return rail.RailResult{}, err
|
||||
@@ -186,20 +188,29 @@ func (g *chainRailGateway) isManagedWallet(ctx context.Context, walletRef string
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func statusFromTransfer(status chainv1.TransferStatus) string {
|
||||
func statusFromTransfer(status chainv1.TransferStatus) rail.TransferStatus {
|
||||
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
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||
return rail.TransferStatusFailed
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return rail.TransferStatusRejected
|
||||
case chainv1.TransferStatus_TRANSFER_SIGNING,
|
||||
chainv1.TransferStatus_TRANSFER_PENDING,
|
||||
chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
||||
return rail.TransferStatusPending
|
||||
return rail.TransferStatusCancelled
|
||||
|
||||
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)
|
||||
if strings.TrimSpace(string(fromRole)) != "" {
|
||||
if result == nil {
|
||||
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 result == nil {
|
||||
result = map[string]string{}
|
||||
}
|
||||
result[pmodel.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
|
||||
result[account_role.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
|
||||
require (
|
||||
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/bits-and-blooms/bitset v1.24.4 // 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/text v0.33.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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
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-20260131145833-e3fabd62fc61/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
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-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/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
|
||||
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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
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 h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||
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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
// 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.
|
||||
type Manager interface {
|
||||
// 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(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/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
// 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.
|
||||
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) == "" {
|
||||
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")
|
||||
}
|
||||
if strings.TrimSpace(network) == "" {
|
||||
if network == pmodel.ChainNetworkUnspecified {
|
||||
m.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
|
||||
return nil, merrors.InvalidArgument("vault key manager: network is required")
|
||||
}
|
||||
|
||||
privateKey, err := ecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
|
||||
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())
|
||||
}
|
||||
privateKeyBytes := crypto.FromECDSA(privateKey)
|
||||
@@ -113,9 +114,9 @@ func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string,
|
||||
publicKeyHex := hex.EncodeToString(publicKeyBytes)
|
||||
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 {
|
||||
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(publicKeyBytes)
|
||||
return nil, err
|
||||
@@ -125,12 +126,12 @@ func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string,
|
||||
|
||||
m.logger.Info("Managed wallet key created",
|
||||
zap.String("wallet_ref", walletRef),
|
||||
zap.String("network", network),
|
||||
zap.String("network", string(network)),
|
||||
zap.String("address", strings.ToLower(address)),
|
||||
)
|
||||
|
||||
return &keymanager.ManagedWalletKey{
|
||||
KeyID: m.buildKeyID(network, walletRef),
|
||||
KeyID: m.buildKeyID(string(network), walletRef),
|
||||
Address: strings.ToLower(address),
|
||||
PublicKey: publicKeyHex,
|
||||
}, nil
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/server/grpcapp"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -48,7 +49,7 @@ type config struct {
|
||||
}
|
||||
|
||||
type chainConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Name pmodel.ChainNetwork `yaml:"name"`
|
||||
RPCURLEnv string `yaml:"rpc_url_env"`
|
||||
ChainID uint64 `yaml:"chain_id"`
|
||||
NativeToken string `yaml:"native_token"`
|
||||
@@ -57,10 +58,10 @@ type chainConfig struct {
|
||||
}
|
||||
|
||||
type serviceWalletConfig struct {
|
||||
Chain string `yaml:"chain"`
|
||||
Address string `yaml:"address"`
|
||||
AddressEnv string `yaml:"address_env"`
|
||||
PrivateKeyEnv string `yaml:"private_key_env"`
|
||||
Chain pmodel.ChainNetwork `yaml:"chain"`
|
||||
Address string `yaml:"address"`
|
||||
AddressEnv string `yaml:"address_env"`
|
||||
PrivateKeyEnv string `yaml:"private_key_env"`
|
||||
}
|
||||
|
||||
type tokenConfig struct {
|
||||
@@ -209,20 +210,16 @@ func (i *Imp) loadConfig() (*config, error) {
|
||||
func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatewayshared.Network, error) {
|
||||
result := make([]gatewayshared.Network, 0, len(chains))
|
||||
for _, chain := range chains {
|
||||
if strings.TrimSpace(chain.Name) == "" {
|
||||
logger.Warn("Skipping unnamed chain configuration")
|
||||
continue
|
||||
}
|
||||
rpcURL := strings.TrimSpace(os.Getenv(chain.RPCURLEnv))
|
||||
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))
|
||||
}
|
||||
contracts := make([]gatewayshared.TokenContract, 0, len(chain.Tokens))
|
||||
for _, token := range chain.Tokens {
|
||||
symbol := strings.TrimSpace(token.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
|
||||
}
|
||||
addr := strings.TrimSpace(token.Contract)
|
||||
@@ -232,9 +229,9 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
|
||||
}
|
||||
if addr == "" {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
@@ -246,7 +243,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
|
||||
|
||||
gasPolicy, err := buildGasTopUpPolicy(chain.Name, chain.GasTopUpPolicy)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -262,7 +259,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
|
||||
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 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -300,7 +297,7 @@ func buildGasTopUpPolicy(chainName string, cfg *gasTopUpPolicyConfig) (*gateways
|
||||
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 {
|
||||
return gatewayshared.GasTopUpRule{}, false, nil
|
||||
}
|
||||
@@ -336,7 +333,7 @@ func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewa
|
||||
if cfg.AddressEnv != "" {
|
||||
logger.Warn("Service wallet address not configured", zap.String("env", cfg.AddressEnv))
|
||||
} 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 == "" {
|
||||
|
||||
@@ -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))
|
||||
return model.TransferDestination{}, err
|
||||
}
|
||||
if !strings.EqualFold(wallet.Network, source.Network) {
|
||||
deps.Logger.Warn("Destination wallet network mismatch", zap.String("source_network", source.Network), zap.String("dest_network", wallet.Network))
|
||||
if wallet.Network != source.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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
chainDriver, err := deps.Drivers.Driver(source.Network)
|
||||
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")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
|
||||
networkCfg, ok := c.deps.Networks.Network(networkKey)
|
||||
networkCfg, ok := c.deps.Networks.Network(sourceWallet.Network)
|
||||
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"))
|
||||
}
|
||||
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"))
|
||||
}
|
||||
chainDriver, err := c.deps.Drivers.Driver(networkKey)
|
||||
chainDriver, err := c.deps.Drivers.Driver(sourceWallet.Network)
|
||||
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"))
|
||||
}
|
||||
|
||||
|
||||
@@ -125,9 +125,9 @@ func (c *ensureGasTopUpCommand) Execute(ctx context.Context, req *chainv1.Ensure
|
||||
Destination: &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: targetWalletRef},
|
||||
},
|
||||
Amount: topUp,
|
||||
Metadata: shared.CloneMetadata(req.GetMetadata()),
|
||||
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
||||
Amount: topUp,
|
||||
Metadata: shared.CloneMetadata(req.GetMetadata()),
|
||||
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
networkKey := strings.ToLower(strings.TrimSpace(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)
|
||||
networkCfg, ok := deps.Networks.Network(walletModel.Network)
|
||||
if !ok {
|
||||
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),
|
||||
}
|
||||
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...)
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ func toProtoTransfer(transfer *model.Transfer) *chainv1.Transfer {
|
||||
SourceWalletRef: transfer.SourceWalletRef,
|
||||
Destination: destination,
|
||||
Asset: asset,
|
||||
RequestedAmount: shared.CloneMoney(transfer.RequestedAmount),
|
||||
NetAmount: shared.CloneMoney(transfer.NetAmount),
|
||||
RequestedAmount: shared.MonenyToProto(transfer.RequestedAmount),
|
||||
NetAmount: shared.MonenyToProto(transfer.NetAmount),
|
||||
Fees: protoFees,
|
||||
Status: shared.TransferStatusToProto(transfer.Status),
|
||||
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))
|
||||
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(networkKey)
|
||||
networkCfg, ok := c.deps.Networks.Network(sourceWallet.Network)
|
||||
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"))
|
||||
}
|
||||
|
||||
@@ -124,17 +123,19 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
||||
transfer := &model.Transfer{
|
||||
IdempotencyKey: idempotencyKey,
|
||||
TransferRef: shared.GenerateTransferRef(),
|
||||
IntentRef: req.IntentRef,
|
||||
OperationRef: req.OperationRef,
|
||||
OrganizationRef: organizationRef,
|
||||
SourceWalletRef: sourceWalletRef,
|
||||
Destination: destination,
|
||||
Network: sourceWallet.Network,
|
||||
TokenSymbol: effectiveTokenSymbol,
|
||||
ContractAddress: effectiveContractAddress,
|
||||
RequestedAmount: shared.CloneMoney(amount),
|
||||
NetAmount: netAmount,
|
||||
RequestedAmount: shared.ProtoToMoney(amount),
|
||||
NetAmount: shared.ProtoToMoney(netAmount),
|
||||
Fees: fees,
|
||||
Status: model.TransferStatusPending,
|
||||
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
||||
Status: model.TransferStatusCreated,
|
||||
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
|
||||
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"))
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
chainKey := shared.ChainKeyFromEnum(asset.GetChain())
|
||||
networkCfg, ok := c.deps.Networks.Network(chainKey)
|
||||
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"))
|
||||
}
|
||||
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"))
|
||||
}
|
||||
chainDriver, err := c.deps.Drivers.Driver(chainKey)
|
||||
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"))
|
||||
}
|
||||
|
||||
@@ -81,7 +77,7 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C
|
||||
if !strings.EqualFold(tokenSymbol, networkCfg.NativeToken) {
|
||||
contractAddress = shared.ResolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func (c *listManagedWalletsCommand) Execute(ctx context.Context, req *chainv1.Li
|
||||
filter.OwnerRefFilter = &ownerRef
|
||||
}
|
||||
if asset := req.GetAsset(); asset != nil {
|
||||
filter.Network, _ = shared.ChainKeyFromEnum(asset.GetChain())
|
||||
filter.Network = shared.ChainKeyFromEnum(asset.GetChain())
|
||||
filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol())
|
||||
}
|
||||
if page := req.GetPage(); page != nil {
|
||||
|
||||
@@ -3,7 +3,6 @@ package wallet
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||
"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")
|
||||
}
|
||||
|
||||
networkKey := strings.ToLower(strings.TrimSpace(wallet.Network))
|
||||
network, ok := deps.Networks.Network(networkKey)
|
||||
network, ok := deps.Networks.Network(wallet.Network)
|
||||
if !ok {
|
||||
logger.Warn("Requested network is not configured",
|
||||
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 {
|
||||
logger.Warn("Chain driver not configured",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", networkKey),
|
||||
zap.String("network", string(wallet.Network)),
|
||||
zap.Error(err),
|
||||
)
|
||||
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()),
|
||||
OrganizationRef: orgRef,
|
||||
SourceWalletRef: source,
|
||||
IntentRef: strings.TrimSpace(op.GetIntentRef()),
|
||||
OperationRef: op.GetOperationRef(),
|
||||
Destination: dest,
|
||||
Amount: amount,
|
||||
Fees: parseChainFees(reader),
|
||||
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||||
ClientReference: strings.TrimSpace(reader.String("client_reference")),
|
||||
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
|
||||
})
|
||||
if err != 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{
|
||||
Receipt: &connectorv1.OperationReceipt{
|
||||
OperationId: opID,
|
||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
||||
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
|
||||
Result: result,
|
||||
},
|
||||
}, nil
|
||||
@@ -238,7 +240,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
||||
return &connectorv1.SubmitOperationResponse{
|
||||
Receipt: &connectorv1.OperationReceipt{
|
||||
OperationId: opID,
|
||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
||||
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
|
||||
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), ""),
|
||||
},
|
||||
}, nil
|
||||
@@ -256,12 +258,14 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
||||
}
|
||||
resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
|
||||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||
IntentRef: strings.TrimSpace(op.GetIntentRef()),
|
||||
OperationRef: strings.TrimSpace(op.GetOperationRef()),
|
||||
OrganizationRef: orgRef,
|
||||
SourceWalletRef: source,
|
||||
TargetWalletRef: target,
|
||||
EstimatedTotalFee: fee,
|
||||
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||||
ClientReference: strings.TrimSpace(reader.String("client_reference")),
|
||||
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
|
||||
})
|
||||
if err != 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{
|
||||
Receipt: &connectorv1.OperationReceipt{
|
||||
OperationId: opID,
|
||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
||||
Status: shared.СhainTransferStatusToOperation(resp.GetTransfer().GetStatus()),
|
||||
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), transferRef),
|
||||
},
|
||||
}, nil
|
||||
@@ -544,25 +548,51 @@ func chainTransferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation
|
||||
|
||||
func chainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
||||
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:
|
||||
return connectorv1.OperationStatus_FAILED
|
||||
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return connectorv1.OperationStatus_CANCELED
|
||||
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||
|
||||
default:
|
||||
return connectorv1.OperationStatus_PENDING
|
||||
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func chainStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
||||
switch status {
|
||||
case connectorv1.OperationStatus_CONFIRMED:
|
||||
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
||||
case connectorv1.OperationStatus_FAILED:
|
||||
|
||||
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_CANCELED:
|
||||
|
||||
case connectorv1.OperationStatus_OPERATION_CANCELLED:
|
||||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||
|
||||
default:
|
||||
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) {
|
||||
d.logger.Debug("Balance request",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
)
|
||||
driverDeps := deps
|
||||
driverDeps.Logger = d.logger
|
||||
@@ -55,13 +55,13 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
|
||||
if err != nil {
|
||||
d.logger.Warn("Balance failed",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Balance result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("amount", result.Amount),
|
||||
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) {
|
||||
d.logger.Debug("Native balance request",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
)
|
||||
driverDeps := deps
|
||||
driverDeps.Logger = d.logger
|
||||
@@ -80,13 +80,13 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
|
||||
if err != nil {
|
||||
d.logger.Warn("Native balance failed",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Native balance result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("amount", result.Amount),
|
||||
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) {
|
||||
d.logger.Debug("Estimate fee request",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("destination", destination),
|
||||
)
|
||||
driverDeps := deps
|
||||
@@ -106,13 +106,13 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
|
||||
if err != nil {
|
||||
d.logger.Warn("Estimate fee failed",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Estimate fee result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("amount", result.Amount),
|
||||
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) {
|
||||
d.logger.Debug("Submit transfer request",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("destination", destination),
|
||||
)
|
||||
driverDeps := deps
|
||||
@@ -132,13 +132,13 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
|
||||
if err != nil {
|
||||
d.logger.Warn("Submit transfer failed",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
d.logger.Debug("Submit transfer result",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
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) {
|
||||
d.logger.Debug("Await confirmation",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
)
|
||||
driverDeps := deps
|
||||
driverDeps.Logger = d.logger
|
||||
@@ -156,13 +156,13 @@ func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, networ
|
||||
if err != nil {
|
||||
d.logger.Warn("Await confirmation failed",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if receipt != nil {
|
||||
d.logger.Debug("Await confirmation result",
|
||||
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("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) {
|
||||
d.logger.Debug("Balance request",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
)
|
||||
driverDeps := deps
|
||||
driverDeps.Logger = d.logger
|
||||
@@ -55,13 +55,13 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
|
||||
if err != nil {
|
||||
d.logger.Warn("Balance failed",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Balance result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("amount", result.Amount),
|
||||
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) {
|
||||
d.logger.Debug("Native balance request",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
)
|
||||
driverDeps := deps
|
||||
driverDeps.Logger = d.logger
|
||||
@@ -80,13 +80,13 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
|
||||
if err != nil {
|
||||
d.logger.Warn("Native balance failed",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Native balance result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("amount", result.Amount),
|
||||
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) {
|
||||
d.logger.Debug("Estimate fee request",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("destination", destination),
|
||||
)
|
||||
driverDeps := deps
|
||||
@@ -106,13 +106,13 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
|
||||
if err != nil {
|
||||
d.logger.Warn("Estimate fee failed",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if result != nil {
|
||||
d.logger.Debug("Estimate fee result",
|
||||
zap.String("wallet_ref", wallet.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("amount", result.Amount),
|
||||
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) {
|
||||
d.logger.Debug("Submit transfer request",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("destination", destination),
|
||||
)
|
||||
driverDeps := deps
|
||||
@@ -132,13 +132,13 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
|
||||
if err != nil {
|
||||
d.logger.Warn("Submit transfer failed",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
d.logger.Debug("Submit transfer result",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
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) {
|
||||
d.logger.Debug("Await confirmation",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
)
|
||||
driverDeps := deps
|
||||
driverDeps.Logger = d.logger
|
||||
@@ -156,13 +156,13 @@ func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, networ
|
||||
if err != nil {
|
||||
d.logger.Warn("Await confirmation failed",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else if receipt != nil {
|
||||
d.logger.Debug("Await confirmation result",
|
||||
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("status", receipt.Status),
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ func NormalizeAddress(address string) (string, error) {
|
||||
func nativeCurrency(network shared.Network) string {
|
||||
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
||||
if currency == "" {
|
||||
currency = strings.ToUpper(network.Name)
|
||||
currency = strings.ToUpper(string(network.Name))
|
||||
}
|
||||
return currency
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func Balance(ctx context.Context, deps driver.Deps, network shared.Network, wall
|
||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||
logFields := []zap.Field{
|
||||
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("contract", strings.ToLower(strings.TrimSpace(wallet.ContractAddress))),
|
||||
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)
|
||||
logFields := []zap.Field{
|
||||
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),
|
||||
}
|
||||
if rpcURL == "" {
|
||||
@@ -260,12 +260,12 @@ func EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network,
|
||||
|
||||
client, err := registry.Client(network.Name)
|
||||
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
|
||||
}
|
||||
rpcClient, err := registry.RPCClient(network.Name)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
|
||||
}
|
||||
rpcURL := strings.TrimSpace(network.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")
|
||||
}
|
||||
if source == nil || transfer == nil {
|
||||
@@ -397,18 +397,18 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
|
||||
logger.Info("Submitting transfer",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("source_wallet_ref", source.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("destination", strings.ToLower(destination)),
|
||||
)
|
||||
|
||||
client, err := registry.Client(network.Name)
|
||||
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
|
||||
}
|
||||
rpcClient, err := registry.RPCClient(network.Name)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
|
||||
gasPrice, err := client.SuggestGasPrice(ctx)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
@@ -532,7 +532,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
|
||||
|
||||
txHash := signedTx.Hash().Hex()
|
||||
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
|
||||
@@ -544,7 +544,7 @@ func AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Net
|
||||
registry := deps.Registry
|
||||
|
||||
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")
|
||||
}
|
||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||
@@ -572,23 +572,23 @@ func AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Net
|
||||
select {
|
||||
case <-ticker.C:
|
||||
logger.Debug("Transaction not yet mined", zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
)
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
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()
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
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()),
|
||||
)
|
||||
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) {
|
||||
if isTronNetwork(network) {
|
||||
if rpcClient == nil {
|
||||
return 0, merrors.Internal("rpc client not initialised")
|
||||
}
|
||||
return estimateGasTron(ctx, rpcClient, callMsg)
|
||||
}
|
||||
return client.EstimateGas(ctx, callMsg)
|
||||
}
|
||||
|
||||
@@ -702,10 +696,6 @@ func tronEstimateCall(callMsg ethereum.CallMsg) map[string]string {
|
||||
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) {
|
||||
value, err := decimal.NewFromString(strings.TrimSpace(amount))
|
||||
if err != nil {
|
||||
|
||||
@@ -29,9 +29,6 @@ func ComputeGasTopUp(network shared.Network, wallet *model.ManagedWallet, estima
|
||||
}
|
||||
|
||||
nativeCurrency := strings.TrimSpace(network.NativeToken)
|
||||
if nativeCurrency == "" {
|
||||
nativeCurrency = strings.ToUpper(strings.TrimSpace(network.Name))
|
||||
}
|
||||
if !strings.EqualFold(nativeCurrency, estimatedFee.GetCurrency()) {
|
||||
return nil, false, merrors.InvalidArgument(fmt.Sprintf("estimated fee currency mismatch (expected %s)", nativeCurrency))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package drivers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||
"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/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Registry maps configured network keys to chain drivers.
|
||||
type Registry struct {
|
||||
byNetwork map[string]driver.Driver
|
||||
byNetwork map[pmodel.ChainNetwork]driver.Driver
|
||||
}
|
||||
|
||||
// NewRegistry selects drivers for the configured networks.
|
||||
@@ -23,18 +23,14 @@ func NewRegistry(logger mlogger.Logger, networks []shared.Network) (*Registry, e
|
||||
if logger == nil {
|
||||
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 {
|
||||
name := strings.ToLower(strings.TrimSpace(network.Name))
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
chainDriver, err := resolveDriver(logger, name)
|
||||
chainDriver, err := resolveDriver(logger, network.Name)
|
||||
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
|
||||
}
|
||||
result.byNetwork[name] = chainDriver
|
||||
result.byNetwork[network.Name] = chainDriver
|
||||
}
|
||||
if len(result.byNetwork) == 0 {
|
||||
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.
|
||||
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 {
|
||||
return nil, merrors.Internal("driver registry is not configured")
|
||||
}
|
||||
key := strings.ToLower(strings.TrimSpace(network))
|
||||
if key == "" {
|
||||
return nil, merrors.InvalidArgument("network is required")
|
||||
}
|
||||
chainDriver, ok := r.byNetwork[key]
|
||||
chainDriver, ok := r.byNetwork[network]
|
||||
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
|
||||
}
|
||||
|
||||
func resolveDriver(logger mlogger.Logger, network string) (driver.Driver, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(network, "tron"):
|
||||
return nil, merrors.InvalidArgument("tron networks must use the tron gateway, not chain gateway")
|
||||
case strings.HasPrefix(network, "arbitrum"):
|
||||
func resolveDriver(logger mlogger.Logger, network pmodel.ChainNetwork) (driver.Driver, error) {
|
||||
switch network {
|
||||
case pmodel.ChainNetworkArbitrumOne:
|
||||
case pmodel.ChainNetworkArbitrumSepolia:
|
||||
return arbitrum.New(logger), nil
|
||||
case strings.HasPrefix(network, "ethereum"):
|
||||
case pmodel.ChainNetworkEthereumMainnet:
|
||||
return ethereum.New(logger), nil
|
||||
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)
|
||||
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")
|
||||
}
|
||||
if source == nil || transfer == nil {
|
||||
@@ -75,19 +75,19 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
|
||||
o.logger.Info("Submitting transfer",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("source_wallet_ref", source.WalletRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.String("destination", strings.ToLower(destinationAddress)),
|
||||
)
|
||||
|
||||
client, err := o.clients.Client(network.Name)
|
||||
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
|
||||
}
|
||||
rpcClient, err := o.clients.RPCClient(network.Name)
|
||||
if err != nil {
|
||||
o.logger.Warn("Failed to initialise RPC client",
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
return "", err
|
||||
@@ -112,7 +112,7 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
|
||||
if err != nil {
|
||||
o.logger.Warn("Failed to suggest gas price",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(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",
|
||||
zap.String("transfer_ref", transfer.TransferRef),
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
)
|
||||
|
||||
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) {
|
||||
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")
|
||||
}
|
||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||
@@ -240,27 +240,27 @@ func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network shared.
|
||||
case <-ticker.C:
|
||||
o.logger.Debug("Transaction not yet mined",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
)
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
o.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()
|
||||
}
|
||||
}
|
||||
o.logger.Warn("Failed to fetch transaction receipt",
|
||||
zap.String("tx_hash", txHash),
|
||||
zap.String("network", network.Name),
|
||||
zap.String("network", string(network.Name)),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, executorInternal("failed to fetch transaction receipt", err)
|
||||
}
|
||||
o.logger.Info("Transaction confirmed",
|
||||
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("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/shared"
|
||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
// Option configures the Service.
|
||||
@@ -34,7 +35,7 @@ func WithNetworks(networks []shared.Network) Option {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
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].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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,14 @@ import (
|
||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Clients holds pre-initialised RPC clients keyed by network name.
|
||||
type Clients struct {
|
||||
logger mlogger.Logger
|
||||
clients map[string]clientEntry
|
||||
clients map[pmodel.ChainNetwork]clientEntry
|
||||
}
|
||||
|
||||
type clientEntry struct {
|
||||
@@ -36,25 +37,20 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
|
||||
clientLogger := logger.Named("rpc_client")
|
||||
result := &Clients{
|
||||
logger: clientLogger,
|
||||
clients: make(map[string]clientEntry),
|
||||
clients: make(map[pmodel.ChainNetwork]clientEntry),
|
||||
}
|
||||
|
||||
for _, network := range networks {
|
||||
name := strings.ToLower(strings.TrimSpace(network.Name))
|
||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||
if name == "" {
|
||||
clientLogger.Warn("Skipping network with empty name during rpc client preparation")
|
||||
continue
|
||||
}
|
||||
if rpcURL == "" {
|
||||
result.Close()
|
||||
err := merrors.InvalidArgument(fmt.Sprintf("rpc url not configured for network %s", name))
|
||||
clientLogger.Warn("Rpc url missing", zap.String("network", name))
|
||||
err := merrors.InvalidArgument(fmt.Sprintf("rpc url not configured for network %s", network.Name))
|
||||
clientLogger.Warn("Rpc url missing", zap.String("network", string(network.Name)))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fields := []zap.Field{
|
||||
zap.String("network", name),
|
||||
zap.String("network", string(network.Name)),
|
||||
}
|
||||
clientLogger.Info("Initialising rpc client", fields...)
|
||||
|
||||
@@ -62,7 +58,7 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
|
||||
httpClient := &http.Client{
|
||||
Transport: &loggingRoundTripper{
|
||||
logger: clientLogger,
|
||||
network: name,
|
||||
network: network.Name,
|
||||
endpoint: rpcURL,
|
||||
base: http.DefaultTransport,
|
||||
},
|
||||
@@ -72,10 +68,10 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
|
||||
if err != nil {
|
||||
result.Close()
|
||||
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)
|
||||
result.clients[name] = clientEntry{
|
||||
result.clients[network.Name] = clientEntry{
|
||||
eth: client,
|
||||
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.
|
||||
func (c *Clients) Client(network string) (*ethclient.Client, error) {
|
||||
func (c *Clients) Client(network pmodel.ChainNetwork) (*ethclient.Client, error) {
|
||||
if c == nil {
|
||||
return nil, merrors.Internal("RPC clients not initialised")
|
||||
}
|
||||
name := strings.ToLower(strings.TrimSpace(network))
|
||||
entry, ok := c.clients[name]
|
||||
entry, ok := c.clients[network]
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, merrors.Internal("rpc clients not initialised")
|
||||
}
|
||||
name := strings.ToLower(strings.TrimSpace(network))
|
||||
entry, ok := c.clients[name]
|
||||
entry, ok := c.clients[network]
|
||||
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
|
||||
}
|
||||
@@ -130,14 +124,14 @@ func (c *Clients) Close() {
|
||||
entry.eth.Close()
|
||||
}
|
||||
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 {
|
||||
logger mlogger.Logger
|
||||
network string
|
||||
network pmodel.ChainNetwork
|
||||
endpoint string
|
||||
base http.RoundTripper
|
||||
}
|
||||
@@ -155,7 +149,7 @@ func (l *loggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
|
||||
}
|
||||
|
||||
fields := []zap.Field{
|
||||
zap.String("network", l.network),
|
||||
zap.String("network", string(l.network)),
|
||||
}
|
||||
if len(reqBody) > 0 {
|
||||
fields = append(fields, zap.String("rpc_request", truncate(string(reqBody), 2048)))
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
package rpcclient
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
// Registry binds static network metadata with prepared RPC clients.
|
||||
type Registry struct {
|
||||
networks map[string]shared.Network
|
||||
networks map[pmodel.ChainNetwork]shared.Network
|
||||
clients *Clients
|
||||
}
|
||||
|
||||
// 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{
|
||||
networks: networks,
|
||||
clients: clients,
|
||||
@@ -24,31 +23,31 @@ func NewRegistry(networks map[string]shared.Network, clients *Clients) *Registry
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return shared.Network{}, false
|
||||
}
|
||||
n, ok := r.networks[strings.ToLower(strings.TrimSpace(key))]
|
||||
n, ok := r.networks[key]
|
||||
return n, ok
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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.
|
||||
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 {
|
||||
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.
|
||||
func (r *Registry) Networks() map[string]shared.Network {
|
||||
func (r *Registry) Networks() map[pmodel.ChainNetwork]shared.Network {
|
||||
return r.networks
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
@@ -43,7 +44,7 @@ type Service struct {
|
||||
|
||||
settings CacheSettings
|
||||
|
||||
networks map[string]shared.Network
|
||||
networks map[pmodel.ChainNetwork]shared.Network
|
||||
serviceWallet shared.ServiceWallet
|
||||
keyManager keymanager.Manager
|
||||
rpcClients *rpcclient.Clients
|
||||
@@ -64,7 +65,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
||||
producer: producer,
|
||||
clock: clockpkg.System{},
|
||||
settings: defaultSettings(),
|
||||
networks: map[string]shared.Network{},
|
||||
networks: map[pmodel.ChainNetwork]shared.Network{},
|
||||
}
|
||||
|
||||
initMetrics()
|
||||
@@ -79,7 +80,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
||||
svc.clock = clockpkg.System{}
|
||||
}
|
||||
if svc.networks == nil {
|
||||
svc.networks = map[string]shared.Network{}
|
||||
svc.networks = map[pmodel.ChainNetwork]shared.Network{}
|
||||
}
|
||||
svc.settings = svc.settings.withDefaults()
|
||||
svc.networkRegistry = rpcclient.NewRegistry(svc.networks, svc.rpcClients)
|
||||
@@ -207,7 +208,7 @@ func (s *Service) startDiscoveryAnnouncers() {
|
||||
announce := discovery.Announcement{
|
||||
Service: "CRYPTO_RAIL_GATEWAY",
|
||||
Rail: "CRYPTO",
|
||||
Network: network.Name,
|
||||
Network: string(network.Name),
|
||||
Operations: []string{"balance.read", "payin.crypto", "payout.crypto", "fee.send", "observe.confirm"},
|
||||
Currencies: currencies,
|
||||
InvokeURI: s.invokeURI,
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
ichainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
@@ -170,6 +171,7 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
||||
Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"},
|
||||
},
|
||||
},
|
||||
IntentRef: "intent-1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, transferResp.GetTransfer())
|
||||
@@ -177,7 +179,7 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
||||
|
||||
stored := repo.transfers.get(transferResp.GetTransfer().GetTransferRef())
|
||||
require.NotNil(t, stored)
|
||||
require.Equal(t, model.TransferStatusPending, stored.Status)
|
||||
require.Equal(t, model.TransferStatusCreated, stored.Status)
|
||||
|
||||
// GetTransfer
|
||||
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
|
||||
}
|
||||
}
|
||||
if filter.Network != "" && !strings.EqualFold(wallet.Network, filter.Network) {
|
||||
if wallet.Network != filter.Network {
|
||||
continue
|
||||
}
|
||||
if filter.TokenSymbol != "" && !strings.EqualFold(wallet.TokenSymbol, filter.TokenSymbol) {
|
||||
@@ -644,9 +646,9 @@ func newTestService(t *testing.T) (*Service, *inMemoryRepository) {
|
||||
|
||||
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{
|
||||
KeyID: fmt.Sprintf("%s/%s", strings.ToLower(network), walletRef),
|
||||
KeyID: fmt.Sprintf("%s/%s", network, walletRef),
|
||||
Address: "0x" + strings.Repeat("a", 40),
|
||||
PublicKey: strings.Repeat("b", 128),
|
||||
}, nil
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
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.
|
||||
type GasTopUpRule struct {
|
||||
@@ -30,3 +34,20 @@ func (p *GasTopUpPolicy) Rule(contractTransfer bool) (GasTopUpRule, bool) {
|
||||
}
|
||||
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"
|
||||
|
||||
"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"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
@@ -49,16 +51,38 @@ func GenerateTransferRef() string {
|
||||
return bson.NewObjectID().Hex()
|
||||
}
|
||||
|
||||
func ChainKeyFromEnum(chain chainv1.ChainNetwork) (string, chainv1.ChainNetwork) {
|
||||
if name, ok := chainv1.ChainNetwork_name[int32(chain)]; ok {
|
||||
key := strings.ToLower(strings.TrimPrefix(name, "CHAIN_NETWORK_"))
|
||||
return key, chain
|
||||
func ChainKeyFromEnum(chain chainv1.ChainNetwork) pmodel.ChainNetwork {
|
||||
switch chain {
|
||||
case chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE:
|
||||
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 {
|
||||
return chainasset.NetworkFromString(name)
|
||||
func ChainEnumFromName(name pmodel.ChainNetwork) chainv1.ChainNetwork {
|
||||
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 {
|
||||
@@ -76,54 +100,94 @@ func ManagedWalletStatusToProto(status model.ManagedWalletStatus) chainv1.Manage
|
||||
|
||||
func TransferStatusToModel(status chainv1.TransferStatus) model.TransferStatus {
|
||||
switch status {
|
||||
case chainv1.TransferStatus_TRANSFER_PENDING:
|
||||
return model.TransferStatusPending
|
||||
case chainv1.TransferStatus_TRANSFER_SIGNING:
|
||||
return model.TransferStatusSigning
|
||||
case chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
||||
return model.TransferStatusSubmitted
|
||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
||||
return model.TransferStatusConfirmed
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||
return model.TransferStatusCreated
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||
return model.TransferStatusProcessing
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||
return model.TransferStatusWaiting
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||
return model.TransferStatusSuccess
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||
return model.TransferStatusFailed
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return model.TransferStatusCancelled
|
||||
|
||||
default:
|
||||
return ""
|
||||
return model.TransferStatus("")
|
||||
}
|
||||
}
|
||||
|
||||
func TransferStatusToProto(status model.TransferStatus) chainv1.TransferStatus {
|
||||
switch status {
|
||||
case model.TransferStatusPending:
|
||||
return chainv1.TransferStatus_TRANSFER_PENDING
|
||||
case model.TransferStatusSigning:
|
||||
return chainv1.TransferStatus_TRANSFER_SIGNING
|
||||
case model.TransferStatusSubmitted:
|
||||
return chainv1.TransferStatus_TRANSFER_SUBMITTED
|
||||
case model.TransferStatusConfirmed:
|
||||
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
||||
|
||||
case model.TransferStatusCreated:
|
||||
return chainv1.TransferStatus_TRANSFER_CREATED
|
||||
|
||||
case model.TransferStatusProcessing:
|
||||
return chainv1.TransferStatus_TRANSFER_PROCESSING
|
||||
|
||||
case model.TransferStatusWaiting:
|
||||
return chainv1.TransferStatus_TRANSFER_WAITING
|
||||
|
||||
case model.TransferStatusSuccess:
|
||||
return chainv1.TransferStatus_TRANSFER_SUCCESS
|
||||
|
||||
case model.TransferStatusFailed:
|
||||
return chainv1.TransferStatus_TRANSFER_FAILED
|
||||
|
||||
case model.TransferStatusCancelled:
|
||||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||
|
||||
default:
|
||||
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.
|
||||
func NativeCurrency(network Network) string {
|
||||
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
||||
if currency == "" {
|
||||
currency = strings.ToUpper(strings.TrimSpace(network.Name))
|
||||
currency = strings.ToUpper(network.Name.String())
|
||||
}
|
||||
return currency
|
||||
}
|
||||
|
||||
// Network describes a supported blockchain network and known token contracts.
|
||||
type Network struct {
|
||||
Name string
|
||||
Name pmodel.ChainNetwork
|
||||
RPCURL string
|
||||
ChainID uint64
|
||||
NativeToken string
|
||||
@@ -139,7 +203,27 @@ type TokenContract struct {
|
||||
|
||||
// ServiceWallet captures the managed service wallet configuration.
|
||||
type ServiceWallet struct {
|
||||
Network string
|
||||
Network pmodel.ChainNetwork
|
||||
Address 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/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -40,35 +41,35 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
||||
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))
|
||||
}
|
||||
|
||||
driverDeps := s.driverDeps()
|
||||
chainDriver, err := s.driverForNetwork(network.Name)
|
||||
if err != nil {
|
||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
return err
|
||||
}
|
||||
|
||||
destinationAddress, err := s.destinationAddress(ctx, chainDriver, transfer.Destination)
|
||||
if err != nil {
|
||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
return err
|
||||
}
|
||||
|
||||
sourceAddress, err := chainDriver.NormalizeAddress(sourceWallet.DepositAddress)
|
||||
if err != nil {
|
||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
return err
|
||||
}
|
||||
if chainDriver.Name() == "tron" && sourceAddress == destinationAddress {
|
||||
s.logger.Info("Self transfer detected; skipping submission",
|
||||
zap.String("transfer_ref", transferRef),
|
||||
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))
|
||||
}
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -94,15 +98,15 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
||||
return err
|
||||
}
|
||||
|
||||
if receipt != nil && receipt.Status == types.ReceiptStatusSuccessful {
|
||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", txHash); err != nil {
|
||||
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
}
|
||||
return nil
|
||||
failureReason := ""
|
||||
pStatus := model.TransferStatusSuccess
|
||||
if receipt != nil && receipt.Status != types.ReceiptStatusSuccessful {
|
||||
failureReason = "transaction reverted"
|
||||
pStatus = model.TransferStatusFailed
|
||||
}
|
||||
|
||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, "transaction reverted", txHash); err != nil {
|
||||
s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
if _, err := s.updateTransferStatus(ctx, transferRef, pStatus, failureReason, txHash); err != nil {
|
||||
s.logger.Warn("Failed to update transfer status", zap.Error(err),
|
||||
zap.String("transfer_ref", transferRef), zap.String("status", string(pStatus)))
|
||||
}
|
||||
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 {
|
||||
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"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
paytypes "github.com/tech/sendico/pkg/payments/types"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
)
|
||||
|
||||
type TransferStatus string
|
||||
|
||||
const (
|
||||
TransferStatusPending TransferStatus = "pending"
|
||||
TransferStatusSigning TransferStatus = "signing"
|
||||
TransferStatusSubmitted TransferStatus = "submitted"
|
||||
TransferStatusConfirmed TransferStatus = "confirmed"
|
||||
TransferStatusFailed TransferStatus = "failed"
|
||||
TransferStatusCancelled TransferStatus = "cancelled"
|
||||
TransferStatusCreated TransferStatus = "created" // record exists, not started
|
||||
TransferStatusProcessing TransferStatus = "processing" // we are working on it
|
||||
TransferStatusWaiting TransferStatus = "waiting" // waiting external world
|
||||
|
||||
TransferStatusSuccess TransferStatus = "success" // final success
|
||||
TransferStatusFailed TransferStatus = "failed" // final failure
|
||||
TransferStatusCancelled TransferStatus = "cancelled" // final cancelled
|
||||
)
|
||||
|
||||
// ServiceFee represents a fee component applied to a transfer.
|
||||
@@ -38,21 +41,23 @@ type TransferDestination struct {
|
||||
type Transfer struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
|
||||
OperationRef string `bson:"operationRef" json:"operationRef"`
|
||||
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"`
|
||||
SourceWalletRef string `bson:"sourceWalletRef" json:"sourceWalletRef"`
|
||||
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"`
|
||||
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
|
||||
RequestedAmount *moneyv1.Money `bson:"requestedAmount" json:"requestedAmount"`
|
||||
NetAmount *moneyv1.Money `bson:"netAmount" json:"netAmount"`
|
||||
RequestedAmount *paytypes.Money `bson:"requestedAmount" json:"requestedAmount"`
|
||||
NetAmount *paytypes.Money `bson:"netAmount" json:"netAmount"`
|
||||
Fees []ServiceFee `bson:"fees,omitempty" json:"fees,omitempty"`
|
||||
Status TransferStatus `bson:"status" json:"status"`
|
||||
TxHash string `bson:"txHash,omitempty" json:"txHash,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"`
|
||||
}
|
||||
|
||||
@@ -82,12 +87,11 @@ func (t *Transfer) Normalize() {
|
||||
t.IdempotencyKey = strings.TrimSpace(t.IdempotencyKey)
|
||||
t.OrganizationRef = strings.TrimSpace(t.OrganizationRef)
|
||||
t.SourceWalletRef = strings.TrimSpace(t.SourceWalletRef)
|
||||
t.Network = strings.TrimSpace(strings.ToLower(t.Network))
|
||||
t.TokenSymbol = strings.TrimSpace(strings.ToUpper(t.TokenSymbol))
|
||||
t.ContractAddress = strings.TrimSpace(strings.ToLower(t.ContractAddress))
|
||||
t.Destination.ManagedWalletRef = strings.TrimSpace(t.Destination.ManagedWalletRef)
|
||||
t.Destination.ExternalAddress = normalizeWalletAddress(t.Destination.ExternalAddress)
|
||||
t.Destination.ExternalAddressOriginal = strings.TrimSpace(t.Destination.ExternalAddressOriginal)
|
||||
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"
|
||||
pkgmodel "github.com/tech/sendico/pkg/model"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
)
|
||||
@@ -23,17 +24,17 @@ type ManagedWallet struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
pkgmodel.Describable `bson:",inline" json:",inline"`
|
||||
|
||||
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
|
||||
WalletRef string `bson:"walletRef" json:"walletRef"`
|
||||
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
|
||||
OwnerRef string `bson:"ownerRef" json:"ownerRef"`
|
||||
Network string `bson:"network" json:"network"`
|
||||
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
|
||||
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
|
||||
DepositAddress string `bson:"depositAddress" json:"depositAddress"`
|
||||
KeyReference string `bson:"keyReference,omitempty" json:"keyReference,omitempty"`
|
||||
Status ManagedWalletStatus `bson:"status" json:"status"`
|
||||
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
|
||||
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
|
||||
WalletRef string `bson:"walletRef" json:"walletRef"`
|
||||
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
|
||||
OwnerRef string `bson:"ownerRef" json:"ownerRef"`
|
||||
Network pkgmodel.ChainNetwork `bson:"network" json:"network"`
|
||||
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
|
||||
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
|
||||
DepositAddress string `bson:"depositAddress" json:"depositAddress"`
|
||||
KeyReference string `bson:"keyReference,omitempty" json:"keyReference,omitempty"`
|
||||
Status ManagedWalletStatus `bson:"status" json:"status"`
|
||||
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// 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 a value: filter for wallets where owner_ref matches
|
||||
OwnerRefFilter *string
|
||||
Network string
|
||||
Network pmodel.ChainNetwork
|
||||
TokenSymbol string
|
||||
Cursor string
|
||||
Limit int32
|
||||
@@ -93,7 +94,6 @@ func (m *ManagedWallet) Normalize() {
|
||||
m.Description = &desc
|
||||
}
|
||||
}
|
||||
m.Network = strings.TrimSpace(strings.ToLower(m.Network))
|
||||
m.TokenSymbol = strings.TrimSpace(strings.ToUpper(m.TokenSymbol))
|
||||
m.ContractAddress = strings.TrimSpace(strings.ToLower(m.ContractAddress))
|
||||
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")
|
||||
}
|
||||
if transfer.Status == "" {
|
||||
transfer.Status = model.TransferStatusPending
|
||||
transfer.Status = model.TransferStatusCreated
|
||||
}
|
||||
if transfer.LastStatusAt.IsZero() {
|
||||
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))
|
||||
}
|
||||
if wallet.Network != "" {
|
||||
fields = append(fields, zap.String("network", wallet.Network))
|
||||
fields = append(fields, zap.String("network", string(wallet.Network)))
|
||||
}
|
||||
if 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)
|
||||
fields = append(fields, zap.String("owner_ref_filter", ownerRef))
|
||||
}
|
||||
if network := strings.TrimSpace(filter.Network); network != "" {
|
||||
normalized := strings.ToLower(network)
|
||||
query = query.Filter(repository.Field("network"), normalized)
|
||||
fields = append(fields, zap.String("network", normalized))
|
||||
}
|
||||
fields = append(fields, zap.String("network", string(filter.Network)))
|
||||
if token := strings.TrimSpace(filter.TokenSymbol); token != "" {
|
||||
normalized := strings.ToUpper(token)
|
||||
query = query.Filter(repository.Field("tokenSymbol"), normalized)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"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"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/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 {
|
||||
return
|
||||
}
|
||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyFromRole]); raw != "" {
|
||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
||||
op.FromRole = pmodel.ToProto(role)
|
||||
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyFromRole]); raw != "" {
|
||||
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||
op.FromRole = account_role.ToProto(role)
|
||||
}
|
||||
}
|
||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyToRole]); raw != "" {
|
||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
||||
op.ToRole = pmodel.ToProto(role)
|
||||
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyToRole]); raw != "" {
|
||||
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||
op.ToRole = account_role.ToProto(role)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,12 +298,22 @@ func minorFromMoney(m *moneyv1.Money) int64 {
|
||||
|
||||
func payoutStatusFromOperation(status connectorv1.OperationStatus) mntxv1.PayoutStatus {
|
||||
switch status {
|
||||
case connectorv1.OperationStatus_CONFIRMED:
|
||||
return mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED
|
||||
case connectorv1.OperationStatus_FAILED:
|
||||
|
||||
case connectorv1.OperationStatus_OPERATION_CREATED:
|
||||
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
|
||||
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:
|
||||
return mntxv1.PayoutStatus_PAYOUT_STATUS_UNSPECIFIED
|
||||
}
|
||||
|
||||
@@ -11,6 +11,17 @@ grpc:
|
||||
metrics:
|
||||
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:
|
||||
driver: NATS
|
||||
settings:
|
||||
|
||||
@@ -11,6 +11,17 @@ grpc:
|
||||
metrics:
|
||||
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:
|
||||
driver: NATS
|
||||
settings:
|
||||
|
||||
@@ -9,6 +9,7 @@ require (
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/shopspring/decimal v1.4.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
|
||||
google.golang.org/grpc v1.78.0
|
||||
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/stringprep v1.0.4 // 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.yaml.in/yaml/v2 v2.4.3 // 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/sys v0.40.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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
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 h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||
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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -15,7 +15,10 @@ import (
|
||||
"github.com/tech/sendico/gateway/mntx/internal/appversion"
|
||||
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/storage"
|
||||
gatewaymongo "github.com/tech/sendico/gateway/mntx/storage/mongo"
|
||||
"github.com/tech/sendico/pkg/api/routers"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
@@ -31,7 +34,7 @@ type Imp struct {
|
||||
debug bool
|
||||
|
||||
config *config
|
||||
app *grpcapp.App[struct{}]
|
||||
app *grpcapp.App[storage.Repository]
|
||||
http *http.Server
|
||||
service *mntxservice.Service
|
||||
}
|
||||
@@ -183,7 +186,7 @@ func (i *Imp) Start() error {
|
||||
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 := ""
|
||||
if cfg.GRPC != nil {
|
||||
invokeURI = cfg.GRPC.DiscoveryInvokeURI()
|
||||
@@ -194,6 +197,7 @@ func (i *Imp) Start() error {
|
||||
mntxservice.WithMonetixConfig(monetixCfg),
|
||||
mntxservice.WithGatewayDescriptor(gatewayDescriptor),
|
||||
mntxservice.WithHTTPClient(&http.Client{Timeout: monetixCfg.Timeout()}),
|
||||
mntxservice.WithStorage(repo),
|
||||
)
|
||||
i.service = svc
|
||||
|
||||
@@ -204,7 +208,11 @@ func (i *Imp) Start() error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -92,10 +92,10 @@ func mapCallbackToState(clock clockpkg.Clock, cfg monetix.Config, cb monetixCall
|
||||
internalStatus := mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||
|
||||
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
|
||||
} else if status == cfg.ProcessingStatus() || opStatus == cfg.ProcessingStatus() {
|
||||
internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
||||
internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING
|
||||
outcome = monetix.OutcomeProcessing
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestMapCallbackToState_StatusMapping(t *testing.T) {
|
||||
paymentStatus: "success",
|
||||
operationStatus: "success",
|
||||
code: "0",
|
||||
expectedStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED,
|
||||
expectedStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS,
|
||||
expectedOutcome: monetix.OutcomeSuccess,
|
||||
},
|
||||
{
|
||||
@@ -59,7 +59,7 @@ func TestMapCallbackToState_StatusMapping(t *testing.T) {
|
||||
paymentStatus: "processing",
|
||||
operationStatus: "success",
|
||||
code: "",
|
||||
expectedStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
|
||||
expectedStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING,
|
||||
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"
|
||||
|
||||
"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"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
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/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"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type cardPayoutProcessor struct {
|
||||
logger mlogger.Logger
|
||||
config monetix.Config
|
||||
clock clockpkg.Clock
|
||||
store *cardPayoutStore
|
||||
store storage.Repository
|
||||
httpClient *http.Client
|
||||
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{
|
||||
logger: logger.Named("card_payout_processor"),
|
||||
config: cfg,
|
||||
@@ -46,18 +49,23 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
||||
if p == nil {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
|
||||
req = sanitizeCardPayoutRequest(req)
|
||||
|
||||
p.logger.Info("Submitting card payout",
|
||||
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||
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) == "" {
|
||||
p.logger.Warn("Monetix configuration is incomplete for payout submission")
|
||||
return nil, merrors.Internal("monetix configuration is incomplete")
|
||||
}
|
||||
|
||||
req = sanitizeCardPayoutRequest(req)
|
||||
if err := validateCardPayoutRequest(req, p.config); err != nil {
|
||||
p.logger.Warn("Card payout validation failed",
|
||||
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")
|
||||
}
|
||||
|
||||
now := timestamppb.New(p.clock.Now())
|
||||
state := &mntxv1.CardPayoutState{
|
||||
PayoutId: req.GetPayoutId(),
|
||||
ProjectId: projectID,
|
||||
CustomerId: req.GetCustomerId(),
|
||||
AmountMinor: req.GetAmountMinor(),
|
||||
Currency: strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
|
||||
Status: mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
now := p.clock.Now()
|
||||
|
||||
state := &model.CardPayout{
|
||||
PayoutID: strings.TrimSpace(req.GetPayoutId()),
|
||||
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
ProjectID: projectID,
|
||||
CustomerID: strings.TrimSpace(req.GetCustomerId()),
|
||||
AmountMinor: req.GetAmountMinor(),
|
||||
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 {
|
||||
if existing.GetCreatedAt() != nil {
|
||||
state.CreatedAt = existing.GetCreatedAt()
|
||||
// Keep CreatedAt/refs if record already exists.
|
||||
if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PayoutID); err == nil && existing != nil {
|
||||
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)
|
||||
apiReq := buildCardPayoutRequest(projectID, req)
|
||||
|
||||
result, err := client.CreateCardPayout(ctx, apiReq)
|
||||
if err != nil {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||
state.Status = model.PayoutStatusFailed
|
||||
state.ProviderMessage = err.Error()
|
||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
||||
p.store.Save(state)
|
||||
state.UpdatedAt = p.clock.Now()
|
||||
|
||||
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",
|
||||
zap.String("payout_id", req.GetPayoutId()),
|
||||
zap.String("customer_id", req.GetCustomerId()),
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
||||
state.Status = model.PayoutStatusWaiting
|
||||
} else {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||
state.ProviderCode = result.ErrorCode
|
||||
state.ProviderMessage = result.ErrorMessage
|
||||
state.Status = model.PayoutStatusFailed
|
||||
state.ProviderCode = strings.TrimSpace(result.ErrorCode)
|
||||
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{
|
||||
Payout: state,
|
||||
Payout: StateToProto(state),
|
||||
Accepted: result.Accepted,
|
||||
ProviderRequestId: result.ProviderRequestID,
|
||||
ErrorCode: result.ErrorCode,
|
||||
@@ -130,8 +173,8 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
||||
}
|
||||
|
||||
p.logger.Info("Card payout submission stored",
|
||||
zap.String("payout_id", state.GetPayoutId()),
|
||||
zap.String("status", state.GetStatus().String()),
|
||||
zap.String("payout_id", state.PayoutID),
|
||||
zap.String("status", string(state.Status)),
|
||||
zap.Bool("accepted", result.Accepted),
|
||||
zap.String("provider_request_id", result.ProviderRequestID),
|
||||
)
|
||||
@@ -143,18 +186,23 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
||||
if p == nil {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
|
||||
req = sanitizeCardTokenPayoutRequest(req)
|
||||
|
||||
p.logger.Info("Submitting card token payout",
|
||||
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||
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) == "" {
|
||||
p.logger.Warn("Monetix configuration is incomplete for token payout submission")
|
||||
return nil, merrors.Internal("monetix configuration is incomplete")
|
||||
}
|
||||
|
||||
req = sanitizeCardTokenPayoutRequest(req)
|
||||
if err := validateCardTokenPayoutRequest(req, p.config); err != nil {
|
||||
p.logger.Warn("Card token payout validation failed",
|
||||
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")
|
||||
}
|
||||
|
||||
now := timestamppb.New(p.clock.Now())
|
||||
state := &mntxv1.CardPayoutState{
|
||||
PayoutId: req.GetPayoutId(),
|
||||
ProjectId: projectID,
|
||||
CustomerId: req.GetCustomerId(),
|
||||
AmountMinor: req.GetAmountMinor(),
|
||||
Currency: strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
|
||||
Status: mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
now := p.clock.Now()
|
||||
state := &model.CardPayout{
|
||||
PayoutID: strings.TrimSpace(req.GetPayoutId()),
|
||||
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
ProjectID: projectID,
|
||||
CustomerID: strings.TrimSpace(req.GetCustomerId()),
|
||||
AmountMinor: req.GetAmountMinor(),
|
||||
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 {
|
||||
if existing.GetCreatedAt() != nil {
|
||||
state.CreatedAt = existing.GetCreatedAt()
|
||||
if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PayoutID); err == nil && existing != nil {
|
||||
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)
|
||||
apiReq := buildCardTokenPayoutRequest(projectID, req)
|
||||
|
||||
result, err := client.CreateCardTokenPayout(ctx, apiReq)
|
||||
if err != nil {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||
state.Status = model.PayoutStatusFailed
|
||||
state.ProviderMessage = err.Error()
|
||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
||||
p.store.Save(state)
|
||||
state.UpdatedAt = p.clock.Now()
|
||||
|
||||
_ = p.updatePayoutStatus(ctx, state)
|
||||
|
||||
p.logger.Warn("Monetix token payout submission failed",
|
||||
zap.String("payout_id", req.GetPayoutId()),
|
||||
zap.String("customer_id", req.GetCustomerId()),
|
||||
zap.String("payout_id", state.PayoutID),
|
||||
zap.String("customer_id", state.CustomerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state.ProviderPaymentId = result.ProviderRequestID
|
||||
state.ProviderPaymentID = strings.TrimSpace(result.ProviderRequestID)
|
||||
if result.Accepted {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
||||
state.Status = model.PayoutStatusWaiting
|
||||
} else {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||
state.ProviderCode = result.ErrorCode
|
||||
state.ProviderMessage = result.ErrorMessage
|
||||
state.Status = model.PayoutStatusFailed
|
||||
state.ProviderCode = strings.TrimSpace(result.ErrorCode)
|
||||
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{
|
||||
Payout: state,
|
||||
Payout: StateToProto(state),
|
||||
Accepted: result.Accepted,
|
||||
ProviderRequestId: result.ProviderRequestID,
|
||||
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",
|
||||
zap.String("payout_id", state.GetPayoutId()),
|
||||
zap.String("status", state.GetStatus().String()),
|
||||
zap.String("payout_id", state.PayoutID),
|
||||
zap.String("status", string(state.Status)),
|
||||
zap.Bool("accepted", result.Accepted),
|
||||
zap.String("provider_request_id", result.ProviderRequestID),
|
||||
)
|
||||
@@ -240,10 +304,12 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
|
||||
if p == nil {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
|
||||
p.logger.Info("Submitting card tokenization",
|
||||
zap.String("request_id", strings.TrimSpace(req.GetRequestId())),
|
||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||
)
|
||||
|
||||
cardInput, err := validateCardTokenizeRequest(req, p.config)
|
||||
if err != nil {
|
||||
p.logger.Warn("Card tokenization validation failed",
|
||||
@@ -265,8 +331,10 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
|
||||
|
||||
req = sanitizeCardTokenizeRequest(req)
|
||||
cardInput = extractTokenizeCard(req)
|
||||
|
||||
client := monetix.NewClient(p.config, p.httpClient, p.logger)
|
||||
apiReq := buildCardTokenizeRequest(projectID, req, cardInput)
|
||||
|
||||
result, err := client.CreateCardTokenization(ctx, apiReq)
|
||||
if err != nil {
|
||||
p.logger.Warn("Monetix tokenization request failed",
|
||||
@@ -298,36 +366,45 @@ func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardToke
|
||||
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 {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
|
||||
id := strings.TrimSpace(payoutID)
|
||||
p.logger.Info("Card payout status requested", zap.String("payout_id", id))
|
||||
|
||||
if id == "" {
|
||||
p.logger.Warn("Payout status requested with empty payout_id")
|
||||
return nil, merrors.InvalidArgument("payout_id is required", "payout_id")
|
||||
}
|
||||
|
||||
state, ok := p.store.Get(id)
|
||||
if !ok || state == nil {
|
||||
p.logger.Warn("Payout status not found", zap.String("payout_id", id))
|
||||
state, err := p.store.Payouts().FindByPaymentID(ctx, id)
|
||||
if err != nil || state == nil {
|
||||
p.logger.Warn("Payout status not found", zap.String("payout_id", id), zap.Error(err))
|
||||
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) {
|
||||
if p == nil {
|
||||
return http.StatusInternalServerError, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
|
||||
p.logger.Debug("Processing Monetix callback", zap.Int("payload_bytes", len(payload)))
|
||||
|
||||
if len(payload) == 0 {
|
||||
p.logger.Warn("Received empty Monetix callback payload")
|
||||
return http.StatusBadRequest, merrors.InvalidArgument("callback body is empty")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(p.config.SecretKey) == "" {
|
||||
p.logger.Warn("Monetix secret key is not configured; cannot verify callback")
|
||||
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
|
||||
}
|
||||
|
||||
state, statusLabel := mapCallbackToState(p.clock, p.config, cb)
|
||||
if existing, ok := p.store.Get(state.GetPayoutId()); ok && existing != nil {
|
||||
if existing.GetCreatedAt() != nil {
|
||||
state.CreatedAt = existing.GetCreatedAt()
|
||||
// mapCallbackToState currently returns proto-state in your code.
|
||||
// Convert it to mongo model and preserve internal refs if record exists.
|
||||
pbState, statusLabel := mapCallbackToState(p.clock, p.config, cb)
|
||||
|
||||
// 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)
|
||||
|
||||
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("provider_code", state.GetProviderCode()),
|
||||
zap.String("provider_message", state.GetProviderMessage()),
|
||||
zap.String("provider_code", state.ProviderCode),
|
||||
zap.String("provider_message", state.ProviderMessage),
|
||||
zap.String("masked_account", cb.Account.Number),
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||
@@ -40,10 +40,11 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
|
||||
AllowedCurrencies: []string{"RUB"},
|
||||
}
|
||||
|
||||
existingCreated := timestamppb.New(time.Date(2020, 2, 3, 0, 0, 0, 0, time.UTC))
|
||||
store := newCardPayoutStore()
|
||||
store.Save(&mntxv1.CardPayoutState{
|
||||
PayoutId: "payout-1",
|
||||
existingCreated := time.Date(2020, 2, 3, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
repo := newMockRepository()
|
||||
repo.payouts.Save(&model.CardPayout{
|
||||
PayoutID: "payout-1",
|
||||
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)
|
||||
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.ProjectId = 0
|
||||
@@ -76,27 +77,38 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
|
||||
if resp.GetPayout().GetProjectId() != cfg.ProjectID {
|
||||
t.Fatalf("expected project id %d, got %d", cfg.ProjectID, resp.GetPayout().GetProjectId())
|
||||
}
|
||||
if resp.GetPayout().GetStatus() != mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING {
|
||||
t.Fatalf("expected pending status, got %v", resp.GetPayout().GetStatus())
|
||||
if resp.GetPayout().GetStatus() != mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING {
|
||||
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())
|
||||
}
|
||||
|
||||
stored, ok := store.Get(req.GetPayoutId())
|
||||
stored, ok := repo.payouts.Get(req.GetPayoutId())
|
||||
if !ok || stored == nil {
|
||||
t.Fatalf("expected payout state stored")
|
||||
}
|
||||
if stored.GetProviderPaymentId() == "" {
|
||||
if stored.ProviderPaymentID == "" {
|
||||
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) {
|
||||
cfg := monetix.Config{
|
||||
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())
|
||||
if err == nil {
|
||||
@@ -114,12 +126,21 @@ func TestCardPayoutProcessor_ProcessCallback(t *testing.T) {
|
||||
StatusProcessing: "processing",
|
||||
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.Payment.Sum.Currency = "RUB"
|
||||
cb.Signature = ""
|
||||
|
||||
sig, err := monetix.SignPayload(cb, cfg.SecretKey)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
state, ok := store.Get(cb.Payment.ID)
|
||||
state, ok := repo.payouts.Get(cb.Payment.ID)
|
||||
if !ok || state == nil {
|
||||
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/pkg/connector/params"
|
||||
"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"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/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) {
|
||||
return &connectorv1.GetCapabilitiesResponse{
|
||||
Capabilities: &connectorv1.ConnectorCapabilities{
|
||||
ConnectorType: mntxConnectorID,
|
||||
Version: appversion.Create().Short(),
|
||||
SupportedAccountKinds: nil,
|
||||
ConnectorType: mntxConnectorID,
|
||||
Version: appversion.Create().Short(),
|
||||
SupportedAccountKinds: nil,
|
||||
SupportedOperationTypes: []connectorv1.OperationType{connectorv1.OperationType_PAYOUT},
|
||||
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 {
|
||||
req := &mntxv1.CardTokenPayoutRequest{
|
||||
PayoutId: payoutID,
|
||||
ProjectId: readerInt64(reader, "project_id"),
|
||||
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
||||
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
||||
PayoutId: payoutID,
|
||||
ProjectId: readerInt64(reader, "project_id"),
|
||||
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
||||
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
||||
CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
|
||||
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
||||
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
||||
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
||||
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
||||
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
||||
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
||||
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
||||
AmountMinor: amountMinor,
|
||||
Currency: currency,
|
||||
CardToken: strings.TrimSpace(reader.String("card_token")),
|
||||
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
||||
MaskedPan: strings.TrimSpace(reader.String("masked_pan")),
|
||||
Metadata: reader.StringMap("metadata"),
|
||||
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
||||
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
||||
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
||||
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
||||
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
||||
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
||||
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
||||
AmountMinor: amountMinor,
|
||||
Currency: currency,
|
||||
CardToken: strings.TrimSpace(reader.String("card_token")),
|
||||
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
||||
MaskedPan: strings.TrimSpace(reader.String("masked_pan")),
|
||||
Metadata: reader.StringMap("metadata"),
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func buildCardPayoutRequestFromParams(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardPayoutRequest {
|
||||
return &mntxv1.CardPayoutRequest{
|
||||
PayoutId: payoutID,
|
||||
ProjectId: readerInt64(reader, "project_id"),
|
||||
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
||||
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
||||
PayoutId: payoutID,
|
||||
ProjectId: readerInt64(reader, "project_id"),
|
||||
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
||||
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
||||
CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
|
||||
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
||||
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
||||
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
||||
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
||||
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
||||
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
||||
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
||||
AmountMinor: amountMinor,
|
||||
Currency: currency,
|
||||
CardPan: strings.TrimSpace(reader.String("card_pan")),
|
||||
CardExpYear: uint32(readerInt64(reader, "card_exp_year")),
|
||||
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")),
|
||||
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
||||
Metadata: reader.StringMap("metadata"),
|
||||
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
||||
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
||||
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
||||
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
||||
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
||||
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
||||
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
||||
AmountMinor: amountMinor,
|
||||
Currency: currency,
|
||||
CardPan: strings.TrimSpace(reader.String("card_pan")),
|
||||
CardExpYear: uint32(readerInt64(reader, "card_exp_year")),
|
||||
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")),
|
||||
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
||||
Metadata: reader.StringMap("metadata"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func readerInt64(reader params.Reader, key string) int64 {
|
||||
func payoutReceipt(state *mntxv1.CardPayoutState) *connectorv1.OperationReceipt {
|
||||
if state == nil {
|
||||
return &connectorv1.OperationReceipt{
|
||||
Status: connectorv1.OperationStatus_PENDING,
|
||||
Status: connectorv1.OperationStatus_OPERATION_PROCESSING,
|
||||
}
|
||||
}
|
||||
return &connectorv1.OperationReceipt{
|
||||
@@ -252,14 +252,24 @@ func minorToDecimal(amount int64) string {
|
||||
|
||||
func payoutStatusToOperation(status mntxv1.PayoutStatus) connectorv1.OperationStatus {
|
||||
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:
|
||||
return connectorv1.OperationStatus_FAILED
|
||||
case mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING:
|
||||
return connectorv1.OperationStatus_PENDING
|
||||
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||
|
||||
case mntxv1.PayoutStatus_PAYOUT_STATUS_CANCELLED:
|
||||
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||
|
||||
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"
|
||||
|
||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||
"github.com/tech/sendico/gateway/mntx/storage"
|
||||
"github.com/tech/sendico/pkg/clock"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
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).
|
||||
func WithHTTPClient(client *http.Client) Option {
|
||||
return func(s *Service) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/gateway/mntx/internal/appversion"
|
||||
"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/gsresponse"
|
||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||
@@ -24,7 +25,7 @@ type Service struct {
|
||||
logger mlogger.Logger
|
||||
clock clockpkg.Clock
|
||||
producer msg.Producer
|
||||
cardStore *cardPayoutStore
|
||||
storage storage.Repository
|
||||
config monetix.Config
|
||||
httpClient *http.Client
|
||||
card *cardPayoutProcessor
|
||||
@@ -60,10 +61,9 @@ func (r reasonedError) Reason() string {
|
||||
// NewService constructs the Monetix gateway service skeleton.
|
||||
func NewService(logger mlogger.Logger, opts ...Option) *Service {
|
||||
svc := &Service{
|
||||
logger: logger.Named("service"),
|
||||
clock: clockpkg.NewSystem(),
|
||||
cardStore: newCardPayoutStore(),
|
||||
config: monetix.DefaultConfig(),
|
||||
logger: logger.Named("service"),
|
||||
clock: clockpkg.NewSystem(),
|
||||
config: monetix.DefaultConfig(),
|
||||
}
|
||||
|
||||
initMetrics()
|
||||
@@ -84,11 +84,7 @@ func NewService(logger mlogger.Logger, opts ...Option) *Service {
|
||||
svc.httpClient.Timeout = svc.config.Timeout()
|
||||
}
|
||||
|
||||
if svc.cardStore == nil {
|
||||
svc.cardStore = newCardPayoutStore()
|
||||
}
|
||||
|
||||
svc.card = newCardPayoutProcessor(svc.logger, svc.config, svc.clock, svc.cardStore, svc.httpClient, svc.producer)
|
||||
svc.card = newCardPayoutProcessor(svc.logger, svc.config, svc.clock, svc.storage, svc.httpClient, svc.producer)
|
||||
svc.startDiscoveryAnnouncer()
|
||||
|
||||
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/sys v0.40.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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
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 h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||
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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
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/merrors"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/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"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -18,9 +18,9 @@ const tgsettleConnectorID = "tgsettle"
|
||||
func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
||||
return &connectorv1.GetCapabilitiesResponse{
|
||||
Capabilities: &connectorv1.ConnectorCapabilities{
|
||||
ConnectorType: tgsettleConnectorID,
|
||||
Version: "",
|
||||
SupportedAccountKinds: nil,
|
||||
ConnectorType: tgsettleConnectorID,
|
||||
Version: "",
|
||||
SupportedAccountKinds: nil,
|
||||
SupportedOperationTypes: []connectorv1.OperationType{connectorv1.OperationType_TRANSFER},
|
||||
OperationParams: tgsettleOperationParams(),
|
||||
},
|
||||
@@ -64,7 +64,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
||||
}
|
||||
paymentIntentID := strings.TrimSpace(reader.String("payment_intent_id"))
|
||||
if paymentIntentID == "" {
|
||||
paymentIntentID = strings.TrimSpace(reader.String("client_reference"))
|
||||
paymentIntentID = strings.TrimSpace(reader.String("payment_ref"))
|
||||
}
|
||||
if paymentIntentID == "" {
|
||||
paymentIntentID = strings.TrimSpace(metadata[metadataPaymentIntentID])
|
||||
@@ -122,7 +122,9 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
||||
Destination: dest,
|
||||
Amount: normalizedAmount,
|
||||
Metadata: metadata,
|
||||
ClientReference: paymentIntentID,
|
||||
PaymentRef: paymentIntentID,
|
||||
IntentRef: strings.TrimSpace(op.GetIntentRef()),
|
||||
OperationRef: strings.TrimSpace(op.GetOperationRef()),
|
||||
})
|
||||
if err != nil {
|
||||
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 {
|
||||
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:
|
||||
return connectorv1.OperationStatus_FAILED
|
||||
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return connectorv1.OperationStatus_CANCELED
|
||||
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED:
|
||||
fallthrough
|
||||
default:
|
||||
return connectorv1.OperationStatus_PENDING
|
||||
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
mb "github.com/tech/sendico/pkg/messaging/broker"
|
||||
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
||||
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"
|
||||
tnotifications "github.com/tech/sendico/pkg/messaging/notifications/telegram"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
@@ -188,12 +187,7 @@ func (s *Service) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransfe
|
||||
return nil, err
|
||||
}
|
||||
if existing != nil {
|
||||
existing, err = s.expirePaymentIfNeeded(ctx, existing)
|
||||
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))))...)
|
||||
s.logger.Info("Submit transfer idempotent hit", append(logFields, zap.String("status", string(existing.Status)))...)
|
||||
return &chainv1.SubmitTransferResponse{Transfer: transferFromPayment(existing, req)}, 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
|
||||
}
|
||||
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,
|
||||
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
|
||||
}
|
||||
@@ -274,27 +263,30 @@ func (s *Service) onIntent(ctx context.Context, intent *model.PaymentGatewayInte
|
||||
return err
|
||||
}
|
||||
if existing != nil {
|
||||
existing, err = s.expirePaymentIfNeeded(ctx, existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logger.Info("Payment gateway intent already recorded",
|
||||
zap.String("idempotency_key", confirmReq.RequestID),
|
||||
zap.String("payment_intent_id", confirmReq.PaymentIntentID),
|
||||
zap.String("quote_ref", confirmReq.QuoteRef),
|
||||
zap.String("rail", confirmReq.Rail),
|
||||
zap.String("status", string(paymentStatus(existing))))
|
||||
zap.String("status", string(existing.Status)))
|
||||
return nil
|
||||
}
|
||||
|
||||
record := paymentRecordFromIntent(intent, confirmReq)
|
||||
if err := s.repo.Payments().Upsert(ctx, record); err != nil {
|
||||
s.logger.Warn("Failed to persist pending payment", zap.Error(err), zap.String("idempotency_key", confirmReq.RequestID))
|
||||
if err := s.updateTransferStatus(ctx, record); err != nil {
|
||||
s.logger.Warn("Failed to persist payment record", zap.Error(err), zap.String("idempotency_key", confirmReq.RequestID))
|
||||
return err
|
||||
}
|
||||
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.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 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 {
|
||||
if result == nil {
|
||||
s.logger.Warn("Confirmation result rejected", zap.String("reason", "result is nil"))
|
||||
return merrors.InvalidArgument("confirmation result is nil", "result")
|
||||
}
|
||||
|
||||
requestID := strings.TrimSpace(result.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")
|
||||
}
|
||||
|
||||
record, err := s.loadPayment(ctx, requestID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Confirmation result lookup failed", zap.Error(err), zap.String("request_id", requestID))
|
||||
return err
|
||||
}
|
||||
if record == nil {
|
||||
s.logger.Warn("Confirmation result ignored: payment not found", zap.String("request_id", requestID))
|
||||
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 err := s.repo.TelegramConfirmations().Upsert(ctx, &storagemodel.TelegramConfirmation{
|
||||
if e := s.repo.TelegramConfirmations().Upsert(ctx, &storagemodel.TelegramConfirmation{
|
||||
RequestID: requestID,
|
||||
PaymentIntentID: record.PaymentIntentID,
|
||||
QuoteRef: record.QuoteRef,
|
||||
RawReply: result.RawReply,
|
||||
}); err != nil {
|
||||
s.logger.Warn("Failed to store telegram confirmation", zap.Error(err), zap.String("request_id", requestID))
|
||||
} else {
|
||||
s.logger.Info("Stored telegram confirmation", zap.String("request_id", requestID),
|
||||
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))
|
||||
}); e != nil {
|
||||
s.logger.Warn("Failed to store confirmation error", zap.Error(e),
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("status", string(result.Status)))
|
||||
}
|
||||
}
|
||||
|
||||
nextStatus := paymentStatusFromResult(result)
|
||||
currentStatus := paymentStatus(record)
|
||||
if currentStatus == storagemodel.PaymentStatusExecuted || currentStatus == storagemodel.PaymentStatusExpired {
|
||||
s.logger.Info("Confirmation result ignored: payment already finalized",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("status", string(currentStatus)))
|
||||
// If the payment is already finalized — ignore the result.
|
||||
switch record.Status {
|
||||
case storagemodel.PaymentStatusSuccess,
|
||||
storagemodel.PaymentStatusFailed,
|
||||
storagemodel.PaymentStatusCancelled:
|
||||
return nil
|
||||
}
|
||||
s.applyPaymentResult(record, nextStatus, result)
|
||||
if err := s.repo.Payments().Upsert(ctx, record); err != nil {
|
||||
s.logger.Warn("Failed to persist payment status", zap.Error(err), zap.String("request_id", requestID))
|
||||
|
||||
now := time.Now()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
intent := intentFromPayment(record)
|
||||
s.publishExecution(intent, result)
|
||||
s.publishTelegramReaction(result)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) buildConfirmationRequest(intent *model.PaymentGatewayIntent) (*model.ConfirmationRequest, error) {
|
||||
targetChatID := strings.TrimSpace(intent.TargetChatID)
|
||||
if targetChatID == "" {
|
||||
targetChatID = s.chatID
|
||||
}
|
||||
targetChatID := s.chatID
|
||||
if targetChatID == "" {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
if s == nil || s.producer == nil || result == nil || result.RawReply == nil {
|
||||
return
|
||||
@@ -490,46 +473,6 @@ func (s *Service) loadPayment(ctx context.Context, requestID string) (*storagemo
|
||||
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() {
|
||||
if s == nil || s.producer == nil {
|
||||
return
|
||||
@@ -557,78 +500,37 @@ func normalizeIntent(intent *model.PaymentGatewayIntent) *model.PaymentGatewayIn
|
||||
cp.IdempotencyKey = strings.TrimSpace(cp.IdempotencyKey)
|
||||
cp.OutgoingLeg = strings.TrimSpace(cp.OutgoingLeg)
|
||||
cp.QuoteRef = strings.TrimSpace(cp.QuoteRef)
|
||||
cp.TargetChatID = strings.TrimSpace(cp.TargetChatID)
|
||||
if cp.RequestedMoney != nil {
|
||||
cp.RequestedMoney.Amount = strings.TrimSpace(cp.RequestedMoney.Amount)
|
||||
cp.RequestedMoney.Currency = strings.TrimSpace(cp.RequestedMoney.Currency)
|
||||
}
|
||||
cp.IntentRef = strings.TrimSpace(cp.IntentRef)
|
||||
cp.OperationRef = strings.TrimSpace(cp.OperationRef)
|
||||
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 {
|
||||
record := &storagemodel.PaymentRecord{
|
||||
Status: storagemodel.PaymentStatusPending,
|
||||
Status: storagemodel.PaymentStatusWaiting,
|
||||
}
|
||||
if intent != nil {
|
||||
record.IdempotencyKey = strings.TrimSpace(intent.IdempotencyKey)
|
||||
record.PaymentIntentID = strings.TrimSpace(intent.PaymentIntentID)
|
||||
record.QuoteRef = strings.TrimSpace(intent.QuoteRef)
|
||||
record.OutgoingLeg = strings.TrimSpace(intent.OutgoingLeg)
|
||||
record.TargetChatID = strings.TrimSpace(intent.TargetChatID)
|
||||
record.RequestedMoney = intent.RequestedMoney
|
||||
record.IntentRef = intent.IntentRef
|
||||
record.OperationRef = intent.OperationRef
|
||||
}
|
||||
if confirmReq != nil {
|
||||
record.IdempotencyKey = strings.TrimSpace(confirmReq.RequestID)
|
||||
record.PaymentIntentID = strings.TrimSpace(confirmReq.PaymentIntentID)
|
||||
record.QuoteRef = strings.TrimSpace(confirmReq.QuoteRef)
|
||||
record.OutgoingLeg = strings.TrimSpace(confirmReq.Rail)
|
||||
record.TargetChatID = strings.TrimSpace(confirmReq.TargetChatID)
|
||||
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 {
|
||||
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 &model.PaymentGatewayIntent{
|
||||
PaymentRef: strings.TrimSpace(record.PaymentRef),
|
||||
PaymentIntentID: strings.TrimSpace(record.PaymentIntentID),
|
||||
IdempotencyKey: strings.TrimSpace(record.IdempotencyKey),
|
||||
OutgoingLeg: strings.TrimSpace(record.OutgoingLeg),
|
||||
QuoteRef: strings.TrimSpace(record.QuoteRef),
|
||||
IntentRef: strings.TrimSpace(record.IntentRef),
|
||||
OperationRef: strings.TrimSpace(record.OperationRef),
|
||||
RequestedMoney: record.RequestedMoney,
|
||||
TargetChatID: strings.TrimSpace(record.TargetChatID),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,13 +582,17 @@ func intentFromSubmitTransfer(req *chainv1.SubmitTransferRequest, defaultRail, d
|
||||
Currency: sourceCurrency,
|
||||
}
|
||||
}
|
||||
paymentIntentID := strings.TrimSpace(req.GetClientReference())
|
||||
paymentIntentID := strings.TrimSpace(req.GetIntentRef())
|
||||
if paymentIntentID == "" {
|
||||
paymentIntentID = strings.TrimSpace(metadata[metadataPaymentIntentID])
|
||||
}
|
||||
if paymentIntentID == "" {
|
||||
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])
|
||||
targetChatID := strings.TrimSpace(metadata[metadataTargetChatID])
|
||||
outgoingLeg := strings.TrimSpace(metadata[metadataOutgoingLeg])
|
||||
@@ -695,12 +603,12 @@ func intentFromSubmitTransfer(req *chainv1.SubmitTransferRequest, defaultRail, d
|
||||
targetChatID = strings.TrimSpace(defaultChatID)
|
||||
}
|
||||
return &model.PaymentGatewayIntent{
|
||||
PaymentRef: paymentRef,
|
||||
PaymentIntentID: paymentIntentID,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OutgoingLeg: outgoingLeg,
|
||||
QuoteRef: quoteRef,
|
||||
RequestedMoney: requestedMoney,
|
||||
TargetChatID: targetChatID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -708,15 +616,14 @@ func transferFromRequest(req *chainv1.SubmitTransferRequest) *chainv1.Transfer {
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
amount := req.GetAmount()
|
||||
return &chainv1.Transfer{
|
||||
TransferRef: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
|
||||
SourceWalletRef: strings.TrimSpace(req.GetSourceWalletRef()),
|
||||
Destination: req.GetDestination(),
|
||||
RequestedAmount: amount,
|
||||
Status: chainv1.TransferStatus_TRANSFER_SUBMITTED,
|
||||
RequestedAmount: req.GetAmount(),
|
||||
Status: chainv1.TransferStatus_TRANSFER_CREATED,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,20 +631,32 @@ func transferFromPayment(record *storagemodel.PaymentRecord, req *chainv1.Submit
|
||||
if record == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var requested *moneyv1.Money
|
||||
if req != nil && req.GetAmount() != nil {
|
||||
requested = req.GetAmount()
|
||||
} else {
|
||||
requested = moneyFromPayment(record.RequestedMoney)
|
||||
}
|
||||
net := moneyFromPayment(record.ExecutedMoney)
|
||||
status := chainv1.TransferStatus_TRANSFER_SUBMITTED
|
||||
switch paymentStatus(record) {
|
||||
case storagemodel.PaymentStatusExecuted:
|
||||
status = chainv1.TransferStatus_TRANSFER_CONFIRMED
|
||||
case storagemodel.PaymentStatusExpired:
|
||||
net := moneyFromPayment(record.RequestedMoney)
|
||||
|
||||
var status chainv1.TransferStatus
|
||||
|
||||
switch record.Status {
|
||||
case storagemodel.PaymentStatusSuccess:
|
||||
status = chainv1.TransferStatus_TRANSFER_SUCCESS
|
||||
case storagemodel.PaymentStatusCancelled:
|
||||
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{
|
||||
TransferRef: strings.TrimSpace(record.IdempotencyKey),
|
||||
IdempotencyKey: strings.TrimSpace(record.IdempotencyKey),
|
||||
@@ -745,11 +664,13 @@ func transferFromPayment(record *storagemodel.PaymentRecord, req *chainv1.Submit
|
||||
NetAmount: net,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
if req != nil {
|
||||
transfer.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
||||
transfer.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
|
||||
transfer.Destination = req.GetDestination()
|
||||
}
|
||||
|
||||
if !record.ExecutedAt.IsZero() {
|
||||
ts := timestamppb.New(record.ExecutedAt)
|
||||
transfer.CreatedAt = ts
|
||||
@@ -761,6 +682,7 @@ func transferFromPayment(record *storagemodel.PaymentRecord, req *chainv1.Submit
|
||||
transfer.CreatedAt = timestamppb.New(record.CreatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
return transfer
|
||||
}
|
||||
|
||||
@@ -787,3 +709,27 @@ func readEnv(env string) string {
|
||||
}
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/gateway/tgsettle/storage"
|
||||
storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model"
|
||||
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"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
notification "github.com/tech/sendico/pkg/model/notification"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
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 {
|
||||
mu sync.Mutex
|
||||
records map[string]*storagemodel.PaymentRecord
|
||||
@@ -26,6 +27,9 @@ type fakePaymentsStore struct {
|
||||
func (f *fakePaymentsStore) FindByIdempotencyKey(_ context.Context, key string) (*storagemodel.PaymentRecord, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.records == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return f.records[key], nil
|
||||
}
|
||||
|
||||
@@ -67,286 +71,212 @@ func (f *fakeRepo) TelegramConfirmations() storage.TelegramConfirmationsStore {
|
||||
return f.tg
|
||||
}
|
||||
|
||||
type captureProducer struct {
|
||||
mu sync.Mutex
|
||||
confirmationRequests []*model.ConfirmationRequest
|
||||
executions []*model.PaymentGatewayExecution
|
||||
}
|
||||
//
|
||||
// FAKE BROKER (ОБЯЗАТЕЛЕН ДЛЯ СЕРВИСА)
|
||||
//
|
||||
|
||||
func (c *captureProducer) SendMessage(env envelope.Envelope) error {
|
||||
_, _ = env.Serialize()
|
||||
switch env.GetSignature().ToString() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
type fakeBroker struct{}
|
||||
|
||||
func (f *fakeBroker) Publish(_ envelope.Envelope) error {
|
||||
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()
|
||||
defer c.mu.Unlock()
|
||||
c.confirmationRequests = nil
|
||||
c.executions = nil
|
||||
c.reactions = append(c.reactions, env)
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestOnIntentCreatesConfirmationRequest(t *testing.T) {
|
||||
//
|
||||
// TESTS
|
||||
//
|
||||
|
||||
func newTestService(_ *testing.T) (*Service, *fakeRepo, *captureProducer) {
|
||||
logger := mloggerfactory.NewLogger(false)
|
||||
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
||||
prod := &captureProducer{}
|
||||
t.Setenv("PGS_CHAT_ID", "-100")
|
||||
svc := NewService(logger, repo, prod, nil, Config{
|
||||
Rail: "card",
|
||||
TargetChatIDEnv: "PGS_CHAT_ID",
|
||||
TimeoutSeconds: 90,
|
||||
AcceptedUserIDs: []string{"42"},
|
||||
|
||||
repo := &fakeRepo{
|
||||
payments: &fakePaymentsStore{},
|
||||
tg: &fakeTelegramStore{},
|
||||
}
|
||||
|
||||
sigEnv := tnotifications.TelegramReaction(string(mservice.PaymentGateway), &model.TelegramReactionRequest{
|
||||
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",
|
||||
IdempotencyKey: "idem-1",
|
||||
OutgoingLeg: "card",
|
||||
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",
|
||||
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{
|
||||
IdempotencyKey: "idem-2",
|
||||
PaymentIntentID: intent.PaymentIntentID,
|
||||
QuoteRef: intent.QuoteRef,
|
||||
OutgoingLeg: intent.OutgoingLeg,
|
||||
RequestedMoney: intent.RequestedMoney,
|
||||
Status: storagemodel.PaymentStatusPending,
|
||||
IdempotencyKey: "idem-2",
|
||||
Status: storagemodel.PaymentStatusWaiting,
|
||||
})
|
||||
|
||||
result := &model.ConfirmationResult{
|
||||
RequestID: "idem-2",
|
||||
Money: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
||||
Status: model.ConfirmationStatusConfirmed,
|
||||
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2", Text: "5 EUR"},
|
||||
Status: model.ConfirmationStatusClarified,
|
||||
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-2"]
|
||||
|
||||
if rec.Status != storagemodel.PaymentStatusWaiting {
|
||||
t.Fatalf("clarified must not change status")
|
||||
}
|
||||
record := repo.payments.records["idem-2"]
|
||||
if record == nil {
|
||||
t.Fatalf("expected payment record to be stored")
|
||||
if repo.tg.records["idem-2"] == nil {
|
||||
t.Fatalf("telegram confirmation must be stored")
|
||||
}
|
||||
if record.Status != storagemodel.PaymentStatusExecuted {
|
||||
t.Fatalf("expected executed status, got %q", record.Status)
|
||||
}
|
||||
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")
|
||||
if len(prod.reactions) != 0 {
|
||||
t.Fatalf("clarified must not publish reaction")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClarifiedResultPersistsExecution(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-clarified",
|
||||
IdempotencyKey: "idem-clarified",
|
||||
QuoteRef: "quote-clarified",
|
||||
OutgoingLeg: "card",
|
||||
RequestedMoney: &paymenttypes.Money{Amount: "12", Currency: "USD"},
|
||||
}
|
||||
func TestRejected(t *testing.T) {
|
||||
svc, repo, prod := newTestService(t)
|
||||
|
||||
// ВАЖНО: чтобы текущий emitTransferStatusEvent не падал на nil,
|
||||
// даем минимально ожидаемые поля + non-nil ExecutedMoney.
|
||||
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
||||
IdempotencyKey: "idem-clarified",
|
||||
PaymentIntentID: intent.PaymentIntentID,
|
||||
QuoteRef: intent.QuoteRef,
|
||||
OutgoingLeg: intent.OutgoingLeg,
|
||||
RequestedMoney: intent.RequestedMoney,
|
||||
Status: storagemodel.PaymentStatusPending,
|
||||
IdempotencyKey: "idem-3",
|
||||
PaymentIntentID: "pi-3",
|
||||
QuoteRef: "quote-3",
|
||||
OutgoingLeg: "card",
|
||||
RequestedMoney: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
||||
ExecutedMoney: &paymenttypes.Money{Amount: "0", Currency: "EUR"},
|
||||
Status: storagemodel.PaymentStatusWaiting,
|
||||
})
|
||||
|
||||
result := &model.ConfirmationResult{
|
||||
RequestID: "idem-clarified",
|
||||
Money: &paymenttypes.Money{Amount: "12", Currency: "USD"},
|
||||
Status: model.ConfirmationStatusClarified,
|
||||
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "3", Text: "12 USD"},
|
||||
RequestID: "idem-3",
|
||||
Status: model.ConfirmationStatusRejected,
|
||||
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-3"]
|
||||
|
||||
if rec.Status != storagemodel.PaymentStatusFailed {
|
||||
t.Fatalf("expected failed")
|
||||
}
|
||||
record := repo.payments.records["idem-clarified"]
|
||||
if record == nil || record.Status != storagemodel.PaymentStatusExecuted {
|
||||
t.Fatalf("expected payment executed status, got %#v", record)
|
||||
if repo.tg.records["idem-3"] == nil {
|
||||
t.Fatalf("telegram confirmation must be stored")
|
||||
}
|
||||
if len(prod.reactions) != 0 {
|
||||
t.Fatalf("rejected must not publish reaction")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdempotencyPreventsDuplicateWrites(t *testing.T) {
|
||||
logger := mloggerfactory.NewLogger(false)
|
||||
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 TestTimeout(t *testing.T) {
|
||||
svc, repo, prod := newTestService(t)
|
||||
|
||||
func TestTimeoutDoesNotPersistExecution(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-4",
|
||||
IdempotencyKey: "idem-4",
|
||||
QuoteRef: "quote-4",
|
||||
OutgoingLeg: "card",
|
||||
RequestedMoney: &paymenttypes.Money{Amount: "8", Currency: "USD"},
|
||||
}
|
||||
// ВАЖНО: чтобы текущий emitTransferStatusEvent не падал на nil,
|
||||
// даем минимально ожидаемые поля + non-nil ExecutedMoney.
|
||||
_ = repo.payments.Upsert(context.Background(), &storagemodel.PaymentRecord{
|
||||
IdempotencyKey: "idem-4",
|
||||
PaymentIntentID: intent.PaymentIntentID,
|
||||
QuoteRef: intent.QuoteRef,
|
||||
OutgoingLeg: intent.OutgoingLeg,
|
||||
RequestedMoney: intent.RequestedMoney,
|
||||
Status: storagemodel.PaymentStatusPending,
|
||||
PaymentIntentID: "pi-4",
|
||||
QuoteRef: "quote-4",
|
||||
OutgoingLeg: "card",
|
||||
RequestedMoney: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
||||
ExecutedMoney: &paymenttypes.Money{Amount: "0", Currency: "EUR"},
|
||||
Status: storagemodel.PaymentStatusWaiting,
|
||||
})
|
||||
|
||||
result := &model.ConfirmationResult{
|
||||
RequestID: "idem-4",
|
||||
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 record == nil || record.Status != storagemodel.PaymentStatusExpired {
|
||||
t.Fatalf("expected expired status for timeout, got %#v", record)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
if repo.tg.records["idem-4"] == nil {
|
||||
t.Fatalf("telegram confirmation must be stored")
|
||||
}
|
||||
if len(prod.reactions) != 0 {
|
||||
t.Fatalf("timeout must not publish reaction")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
const (
|
||||
PaymentStatusPending PaymentStatus = "pending"
|
||||
PaymentStatusExpired PaymentStatus = "expired"
|
||||
PaymentStatusExecuted PaymentStatus = "executed"
|
||||
PaymentStatusCreated PaymentStatus = "created" // created
|
||||
PaymentStatusProcessing PaymentStatus = "processing" // processing
|
||||
PaymentStatusWaiting PaymentStatus = "waiting" // waiting external action
|
||||
PaymentStatusSuccess PaymentStatus = "success" // final success
|
||||
PaymentStatusFailed PaymentStatus = "failed" // final failure
|
||||
PaymentStatusCancelled PaymentStatus = "cancelled" // cancelled final
|
||||
)
|
||||
|
||||
type PaymentRecord struct {
|
||||
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"`
|
||||
PaymentIntentID string `bson:"paymentIntentId,omitempty" json:"payment_intent_id,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"`
|
||||
TargetChatID string `bson:"targetChatId,omitempty" json:"target_chat_id,omitempty"`
|
||||
RequestedMoney *paymenttypes.Money `bson:"requestedMoney,omitempty" json:"requested_money,omitempty"`
|
||||
ExecutedMoney *paymenttypes.Money `bson:"executedMoney,omitempty" json:"executed_money,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"`
|
||||
UpdatedAt time.Time `bson:"updatedAt,omitempty" json:"updated_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 == "" {
|
||||
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()
|
||||
if record.CreatedAt.IsZero() {
|
||||
record.CreatedAt = now
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
chainasset "github.com/tech/sendico/pkg/chain"
|
||||
"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"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/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{}{
|
||||
"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 != "" {
|
||||
params["destination_memo"] = memo
|
||||
@@ -472,14 +472,14 @@ func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[strin
|
||||
if op == nil || len(metadata) == 0 {
|
||||
return
|
||||
}
|
||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyFromRole]); raw != "" {
|
||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
||||
op.FromRole = pmodel.ToProto(role)
|
||||
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyFromRole]); raw != "" {
|
||||
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||
op.FromRole = account_role.ToProto(role)
|
||||
}
|
||||
}
|
||||
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyToRole]); raw != "" {
|
||||
if role, ok := pmodel.Parse(raw); ok && role != "" {
|
||||
op.ToRole = pmodel.ToProto(role)
|
||||
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyToRole]); raw != "" {
|
||||
if role, ok := account_role.Parse(raw); ok && role != "" {
|
||||
op.ToRole = account_role.ToProto(role)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -619,7 +619,7 @@ func gasTopUpEnsureOperation(req *chainv1.EnsureGasTopUpRequest) (*connectorv1.O
|
||||
"mode": "ensure",
|
||||
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||
"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()},
|
||||
}
|
||||
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 {
|
||||
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:
|
||||
return connectorv1.OperationStatus_FAILED
|
||||
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return connectorv1.OperationStatus_CANCELED
|
||||
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||
|
||||
default:
|
||||
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"
|
||||
|
||||
"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"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/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,
|
||||
Amount: amountValue,
|
||||
},
|
||||
Fees: fees,
|
||||
Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
|
||||
ClientReference: strings.TrimSpace(req.ClientReference),
|
||||
Fees: fees,
|
||||
Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
|
||||
PaymentRef: strings.TrimSpace(req.PaymentRef),
|
||||
OperationRef: strings.TrimSpace(req.OperationRef),
|
||||
IntentRef: strings.TrimSpace(req.OperationRef),
|
||||
})
|
||||
if err != nil {
|
||||
return rail.RailResult{}, err
|
||||
@@ -186,20 +188,29 @@ func (g *chainRailGateway) isManagedWallet(ctx context.Context, walletRef string
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func statusFromTransfer(status chainv1.TransferStatus) string {
|
||||
func statusFromTransfer(status chainv1.TransferStatus) rail.TransferStatus {
|
||||
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
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||
return rail.TransferStatusFailed
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return rail.TransferStatusRejected
|
||||
case chainv1.TransferStatus_TRANSFER_SIGNING,
|
||||
chainv1.TransferStatus_TRANSFER_PENDING,
|
||||
chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
||||
return rail.TransferStatusPending
|
||||
return rail.TransferStatusCancelled
|
||||
|
||||
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)
|
||||
if strings.TrimSpace(string(fromRole)) != "" {
|
||||
if result == nil {
|
||||
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 result == nil {
|
||||
result = map[string]string{}
|
||||
}
|
||||
result[pmodel.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
|
||||
result[account_role.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -23,7 +23,7 @@ require (
|
||||
|
||||
require (
|
||||
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/bits-and-blooms/bitset v1.24.4 // 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/text v0.33.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/rpc 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-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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
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-20260131145833-e3fabd62fc61/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
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-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/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
|
||||
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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260202165425-ce8ad4cf556b h1:SGYyueaEovpqmWmtTvwtVgo638V/QFE2zlTCnRrR3jg=
|
||||
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-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||
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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
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_ManagedWalletRef{ManagedWalletRef: targetWalletRef},
|
||||
},
|
||||
Amount: topUp,
|
||||
Metadata: shared.CloneMetadata(req.GetMetadata()),
|
||||
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
||||
Amount: topUp,
|
||||
Metadata: shared.CloneMetadata(req.GetMetadata()),
|
||||
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)
|
||||
|
||||
@@ -38,11 +38,12 @@ func toProtoTransfer(transfer *model.Transfer) *chainv1.Transfer {
|
||||
TransferRef: transfer.TransferRef,
|
||||
IdempotencyKey: transfer.IdempotencyKey,
|
||||
OrganizationRef: transfer.OrganizationRef,
|
||||
IntentRef: transfer.IntentRef,
|
||||
SourceWalletRef: transfer.SourceWalletRef,
|
||||
Destination: destination,
|
||||
Asset: asset,
|
||||
RequestedAmount: shared.CloneMoney(transfer.RequestedAmount),
|
||||
NetAmount: shared.CloneMoney(transfer.NetAmount),
|
||||
RequestedAmount: shared.MonenyToProto(transfer.RequestedAmount),
|
||||
NetAmount: shared.MonenyToProto(transfer.NetAmount),
|
||||
Fees: protoFees,
|
||||
Status: shared.TransferStatusToProto(transfer.Status),
|
||||
TransactionHash: transfer.TxHash,
|
||||
|
||||
@@ -38,6 +38,17 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
||||
c.deps.Logger.Warn("Missing idempotency key")
|
||||
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())
|
||||
if organizationRef == "" {
|
||||
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")
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -123,6 +139,8 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
||||
|
||||
transfer := &model.Transfer{
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OperationRef: operationRef,
|
||||
IntentRef: intentRef,
|
||||
TransferRef: shared.GenerateTransferRef(),
|
||||
OrganizationRef: organizationRef,
|
||||
SourceWalletRef: sourceWalletRef,
|
||||
@@ -130,11 +148,11 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
|
||||
Network: sourceWallet.Network,
|
||||
TokenSymbol: effectiveTokenSymbol,
|
||||
ContractAddress: effectiveContractAddress,
|
||||
RequestedAmount: shared.CloneMoney(amount),
|
||||
NetAmount: netAmount,
|
||||
RequestedAmount: shared.ProtoToMoney(amount),
|
||||
NetAmount: shared.ProtoToMoney(netAmount),
|
||||
PaymentRef: paymentRef,
|
||||
Fees: fees,
|
||||
Status: model.TransferStatusPending,
|
||||
ClientReference: strings.TrimSpace(req.GetClientReference()),
|
||||
Status: model.TransferStatusCreated,
|
||||
LastStatusAt: c.deps.Clock.Now().UTC(),
|
||||
}
|
||||
|
||||
|
||||
@@ -169,7 +169,9 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
||||
Amount: amount,
|
||||
Fees: parseChainFees(reader),
|
||||
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 {
|
||||
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{
|
||||
Receipt: &connectorv1.OperationReceipt{
|
||||
OperationId: opID,
|
||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
||||
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
|
||||
Result: result,
|
||||
},
|
||||
}, nil
|
||||
@@ -238,7 +240,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
||||
return &connectorv1.SubmitOperationResponse{
|
||||
Receipt: &connectorv1.OperationReceipt{
|
||||
OperationId: opID,
|
||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
||||
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
|
||||
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), ""),
|
||||
},
|
||||
}, nil
|
||||
@@ -256,12 +258,14 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
||||
}
|
||||
resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
|
||||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||
IntentRef: strings.TrimSpace(op.GetIntentRef()),
|
||||
OperationRef: strings.TrimSpace(op.GetOperationRef()),
|
||||
OrganizationRef: orgRef,
|
||||
SourceWalletRef: source,
|
||||
TargetWalletRef: target,
|
||||
EstimatedTotalFee: fee,
|
||||
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
|
||||
ClientReference: strings.TrimSpace(reader.String("client_reference")),
|
||||
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
|
||||
})
|
||||
if err != 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{
|
||||
Receipt: &connectorv1.OperationReceipt{
|
||||
OperationId: opID,
|
||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
||||
Status: shared.ChainTransferStatusToOperation(resp.GetTransfer().GetStatus()),
|
||||
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), transferRef),
|
||||
},
|
||||
}, nil
|
||||
@@ -544,25 +548,51 @@ func chainTransferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation
|
||||
|
||||
func chainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
|
||||
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:
|
||||
return connectorv1.OperationStatus_FAILED
|
||||
return connectorv1.OperationStatus_OPERATION_FAILED
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return connectorv1.OperationStatus_CANCELED
|
||||
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
||||
|
||||
default:
|
||||
return connectorv1.OperationStatus_PENDING
|
||||
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func chainStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
|
||||
switch status {
|
||||
case connectorv1.OperationStatus_CONFIRMED:
|
||||
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
||||
case connectorv1.OperationStatus_FAILED:
|
||||
|
||||
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_CANCELED:
|
||||
|
||||
case connectorv1.OperationStatus_OPERATION_CANCELLED:
|
||||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||
|
||||
default:
|
||||
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package tron
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
@@ -157,9 +156,3 @@ func GetTransactionStatus(
|
||||
}
|
||||
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",
|
||||
OrganizationRef: "org-1",
|
||||
SourceWalletRef: srcRef,
|
||||
PaymentRef: "ref-1",
|
||||
Destination: &ichainv1.TransferDestination{
|
||||
Destination: &ichainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef},
|
||||
},
|
||||
@@ -172,6 +173,8 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
||||
Amount: &moneyv1.Money{Currency: "USDT", Amount: "5"},
|
||||
},
|
||||
},
|
||||
OperationRef: "oper-1",
|
||||
IntentRef: "intent-1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, transferResp.GetTransfer())
|
||||
@@ -179,7 +182,7 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
|
||||
|
||||
stored := repo.transfers.get(transferResp.GetTransfer().GetTransferRef())
|
||||
require.NotNil(t, stored)
|
||||
require.Equal(t, model.TransferStatusPending, stored.Status)
|
||||
require.Equal(t, model.TransferStatusCreated, stored.Status)
|
||||
|
||||
// GetTransfer
|
||||
getResp, err := svc.GetTransfer(ctx, &ichainv1.GetTransferRequest{TransferRef: stored.TransferRef})
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
"github.com/tech/sendico/gateway/tron/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"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
"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 {
|
||||
switch status {
|
||||
case chainv1.TransferStatus_TRANSFER_PENDING:
|
||||
return model.TransferStatusPending
|
||||
case chainv1.TransferStatus_TRANSFER_SIGNING:
|
||||
return model.TransferStatusSigning
|
||||
case chainv1.TransferStatus_TRANSFER_SUBMITTED:
|
||||
return model.TransferStatusSubmitted
|
||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
||||
return model.TransferStatusConfirmed
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CREATED:
|
||||
return model.TransferStatusCreated
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_PROCESSING:
|
||||
return model.TransferStatusProcessing
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_WAITING:
|
||||
return model.TransferStatusWaiting
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_SUCCESS:
|
||||
return model.TransferStatusSuccess
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||
return model.TransferStatusFailed
|
||||
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return model.TransferStatusCancelled
|
||||
|
||||
default:
|
||||
return ""
|
||||
return model.TransferStatus("")
|
||||
}
|
||||
}
|
||||
|
||||
func TransferStatusToProto(status model.TransferStatus) chainv1.TransferStatus {
|
||||
switch status {
|
||||
case model.TransferStatusPending:
|
||||
return chainv1.TransferStatus_TRANSFER_PENDING
|
||||
case model.TransferStatusSigning:
|
||||
return chainv1.TransferStatus_TRANSFER_SIGNING
|
||||
case model.TransferStatusSubmitted:
|
||||
return chainv1.TransferStatus_TRANSFER_SUBMITTED
|
||||
case model.TransferStatusConfirmed:
|
||||
return chainv1.TransferStatus_TRANSFER_CONFIRMED
|
||||
|
||||
case model.TransferStatusCreated:
|
||||
return chainv1.TransferStatus_TRANSFER_CREATED
|
||||
|
||||
case model.TransferStatusProcessing:
|
||||
return chainv1.TransferStatus_TRANSFER_PROCESSING
|
||||
|
||||
case model.TransferStatusWaiting:
|
||||
return chainv1.TransferStatus_TRANSFER_WAITING
|
||||
|
||||
case model.TransferStatusSuccess:
|
||||
return chainv1.TransferStatus_TRANSFER_SUCCESS
|
||||
|
||||
case model.TransferStatusFailed:
|
||||
return chainv1.TransferStatus_TRANSFER_FAILED
|
||||
|
||||
case model.TransferStatusCancelled:
|
||||
return chainv1.TransferStatus_TRANSFER_CANCELLED
|
||||
|
||||
default:
|
||||
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.
|
||||
func NativeCurrency(network Network) string {
|
||||
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
||||
@@ -146,3 +188,23 @@ type ServiceWallet struct {
|
||||
Address 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
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
driverDeps := s.driverDeps()
|
||||
chainDriver, err := s.driverForNetwork(network.Name.String())
|
||||
if err != nil {
|
||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
return err
|
||||
}
|
||||
|
||||
destinationAddress, err := s.destinationAddress(ctx, chainDriver, transfer.Destination)
|
||||
if err != nil {
|
||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
return err
|
||||
}
|
||||
|
||||
sourceAddress, err := chainDriver.NormalizeAddress(sourceWallet.DepositAddress)
|
||||
if err != nil {
|
||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||
return err
|
||||
}
|
||||
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("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))
|
||||
}
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -94,15 +97,15 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
||||
return err
|
||||
}
|
||||
|
||||
if receipt != nil && receipt.Status == types.ReceiptStatusSuccessful {
|
||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", txHash); err != nil {
|
||||
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
}
|
||||
return nil
|
||||
failureReason := ""
|
||||
pStatus := model.TransferStatusSuccess
|
||||
if receipt != nil && receipt.Status != types.ReceiptStatusSuccessful {
|
||||
failureReason = "transaction reverted"
|
||||
pStatus = model.TransferStatusFailed
|
||||
}
|
||||
|
||||
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, "transaction reverted", txHash); err != nil {
|
||||
s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||
if _, err := s.updateTransferStatus(ctx, transferRef, pStatus, failureReason, txHash); err != nil {
|
||||
s.logger.Warn("Failed to update transfer status", zap.Error(err),
|
||||
zap.String("transfer_ref", transferRef), zap.String("status", string(pStatus)))
|
||||
}
|
||||
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/mservice"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
)
|
||||
|
||||
type TransferStatus string
|
||||
|
||||
const (
|
||||
TransferStatusPending TransferStatus = "pending"
|
||||
TransferStatusSigning TransferStatus = "signing"
|
||||
TransferStatusSubmitted TransferStatus = "submitted"
|
||||
TransferStatusConfirmed TransferStatus = "confirmed"
|
||||
TransferStatusFailed TransferStatus = "failed"
|
||||
TransferStatusCancelled TransferStatus = "cancelled"
|
||||
TransferStatusCreated TransferStatus = "created" // record exists, not started
|
||||
TransferStatusProcessing TransferStatus = "processing" // we are working on it
|
||||
TransferStatusWaiting TransferStatus = "waiting" // waiting external world
|
||||
|
||||
TransferStatusSuccess TransferStatus = "success" // final success
|
||||
TransferStatusFailed TransferStatus = "failed" // final failure
|
||||
TransferStatusCancelled TransferStatus = "cancelled" // final cancelled
|
||||
)
|
||||
|
||||
// ServiceFee represents a fee component applied to a transfer.
|
||||
@@ -38,21 +40,23 @@ type TransferDestination struct {
|
||||
type Transfer struct {
|
||||
storable.Base `bson:",inline" json:",inline"`
|
||||
|
||||
OperationRef string `bson:"operationRef" json:"operationRef"`
|
||||
TransferRef string `bson:"transferRef" json:"transferRef"`
|
||||
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
|
||||
IntentRef string `bson:"intentRef" json:"intentRef"`
|
||||
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
|
||||
SourceWalletRef string `bson:"sourceWalletRef" json:"sourceWalletRef"`
|
||||
Destination TransferDestination `bson:"destination" json:"destination"`
|
||||
Network string `bson:"network" json:"network"`
|
||||
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
|
||||
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
|
||||
RequestedAmount *moneyv1.Money `bson:"requestedAmount" json:"requestedAmount"`
|
||||
NetAmount *moneyv1.Money `bson:"netAmount" json:"netAmount"`
|
||||
RequestedAmount *paymenttypes.Money `bson:"requestedAmount" json:"requestedAmount"`
|
||||
NetAmount *paymenttypes.Money `bson:"netAmount" json:"netAmount"`
|
||||
Fees []ServiceFee `bson:"fees,omitempty" json:"fees,omitempty"`
|
||||
Status TransferStatus `bson:"status" json:"status"`
|
||||
TxHash string `bson:"txHash,omitempty" json:"txHash,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"`
|
||||
}
|
||||
|
||||
@@ -81,6 +85,7 @@ func (t *Transfer) Normalize() {
|
||||
t.TransferRef = strings.TrimSpace(t.TransferRef)
|
||||
t.IdempotencyKey = strings.TrimSpace(t.IdempotencyKey)
|
||||
t.OrganizationRef = strings.TrimSpace(t.OrganizationRef)
|
||||
t.IntentRef = strings.TrimSpace(t.IntentRef)
|
||||
t.SourceWalletRef = strings.TrimSpace(t.SourceWalletRef)
|
||||
t.Network = strings.TrimSpace(strings.ToLower(t.Network))
|
||||
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.ExternalAddressOriginal = strings.TrimSpace(t.Destination.ExternalAddressOriginal)
|
||||
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")
|
||||
}
|
||||
if transfer.Status == "" {
|
||||
transfer.Status = model.TransferStatusPending
|
||||
transfer.Status = model.TransferStatusCreated
|
||||
}
|
||||
if transfer.LastStatusAt.IsZero() {
|
||||
transfer.LastStatusAt = time.Now().UTC()
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/pkg/ledgerconv"
|
||||
"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"
|
||||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/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()) == "" {
|
||||
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{
|
||||
AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())},
|
||||
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()) == "" {
|
||||
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{
|
||||
AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())},
|
||||
TargetState: connectorv1.AccountState_ACCOUNT_ACTIVE,
|
||||
@@ -430,8 +430,8 @@ func (c *ledgerClient) submitLedgerOperationWithExtras(ctx context.Context, opTy
|
||||
charges []*ledgerv1.PostingLine
|
||||
eventTime *timestamppb.Timestamp
|
||||
contraRef string
|
||||
fromRole model.AccountRole
|
||||
toRole model.AccountRole
|
||||
fromRole account_role.AccountRole
|
||||
toRole account_role.AccountRole
|
||||
)
|
||||
|
||||
switch r := req.(type) {
|
||||
@@ -487,10 +487,10 @@ func (c *ledgerClient) submitLedgerOperationWithExtras(ctx context.Context, opTy
|
||||
op.To = accountParty(toRef)
|
||||
}
|
||||
if fromRole != "" {
|
||||
op.FromRole = model.ToProto(fromRole)
|
||||
op.FromRole = account_role.ToProto(fromRole)
|
||||
}
|
||||
if toRole != "" {
|
||||
op.ToRole = model.ToProto(toRole)
|
||||
op.ToRole = account_role.ToProto(toRole)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func accountRoleFromLedgerProto(role ledgerv1.AccountRole) model.AccountRole {
|
||||
func accountRoleFromLedgerProto(role ledgerv1.AccountRole) account_role.AccountRole {
|
||||
switch role {
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING:
|
||||
return model.AccountRoleOperating
|
||||
return account_role.AccountRoleOperating
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD:
|
||||
return model.AccountRoleHold
|
||||
return account_role.AccountRoleHold
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT:
|
||||
return model.AccountRoleTransit
|
||||
return account_role.AccountRoleTransit
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT:
|
||||
return model.AccountRoleSettlement
|
||||
return account_role.AccountRoleSettlement
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING:
|
||||
return model.AccountRoleClearing
|
||||
return account_role.AccountRoleClearing
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING:
|
||||
return model.AccountRolePending
|
||||
return account_role.AccountRolePending
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE:
|
||||
return model.AccountRoleReserve
|
||||
return account_role.AccountRoleReserve
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY:
|
||||
return model.AccountRoleLiquidity
|
||||
return account_role.AccountRoleLiquidity
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_FEE:
|
||||
return model.AccountRoleFee
|
||||
return account_role.AccountRoleFee
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK:
|
||||
return model.AccountRoleChargeback
|
||||
return account_role.AccountRoleChargeback
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT:
|
||||
return model.AccountRoleAdjustment
|
||||
return account_role.AccountRoleAdjustment
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -49,5 +49,5 @@ require (
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||
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 h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
|
||||
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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/model/account_role"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,7 @@ type Account struct {
|
||||
// Posting policy & lifecycle
|
||||
AllowNegative bool `bson:"allowNegative" json:"allowNegative"`
|
||||
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
|
||||
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 != "" {
|
||||
switch a.Role {
|
||||
case model.AccountRoleOperating,
|
||||
model.AccountRoleHold,
|
||||
model.AccountRoleTransit,
|
||||
model.AccountRoleSettlement,
|
||||
model.AccountRoleClearing,
|
||||
model.AccountRolePending,
|
||||
model.AccountRoleReserve,
|
||||
model.AccountRoleLiquidity,
|
||||
model.AccountRoleFee,
|
||||
model.AccountRoleChargeback,
|
||||
model.AccountRoleAdjustment:
|
||||
case account_role.AccountRoleOperating,
|
||||
account_role.AccountRoleHold,
|
||||
account_role.AccountRoleTransit,
|
||||
account_role.AccountRoleSettlement,
|
||||
account_role.AccountRoleClearing,
|
||||
account_role.AccountRolePending,
|
||||
account_role.AccountRoleReserve,
|
||||
account_role.AccountRoleLiquidity,
|
||||
account_role.AccountRoleFee,
|
||||
account_role.AccountRoleChargeback,
|
||||
account_role.AccountRoleAdjustment:
|
||||
default:
|
||||
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/merrors"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/model/account_role"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
@@ -25,7 +26,7 @@ type createAccountParams struct {
|
||||
currency string
|
||||
modelType pmodel.LedgerAccountType
|
||||
modelStatus pmodel.LedgerAccountStatus
|
||||
modelRole pmodel.AccountRole
|
||||
modelRole account_role.AccountRole
|
||||
}
|
||||
|
||||
// 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.
|
||||
if p.modelRole != pmodel.AccountRoleSettlement {
|
||||
if p.modelRole != account_role.AccountRoleSettlement {
|
||||
if _, err := s.ensureSettlementAccount(ctx, p.orgRef, p.currency); err != nil {
|
||||
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.
|
||||
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 {
|
||||
recordAccountOperation("create", "error")
|
||||
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 {
|
||||
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:
|
||||
return pmodel.AccountRoleHold, nil
|
||||
return account_role.AccountRoleHold, nil
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT:
|
||||
return pmodel.AccountRoleTransit, nil
|
||||
return account_role.AccountRoleTransit, nil
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT:
|
||||
return pmodel.AccountRoleSettlement, nil
|
||||
return account_role.AccountRoleSettlement, nil
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING:
|
||||
return pmodel.AccountRoleClearing, nil
|
||||
return account_role.AccountRoleClearing, nil
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING:
|
||||
return pmodel.AccountRolePending, nil
|
||||
return account_role.AccountRolePending, nil
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE:
|
||||
return pmodel.AccountRoleReserve, nil
|
||||
return account_role.AccountRoleReserve, nil
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY:
|
||||
return pmodel.AccountRoleLiquidity, nil
|
||||
return account_role.AccountRoleLiquidity, nil
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_FEE:
|
||||
return pmodel.AccountRoleFee, nil
|
||||
return account_role.AccountRoleFee, nil
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK:
|
||||
return pmodel.AccountRoleChargeback, nil
|
||||
return account_role.AccountRoleChargeback, nil
|
||||
case ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT:
|
||||
return pmodel.AccountRoleAdjustment, nil
|
||||
return account_role.AccountRoleAdjustment, nil
|
||||
default:
|
||||
return "", merrors.InvalidArgument("invalid account role")
|
||||
}
|
||||
}
|
||||
|
||||
func modelAccountRoleToProto(r pmodel.AccountRole) ledgerv1.AccountRole {
|
||||
func modelAccountRoleToProto(r account_role.AccountRole) ledgerv1.AccountRole {
|
||||
switch r {
|
||||
case pmodel.AccountRoleOperating, "":
|
||||
case account_role.AccountRoleOperating, "":
|
||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING
|
||||
case pmodel.AccountRoleHold:
|
||||
case account_role.AccountRoleHold:
|
||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD
|
||||
case pmodel.AccountRoleTransit:
|
||||
case account_role.AccountRoleTransit:
|
||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT
|
||||
case pmodel.AccountRoleSettlement:
|
||||
case account_role.AccountRoleSettlement:
|
||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT
|
||||
case pmodel.AccountRoleClearing:
|
||||
case account_role.AccountRoleClearing:
|
||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING
|
||||
case pmodel.AccountRolePending:
|
||||
case account_role.AccountRolePending:
|
||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING
|
||||
case pmodel.AccountRoleReserve:
|
||||
case account_role.AccountRoleReserve:
|
||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE
|
||||
case pmodel.AccountRoleLiquidity:
|
||||
case account_role.AccountRoleLiquidity:
|
||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY
|
||||
case pmodel.AccountRoleFee:
|
||||
case account_role.AccountRoleFee:
|
||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_FEE
|
||||
case pmodel.AccountRoleChargeback:
|
||||
case account_role.AccountRoleChargeback:
|
||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK
|
||||
case pmodel.AccountRoleAdjustment:
|
||||
case account_role.AccountRoleAdjustment:
|
||||
return ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT
|
||||
default:
|
||||
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) {
|
||||
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 {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/tech/sendico/ledger/storage"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/model/account_role"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
)
|
||||
|
||||
@@ -21,14 +22,14 @@ type accountStoreStub struct {
|
||||
created []*pmodel.LedgerAccount
|
||||
existing *pmodel.LedgerAccount
|
||||
existingErr error
|
||||
existingByRole map[pmodel.AccountRole]*pmodel.LedgerAccount
|
||||
existingByRole map[account_role.AccountRole]*pmodel.LedgerAccount
|
||||
defaultSettlement *pmodel.LedgerAccount
|
||||
defaultErr error
|
||||
createErrs []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 {
|
||||
return s.createErrSettlement
|
||||
}
|
||||
@@ -66,7 +67,7 @@ func (s *accountStoreStub) Get(context.Context, bson.ObjectID) (*pmodel.LedgerAc
|
||||
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 acc, ok := s.existingByRole[role]; ok {
|
||||
return acc, nil
|
||||
@@ -190,14 +191,14 @@ func TestCreateAccountResponder_AutoCreatesSettlementAccount(t *testing.T) {
|
||||
var settlement *pmodel.LedgerAccount
|
||||
var operating *pmodel.LedgerAccount
|
||||
|
||||
roles := make(map[pmodel.AccountRole]bool)
|
||||
roles := make(map[account_role.AccountRole]bool)
|
||||
for _, acc := range accountStore.created {
|
||||
roles[acc.Role] = true
|
||||
|
||||
if acc.Role == pmodel.AccountRoleSettlement {
|
||||
if acc.Role == account_role.AccountRoleSettlement {
|
||||
settlement = acc
|
||||
}
|
||||
if acc.Role == pmodel.AccountRoleOperating {
|
||||
if acc.Role == account_role.AccountRoleOperating {
|
||||
operating = acc
|
||||
}
|
||||
|
||||
@@ -230,7 +231,7 @@ func TestCreateAccountResponder_AutoCreatesSettlementAccount(t *testing.T) {
|
||||
require.Equal(t, pmodel.LedgerAccountTypeAsset, settlement.AccountType)
|
||||
require.Equal(t, "USD", settlement.Currency)
|
||||
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"])
|
||||
}
|
||||
|
||||
@@ -265,7 +266,7 @@ func TestCreateAccountResponder_RetriesOnConflict(t *testing.T) {
|
||||
|
||||
var createdFee *pmodel.LedgerAccount
|
||||
for _, acc := range accountStore.created {
|
||||
if acc.Role == pmodel.AccountRoleFee {
|
||||
if acc.Role == account_role.AccountRoleFee {
|
||||
createdFee = acc
|
||||
break
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user