Compare commits
17 Commits
cd89171cf0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dedde76dd7 | |||
|
|
9e747e7251 | ||
| 33647a0f3d | |||
|
|
890f78a42e | ||
| c0ba167f69 | |||
|
|
3aa5d56cc3 | ||
| 326fc5a885 | |||
|
|
43edbc109d | ||
| 12700c5595 | |||
|
|
4da9e0b522 | ||
| 5d443230f4 | |||
|
|
3e83cc51d7 | ||
| 9c2ef52d07 | |||
|
|
e84854d875 | ||
|
|
2f34b5a827 | ||
| 9a5c087940 | |||
|
|
4fb2e0433c |
@@ -36,7 +36,7 @@ require (
|
|||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.2.0 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
|||||||
@@ -15,3 +15,6 @@ messaging:
|
|||||||
broker_name: Discovery Service
|
broker_name: Discovery Service
|
||||||
max_reconnects: 10
|
max_reconnects: 10
|
||||||
reconnect_wait: 5
|
reconnect_wait: 5
|
||||||
|
|
||||||
|
registry:
|
||||||
|
kv_ttl_seconds: 3600
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ require (
|
|||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.2.0 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
|||||||
@@ -16,12 +16,17 @@ type config struct {
|
|||||||
Runtime *grpcapp.RuntimeConfig `yaml:"runtime"`
|
Runtime *grpcapp.RuntimeConfig `yaml:"runtime"`
|
||||||
Messaging *msg.Config `yaml:"messaging"`
|
Messaging *msg.Config `yaml:"messaging"`
|
||||||
Metrics *metricsConfig `yaml:"metrics"`
|
Metrics *metricsConfig `yaml:"metrics"`
|
||||||
|
Registry *registryConfig `yaml:"registry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type metricsConfig struct {
|
type metricsConfig struct {
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type registryConfig struct {
|
||||||
|
KVTTLSeconds *int `yaml:"kv_ttl_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Imp) loadConfig() (*config, error) {
|
func (i *Imp) loadConfig() (*config, error) {
|
||||||
data, err := os.ReadFile(i.file)
|
data, err := os.ReadFile(i.file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package serverimp
|
package serverimp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/discovery/internal/appversion"
|
"github.com/tech/sendico/discovery/internal/appversion"
|
||||||
"github.com/tech/sendico/pkg/discovery"
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
@@ -23,7 +25,16 @@ func (i *Imp) startDiscovery(cfg *config) error {
|
|||||||
producer := msgproducer.NewProducer(i.logger.Named("discovery_producer"), broker)
|
producer := msgproducer.NewProducer(i.logger.Named("discovery_producer"), broker)
|
||||||
|
|
||||||
registry := discovery.NewRegistry()
|
registry := discovery.NewRegistry()
|
||||||
svc, err := discovery.NewRegistryService(i.logger, broker, producer, registry, string(mservice.Discovery))
|
var registryOpts []discovery.RegistryOption
|
||||||
|
if cfg.Registry != nil && cfg.Registry.KVTTLSeconds != nil {
|
||||||
|
ttlSeconds := *cfg.Registry.KVTTLSeconds
|
||||||
|
if ttlSeconds < 0 {
|
||||||
|
i.logger.Warn("Discovery registry TTL is negative, disabling TTL", zap.Int("ttl_seconds", ttlSeconds))
|
||||||
|
ttlSeconds = 0
|
||||||
|
}
|
||||||
|
registryOpts = append(registryOpts, discovery.WithRegistryKVTTL(time.Duration(ttlSeconds)*time.Second))
|
||||||
|
}
|
||||||
|
svc, err := discovery.NewRegistryService(i.logger, broker, producer, registry, string(mservice.Discovery), registryOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ require (
|
|||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.2.0 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ require (
|
|||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.2.0 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
|||||||
@@ -373,6 +373,20 @@ func managedWalletFromAccount(account *connectorv1.Account) *chainv1.ManagedWall
|
|||||||
if asset.GetTokenSymbol() == "" {
|
if asset.GetTokenSymbol() == "" {
|
||||||
asset.TokenSymbol = strings.TrimSpace(tokenFromAssetString(account.GetAsset()))
|
asset.TokenSymbol = strings.TrimSpace(tokenFromAssetString(account.GetAsset()))
|
||||||
}
|
}
|
||||||
|
describable := account.GetDescribable()
|
||||||
|
label := strings.TrimSpace(account.GetLabel())
|
||||||
|
if describable == nil {
|
||||||
|
if label != "" {
|
||||||
|
describable = &describablev1.Describable{Name: label}
|
||||||
|
}
|
||||||
|
} else if strings.TrimSpace(describable.GetName()) == "" && label != "" {
|
||||||
|
desc := strings.TrimSpace(describable.GetDescription())
|
||||||
|
if desc == "" {
|
||||||
|
describable = &describablev1.Describable{Name: label}
|
||||||
|
} else {
|
||||||
|
describable = &describablev1.Describable{Name: label, Description: &desc}
|
||||||
|
}
|
||||||
|
}
|
||||||
return &chainv1.ManagedWallet{
|
return &chainv1.ManagedWallet{
|
||||||
WalletRef: walletRef,
|
WalletRef: walletRef,
|
||||||
OrganizationRef: organizationRef,
|
OrganizationRef: organizationRef,
|
||||||
@@ -382,9 +396,7 @@ func managedWalletFromAccount(account *connectorv1.Account) *chainv1.ManagedWall
|
|||||||
Status: managedWalletStatusFromAccount(account.GetState()),
|
Status: managedWalletStatusFromAccount(account.GetState()),
|
||||||
CreatedAt: account.GetCreatedAt(),
|
CreatedAt: account.GetCreatedAt(),
|
||||||
UpdatedAt: account.GetUpdatedAt(),
|
UpdatedAt: account.GetUpdatedAt(),
|
||||||
Describable: &describablev1.Describable{
|
Describable: describable,
|
||||||
Name: strings.TrimSpace(account.GetLabel()),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ require (
|
|||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||||
|
|||||||
@@ -239,8 +239,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ func chainWalletToAccount(wallet *chainv1.ManagedWallet) *connectorv1.Account {
|
|||||||
ProviderDetails: details,
|
ProviderDetails: details,
|
||||||
CreatedAt: wallet.GetCreatedAt(),
|
CreatedAt: wallet.GetCreatedAt(),
|
||||||
UpdatedAt: wallet.GetUpdatedAt(),
|
UpdatedAt: wallet.GetUpdatedAt(),
|
||||||
|
Describable: wallet.GetDescribable(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +395,7 @@ func chainWalletState(status chainv1.ManagedWalletStatus) connectorv1.AccountSta
|
|||||||
|
|
||||||
func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
|
func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
|
||||||
if op == nil {
|
if op == nil {
|
||||||
return nil, fmt.Errorf("transfer: operation is required")
|
return nil, merrors.InvalidArgument("transfer: operation is required")
|
||||||
}
|
}
|
||||||
if to := op.GetTo(); to != nil {
|
if to := op.GetTo(); to != nil {
|
||||||
if account := to.GetAccount(); account != nil {
|
if account := to.GetAccount(); account != nil {
|
||||||
@@ -404,7 +405,7 @@ func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.Trans
|
|||||||
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(ext.GetExternalRef())}}, nil
|
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(ext.GetExternalRef())}}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("transfer: to.account or to.external is required")
|
return nil, merrors.InvalidArgument("transfer: to.account or to.external is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeMoneyForChain(m *moneyv1.Money) *moneyv1.Money {
|
func normalizeMoneyForChain(m *moneyv1.Money) *moneyv1.Money {
|
||||||
@@ -451,12 +452,12 @@ func parseChainFees(reader params.Reader) []*chainv1.ServiceFeeBreakdown {
|
|||||||
|
|
||||||
func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
|
func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
|
||||||
if raw == nil {
|
if raw == nil {
|
||||||
return nil, fmt.Errorf("money is required")
|
return nil, merrors.InvalidArgument("money is required")
|
||||||
}
|
}
|
||||||
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
|
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
|
||||||
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
|
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
|
||||||
if amount == "" || currency == "" {
|
if amount == "" || currency == "" {
|
||||||
return nil, fmt.Errorf("money is required")
|
return nil, merrors.InvalidArgument("money is required")
|
||||||
}
|
}
|
||||||
return &moneyv1.Money{
|
return &moneyv1.Money{
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
@@ -575,11 +576,11 @@ func parseChainAsset(assetString string, reader params.Reader) (*chainv1.Asset,
|
|||||||
network = networkFromAssetString(assetString)
|
network = networkFromAssetString(assetString)
|
||||||
}
|
}
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return nil, fmt.Errorf("asset: token_symbol is required")
|
return nil, merrors.InvalidArgument("asset: token_symbol is required")
|
||||||
}
|
}
|
||||||
chain := shared.ChainEnumFromName(network)
|
chain := shared.ChainEnumFromName(network)
|
||||||
if chain == chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED {
|
if chain == chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED {
|
||||||
return nil, fmt.Errorf("asset: network is required")
|
return nil, merrors.InvalidArgument("asset: network is required")
|
||||||
}
|
}
|
||||||
return &chainv1.Asset{
|
return &chainv1.Asset{
|
||||||
Chain: chain,
|
Chain: chain,
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package shared
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errHexEmpty = errors.New("hex value is empty")
|
errHexEmpty = merrors.InvalidArgument("hex value is empty")
|
||||||
errHexInvalid = errors.New("invalid hex number")
|
errHexInvalid = merrors.InvalidArgument("invalid hex number")
|
||||||
errHexOutOfRange = errors.New("hex number out of range")
|
errHexOutOfRange = merrors.InvalidArgument("hex number out of range")
|
||||||
)
|
)
|
||||||
|
|
||||||
// DecodeHexBig parses a hex string that may include leading zero digits.
|
// DecodeHexBig parses a hex string that may include leading zero digits.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ require (
|
|||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
@@ -125,6 +125,8 @@ github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/i
|
|||||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ func (i *Imp) resolveCallbackConfig(cfg callbackConfig) (callbackRuntimeConfig,
|
|||||||
|
|
||||||
func (i *Imp) startHTTPCallbackServer(svc *mntxservice.Service, cfg callbackRuntimeConfig) error {
|
func (i *Imp) startHTTPCallbackServer(svc *mntxservice.Service, cfg callbackRuntimeConfig) error {
|
||||||
if svc == nil {
|
if svc == nil {
|
||||||
return errors.New("nil service provided for callback server")
|
return merrors.InvalidArgument("nil service provided for callback server")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(cfg.Address) == "" {
|
if strings.TrimSpace(cfg.Address) == "" {
|
||||||
i.logger.Info("Monetix callback server disabled: address is empty")
|
i.logger.Info("Monetix callback server disabled: address is empty")
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package gateway
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
@@ -68,13 +67,13 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(reader.String("card_token")) != "" {
|
if strings.TrimSpace(reader.String("card_token")) != "" {
|
||||||
resp, err := s.CreateCardTokenPayout(ctx, buildCardTokenPayoutRequest(reader, payoutID, amountMinor, currency))
|
resp, err := s.CreateCardTokenPayout(ctx, buildCardTokenPayoutRequestFromParams(reader, payoutID, amountMinor, currency))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
}
|
}
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: payoutReceipt(resp.GetPayout())}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: payoutReceipt(resp.GetPayout())}, nil
|
||||||
}
|
}
|
||||||
resp, err := s.CreateCardPayout(ctx, buildCardPayoutRequest(reader, payoutID, amountMinor, currency))
|
resp, err := s.CreateCardPayout(ctx, buildCardPayoutRequestFromParams(reader, payoutID, amountMinor, currency))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
}
|
}
|
||||||
@@ -124,26 +123,26 @@ func mntxOperationParams() []*connectorv1.OperationParamSpec {
|
|||||||
|
|
||||||
func payoutAmount(op *connectorv1.Operation, reader params.Reader) (int64, string, error) {
|
func payoutAmount(op *connectorv1.Operation, reader params.Reader) (int64, string, error) {
|
||||||
if op == nil {
|
if op == nil {
|
||||||
return 0, "", fmt.Errorf("payout: operation is required")
|
return 0, "", merrors.InvalidArgument("payout: operation is required")
|
||||||
}
|
}
|
||||||
currency := currencyFromOperation(op)
|
currency := currencyFromOperation(op)
|
||||||
if currency == "" {
|
if currency == "" {
|
||||||
return 0, "", fmt.Errorf("payout: currency is required")
|
return 0, "", merrors.InvalidArgument("payout: currency is required")
|
||||||
}
|
}
|
||||||
if minor, ok := reader.Int64("amount_minor"); ok && minor > 0 {
|
if minor, ok := reader.Int64("amount_minor"); ok && minor > 0 {
|
||||||
return minor, currency, nil
|
return minor, currency, nil
|
||||||
}
|
}
|
||||||
money := op.GetMoney()
|
money := op.GetMoney()
|
||||||
if money == nil {
|
if money == nil {
|
||||||
return 0, "", fmt.Errorf("payout: money is required")
|
return 0, "", merrors.InvalidArgument("payout: money is required")
|
||||||
}
|
}
|
||||||
amount := strings.TrimSpace(money.GetAmount())
|
amount := strings.TrimSpace(money.GetAmount())
|
||||||
if amount == "" {
|
if amount == "" {
|
||||||
return 0, "", fmt.Errorf("payout: amount is required")
|
return 0, "", merrors.InvalidArgument("payout: amount is required")
|
||||||
}
|
}
|
||||||
dec, err := decimal.NewFromString(amount)
|
dec, err := decimal.NewFromString(amount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", fmt.Errorf("payout: invalid amount")
|
return 0, "", merrors.InvalidArgument("payout: invalid amount")
|
||||||
}
|
}
|
||||||
minor := dec.Mul(decimal.NewFromInt(100)).IntPart()
|
minor := dec.Mul(decimal.NewFromInt(100)).IntPart()
|
||||||
return minor, currency, nil
|
return minor, currency, nil
|
||||||
@@ -160,7 +159,7 @@ func currencyFromOperation(op *connectorv1.Operation) string {
|
|||||||
return strings.ToUpper(currency)
|
return strings.ToUpper(currency)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCardTokenPayoutRequest(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardTokenPayoutRequest {
|
func buildCardTokenPayoutRequestFromParams(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardTokenPayoutRequest {
|
||||||
req := &mntxv1.CardTokenPayoutRequest{
|
req := &mntxv1.CardTokenPayoutRequest{
|
||||||
PayoutId: payoutID,
|
PayoutId: payoutID,
|
||||||
ProjectId: readerInt64(reader, "project_id"),
|
ProjectId: readerInt64(reader, "project_id"),
|
||||||
@@ -184,7 +183,7 @@ func buildCardTokenPayoutRequest(reader params.Reader, payoutID string, amountMi
|
|||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCardPayoutRequest(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardPayoutRequest {
|
func buildCardPayoutRequestFromParams(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardPayoutRequest {
|
||||||
return &mntxv1.CardPayoutRequest{
|
return &mntxv1.CardPayoutRequest{
|
||||||
PayoutId: payoutID,
|
PayoutId: payoutID,
|
||||||
ProjectId: readerInt64(reader, "project_id"),
|
ProjectId: readerInt64(reader, "project_id"),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListGatewayInstances exposes the Monetix gateway instance descriptors.
|
// ListGatewayInstances exposes the Monetix gateway instance descriptors.
|
||||||
@@ -25,16 +26,15 @@ func cloneGatewayDescriptor(src *gatewayv1.GatewayInstanceDescriptor) *gatewayv1
|
|||||||
if src == nil {
|
if src == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cp := *src
|
cp := proto.Clone(src).(*gatewayv1.GatewayInstanceDescriptor)
|
||||||
if src.Currencies != nil {
|
if src.Currencies != nil {
|
||||||
cp.Currencies = append([]string(nil), src.Currencies...)
|
cp.Currencies = append([]string(nil), src.Currencies...)
|
||||||
}
|
}
|
||||||
if src.Capabilities != nil {
|
if src.Capabilities != nil {
|
||||||
cap := *src.Capabilities
|
cp.Capabilities = proto.Clone(src.Capabilities).(*gatewayv1.RailCapabilities)
|
||||||
cp.Capabilities = &cap
|
|
||||||
}
|
}
|
||||||
if src.Limits != nil {
|
if src.Limits != nil {
|
||||||
limits := *src.Limits
|
limits := &gatewayv1.Limits{}
|
||||||
if src.Limits.VolumeLimit != nil {
|
if src.Limits.VolumeLimit != nil {
|
||||||
limits.VolumeLimit = map[string]string{}
|
limits.VolumeLimit = map[string]string{}
|
||||||
for key, value := range src.Limits.VolumeLimit {
|
for key, value := range src.Limits.VolumeLimit {
|
||||||
@@ -53,11 +53,10 @@ func cloneGatewayDescriptor(src *gatewayv1.GatewayInstanceDescriptor) *gatewayv1
|
|||||||
if value == nil {
|
if value == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
clone := *value
|
limits.CurrencyLimits[key] = proto.Clone(value).(*gatewayv1.LimitsOverride)
|
||||||
limits.CurrencyLimits[key] = &clone
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cp.Limits = &limits
|
cp.Limits = limits
|
||||||
}
|
}
|
||||||
return &cp
|
return cp
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ require (
|
|||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.2.0 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package gateway
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/connector/params"
|
"github.com/tech/sendico/pkg/connector/params"
|
||||||
@@ -139,7 +138,7 @@ func tgsettleOperationParams() []*connectorv1.OperationParamSpec {
|
|||||||
|
|
||||||
func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
|
func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
|
||||||
if op == nil {
|
if op == nil {
|
||||||
return nil, fmt.Errorf("transfer: operation is required")
|
return nil, merrors.InvalidArgument("transfer: operation is required")
|
||||||
}
|
}
|
||||||
if to := op.GetTo(); to != nil {
|
if to := op.GetTo(); to != nil {
|
||||||
if account := to.GetAccount(); account != nil {
|
if account := to.GetAccount(); account != nil {
|
||||||
@@ -149,7 +148,7 @@ func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.Trans
|
|||||||
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(ext.GetExternalRef())}}, nil
|
return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(ext.GetExternalRef())}}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("transfer: to.account or to.external is required")
|
return nil, merrors.InvalidArgument("transfer: to.account or to.external is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeMoneyForTransfer(m *moneyv1.Money) *moneyv1.Money {
|
func normalizeMoneyForTransfer(m *moneyv1.Money) *moneyv1.Money {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/tgsettle/storage/model"
|
"github.com/tech/sendico/gateway/tgsettle/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrDuplicate = errors.New("payment gateway storage: duplicate record")
|
var ErrDuplicate = merrors.DataConflict("payment gateway storage: duplicate record")
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
Payments() PaymentsStore
|
Payments() PaymentsStore
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/payments/rail"
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
@@ -196,12 +197,23 @@ func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAc
|
|||||||
"allow_negative": req.GetAllowNegative(),
|
"allow_negative": req.GetAllowNegative(),
|
||||||
"is_settlement": req.GetIsSettlement(),
|
"is_settlement": req.GetIsSettlement(),
|
||||||
}
|
}
|
||||||
|
label := ""
|
||||||
|
if desc := req.GetDescribable(); desc != nil {
|
||||||
|
label = strings.TrimSpace(desc.GetName())
|
||||||
|
if desc.Description != nil {
|
||||||
|
trimmed := strings.TrimSpace(desc.GetDescription())
|
||||||
|
if trimmed != "" {
|
||||||
|
params["description"] = trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(req.GetMetadata()) > 0 {
|
if len(req.GetMetadata()) > 0 {
|
||||||
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
||||||
}
|
}
|
||||||
resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{
|
resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{
|
||||||
Kind: connectorv1.AccountKind_LEDGER_ACCOUNT,
|
Kind: connectorv1.AccountKind_LEDGER_ACCOUNT,
|
||||||
Asset: strings.TrimSpace(req.GetCurrency()),
|
Asset: strings.TrimSpace(req.GetCurrency()),
|
||||||
|
Label: label,
|
||||||
Params: structFromMap(params),
|
Params: structFromMap(params),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -469,6 +481,18 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
|
|||||||
if ref := account.GetRef(); ref != nil {
|
if ref := account.GetRef(); ref != nil {
|
||||||
accountID = strings.TrimSpace(ref.GetAccountId())
|
accountID = strings.TrimSpace(ref.GetAccountId())
|
||||||
}
|
}
|
||||||
|
describable := account.GetDescribable()
|
||||||
|
label := strings.TrimSpace(account.GetLabel())
|
||||||
|
if describable == nil && label != "" {
|
||||||
|
describable = &describablev1.Describable{Name: label}
|
||||||
|
} else if describable != nil && strings.TrimSpace(describable.GetName()) == "" && label != "" {
|
||||||
|
desc := strings.TrimSpace(describable.GetDescription())
|
||||||
|
if desc == "" {
|
||||||
|
describable = &describablev1.Describable{Name: label}
|
||||||
|
} else {
|
||||||
|
describable = &describablev1.Describable{Name: label, Description: &desc}
|
||||||
|
}
|
||||||
|
}
|
||||||
return &ledgerv1.LedgerAccount{
|
return &ledgerv1.LedgerAccount{
|
||||||
LedgerAccountRef: accountID,
|
LedgerAccountRef: accountID,
|
||||||
OrganizationRef: strings.TrimSpace(account.GetOwnerRef()),
|
OrganizationRef: strings.TrimSpace(account.GetOwnerRef()),
|
||||||
@@ -480,6 +504,7 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
|
|||||||
IsSettlement: isSettlement,
|
IsSettlement: isSettlement,
|
||||||
CreatedAt: account.GetCreatedAt(),
|
CreatedAt: account.GetCreatedAt(),
|
||||||
UpdatedAt: account.GetUpdatedAt(),
|
UpdatedAt: account.GetUpdatedAt(),
|
||||||
|
Describable: describable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ require (
|
|||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.2.0 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
|||||||
@@ -3,13 +3,18 @@ package ledger
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/ledger/storage"
|
||||||
"github.com/tech/sendico/ledger/storage/model"
|
"github.com/tech/sendico/ledger/storage/model"
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,11 +62,19 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !req.GetIsSettlement() {
|
||||||
|
if _, err := s.ensureSettlementAccount(ctx, orgRef, currency); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
metadata := req.GetMetadata()
|
metadata := req.GetMetadata()
|
||||||
if len(metadata) == 0 {
|
if len(metadata) == 0 {
|
||||||
metadata = nil
|
metadata = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describable := describableFromProto(req.GetDescribable())
|
||||||
|
|
||||||
account := &model.Account{
|
account := &model.Account{
|
||||||
AccountCode: accountCode,
|
AccountCode: accountCode,
|
||||||
Currency: currency,
|
Currency: currency,
|
||||||
@@ -71,6 +84,9 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
|
|||||||
IsSettlement: req.GetIsSettlement(),
|
IsSettlement: req.GetIsSettlement(),
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
|
if describable != nil {
|
||||||
|
account.Describable = *describable
|
||||||
|
}
|
||||||
account.OrganizationRef = orgRef
|
account.OrganizationRef = orgRef
|
||||||
|
|
||||||
err = s.storage.Accounts().Create(ctx, account)
|
err = s.storage.Accounts().Create(ctx, account)
|
||||||
@@ -204,5 +220,115 @@ func toProtoAccount(account *model.Account) *ledgerv1.LedgerAccount {
|
|||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
UpdatedAt: updatedAt,
|
UpdatedAt: updatedAt,
|
||||||
|
Describable: describableToProto(account.Describable),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func describableFromProto(desc *describablev1.Describable) *pmodel.Describable {
|
||||||
|
if desc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(desc.GetName())
|
||||||
|
var description *string
|
||||||
|
if desc.Description != nil {
|
||||||
|
trimmed := strings.TrimSpace(desc.GetDescription())
|
||||||
|
if trimmed != "" {
|
||||||
|
description = &trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name == "" && description == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &pmodel.Describable{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func describableToProto(desc pmodel.Describable) *describablev1.Describable {
|
||||||
|
name := strings.TrimSpace(desc.Name)
|
||||||
|
var description *string
|
||||||
|
if desc.Description != nil {
|
||||||
|
trimmed := strings.TrimSpace(*desc.Description)
|
||||||
|
if trimmed != "" {
|
||||||
|
description = &trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name == "" && description == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &describablev1.Describable{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ensureSettlementAccount(ctx context.Context, orgRef primitive.ObjectID, currency string) (*model.Account, error) {
|
||||||
|
if s.storage == nil || s.storage.Accounts() == nil {
|
||||||
|
return nil, errStorageNotInitialized
|
||||||
|
}
|
||||||
|
normalizedCurrency := strings.ToUpper(strings.TrimSpace(currency))
|
||||||
|
if normalizedCurrency == "" {
|
||||||
|
return nil, merrors.InvalidArgument("currency is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := s.storage.Accounts().GetDefaultSettlement(ctx, orgRef, normalizedCurrency)
|
||||||
|
if err == nil {
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, storage.ErrAccountNotFound) {
|
||||||
|
s.logger.Warn("failed to resolve default settlement account",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("organizationRef", orgRef.Hex()),
|
||||||
|
zap.String("currency", normalizedCurrency))
|
||||||
|
return nil, merrors.Internal("failed to resolve settlement account")
|
||||||
|
}
|
||||||
|
|
||||||
|
accountCode := defaultSettlementAccountCode(normalizedCurrency)
|
||||||
|
description := "Auto-created default settlement account"
|
||||||
|
account = &model.Account{
|
||||||
|
AccountCode: accountCode,
|
||||||
|
AccountType: model.AccountTypeAsset,
|
||||||
|
Currency: normalizedCurrency,
|
||||||
|
Status: model.AccountStatusActive,
|
||||||
|
AllowNegative: true,
|
||||||
|
IsSettlement: true,
|
||||||
|
}
|
||||||
|
account.OrganizationRef = orgRef
|
||||||
|
account.Name = fmt.Sprintf("Settlement %s", normalizedCurrency)
|
||||||
|
account.Description = &description
|
||||||
|
|
||||||
|
if err := s.storage.Accounts().Create(ctx, account); err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrDataConflict) {
|
||||||
|
existing, lookupErr := s.storage.Accounts().GetDefaultSettlement(ctx, orgRef, normalizedCurrency)
|
||||||
|
if lookupErr == nil && existing != nil {
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
|
s.logger.Warn("duplicate settlement account create but failed to load existing",
|
||||||
|
zap.Error(lookupErr),
|
||||||
|
zap.String("organizationRef", orgRef.Hex()),
|
||||||
|
zap.String("currency", normalizedCurrency))
|
||||||
|
return nil, merrors.Internal("failed to resolve settlement account after conflict")
|
||||||
|
}
|
||||||
|
s.logger.Warn("failed to create default settlement account",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("organizationRef", orgRef.Hex()),
|
||||||
|
zap.String("currency", normalizedCurrency),
|
||||||
|
zap.String("accountCode", accountCode))
|
||||||
|
return nil, merrors.Internal("failed to create settlement account")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("default settlement account created",
|
||||||
|
zap.String("organizationRef", orgRef.Hex()),
|
||||||
|
zap.String("currency", normalizedCurrency),
|
||||||
|
zap.String("accountCode", accountCode))
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultSettlementAccountCode(currency string) string {
|
||||||
|
cleaned := strings.ToLower(strings.TrimSpace(currency))
|
||||||
|
if cleaned == "" {
|
||||||
|
return "asset:settlement"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("asset:settlement:%s", cleaned)
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,14 +16,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type accountStoreStub struct {
|
type accountStoreStub struct {
|
||||||
createErr error
|
createErr error
|
||||||
created []*model.Account
|
createErrSettlement error
|
||||||
existing *model.Account
|
created []*model.Account
|
||||||
existingErr error
|
existing *model.Account
|
||||||
|
existingErr error
|
||||||
|
defaultSettlement *model.Account
|
||||||
|
defaultErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *accountStoreStub) Create(_ context.Context, account *model.Account) error {
|
func (s *accountStoreStub) Create(_ context.Context, account *model.Account) error {
|
||||||
if s.createErr != nil {
|
if account.IsSettlement {
|
||||||
|
if s.createErrSettlement != nil {
|
||||||
|
return s.createErrSettlement
|
||||||
|
}
|
||||||
|
} else if s.createErr != nil {
|
||||||
return s.createErr
|
return s.createErr
|
||||||
}
|
}
|
||||||
if account.GetID() == nil || account.GetID().IsZero() {
|
if account.GetID() == nil || account.GetID().IsZero() {
|
||||||
@@ -47,6 +54,12 @@ func (s *accountStoreStub) Get(context.Context, primitive.ObjectID) (*model.Acco
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *accountStoreStub) GetDefaultSettlement(context.Context, primitive.ObjectID, string) (*model.Account, error) {
|
func (s *accountStoreStub) GetDefaultSettlement(context.Context, primitive.ObjectID, string) (*model.Account, error) {
|
||||||
|
if s.defaultErr != nil {
|
||||||
|
return nil, s.defaultErr
|
||||||
|
}
|
||||||
|
if s.defaultSettlement != nil {
|
||||||
|
return s.defaultSettlement, nil
|
||||||
|
}
|
||||||
return nil, storage.ErrAccountNotFound
|
return nil, storage.ErrAccountNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +117,47 @@ func TestCreateAccountResponder_Success(t *testing.T) {
|
|||||||
require.Len(t, accountStore.created, 1)
|
require.Len(t, accountStore.created, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateAccountResponder_AutoCreatesSettlementAccount(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
orgRef := primitive.NewObjectID()
|
||||||
|
|
||||||
|
accountStore := &accountStoreStub{}
|
||||||
|
svc := &Service{
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
storage: &repositoryStub{accounts: accountStore},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &ledgerv1.CreateAccountRequest{
|
||||||
|
OrganizationRef: orgRef.Hex(),
|
||||||
|
AccountCode: "liability:customer:1",
|
||||||
|
AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY,
|
||||||
|
Currency: "usd",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := svc.createAccountResponder(context.Background(), req)(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
require.NotNil(t, resp.Account)
|
||||||
|
require.Len(t, accountStore.created, 2)
|
||||||
|
|
||||||
|
var settlement *model.Account
|
||||||
|
var created *model.Account
|
||||||
|
for _, acc := range accountStore.created {
|
||||||
|
if acc.IsSettlement {
|
||||||
|
settlement = acc
|
||||||
|
}
|
||||||
|
if acc.AccountCode == "liability:customer:1" {
|
||||||
|
created = acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, settlement)
|
||||||
|
require.NotNil(t, created)
|
||||||
|
require.Equal(t, defaultSettlementAccountCode("USD"), settlement.AccountCode)
|
||||||
|
require.Equal(t, model.AccountTypeAsset, settlement.AccountType)
|
||||||
|
require.Equal(t, "USD", settlement.Currency)
|
||||||
|
require.True(t, settlement.AllowNegative)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateAccountResponder_DuplicateReturnsExisting(t *testing.T) {
|
func TestCreateAccountResponder_DuplicateReturnsExisting(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import (
|
|||||||
"github.com/tech/sendico/ledger/internal/appversion"
|
"github.com/tech/sendico/ledger/internal/appversion"
|
||||||
"github.com/tech/sendico/pkg/connector/params"
|
"github.com/tech/sendico/pkg/connector/params"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
const ledgerConnectorID = "ledger"
|
const ledgerConnectorID = "ledger"
|
||||||
|
|
||||||
type connectorAdapter struct {
|
type connectorAdapter struct {
|
||||||
|
connectorv1.UnimplementedConnectorServiceServer
|
||||||
svc *Service
|
svc *Service
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,9 +32,9 @@ func newConnectorAdapter(svc *Service) *connectorAdapter {
|
|||||||
func (c *connectorAdapter) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
func (c *connectorAdapter) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
||||||
return &connectorv1.GetCapabilitiesResponse{
|
return &connectorv1.GetCapabilitiesResponse{
|
||||||
Capabilities: &connectorv1.ConnectorCapabilities{
|
Capabilities: &connectorv1.ConnectorCapabilities{
|
||||||
ConnectorType: ledgerConnectorID,
|
ConnectorType: ledgerConnectorID,
|
||||||
Version: appversion.Create().Short(),
|
Version: appversion.Create().Short(),
|
||||||
SupportedAccountKinds: []connectorv1.AccountKind{connectorv1.AccountKind_LEDGER_ACCOUNT},
|
SupportedAccountKinds: []connectorv1.AccountKind{connectorv1.AccountKind_LEDGER_ACCOUNT},
|
||||||
SupportedOperationTypes: []connectorv1.OperationType{
|
SupportedOperationTypes: []connectorv1.OperationType{
|
||||||
connectorv1.OperationType_CREDIT,
|
connectorv1.OperationType_CREDIT,
|
||||||
connectorv1.OperationType_DEBIT,
|
connectorv1.OperationType_DEBIT,
|
||||||
@@ -75,6 +77,7 @@ func (c *connectorAdapter) OpenAccount(ctx context.Context, req *connectorv1.Ope
|
|||||||
|
|
||||||
status := parseLedgerAccountStatus(reader, "status")
|
status := parseLedgerAccountStatus(reader, "status")
|
||||||
metadata := mergeMetadata(reader.StringMap("metadata"), req.GetLabel(), req.GetOwnerRef(), req.GetCorrelationId(), req.GetParentIntentId())
|
metadata := mergeMetadata(reader.StringMap("metadata"), req.GetLabel(), req.GetOwnerRef(), req.GetCorrelationId(), req.GetParentIntentId())
|
||||||
|
describable := describableFromLabel(req.GetLabel(), reader.String("description"))
|
||||||
|
|
||||||
resp, err := c.svc.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
|
resp, err := c.svc.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
|
||||||
OrganizationRef: orgRef,
|
OrganizationRef: orgRef,
|
||||||
@@ -85,6 +88,7 @@ func (c *connectorAdapter) OpenAccount(ctx context.Context, req *connectorv1.Ope
|
|||||||
AllowNegative: reader.Bool("allow_negative"),
|
AllowNegative: reader.Bool("allow_negative"),
|
||||||
IsSettlement: reader.Bool("is_settlement"),
|
IsSettlement: reader.Bool("is_settlement"),
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
|
Describable: describable,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, "")}, nil
|
return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, "")}, nil
|
||||||
@@ -142,10 +146,10 @@ func (c *connectorAdapter) GetBalance(ctx context.Context, req *connectorv1.GetB
|
|||||||
}
|
}
|
||||||
return &connectorv1.GetBalanceResponse{
|
return &connectorv1.GetBalanceResponse{
|
||||||
Balance: &connectorv1.Balance{
|
Balance: &connectorv1.Balance{
|
||||||
AccountRef: req.GetAccountRef(),
|
AccountRef: req.GetAccountRef(),
|
||||||
Available: resp.GetBalance(),
|
Available: resp.GetBalance(),
|
||||||
CalculatedAt: resp.GetLastUpdated(),
|
CalculatedAt: resp.GetLastUpdated(),
|
||||||
PendingInbound: nil,
|
PendingInbound: nil,
|
||||||
PendingOutbound: nil,
|
PendingOutbound: nil,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -221,15 +225,15 @@ func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1
|
|||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: from.account and to.account are required", op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: from.account and to.account are required", op, "")}}, nil
|
||||||
}
|
}
|
||||||
resp, err := c.svc.TransferInternal(ctx, &ledgerv1.TransferRequest{
|
resp, err := c.svc.TransferInternal(ctx, &ledgerv1.TransferRequest{
|
||||||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
OrganizationRef: orgRef,
|
OrganizationRef: orgRef,
|
||||||
FromLedgerAccountRef: fromID,
|
FromLedgerAccountRef: fromID,
|
||||||
ToLedgerAccountRef: toID,
|
ToLedgerAccountRef: toID,
|
||||||
Money: op.GetMoney(),
|
Money: op.GetMoney(),
|
||||||
Description: description,
|
Description: description,
|
||||||
Charges: charges,
|
Charges: charges,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
EventTime: eventTime,
|
EventTime: eventTime,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
@@ -246,17 +250,17 @@ func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1
|
|||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||||
}
|
}
|
||||||
resp, err := c.svc.ApplyFXWithCharges(ctx, &ledgerv1.FXRequest{
|
resp, err := c.svc.ApplyFXWithCharges(ctx, &ledgerv1.FXRequest{
|
||||||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||||
OrganizationRef: orgRef,
|
OrganizationRef: orgRef,
|
||||||
FromLedgerAccountRef: fromID,
|
FromLedgerAccountRef: fromID,
|
||||||
ToLedgerAccountRef: toID,
|
ToLedgerAccountRef: toID,
|
||||||
FromMoney: op.GetMoney(),
|
FromMoney: op.GetMoney(),
|
||||||
ToMoney: toMoney,
|
ToMoney: toMoney,
|
||||||
Rate: strings.TrimSpace(reader.String("rate")),
|
Rate: strings.TrimSpace(reader.String("rate")),
|
||||||
Description: description,
|
Description: description,
|
||||||
Charges: charges,
|
Charges: charges,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
EventTime: eventTime,
|
EventTime: eventTime,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
@@ -333,12 +337,13 @@ func ledgerAccountToConnector(account *ledgerv1.LedgerAccount) *connectorv1.Acco
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
details, _ := structpb.NewStruct(map[string]interface{}{
|
details, _ := structpb.NewStruct(map[string]interface{}{
|
||||||
"account_code": account.GetAccountCode(),
|
"account_code": account.GetAccountCode(),
|
||||||
"account_type": account.GetAccountType().String(),
|
"account_type": account.GetAccountType().String(),
|
||||||
"status": account.GetStatus().String(),
|
"status": account.GetStatus().String(),
|
||||||
"allow_negative": account.GetAllowNegative(),
|
"allow_negative": account.GetAllowNegative(),
|
||||||
"is_settlement": account.GetIsSettlement(),
|
"is_settlement": account.GetIsSettlement(),
|
||||||
})
|
})
|
||||||
|
describable := ledgerAccountDescribable(account)
|
||||||
return &connectorv1.Account{
|
return &connectorv1.Account{
|
||||||
Ref: &connectorv1.AccountRef{
|
Ref: &connectorv1.AccountRef{
|
||||||
ConnectorId: ledgerConnectorID,
|
ConnectorId: ledgerConnectorID,
|
||||||
@@ -352,6 +357,7 @@ func ledgerAccountToConnector(account *ledgerv1.LedgerAccount) *connectorv1.Acco
|
|||||||
ProviderDetails: details,
|
ProviderDetails: details,
|
||||||
CreatedAt: account.GetCreatedAt(),
|
CreatedAt: account.GetCreatedAt(),
|
||||||
UpdatedAt: account.GetUpdatedAt(),
|
UpdatedAt: account.GetUpdatedAt(),
|
||||||
|
Describable: describable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +372,71 @@ func ledgerAccountState(status ledgerv1.AccountStatus) connectorv1.AccountState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ledgerAccountDescribable(account *ledgerv1.LedgerAccount) *describablev1.Describable {
|
||||||
|
if account == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if desc := cleanedDescribable(account.GetDescribable()); desc != nil {
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
metadata := account.GetMetadata()
|
||||||
|
name := ""
|
||||||
|
if metadata != nil {
|
||||||
|
if v := strings.TrimSpace(metadata["name"]); v != "" {
|
||||||
|
name = v
|
||||||
|
} else if v := strings.TrimSpace(metadata["label"]); v != "" {
|
||||||
|
name = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
name = strings.TrimSpace(account.GetAccountCode())
|
||||||
|
}
|
||||||
|
desc := ""
|
||||||
|
if metadata != nil {
|
||||||
|
desc = strings.TrimSpace(metadata["description"])
|
||||||
|
}
|
||||||
|
if name == "" && desc == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if desc == "" {
|
||||||
|
return &describablev1.Describable{Name: name}
|
||||||
|
}
|
||||||
|
return &describablev1.Describable{Name: name, Description: &desc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func describableFromLabel(label, description string) *describablev1.Describable {
|
||||||
|
label = strings.TrimSpace(label)
|
||||||
|
description = strings.TrimSpace(description)
|
||||||
|
if label == "" && description == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if description == "" {
|
||||||
|
return &describablev1.Describable{Name: label}
|
||||||
|
}
|
||||||
|
return &describablev1.Describable{Name: label, Description: &description}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanedDescribable(desc *describablev1.Describable) *describablev1.Describable {
|
||||||
|
if desc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(desc.GetName())
|
||||||
|
var description *string
|
||||||
|
if desc.Description != nil {
|
||||||
|
trimmed := strings.TrimSpace(desc.GetDescription())
|
||||||
|
if trimmed != "" {
|
||||||
|
description = &trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name == "" && description == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &describablev1.Describable{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ledgerReceipt(ref string, status connectorv1.OperationStatus) *connectorv1.OperationReceipt {
|
func ledgerReceipt(ref string, status connectorv1.OperationStatus) *connectorv1.OperationReceipt {
|
||||||
return &connectorv1.OperationReceipt{
|
return &connectorv1.OperationReceipt{
|
||||||
OperationId: strings.TrimSpace(ref),
|
OperationId: strings.TrimSpace(ref),
|
||||||
@@ -453,7 +524,7 @@ func operationAccountID(party *connectorv1.OperationParty) string {
|
|||||||
func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountType, error) {
|
func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountType, error) {
|
||||||
value, ok := reader.Value(key)
|
value, ok := reader.Value(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: account_type is required")
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: account_type is required")
|
||||||
}
|
}
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case string:
|
case string:
|
||||||
@@ -465,7 +536,7 @@ func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountT
|
|||||||
case int64:
|
case int64:
|
||||||
return ledgerv1.AccountType(v), nil
|
return ledgerv1.AccountType(v), nil
|
||||||
default:
|
default:
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: account_type is required")
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: account_type is required")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,7 +551,7 @@ func parseLedgerAccountTypeString(value string) (ledgerv1.AccountType, error) {
|
|||||||
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
|
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, nil
|
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, nil
|
||||||
default:
|
default:
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: invalid account_type")
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: invalid account_type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,15 +588,15 @@ func parseLedgerCharges(reader params.Reader) ([]*ledgerv1.PostingLine, error) {
|
|||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
raw, ok := item.(map[string]interface{})
|
raw, ok := item.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("charges[%d]: invalid charge entry", i)
|
return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: invalid charge entry", i))
|
||||||
}
|
}
|
||||||
accountRef := strings.TrimSpace(fmt.Sprint(raw["ledger_account_ref"]))
|
accountRef := strings.TrimSpace(fmt.Sprint(raw["ledger_account_ref"]))
|
||||||
if accountRef == "" {
|
if accountRef == "" {
|
||||||
return nil, fmt.Errorf("charges[%d]: ledger_account_ref is required", i)
|
return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: ledger_account_ref is required", i))
|
||||||
}
|
}
|
||||||
money, err := parseMoneyFromMap(raw)
|
money, err := parseMoneyFromMap(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("charges[%d]: %w", i, err)
|
return nil, merrors.InvalidArgumentWrap(err, fmt.Sprintf("charges[%d]: invalid money", i))
|
||||||
}
|
}
|
||||||
lineType := parseLedgerLineType(fmt.Sprint(raw["line_type"]))
|
lineType := parseLedgerLineType(fmt.Sprint(raw["line_type"]))
|
||||||
result = append(result, &ledgerv1.PostingLine{
|
result = append(result, &ledgerv1.PostingLine{
|
||||||
@@ -552,12 +623,12 @@ func parseLedgerLineType(value string) ledgerv1.LineType {
|
|||||||
|
|
||||||
func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
|
func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
|
||||||
if raw == nil {
|
if raw == nil {
|
||||||
return nil, fmt.Errorf("money is required")
|
return nil, merrors.InvalidArgument("money is required")
|
||||||
}
|
}
|
||||||
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
|
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
|
||||||
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
|
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
|
||||||
if amount == "" || currency == "" {
|
if amount == "" || currency == "" {
|
||||||
return nil, fmt.Errorf("money is required")
|
return nil, merrors.InvalidArgument("money is required")
|
||||||
}
|
}
|
||||||
return &moneyv1.Money{
|
return &moneyv1.Money{
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
@@ -594,9 +665,9 @@ func mergeMetadata(base map[string]string, label, ownerRef, correlationID, paren
|
|||||||
|
|
||||||
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
|
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
|
||||||
err := &connectorv1.ConnectorError{
|
err := &connectorv1.ConnectorError{
|
||||||
Code: code,
|
Code: code,
|
||||||
Message: strings.TrimSpace(message),
|
Message: strings.TrimSpace(message),
|
||||||
AccountId: strings.TrimSpace(accountID),
|
AccountId: strings.TrimSpace(accountID),
|
||||||
}
|
}
|
||||||
if op != nil {
|
if op != nil {
|
||||||
err.CorrelationId = strings.TrimSpace(op.GetCorrelationId())
|
err.CorrelationId = strings.TrimSpace(op.GetCorrelationId())
|
||||||
|
|||||||
@@ -236,13 +236,6 @@ func (s *Service) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRe
|
|||||||
return responder(ctx)
|
return responder(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) pingStorage(ctx context.Context) error {
|
|
||||||
if s.storage == nil {
|
|
||||||
return errStorageNotInitialized
|
|
||||||
}
|
|
||||||
return s.storage.Ping(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) quoteFeesForCredit(ctx context.Context, req *ledgerv1.PostCreditRequest) ([]*ledgerv1.PostingLine, error) {
|
func (s *Service) quoteFeesForCredit(ctx context.Context, req *ledgerv1.PostCreditRequest) ([]*ledgerv1.PostingLine, error) {
|
||||||
if !s.fees.available() {
|
if !s.fees.available() {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
type Account struct {
|
type Account struct {
|
||||||
storable.Base `bson:",inline" json:",inline"`
|
storable.Base `bson:",inline" json:",inline"`
|
||||||
model.PermissionBound `bson:",inline" json:",inline"`
|
model.PermissionBound `bson:",inline" json:",inline"`
|
||||||
|
model.Describable `bson:",inline" json:",inline"`
|
||||||
|
|
||||||
AccountCode string `bson:"accountCode" json:"accountCode"` // e.g., "asset:cash:usd"
|
AccountCode string `bson:"accountCode" json:"accountCode"` // e.g., "asset:cash:usd"
|
||||||
Currency string `bson:"currency" json:"currency"` // ISO 4217 currency code
|
Currency string `bson:"currency" json:"currency"` // ISO 4217 currency code
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ require (
|
|||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
github.com/sendgrid/rest v2.6.9+incompatible // indirect
|
||||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package notificationimp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -75,7 +74,7 @@ func (m *confirmationManager) Stop() {
|
|||||||
|
|
||||||
func (m *confirmationManager) HandleRequest(ctx context.Context, request *model.ConfirmationRequest) error {
|
func (m *confirmationManager) HandleRequest(ctx context.Context, request *model.ConfirmationRequest) error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return errors.New("confirmation manager is nil")
|
return merrors.Internal("confirmation manager is nil")
|
||||||
}
|
}
|
||||||
if request == nil {
|
if request == nil {
|
||||||
return merrors.InvalidArgument("confirmation request is nil", "request")
|
return merrors.InvalidArgument("confirmation request is nil", "request")
|
||||||
@@ -338,25 +337,25 @@ var currencyPattern = regexp.MustCompile(`^[A-Za-z]{3,10}$`)
|
|||||||
func parseConfirmationReply(text string) (*paymenttypes.Money, string, error) {
|
func parseConfirmationReply(text string) (*paymenttypes.Money, string, error) {
|
||||||
text = strings.TrimSpace(text)
|
text = strings.TrimSpace(text)
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return nil, "empty", errors.New("empty reply")
|
return nil, "empty", merrors.InvalidArgument("empty reply")
|
||||||
}
|
}
|
||||||
parts := strings.Fields(text)
|
parts := strings.Fields(text)
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
if len(parts) == 1 && amountPattern.MatchString(parts[0]) {
|
if len(parts) == 1 && amountPattern.MatchString(parts[0]) {
|
||||||
return nil, "missing_currency", errors.New("currency is required")
|
return nil, "missing_currency", merrors.InvalidArgument("currency is required")
|
||||||
}
|
}
|
||||||
return nil, "missing_amount", errors.New("amount is required")
|
return nil, "missing_amount", merrors.InvalidArgument("amount is required")
|
||||||
}
|
}
|
||||||
if len(parts) > 2 {
|
if len(parts) > 2 {
|
||||||
return nil, "format", errors.New("reply format is invalid")
|
return nil, "format", merrors.InvalidArgument("reply format is invalid")
|
||||||
}
|
}
|
||||||
amount := parts[0]
|
amount := parts[0]
|
||||||
currency := parts[1]
|
currency := parts[1]
|
||||||
if !amountPattern.MatchString(amount) {
|
if !amountPattern.MatchString(amount) {
|
||||||
return nil, "invalid_amount", errors.New("amount format is invalid")
|
return nil, "invalid_amount", merrors.InvalidArgument("amount format is invalid")
|
||||||
}
|
}
|
||||||
if !currencyPattern.MatchString(currency) {
|
if !currencyPattern.MatchString(currency) {
|
||||||
return nil, "invalid_currency", errors.New("currency format is invalid")
|
return nil, "invalid_currency", merrors.InvalidArgument("currency format is invalid")
|
||||||
}
|
}
|
||||||
return &paymenttypes.Money{
|
return &paymenttypes.Money{
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ require (
|
|||||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.2.0 // indirect
|
github.com/xdg-go/scram v1.2.0 // indirect
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
|||||||
11
api/pkg/db/chainassets/assets.go
Normal file
11
api/pkg/db/chainassets/assets.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package chainassets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB interface {
|
||||||
|
Resolve(ctx context.Context, chainAsset model.ChainAssetKey) (*model.ChainAssetDescription, error)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package db
|
|||||||
import (
|
import (
|
||||||
"github.com/tech/sendico/pkg/auth"
|
"github.com/tech/sendico/pkg/auth"
|
||||||
"github.com/tech/sendico/pkg/db/account"
|
"github.com/tech/sendico/pkg/db/account"
|
||||||
|
"github.com/tech/sendico/pkg/db/chainassets"
|
||||||
"github.com/tech/sendico/pkg/db/confirmation"
|
"github.com/tech/sendico/pkg/db/confirmation"
|
||||||
mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo"
|
mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo"
|
||||||
"github.com/tech/sendico/pkg/db/invitation"
|
"github.com/tech/sendico/pkg/db/invitation"
|
||||||
@@ -22,6 +23,8 @@ type Factory interface {
|
|||||||
NewRefreshTokensDB() (refreshtokens.DB, error)
|
NewRefreshTokensDB() (refreshtokens.DB, error)
|
||||||
NewConfirmationsDB() (confirmation.DB, error)
|
NewConfirmationsDB() (confirmation.DB, error)
|
||||||
|
|
||||||
|
NewChainAsstesDB() (chainassets.DB, error)
|
||||||
|
|
||||||
NewAccountDB() (account.DB, error)
|
NewAccountDB() (account.DB, error)
|
||||||
NewOrganizationDB() (organization.DB, error)
|
NewOrganizationDB() (organization.DB, error)
|
||||||
NewInvitationsDB() (invitation.DB, error)
|
NewInvitationsDB() (invitation.DB, error)
|
||||||
|
|||||||
73
api/pkg/db/internal/mongo/chainassetsdb/db.go
Normal file
73
api/pkg/db/internal/mongo/chainassetsdb/db.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package chainassetsdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
|
"github.com/tech/sendico/pkg/db/template"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChainAssetsDB struct {
|
||||||
|
template.DBImp[*model.ChainAssetDescription]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(logger mlogger.Logger, db *mongo.Database) (*ChainAssetsDB, error) {
|
||||||
|
p := &ChainAssetsDB{
|
||||||
|
DBImp: *template.Create[*model.ChainAssetDescription](logger, mservice.ChainAssets, db),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Canonical lookup: enforce single (chain, tokenSymbol)
|
||||||
|
if err := p.Repository.CreateIndex(&ri.Definition{
|
||||||
|
Name: "idx_chain_symbol",
|
||||||
|
Unique: true,
|
||||||
|
Keys: []ri.Key{
|
||||||
|
{Field: "asset.chain", Sort: ri.Asc},
|
||||||
|
{Field: "asset.tokenSymbol", Sort: ri.Asc},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
p.Logger.Error("failed index (chain, symbol) unique", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Prevent duplicate contracts inside the same chain, but only when contract exists
|
||||||
|
if err := p.Repository.CreateIndex(&ri.Definition{
|
||||||
|
Name: "idx_chain_contract_unique",
|
||||||
|
Unique: true,
|
||||||
|
Sparse: true,
|
||||||
|
Keys: []ri.Key{
|
||||||
|
{Field: "asset.chain", Sort: ri.Asc},
|
||||||
|
{Field: "asset.contractAddress", Sort: ri.Asc},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
p.Logger.Error("failed index (chain, contract) unique", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Fast contract lookup, skip docs without contractAddress (native assets)
|
||||||
|
if err := p.Repository.CreateIndex(&ri.Definition{
|
||||||
|
Name: "idx_contract_lookup",
|
||||||
|
Sparse: true,
|
||||||
|
Keys: []ri.Key{
|
||||||
|
{Field: "asset.contractAddress", Sort: ri.Asc},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
p.Logger.Error("failed index contract lookup", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) List assets per chain
|
||||||
|
if err := p.Repository.CreateIndex(&ri.Definition{
|
||||||
|
Name: "idx_chain_list",
|
||||||
|
Keys: []ri.Key{
|
||||||
|
{Field: "asset.chain", Sort: ri.Asc},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
p.Logger.Error("failed index chain list", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
18
api/pkg/db/internal/mongo/chainassetsdb/resolve.go
Normal file
18
api/pkg/db/internal/mongo/chainassetsdb/resolve.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package chainassetsdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db/repository"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *ChainAssetsDB) Resolve(ctx context.Context, chainAsset model.ChainAssetKey) (*model.ChainAssetDescription, error) {
|
||||||
|
var assetDescription model.ChainAssetDescription
|
||||||
|
assetField := repository.Field("asset")
|
||||||
|
q := repository.Query().And(
|
||||||
|
repository.Query().Filter(assetField.Dot("chain"), chainAsset.Chain),
|
||||||
|
repository.Query().Filter(assetField.Dot("tokenSymbol"), chainAsset.TokenSymbol),
|
||||||
|
)
|
||||||
|
return &assetDescription, db.DBImp.FindOne(ctx, q, &assetDescription)
|
||||||
|
}
|
||||||
@@ -10,8 +10,10 @@ import (
|
|||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/tech/sendico/pkg/auth"
|
"github.com/tech/sendico/pkg/auth"
|
||||||
"github.com/tech/sendico/pkg/db/account"
|
"github.com/tech/sendico/pkg/db/account"
|
||||||
|
"github.com/tech/sendico/pkg/db/chainassets"
|
||||||
"github.com/tech/sendico/pkg/db/confirmation"
|
"github.com/tech/sendico/pkg/db/confirmation"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/accountdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/accountdb"
|
||||||
|
"github.com/tech/sendico/pkg/db/internal/mongo/chainassetsdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/confirmationdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/confirmationdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/invitationdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/invitationdb"
|
||||||
"github.com/tech/sendico/pkg/db/internal/mongo/organizationdb"
|
"github.com/tech/sendico/pkg/db/internal/mongo/organizationdb"
|
||||||
@@ -312,6 +314,10 @@ func collectReplicaHosts(configuredHosts []string, replicaSet, defaultPort, host
|
|||||||
return hosts
|
return hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) NewChainAsstesDB() (chainassets.DB, error) {
|
||||||
|
return chainassetsdb.Create(db.logger, db.db())
|
||||||
|
}
|
||||||
|
|
||||||
func (db *DB) Permissions() auth.Provider {
|
func (db *DB) Permissions() auth.Provider {
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ func (r *MongoRepository) CreateIndex(def *ri.Definition) error {
|
|||||||
if def.PartialFilter != nil {
|
if def.PartialFilter != nil {
|
||||||
opts.SetPartialFilterExpression(def.PartialFilter.BuildQuery())
|
opts.SetPartialFilterExpression(def.PartialFilter.BuildQuery())
|
||||||
}
|
}
|
||||||
|
if def.Sparse {
|
||||||
|
opts.SetSparse(def.Sparse)
|
||||||
|
}
|
||||||
|
|
||||||
_, err := r.collection.Indexes().CreateOne(
|
_, err := r.collection.Indexes().CreateOne(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type Key struct {
|
|||||||
type Definition struct {
|
type Definition struct {
|
||||||
Keys []Key // mandatory, at least one element
|
Keys []Key // mandatory, at least one element
|
||||||
Unique bool // unique constraint?
|
Unique bool // unique constraint?
|
||||||
|
Sparse bool // sparse?
|
||||||
TTL *int32 // seconds; nil means “no TTL”
|
TTL *int32 // seconds; nil means “no TTL”
|
||||||
Name string // optional explicit name
|
Name string // optional explicit name
|
||||||
PartialFilter builder.Query // optional: partialFilterExpression for conditional indexes
|
PartialFilter builder.Query // optional: partialFilterExpression for conditional indexes
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ type Announcer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewAnnouncer(logger mlogger.Logger, producer msg.Producer, sender string, announce Announcement) *Announcer {
|
func NewAnnouncer(logger mlogger.Logger, producer msg.Producer, sender string, announce Announcement) *Announcer {
|
||||||
if logger != nil {
|
if logger == nil {
|
||||||
logger = logger.Named("discovery")
|
logger = zap.NewNop()
|
||||||
}
|
}
|
||||||
|
logger = logger.Named("discovery")
|
||||||
announce = normalizeAnnouncement(announce)
|
announce = normalizeAnnouncement(announce)
|
||||||
if announce.Service == "" {
|
if announce.Service == "" {
|
||||||
announce.Service = strings.TrimSpace(sender)
|
announce.Service = strings.TrimSpace(sender)
|
||||||
@@ -132,14 +133,14 @@ func (a *Announcer) sendHeartbeat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announcer) logInfo(message string, fields ...zap.Field) {
|
func (a *Announcer) logInfo(message string, fields ...zap.Field) {
|
||||||
if a.logger == nil {
|
if a == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.logger.Info(message, fields...)
|
a.logger.Info(message, fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Announcer) logWarn(message string, fields ...zap.Field) {
|
func (a *Announcer) logWarn(message string, fields ...zap.Field) {
|
||||||
if a.logger == nil {
|
if a == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
a.logger.Warn(message, fields...)
|
a.logger.Warn(message, fields...)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package discovery
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -13,6 +12,7 @@ import (
|
|||||||
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
||||||
me "github.com/tech/sendico/pkg/messaging/envelope"
|
me "github.com/tech/sendico/pkg/messaging/envelope"
|
||||||
msgproducer "github.com/tech/sendico/pkg/messaging/producer"
|
msgproducer "github.com/tech/sendico/pkg/messaging/producer"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -29,11 +29,12 @@ type Client struct {
|
|||||||
|
|
||||||
func NewClient(logger mlogger.Logger, msgBroker mb.Broker, producer msg.Producer, sender string) (*Client, error) {
|
func NewClient(logger mlogger.Logger, msgBroker mb.Broker, producer msg.Producer, sender string) (*Client, error) {
|
||||||
if msgBroker == nil {
|
if msgBroker == nil {
|
||||||
return nil, errors.New("discovery client: broker is nil")
|
return nil, merrors.InvalidArgument("discovery client: broker is nil")
|
||||||
}
|
}
|
||||||
if logger != nil {
|
if logger == nil {
|
||||||
logger = logger.Named("discovery_client")
|
logger = zap.NewNop()
|
||||||
}
|
}
|
||||||
|
logger = logger.Named("discovery_client")
|
||||||
if producer == nil {
|
if producer == nil {
|
||||||
producer = msgproducer.NewProducer(logger, msgBroker)
|
producer = msgproducer.NewProducer(logger, msgBroker)
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,7 @@ func NewClient(logger mlogger.Logger, msgBroker mb.Broker, producer msg.Producer
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := consumer.ConsumeMessages(client.handleLookupResponse); err != nil && client.logger != nil {
|
if err := consumer.ConsumeMessages(client.handleLookupResponse); err != nil {
|
||||||
client.logger.Warn("Discovery lookup consumer stopped", zap.String("event", LookupResponseEvent().ToString()), zap.Error(err))
|
client.logger.Warn("Discovery lookup consumer stopped", zap.String("event", LookupResponseEvent().ToString()), zap.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -81,7 +82,7 @@ func (c *Client) Close() {
|
|||||||
|
|
||||||
func (c *Client) Lookup(ctx context.Context) (LookupResponse, error) {
|
func (c *Client) Lookup(ctx context.Context) (LookupResponse, error) {
|
||||||
if c == nil || c.producer == nil {
|
if c == nil || c.producer == nil {
|
||||||
return LookupResponse{}, errors.New("discovery client: producer not configured")
|
return LookupResponse{}, merrors.Internal("discovery client: producer not configured")
|
||||||
}
|
}
|
||||||
requestID := uuid.NewString()
|
requestID := uuid.NewString()
|
||||||
ch := make(chan LookupResponse, 1)
|
ch := make(chan LookupResponse, 1)
|
||||||
@@ -131,7 +132,7 @@ func (c *Client) handleLookupResponse(_ context.Context, env me.Envelope) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) logWarn(message string, fields ...zap.Field) {
|
func (c *Client) logWarn(message string, fields ...zap.Field) {
|
||||||
if c == nil || c.logger == nil {
|
if c == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.logger.Warn(message, fields...)
|
c.logger.Warn(message, fields...)
|
||||||
|
|||||||
@@ -4,31 +4,63 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultKVBucket = "discovery_registry"
|
const DefaultKVBucket = "discovery_registry"
|
||||||
|
|
||||||
|
type kvStoreOptions struct {
|
||||||
|
ttl time.Duration
|
||||||
|
ttlSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type KVStoreOption func(*kvStoreOptions)
|
||||||
|
|
||||||
|
func WithKVTTL(ttl time.Duration) KVStoreOption {
|
||||||
|
return func(opts *kvStoreOptions) {
|
||||||
|
if opts == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts.ttl = ttl
|
||||||
|
opts.ttlSet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newKVStoreOptions(opts ...KVStoreOption) kvStoreOptions {
|
||||||
|
var options kvStoreOptions
|
||||||
|
for _, opt := range opts {
|
||||||
|
if opt != nil {
|
||||||
|
opt(&options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
type KVStore struct {
|
type KVStore struct {
|
||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
kv nats.KeyValue
|
kv nats.KeyValue
|
||||||
bucket string
|
bucket string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKVStore(logger mlogger.Logger, js nats.JetStreamContext, bucket string) (*KVStore, error) {
|
func NewKVStore(logger mlogger.Logger, js nats.JetStreamContext, bucket string, opts ...KVStoreOption) (*KVStore, error) {
|
||||||
if js == nil {
|
if js == nil {
|
||||||
return nil, errors.New("discovery kv: jetstream is nil")
|
return nil, merrors.InvalidArgument("discovery kv: jetstream is nil")
|
||||||
}
|
}
|
||||||
if logger != nil {
|
if logger == nil {
|
||||||
logger = logger.Named("discovery_kv")
|
logger = zap.NewNop()
|
||||||
}
|
}
|
||||||
|
logger = logger.Named("discovery_kv")
|
||||||
bucket = strings.TrimSpace(bucket)
|
bucket = strings.TrimSpace(bucket)
|
||||||
if bucket == "" {
|
if bucket == "" {
|
||||||
bucket = DefaultKVBucket
|
bucket = DefaultKVBucket
|
||||||
}
|
}
|
||||||
|
options := newKVStoreOptions(opts...)
|
||||||
|
ttl := options.ttl
|
||||||
kv, err := js.KeyValue(bucket)
|
kv, err := js.KeyValue(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, nats.ErrBucketNotFound) {
|
if errors.Is(err, nats.ErrBucketNotFound) {
|
||||||
@@ -36,14 +68,21 @@ func NewKVStore(logger mlogger.Logger, js nats.JetStreamContext, bucket string)
|
|||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Description: "service discovery registry",
|
Description: "service discovery registry",
|
||||||
History: 1,
|
History: 1,
|
||||||
|
TTL: ttl,
|
||||||
})
|
})
|
||||||
if err == nil && logger != nil {
|
if err == nil {
|
||||||
logger.Info("Discovery KV bucket created", zap.String("bucket", bucket))
|
fields := []zap.Field{zap.String("bucket", bucket)}
|
||||||
|
if options.ttlSet {
|
||||||
|
fields = append(fields, zap.Duration("ttl", ttl))
|
||||||
|
}
|
||||||
|
logger.Info("Discovery KV bucket created", fields...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
} else if options.ttlSet {
|
||||||
|
ensureKVTTL(logger, js, kv, bucket, ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &KVStore{
|
return &KVStore{
|
||||||
@@ -53,20 +92,47 @@ func NewKVStore(logger mlogger.Logger, js nats.JetStreamContext, bucket string)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureKVTTL(logger mlogger.Logger, js nats.JetStreamContext, kv nats.KeyValue, bucket string, ttl time.Duration) {
|
||||||
|
if kv == nil || js == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status, err := kv.Status()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to read discovery KV status", zap.String("bucket", bucket), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status.TTL() == ttl {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stream := "KV_" + bucket
|
||||||
|
info, err := js.StreamInfo(stream)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to read discovery KV stream info", zap.String("bucket", bucket), zap.String("stream", stream), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg := info.Config
|
||||||
|
cfg.MaxAge = ttl
|
||||||
|
if _, err := js.UpdateStream(&cfg); err != nil {
|
||||||
|
logger.Warn("Failed to update discovery KV TTL", zap.String("bucket", bucket), zap.Duration("ttl", ttl), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Discovery KV TTL updated", zap.String("bucket", bucket), zap.Duration("ttl", ttl))
|
||||||
|
}
|
||||||
|
|
||||||
func (s *KVStore) Put(entry RegistryEntry) error {
|
func (s *KVStore) Put(entry RegistryEntry) error {
|
||||||
if s == nil || s.kv == nil {
|
if s == nil || s.kv == nil {
|
||||||
return errors.New("discovery kv: not configured")
|
return merrors.Internal("discovery kv: not configured")
|
||||||
}
|
}
|
||||||
key := registryEntryKey(normalizeEntry(entry))
|
key := registryEntryKey(normalizeEntry(entry))
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return errors.New("discovery kv: entry key is empty")
|
return merrors.InvalidArgument("discovery kv: entry key is empty")
|
||||||
}
|
}
|
||||||
payload, err := json.Marshal(entry)
|
payload, err := json.Marshal(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = s.kv.Put(kvKeyFromRegistryKey(key), payload)
|
_, err = s.kv.Put(kvKeyFromRegistryKey(key), payload)
|
||||||
if err != nil && s.logger != nil {
|
if err != nil {
|
||||||
fields := append(entryFields(entry), zap.String("bucket", s.bucket), zap.String("key", key), zap.Error(err))
|
fields := append(entryFields(entry), zap.String("bucket", s.bucket), zap.String("key", key), zap.Error(err))
|
||||||
s.logger.Warn("Failed to persist discovery entry", fields...)
|
s.logger.Warn("Failed to persist discovery entry", fields...)
|
||||||
}
|
}
|
||||||
@@ -75,13 +141,13 @@ func (s *KVStore) Put(entry RegistryEntry) error {
|
|||||||
|
|
||||||
func (s *KVStore) Delete(id string) error {
|
func (s *KVStore) Delete(id string) error {
|
||||||
if s == nil || s.kv == nil {
|
if s == nil || s.kv == nil {
|
||||||
return errors.New("discovery kv: not configured")
|
return merrors.Internal("discovery kv: not configured")
|
||||||
}
|
}
|
||||||
key := kvKeyFromRegistryKey(id)
|
key := kvKeyFromRegistryKey(id)
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := s.kv.Delete(key); err != nil && s.logger != nil {
|
if err := s.kv.Delete(key); err != nil {
|
||||||
s.logger.Warn("Failed to delete discovery entry", zap.String("bucket", s.bucket), zap.String("key", key), zap.Error(err))
|
s.logger.Warn("Failed to delete discovery entry", zap.String("bucket", s.bucket), zap.String("key", key), zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -90,7 +156,7 @@ func (s *KVStore) Delete(id string) error {
|
|||||||
|
|
||||||
func (s *KVStore) WatchAll() (nats.KeyWatcher, error) {
|
func (s *KVStore) WatchAll() (nats.KeyWatcher, error) {
|
||||||
if s == nil || s.kv == nil {
|
if s == nil || s.kv == nil {
|
||||||
return nil, errors.New("discovery kv: not configured")
|
return nil, merrors.Internal("discovery kv: not configured")
|
||||||
}
|
}
|
||||||
return s.kv.WatchAll()
|
return s.kv.WatchAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package discovery
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
|
|
||||||
messaging "github.com/tech/sendico/pkg/messaging/envelope"
|
messaging "github.com/tech/sendico/pkg/messaging/envelope"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ type jsonEnvelope struct {
|
|||||||
|
|
||||||
func (e *jsonEnvelope) Serialize() ([]byte, error) {
|
func (e *jsonEnvelope) Serialize() ([]byte, error) {
|
||||||
if e.payload == nil {
|
if e.payload == nil {
|
||||||
return nil, errors.New("discovery envelope payload is nil")
|
return nil, merrors.InvalidArgument("discovery envelope payload is nil")
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(e.payload)
|
data, err := json.Marshal(e.payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package discovery
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
msg "github.com/tech/sendico/pkg/messaging"
|
msg "github.com/tech/sendico/pkg/messaging"
|
||||||
mb "github.com/tech/sendico/pkg/messaging/broker"
|
mb "github.com/tech/sendico/pkg/messaging/broker"
|
||||||
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
||||||
@@ -17,6 +17,17 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RegistryOption func(*RegistryService)
|
||||||
|
|
||||||
|
func WithRegistryKVTTL(ttl time.Duration) RegistryOption {
|
||||||
|
return func(s *RegistryService) {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.kvOptions = append(s.kvOptions, WithKVTTL(ttl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type RegistryService struct {
|
type RegistryService struct {
|
||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
registry *Registry
|
registry *Registry
|
||||||
@@ -25,6 +36,7 @@ type RegistryService struct {
|
|||||||
consumers []consumerHandler
|
consumers []consumerHandler
|
||||||
kv *KVStore
|
kv *KVStore
|
||||||
kvWatcher nats.KeyWatcher
|
kvWatcher nats.KeyWatcher
|
||||||
|
kvOptions []KVStoreOption
|
||||||
|
|
||||||
startOnce sync.Once
|
startOnce sync.Once
|
||||||
stopOnce sync.Once
|
stopOnce sync.Once
|
||||||
@@ -36,16 +48,17 @@ type consumerHandler struct {
|
|||||||
event string
|
event string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegistryService(logger mlogger.Logger, msgBroker mb.Broker, producer msg.Producer, registry *Registry, sender string) (*RegistryService, error) {
|
func NewRegistryService(logger mlogger.Logger, msgBroker mb.Broker, producer msg.Producer, registry *Registry, sender string, opts ...RegistryOption) (*RegistryService, error) {
|
||||||
if msgBroker == nil {
|
if msgBroker == nil {
|
||||||
return nil, errors.New("discovery registry: broker is nil")
|
return nil, merrors.InvalidArgument("discovery registry: broker is nil", "broker")
|
||||||
}
|
}
|
||||||
if registry == nil {
|
if registry == nil {
|
||||||
registry = NewRegistry()
|
registry = NewRegistry()
|
||||||
}
|
}
|
||||||
if logger != nil {
|
if logger == nil {
|
||||||
logger = logger.Named("discovery_registry")
|
return nil, merrors.InvalidArgument("discovery registry: no logger provided", "logger")
|
||||||
}
|
}
|
||||||
|
logger = logger.Named("discovery_registry")
|
||||||
sender = strings.TrimSpace(sender)
|
sender = strings.TrimSpace(sender)
|
||||||
if sender == "" {
|
if sender == "" {
|
||||||
sender = "discovery"
|
sender = "discovery"
|
||||||
@@ -74,6 +87,11 @@ func NewRegistryService(logger mlogger.Logger, msgBroker mb.Broker, producer msg
|
|||||||
producer: producer,
|
producer: producer,
|
||||||
sender: sender,
|
sender: sender,
|
||||||
}
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
if opt != nil {
|
||||||
|
opt(svc)
|
||||||
|
}
|
||||||
|
}
|
||||||
svc.consumers = []consumerHandler{
|
svc.consumers = []consumerHandler{
|
||||||
{consumer: serviceConsumer, event: ServiceAnnounceEvent().ToString(), handler: func(ctx context.Context, env me.Envelope) error {
|
{consumer: serviceConsumer, event: ServiceAnnounceEvent().ToString(), handler: func(ctx context.Context, env me.Envelope) error {
|
||||||
return svc.handleAnnounce(ctx, env)
|
return svc.handleAnnounce(ctx, env)
|
||||||
@@ -103,7 +121,7 @@ func (s *RegistryService) Start() {
|
|||||||
for _, ch := range s.consumers {
|
for _, ch := range s.consumers {
|
||||||
ch := ch
|
ch := ch
|
||||||
go func() {
|
go func() {
|
||||||
if err := ch.consumer.ConsumeMessages(ch.handler); err != nil && s.logger != nil {
|
if err := ch.consumer.ConsumeMessages(ch.handler); err != nil {
|
||||||
s.logger.Warn("Discovery consumer stopped with error", zap.String("event", ch.event), zap.Error(err))
|
s.logger.Warn("Discovery consumer stopped with error", zap.String("event", ch.event), zap.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -247,7 +265,7 @@ func (s *RegistryService) initKV(msgBroker mb.Broker) {
|
|||||||
s.logWarn("Discovery KV disabled: JetStream not configured")
|
s.logWarn("Discovery KV disabled: JetStream not configured")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
store, err := NewKVStore(s.logger, js, "")
|
store, err := NewKVStore(s.logger, js, "", s.kvOptions...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logWarn("Failed to initialise discovery KV store", zap.Error(err))
|
s.logWarn("Failed to initialise discovery KV store", zap.Error(err))
|
||||||
return
|
return
|
||||||
@@ -331,21 +349,21 @@ func (s *RegistryService) persistEntry(entry RegistryEntry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *RegistryService) logWarn(message string, fields ...zap.Field) {
|
func (s *RegistryService) logWarn(message string, fields ...zap.Field) {
|
||||||
if s.logger == nil {
|
if s == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.logger.Warn(message, fields...)
|
s.logger.Warn(message, fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RegistryService) logDebug(message string, fields ...zap.Field) {
|
func (s *RegistryService) logDebug(message string, fields ...zap.Field) {
|
||||||
if s.logger == nil {
|
if s == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.logger.Debug(message, fields...)
|
s.logger.Debug(message, fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RegistryService) logInfo(message string, fields ...zap.Field) {
|
func (s *RegistryService) logInfo(message string, fields ...zap.Field) {
|
||||||
if s.logger == nil {
|
if s == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.logger.Info(message, fields...)
|
s.logger.Info(message, fields...)
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package discovery
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
mb "github.com/tech/sendico/pkg/messaging/broker"
|
mb "github.com/tech/sendico/pkg/messaging/broker"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -23,21 +23,22 @@ type RegistryWatcher struct {
|
|||||||
|
|
||||||
func NewRegistryWatcher(logger mlogger.Logger, msgBroker mb.Broker, registry *Registry) (*RegistryWatcher, error) {
|
func NewRegistryWatcher(logger mlogger.Logger, msgBroker mb.Broker, registry *Registry) (*RegistryWatcher, error) {
|
||||||
if msgBroker == nil {
|
if msgBroker == nil {
|
||||||
return nil, errors.New("discovery watcher: broker is nil")
|
return nil, merrors.InvalidArgument("discovery watcher: broker is nil")
|
||||||
}
|
}
|
||||||
if registry == nil {
|
if registry == nil {
|
||||||
registry = NewRegistry()
|
registry = NewRegistry()
|
||||||
}
|
}
|
||||||
if logger != nil {
|
if logger == nil {
|
||||||
logger = logger.Named("discovery_watcher")
|
return nil, merrors.InvalidArgument("discovery logger: logger must be provided")
|
||||||
}
|
}
|
||||||
|
logger = logger.Named("discovery_watcher")
|
||||||
provider, ok := msgBroker.(jetStreamProvider)
|
provider, ok := msgBroker.(jetStreamProvider)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("discovery watcher: jetstream not available")
|
return nil, merrors.Internal("discovery watcher: jetstream not available")
|
||||||
}
|
}
|
||||||
js := provider.JetStream()
|
js := provider.JetStream()
|
||||||
if js == nil {
|
if js == nil {
|
||||||
return nil, errors.New("discovery watcher: jetstream not configured")
|
return nil, merrors.Internal("discovery watcher: jetstream not configured")
|
||||||
}
|
}
|
||||||
store, err := NewKVStore(logger, js, "")
|
store, err := NewKVStore(logger, js, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -53,16 +54,14 @@ func NewRegistryWatcher(logger mlogger.Logger, msgBroker mb.Broker, registry *Re
|
|||||||
|
|
||||||
func (w *RegistryWatcher) Start() error {
|
func (w *RegistryWatcher) Start() error {
|
||||||
if w == nil || w.kv == nil {
|
if w == nil || w.kv == nil {
|
||||||
return errors.New("discovery watcher: not configured")
|
return merrors.Internal("discovery watcher: not configured")
|
||||||
}
|
}
|
||||||
watcher, err := w.kv.WatchAll()
|
watcher, err := w.kv.WatchAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.watcher = watcher
|
w.watcher = watcher
|
||||||
if w.logger != nil {
|
w.logger.Info("Discovery registry watcher started", zap.String("bucket", w.kv.Bucket()))
|
||||||
w.logger.Info("Discovery registry watcher started", zap.String("bucket", w.kv.Bucket()))
|
|
||||||
}
|
|
||||||
go w.consume(watcher)
|
go w.consume(watcher)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -75,9 +74,7 @@ func (w *RegistryWatcher) Stop() {
|
|||||||
if w.watcher != nil {
|
if w.watcher != nil {
|
||||||
_ = w.watcher.Stop()
|
_ = w.watcher.Stop()
|
||||||
}
|
}
|
||||||
if w.logger != nil {
|
w.logger.Info("Discovery registry watcher stopped")
|
||||||
w.logger.Info("Discovery registry watcher stopped")
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +93,7 @@ func (w *RegistryWatcher) consume(watcher nats.KeyWatcher) {
|
|||||||
initialCount := 0
|
initialCount := 0
|
||||||
for entry := range watcher.Updates() {
|
for entry := range watcher.Updates() {
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
if initial && w.logger != nil {
|
if initial {
|
||||||
fields := []zap.Field{zap.Int("entries", initialCount)}
|
fields := []zap.Field{zap.Int("entries", initialCount)}
|
||||||
if w.kv != nil {
|
if w.kv != nil {
|
||||||
fields = append(fields, zap.String("bucket", w.kv.Bucket()))
|
fields = append(fields, zap.String("bucket", w.kv.Bucket()))
|
||||||
@@ -113,7 +110,7 @@ func (w *RegistryWatcher) consume(watcher nats.KeyWatcher) {
|
|||||||
case nats.KeyValueDelete, nats.KeyValuePurge:
|
case nats.KeyValueDelete, nats.KeyValuePurge:
|
||||||
key := registryKeyFromKVKey(entry.Key())
|
key := registryKeyFromKVKey(entry.Key())
|
||||||
if key != "" {
|
if key != "" {
|
||||||
if w.registry.Delete(key) && w.logger != nil {
|
if w.registry.Delete(key) {
|
||||||
w.logger.Info("Discovery registry entry removed", zap.String("key", key))
|
w.logger.Info("Discovery registry entry removed", zap.String("key", key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,13 +122,11 @@ func (w *RegistryWatcher) consume(watcher nats.KeyWatcher) {
|
|||||||
|
|
||||||
var payload RegistryEntry
|
var payload RegistryEntry
|
||||||
if err := json.Unmarshal(entry.Value(), &payload); err != nil {
|
if err := json.Unmarshal(entry.Value(), &payload); err != nil {
|
||||||
if w.logger != nil {
|
w.logger.Warn("Failed to decode discovery KV entry", zap.String("key", entry.Key()), zap.Error(err))
|
||||||
w.logger.Warn("Failed to decode discovery KV entry", zap.String("key", entry.Key()), zap.Error(err))
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
result := w.registry.UpsertEntry(payload, time.Now())
|
result := w.registry.UpsertEntry(payload, time.Now())
|
||||||
if w.logger != nil && (result.IsNew || result.BecameHealthy) {
|
if result.IsNew || result.BecameHealthy {
|
||||||
fields := append(entryFields(result.Entry), zap.Bool("is_new", result.IsNew), zap.Bool("became_healthy", result.BecameHealthy))
|
fields := append(entryFields(result.Entry), zap.Bool("is_new", result.IsNew), zap.Bool("became_healthy", result.BecameHealthy))
|
||||||
w.logger.Info("Discovery registry entry updated from KV", fields...)
|
w.logger.Info("Discovery registry entry updated from KV", fields...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ require (
|
|||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
|||||||
26
api/pkg/model/chainasset.go
Normal file
26
api/pkg/model/chainasset.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChainAssetKey struct {
|
||||||
|
Chain ChainNetwork `bson:"chain" json:"chain" yaml:"chain" mapstructure:"chain"`
|
||||||
|
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol" yaml:"tokenSymbol" mapstructure:"tokenSymbol"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChainAsset struct {
|
||||||
|
ChainAssetKey `bson:",inline" json:",inline"`
|
||||||
|
ContractAddress *string `bson:"contractAddress,omitempty" json:"contractAddress,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChainAssetDescription struct {
|
||||||
|
storable.Storable `bson:",inline" json:",inline"`
|
||||||
|
Describable `bson:",inline" json:",inline"`
|
||||||
|
Asset ChainAsset `bson:"asset" json:"asset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Collection(*ChainAssetDescription) mservice.Type {
|
||||||
|
return mservice.ChainAssets
|
||||||
|
}
|
||||||
11
api/pkg/model/chains.go
Normal file
11
api/pkg/model/chains.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type ChainNetwork string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChainNetworkARB ChainNetwork = "arbitrum_one"
|
||||||
|
ChainNetworkEthMain ChainNetwork = "ethereum_mainnet"
|
||||||
|
ChainNetworkTronMain ChainNetwork = "tron_mainnet"
|
||||||
|
ChainNetworkTronNile ChainNetwork = "tron_nile"
|
||||||
|
ChainNetworkUnspecified ChainNetwork = "unspecified"
|
||||||
|
)
|
||||||
@@ -5,50 +5,51 @@ import "github.com/tech/sendico/pkg/merrors"
|
|||||||
type Type = string
|
type Type = string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Accounts Type = "accounts" // Represents user accounts in the system
|
Accounts Type = "accounts" // Represents user accounts in the system
|
||||||
Confirmations Type = "confirmations" // Represents confirmation code flows
|
Confirmations Type = "confirmations" // Represents confirmation code flows
|
||||||
Amplitude Type = "amplitude" // Represents analytics integration with Amplitude
|
Amplitude Type = "amplitude" // Represents analytics integration with Amplitude
|
||||||
Discovery Type = "discovery" // Represents service discovery registry
|
Discovery Type = "discovery" // Represents service discovery registry
|
||||||
Site Type = "site" // Represents public site endpoints
|
Site Type = "site" // Represents public site endpoints
|
||||||
Changes Type = "changes" // Tracks changes made to resources
|
Changes Type = "changes" // Tracks changes made to resources
|
||||||
Clients Type = "clients" // Represents client information
|
Clients Type = "clients" // Represents client information
|
||||||
ChainGateway Type = "chain_gateway" // Represents chain gateway microservice
|
ChainGateway Type = "chain_gateway" // Represents chain gateway microservice
|
||||||
MntxGateway Type = "mntx_gateway" // Represents Monetix gateway microservice
|
MntxGateway Type = "mntx_gateway" // Represents Monetix gateway microservice
|
||||||
PaymentGateway Type = "payment_gateway" // Represents payment gateway microservice
|
PaymentGateway Type = "payment_gateway" // Represents payment gateway microservice
|
||||||
FXOracle Type = "fx_oracle" // Represents FX oracle microservice
|
FXOracle Type = "fx_oracle" // Represents FX oracle microservice
|
||||||
FeePlans Type = "fee_plans" // Represents fee plans microservice
|
FeePlans Type = "fee_plans" // Represents fee plans microservice
|
||||||
FilterProjects Type = "filter_projects" // Represents comments on tasks or other resources
|
FilterProjects Type = "filter_projects" // Represents comments on tasks or other resources
|
||||||
Invitations Type = "invitations" // Represents invitations sent to users
|
Invitations Type = "invitations" // Represents invitations sent to users
|
||||||
Invoices Type = "invoices" // Represents invoices
|
Invoices Type = "invoices" // Represents invoices
|
||||||
Logo Type = "logo" // Represents logos for organizations or projects
|
Logo Type = "logo" // Represents logos for organizations or projects
|
||||||
Ledger Type = "ledger" // Represents ledger microservice
|
Ledger Type = "ledger" // Represents ledger microservice
|
||||||
LedgerAccounts Type = "ledger_accounts" // Represents ledger accounts microservice
|
LedgerAccounts Type = "ledger_accounts" // Represents ledger accounts microservice
|
||||||
LedgerBalances Type = "ledger_balances" // Represents ledger account balances microservice
|
LedgerBalances Type = "ledger_balances" // Represents ledger account balances microservice
|
||||||
LedgerEntries Type = "ledger_journal_entries" // Represents ledger journal entries microservice
|
LedgerEntries Type = "ledger_journal_entries" // Represents ledger journal entries microservice
|
||||||
LedgerOutbox Type = "ledger_outbox" // Represents ledger outbox microservice
|
LedgerOutbox Type = "ledger_outbox" // Represents ledger outbox microservice
|
||||||
LedgerParties Type = "ledger_parties" // Represents ledger account owner parties microservice
|
LedgerParties Type = "ledger_parties" // Represents ledger account owner parties microservice
|
||||||
LedgerPlines Type = "ledger_posting_lines" // Represents ledger journal posting lines microservice
|
LedgerPlines Type = "ledger_posting_lines" // Represents ledger journal posting lines microservice
|
||||||
PaymentOrchestrator Type = "payment_orchestrator" // Represents payment orchestration microservice
|
PaymentOrchestrator Type = "payment_orchestrator" // Represents payment orchestration microservice
|
||||||
ChainWallets Type = "chain_wallets" // Represents managed chain wallets
|
ChainAssets Type = "chain_assets" // Represents managed chain assets
|
||||||
ChainWalletBalances Type = "chain_wallet_balances" // Represents managed chain wallet balances
|
ChainWallets Type = "chain_wallets" // Represents managed chain wallets
|
||||||
ChainTransfers Type = "chain_transfers" // Represents chain transfers
|
ChainWalletBalances Type = "chain_wallet_balances" // Represents managed chain wallet balances
|
||||||
ChainDeposits Type = "chain_deposits" // Represents chain deposits
|
ChainTransfers Type = "chain_transfers" // Represents chain transfers
|
||||||
Notifications Type = "notifications" // Represents notifications sent to users
|
ChainDeposits Type = "chain_deposits" // Represents chain deposits
|
||||||
Organizations Type = "organizations" // Represents organizations in the system
|
Notifications Type = "notifications" // Represents notifications sent to users
|
||||||
Payments Type = "payments" // Represents payments service
|
Organizations Type = "organizations" // Represents organizations in the system
|
||||||
PaymentRoutes Type = "payment_routes" // Represents payment routing definitions
|
Payments Type = "payments" // Represents payments service
|
||||||
|
PaymentRoutes Type = "payment_routes" // Represents payment routing definitions
|
||||||
PaymentPlanTemplates Type = "payment_plan_templates" // Represents payment plan templates
|
PaymentPlanTemplates Type = "payment_plan_templates" // Represents payment plan templates
|
||||||
PaymentMethods Type = "payment_methods" // Represents payment methods service
|
PaymentMethods Type = "payment_methods" // Represents payment methods service
|
||||||
Permissions Type = "permissions" // Represents permissiosns service
|
Permissions Type = "permissions" // Represents permissiosns service
|
||||||
Policies Type = "policies" // Represents access control policies
|
Policies Type = "policies" // Represents access control policies
|
||||||
PolicyAssignements Type = "policy_assignments" // Represents policy assignments database
|
PolicyAssignements Type = "policy_assignments" // Represents policy assignments database
|
||||||
Recipients Type = "recipients" // Represents payment recipients
|
Recipients Type = "recipients" // Represents payment recipients
|
||||||
RefreshTokens Type = "refresh_tokens" // Represents refresh tokens for authentication
|
RefreshTokens Type = "refresh_tokens" // Represents refresh tokens for authentication
|
||||||
Roles Type = "roles" // Represents roles in access control
|
Roles Type = "roles" // Represents roles in access control
|
||||||
Storage Type = "storage" // Represents statuses of tasks or projects
|
Storage Type = "storage" // Represents statuses of tasks or projects
|
||||||
Tenants Type = "tenants" // Represents tenants managed in the system
|
Tenants Type = "tenants" // Represents tenants managed in the system
|
||||||
Wallets Type = "wallets" // Represents workflows for tasks or projects
|
Wallets Type = "wallets" // Represents workflows for tasks or projects
|
||||||
Workflows Type = "workflows" // Represents workflows for tasks or projects
|
Workflows Type = "workflows" // Represents workflows for tasks or projects
|
||||||
)
|
)
|
||||||
|
|
||||||
func StringToSType(s string) (Type, error) {
|
func StringToSType(s string) (Type, error) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ option go_package = "github.com/tech/sendico/pkg/proto/connector/v1;connectorv1"
|
|||||||
|
|
||||||
import "google/protobuf/struct.proto";
|
import "google/protobuf/struct.proto";
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "common/describable/v1/describable.proto";
|
||||||
import "common/money/v1/money.proto";
|
import "common/money/v1/money.proto";
|
||||||
import "common/pagination/v1/cursor.proto";
|
import "common/pagination/v1/cursor.proto";
|
||||||
|
|
||||||
@@ -127,10 +128,11 @@ message Account {
|
|||||||
string asset = 3; // canonical asset string (USD, ETH, USDT-TRC20)
|
string asset = 3; // canonical asset string (USD, ETH, USDT-TRC20)
|
||||||
AccountState state = 4;
|
AccountState state = 4;
|
||||||
string label = 5;
|
string label = 5;
|
||||||
string owner_ref = 6;
|
string owner_ref = 6; // optional account_ref; empty means organization-owned
|
||||||
google.protobuf.Struct provider_details = 7;
|
google.protobuf.Struct provider_details = 7;
|
||||||
google.protobuf.Timestamp created_at = 8;
|
google.protobuf.Timestamp created_at = 8;
|
||||||
google.protobuf.Timestamp updated_at = 9;
|
google.protobuf.Timestamp updated_at = 9;
|
||||||
|
common.describable.v1.Describable describable = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Balance {
|
message Balance {
|
||||||
@@ -186,7 +188,7 @@ message OpenAccountRequest {
|
|||||||
AccountKind kind = 2;
|
AccountKind kind = 2;
|
||||||
string asset = 3; // canonical asset string (USD, ETH, USDT-TRC20)
|
string asset = 3; // canonical asset string (USD, ETH, USDT-TRC20)
|
||||||
string label = 4;
|
string label = 4;
|
||||||
string owner_ref = 5;
|
string owner_ref = 5; // optional account_ref; empty means organization-owned
|
||||||
google.protobuf.Struct params = 6;
|
google.protobuf.Struct params = 6;
|
||||||
string correlation_id = 7;
|
string correlation_id = 7;
|
||||||
string parent_intent_id = 8;
|
string parent_intent_id = 8;
|
||||||
@@ -210,6 +212,7 @@ message ListAccountsRequest {
|
|||||||
AccountKind kind = 2;
|
AccountKind kind = 2;
|
||||||
string asset = 3; // canonical asset string (USD, ETH, USDT-TRC20)
|
string asset = 3; // canonical asset string (USD, ETH, USDT-TRC20)
|
||||||
common.pagination.v1.CursorPageRequest page = 4;
|
common.pagination.v1.CursorPageRequest page = 4;
|
||||||
|
string organization_ref = 5; // optional org scope (preferred over owner_ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListAccountsResponse {
|
message ListAccountsResponse {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package ledger.v1;
|
|||||||
option go_package = "github.com/tech/sendico/pkg/proto/ledger/v1;ledgerv1";
|
option go_package = "github.com/tech/sendico/pkg/proto/ledger/v1;ledgerv1";
|
||||||
|
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "common/describable/v1/describable.proto";
|
||||||
import "common/money/v1/money.proto";
|
import "common/money/v1/money.proto";
|
||||||
|
|
||||||
// ===== Enums =====
|
// ===== Enums =====
|
||||||
@@ -55,26 +56,29 @@ message LedgerAccount {
|
|||||||
map<string, string> metadata = 9;
|
map<string, string> metadata = 9;
|
||||||
google.protobuf.Timestamp created_at = 10;
|
google.protobuf.Timestamp created_at = 10;
|
||||||
google.protobuf.Timestamp updated_at = 11;
|
google.protobuf.Timestamp updated_at = 11;
|
||||||
|
common.describable.v1.Describable describable = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A single posting line (mirrors your PostingLine model)
|
// A single posting line (mirrors your PostingLine model)
|
||||||
message PostingLine {
|
message PostingLine {
|
||||||
string ledger_account_ref = 1;
|
string ledger_account_ref = 1;
|
||||||
common.money.v1.Money money = 2;
|
common.money.v1.Money money = 2;
|
||||||
LineType line_type = 3; // MAIN, FEE, SPREAD, ...
|
LineType line_type = 3; // MAIN, FEE, SPREAD, ...
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Requests/Responses =====
|
// ===== Requests/Responses =====
|
||||||
|
|
||||||
message CreateAccountRequest {
|
message CreateAccountRequest {
|
||||||
string organization_ref = 1;
|
string organization_ref = 1;
|
||||||
string account_code = 2;
|
string owner_ref = 2;
|
||||||
AccountType account_type = 3;
|
string account_code = 3;
|
||||||
string currency = 4;
|
AccountType account_type = 4;
|
||||||
AccountStatus status = 5;
|
string currency = 5;
|
||||||
bool allow_negative = 6;
|
AccountStatus status = 6;
|
||||||
bool is_settlement = 7;
|
bool allow_negative = 7;
|
||||||
map<string, string> metadata = 8;
|
bool is_settlement = 8;
|
||||||
|
map<string, string> metadata = 9;
|
||||||
|
common.describable.v1.Describable describable = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateAccountResponse {
|
message CreateAccountResponse {
|
||||||
@@ -124,12 +128,12 @@ message FXRequest {
|
|||||||
string from_ledger_account_ref = 3;
|
string from_ledger_account_ref = 3;
|
||||||
string to_ledger_account_ref = 4;
|
string to_ledger_account_ref = 4;
|
||||||
|
|
||||||
common.money.v1.Money from_money = 5; // debited
|
common.money.v1.Money from_money = 5; // debited
|
||||||
common.money.v1.Money to_money = 6; // credited
|
common.money.v1.Money to_money = 6; // credited
|
||||||
string rate = 7; // quoted rate as string (snapshot for audit)
|
string rate = 7; // quoted rate as string (snapshot for audit)
|
||||||
|
|
||||||
string description = 8;
|
string description = 8;
|
||||||
repeated PostingLine charges = 9; // FEE/SPREAD lines
|
repeated PostingLine charges = 9; // FEE/SPREAD lines
|
||||||
map<string, string> metadata = 10;
|
map<string, string> metadata = 10;
|
||||||
google.protobuf.Timestamp event_time = 11;
|
google.protobuf.Timestamp event_time = 11;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ require (
|
|||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.4 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/segmentio/asm v1.2.1 // indirect
|
github.com/segmentio/asm v1.2.1 // indirect
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||||
|
|||||||
@@ -198,8 +198,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
|||||||
51
api/server/interface/api/srequest/ledger.go
Normal file
51
api/server/interface/api/srequest/ledger.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package srequest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LedgerAccountType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LedgerAccountTypeUnspecified LedgerAccountType = "unspecified"
|
||||||
|
LedgerAccountTypeAsset LedgerAccountType = "asset"
|
||||||
|
LedgerAccountTypeLiability LedgerAccountType = "liability"
|
||||||
|
LedgerAccountTypeRevenue LedgerAccountType = "revenue"
|
||||||
|
LedgerAccountTypeExpense LedgerAccountType = "expense"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LedgerAccountStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LedgerAccountStatusUnspecified LedgerAccountStatus = "unspecified"
|
||||||
|
LedgerAccountStatusActive LedgerAccountStatus = "active"
|
||||||
|
LedgerAccountStatusFrozen LedgerAccountStatus = "frozen"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateLedgerAccount struct {
|
||||||
|
AccountCode string `json:"accountCode"`
|
||||||
|
AccountType LedgerAccountType `json:"accountType"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Status LedgerAccountStatus `json:"status,omitempty"`
|
||||||
|
AllowNegative bool `json:"allowNegative,omitempty"`
|
||||||
|
IsSettlement bool `json:"isSettlement,omitempty"`
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
|
Describable model.Describable `json:"describable"`
|
||||||
|
IsOrgWallet bool `json:"isOrgWallet"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CreateLedgerAccount) Validate() error {
|
||||||
|
if strings.TrimSpace(r.AccountCode) == "" {
|
||||||
|
return merrors.InvalidArgument("accountCode is required", "accountCode")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(r.Currency) == "" {
|
||||||
|
return merrors.InvalidArgument("currency is required", "currency")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(r.AccountType)) == "" || strings.EqualFold(string(r.AccountType), string(LedgerAccountTypeUnspecified)) {
|
||||||
|
return merrors.InvalidArgument("accountType is required", "accountType")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ type Signup struct {
|
|||||||
Organization model.Describable `json:"organization"`
|
Organization model.Describable `json:"organization"`
|
||||||
OrganizationTimeZone string `json:"organizationTimeZone"`
|
OrganizationTimeZone string `json:"organizationTimeZone"`
|
||||||
OwnerRole model.Describable `json:"ownerRole"`
|
OwnerRole model.Describable `json:"ownerRole"`
|
||||||
|
CryptoWallet model.Describable `json:"cryptoWallet"`
|
||||||
|
LedgerWallet model.Describable `json:"ledgerWallet"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON enforces strict parsing to catch malformed or unexpected fields.
|
// UnmarshalJSON enforces strict parsing to catch malformed or unexpected fields.
|
||||||
|
|||||||
9
api/server/interface/api/srequest/wallet.go
Normal file
9
api/server/interface/api/srequest/wallet.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package srequest
|
||||||
|
|
||||||
|
import "github.com/tech/sendico/pkg/model"
|
||||||
|
|
||||||
|
type CreateWallet struct {
|
||||||
|
Description model.Describable `json:"description"`
|
||||||
|
IsOrgWallet bool `json:"isOrgWallet"`
|
||||||
|
Asset model.ChainAssetKey `json:"asset"`
|
||||||
|
}
|
||||||
@@ -29,6 +29,11 @@ type ledgerAccountsResponse struct {
|
|||||||
Accounts []ledgerAccount `json:"accounts"`
|
Accounts []ledgerAccount `json:"accounts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ledgerAccountResponse struct {
|
||||||
|
authResponse `json:",inline"`
|
||||||
|
Account ledgerAccount `json:"account"`
|
||||||
|
}
|
||||||
|
|
||||||
type ledgerMoney struct {
|
type ledgerMoney struct {
|
||||||
Amount string `json:"amount"`
|
Amount string `json:"amount"`
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
@@ -57,6 +62,13 @@ func LedgerAccounts(logger mlogger.Logger, accounts []*ledgerv1.LedgerAccount, a
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LedgerAccountCreated(logger mlogger.Logger, account *ledgerv1.LedgerAccount, accessToken *TokenData) http.HandlerFunc {
|
||||||
|
return response.Created(logger, ledgerAccountResponse{
|
||||||
|
Account: toLedgerAccount(account),
|
||||||
|
authResponse: authResponse{AccessToken: *accessToken},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func LedgerBalance(logger mlogger.Logger, resp *ledgerv1.BalanceResponse, accessToken *TokenData) http.HandlerFunc {
|
func LedgerBalance(logger mlogger.Logger, resp *ledgerv1.BalanceResponse, accessToken *TokenData) http.HandlerFunc {
|
||||||
return response.Ok(logger, ledgerBalanceResponse{
|
return response.Ok(logger, ledgerBalanceResponse{
|
||||||
Balance: toLedgerBalance(resp),
|
Balance: toLedgerBalance(resp),
|
||||||
|
|||||||
45
api/server/internal/mutil/proto/chain.go
Normal file
45
api/server/internal/mutil/proto/chain.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Network2Proto(network model.ChainNetwork) (chainv1.ChainNetwork, error) {
|
||||||
|
switch network {
|
||||||
|
case model.ChainNetworkARB:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE, nil
|
||||||
|
case model.ChainNetworkEthMain:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, nil
|
||||||
|
case model.ChainNetworkTronMain:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET, nil
|
||||||
|
case model.ChainNetworkTronNile:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_NILE, nil
|
||||||
|
case model.ChainNetworkUnspecified:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, nil
|
||||||
|
default:
|
||||||
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument(fmt.Sprintf("Unkwnown chain network value '%s'", network), "network")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Asset2Proto(asset *model.ChainAsset) (*chainv1.Asset, error) {
|
||||||
|
if asset == nil {
|
||||||
|
return nil, merrors.InvalidArgument("Asset must be provided", "asset")
|
||||||
|
}
|
||||||
|
netw, err := Network2Proto(asset.Chain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var contract string
|
||||||
|
if asset.ContractAddress != nil {
|
||||||
|
contract = *asset.ContractAddress
|
||||||
|
}
|
||||||
|
return &chainv1.Asset{
|
||||||
|
Chain: netw,
|
||||||
|
TokenSymbol: asset.TokenSymbol,
|
||||||
|
ContractAddress: contract,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
"github.com/tech/sendico/server/interface/api/srequest"
|
"github.com/tech/sendico/server/interface/api/srequest"
|
||||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||||
@@ -69,7 +70,7 @@ func (a *AccountAPI) signup(r *http.Request) http.HandlerFunc {
|
|||||||
var sr srequest.Signup
|
var sr srequest.Signup
|
||||||
if err := json.NewDecoder(r.Body).Decode(&sr); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&sr); err != nil {
|
||||||
a.logger.Warn("Failed to decode signup request", zap.Error(err))
|
a.logger.Warn("Failed to decode signup request", zap.Error(err))
|
||||||
return response.BadRequest(a.logger, a.Name(), "", err.Error())
|
return response.BadPayload(a.logger, a.Name(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sr.Account.Login = strings.ToLower(strings.TrimSpace(sr.Account.Login))
|
sr.Account.Login = strings.ToLower(strings.TrimSpace(sr.Account.Login))
|
||||||
@@ -251,8 +252,11 @@ func (a *AccountAPI) openOrgWallet(ctx context.Context, org *model.Organization,
|
|||||||
req := &chainv1.CreateManagedWalletRequest{
|
req := &chainv1.CreateManagedWalletRequest{
|
||||||
IdempotencyKey: uuid.NewString(),
|
IdempotencyKey: uuid.NewString(),
|
||||||
OrganizationRef: org.ID.Hex(),
|
OrganizationRef: org.ID.Hex(),
|
||||||
OwnerRef: org.ID.Hex(),
|
Describable: &describablev1.Describable{
|
||||||
Asset: a.chainAsset,
|
Name: sr.CryptoWallet.Name,
|
||||||
|
Description: sr.CryptoWallet.Description,
|
||||||
|
},
|
||||||
|
Asset: a.chainAsset,
|
||||||
Metadata: map[string]string{
|
Metadata: map[string]string{
|
||||||
"source": "signup",
|
"source": "signup",
|
||||||
"login": sr.Account.Login,
|
"login": sr.Account.Login,
|
||||||
|
|||||||
@@ -15,14 +15,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errConfirmationNotFound = errors.New("confirmation not found or expired")
|
errConfirmationNotFound confirmationError = "confirmation not found or expired"
|
||||||
errConfirmationUsed = errors.New("confirmation already used")
|
errConfirmationUsed confirmationError = "confirmation already used"
|
||||||
errConfirmationMismatch = errors.New("confirmation code mismatch")
|
errConfirmationMismatch confirmationError = "confirmation code mismatch"
|
||||||
errConfirmationAttemptsExceeded = errors.New("confirmation attempts exceeded")
|
errConfirmationAttemptsExceeded confirmationError = "confirmation attempts exceeded"
|
||||||
errConfirmationCooldown = errors.New("confirmation cooldown active")
|
errConfirmationCooldown confirmationError = "confirmation cooldown active"
|
||||||
errConfirmationResendLimit = errors.New("confirmation resend limit reached")
|
errConfirmationResendLimit confirmationError = "confirmation resend limit reached"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type confirmationError string
|
||||||
|
|
||||||
|
func (e confirmationError) Error() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
type ConfirmationStore struct {
|
type ConfirmationStore struct {
|
||||||
db confirmation.DB
|
db confirmation.DB
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package ledgerapiimp
|
package ledgerapiimp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/api/http/response"
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
@@ -24,7 +24,7 @@ func (a *LedgerAPI) getBalance(r *http.Request, account *model.Account, token *s
|
|||||||
|
|
||||||
accountRef := strings.TrimSpace(a.aph.GetID(r))
|
accountRef := strings.TrimSpace(a.aph.GetID(r))
|
||||||
if accountRef == "" {
|
if accountRef == "" {
|
||||||
return response.BadReference(a.logger, a.Name(), a.aph.Name(), a.aph.GetID(r), errors.New("ledger account reference is required"))
|
return response.BadReference(a.logger, a.Name(), a.aph.Name(), a.aph.GetID(r), merrors.InvalidArgument("ledger account reference is required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
@@ -38,7 +38,7 @@ func (a *LedgerAPI) getBalance(r *http.Request, account *model.Account, token *s
|
|||||||
return response.AccessDenied(a.logger, a.Name(), "ledger balance read permission denied")
|
return response.AccessDenied(a.logger, a.Name(), "ledger balance read permission denied")
|
||||||
}
|
}
|
||||||
if a.client == nil {
|
if a.client == nil {
|
||||||
return response.Internal(a.logger, mservice.Ledger, errors.New("ledger client is not configured"))
|
return response.Internal(a.logger, mservice.Ledger, merrors.Internal("ledger client is not configured"))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.client.GetBalance(ctx, &ledgerv1.GetBalanceRequest{
|
resp, err := a.client.GetBalance(ctx, &ledgerv1.GetBalanceRequest{
|
||||||
|
|||||||
153
api/server/internal/server/ledgerapiimp/create.go
Normal file
153
api/server/internal/server/ledgerapiimp/create.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package ledgerapiimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
|
"github.com/tech/sendico/server/interface/api/srequest"
|
||||||
|
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||||
|
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
|
||||||
|
orgRef, err := a.oph.GetRef(r)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to parse organization reference for ledger account create", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r)))
|
||||||
|
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionCreate)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to check ledger accounts access permissions", zap.Error(err), mutil.PLog(a.oph, r))
|
||||||
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
a.logger.Debug("Access denied when creating ledger account", mutil.PLog(a.oph, r))
|
||||||
|
return response.AccessDenied(a.logger, a.Name(), "ledger accounts write permission denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := decodeLedgerAccountCreatePayload(r)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to decode ledger account create payload", zap.Error(err), mutil.PLog(a.oph, r))
|
||||||
|
return response.BadPayload(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountType, err := mapLedgerAccountType(payload.AccountType)
|
||||||
|
if err != nil {
|
||||||
|
return response.BadPayload(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
status, err := mapLedgerAccountStatus(payload.Status)
|
||||||
|
if err != nil {
|
||||||
|
return response.BadPayload(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.client == nil {
|
||||||
|
return response.Internal(a.logger, mservice.Ledger, merrors.Internal("ledger client is not configured"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var describable *describablev1.Describable
|
||||||
|
name := strings.TrimSpace(payload.Describable.Name)
|
||||||
|
var description *string
|
||||||
|
if payload.Describable.Description != nil {
|
||||||
|
trimmed := strings.TrimSpace(*payload.Describable.Description)
|
||||||
|
if trimmed != "" {
|
||||||
|
description = &trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name != "" || description != nil {
|
||||||
|
describable = &describablev1.Describable{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var ownerRef string
|
||||||
|
if !payload.IsOrgWallet {
|
||||||
|
ownerRef = account.ID.Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := a.client.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
|
||||||
|
OrganizationRef: orgRef.Hex(),
|
||||||
|
OwnerRef: ownerRef,
|
||||||
|
AccountCode: payload.AccountCode,
|
||||||
|
AccountType: accountType,
|
||||||
|
Currency: payload.Currency,
|
||||||
|
Status: status,
|
||||||
|
AllowNegative: payload.AllowNegative,
|
||||||
|
IsSettlement: payload.IsSettlement,
|
||||||
|
Metadata: payload.Metadata,
|
||||||
|
Describable: describable,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to create ledger account", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||||
|
return response.Auto(a.logger, mservice.Ledger, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sresponse.LedgerAccountCreated(a.logger, resp.GetAccount(), token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAccount, error) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
payload := srequest.CreateLedgerAccount{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
|
||||||
|
}
|
||||||
|
payload.AccountCode = strings.TrimSpace(payload.AccountCode)
|
||||||
|
payload.Currency = strings.ToUpper(strings.TrimSpace(payload.Currency))
|
||||||
|
payload.Describable.Name = strings.TrimSpace(payload.Describable.Name)
|
||||||
|
if payload.Describable.Description != nil {
|
||||||
|
trimmed := strings.TrimSpace(*payload.Describable.Description)
|
||||||
|
if trimmed == "" {
|
||||||
|
payload.Describable.Description = nil
|
||||||
|
} else {
|
||||||
|
payload.Describable.Description = &trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(payload.Metadata) == 0 {
|
||||||
|
payload.Metadata = nil
|
||||||
|
}
|
||||||
|
if err := payload.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapLedgerAccountType(accountType srequest.LedgerAccountType) (ledgerv1.AccountType, error) {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(string(accountType))) {
|
||||||
|
case "ACCOUNT_TYPE_ASSET", "ASSET":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, nil
|
||||||
|
case "ACCOUNT_TYPE_LIABILITY", "LIABILITY":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY, nil
|
||||||
|
case "ACCOUNT_TYPE_REVENUE", "REVENUE":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE, nil
|
||||||
|
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, nil
|
||||||
|
case "", "ACCOUNT_TYPE_UNSPECIFIED", "UNSPECIFIED":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("accountType is required", "accountType")
|
||||||
|
default:
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("unsupported accountType: "+string(accountType), "accountType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapLedgerAccountStatus(status srequest.LedgerAccountStatus) (ledgerv1.AccountStatus, error) {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(string(status))) {
|
||||||
|
case "", "ACCOUNT_STATUS_UNSPECIFIED", "UNSPECIFIED":
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED, nil
|
||||||
|
case "ACCOUNT_STATUS_ACTIVE", "ACTIVE":
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE, nil
|
||||||
|
case "ACCOUNT_STATUS_FROZEN", "FROZEN":
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN, nil
|
||||||
|
default:
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED, merrors.InvalidArgument("unsupported status: "+string(status), "status")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package ledgerapiimp
|
package ledgerapiimp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/api/http/response"
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
@@ -32,7 +32,7 @@ func (a *LedgerAPI) listAccounts(r *http.Request, account *model.Account, token
|
|||||||
return response.AccessDenied(a.logger, a.Name(), "ledger accounts read permission denied")
|
return response.AccessDenied(a.logger, a.Name(), "ledger accounts read permission denied")
|
||||||
}
|
}
|
||||||
if a.client == nil {
|
if a.client == nil {
|
||||||
return response.Internal(a.logger, mservice.Ledger, errors.New("ledger client is not configured"))
|
return response.Internal(a.logger, mservice.Ledger, merrors.Internal("ledger client is not configured"))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.client.ListAccounts(ctx, &ledgerv1.ListAccountsRequest{
|
resp, err := a.client.ListAccounts(ctx, &ledgerv1.ListAccountsRequest{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ledgerClient interface {
|
type ledgerClient interface {
|
||||||
|
CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
|
||||||
ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
|
ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
|
||||||
GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error)
|
GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error)
|
||||||
Close() error
|
Close() error
|
||||||
@@ -75,6 +76,7 @@ func CreateAPI(apiCtx eapi.API) (*LedgerAPI, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listAccounts)
|
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listAccounts)
|
||||||
|
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Post, p.createAccount)
|
||||||
apiCtx.Register().AccountHandler(p.Name(), p.aph.AddRef(p.oph.AddRef("/"))+"/balance", api.Get, p.getBalance)
|
apiCtx.Register().AccountHandler(p.Name(), p.aph.AddRef(p.oph.AddRef("/"))+"/balance", api.Get, p.getBalance)
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ package paymentapiimp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/api/http/response"
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
"github.com/tech/sendico/pkg/discovery"
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
me "github.com/tech/sendico/pkg/messaging/envelope"
|
me "github.com/tech/sendico/pkg/messaging/envelope"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||||
@@ -21,7 +21,7 @@ const discoveryLookupTimeout = 3 * time.Second
|
|||||||
|
|
||||||
func (a *PaymentAPI) listDiscoveryRegistry(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
func (a *PaymentAPI) listDiscoveryRegistry(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
||||||
if a.discovery == nil {
|
if a.discovery == nil {
|
||||||
return response.Internal(a.logger, a.Name(), errors.New("discovery client is not configured"))
|
return response.Internal(a.logger, a.Name(), merrors.Internal("discovery client is not configured"))
|
||||||
}
|
}
|
||||||
|
|
||||||
orgRef, err := a.oph.GetRef(r)
|
orgRef, err := a.oph.GetRef(r)
|
||||||
@@ -55,7 +55,7 @@ func (a *PaymentAPI) listDiscoveryRegistry(r *http.Request, account *model.Accou
|
|||||||
|
|
||||||
func (a *PaymentAPI) getDiscoveryRefresh(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
func (a *PaymentAPI) getDiscoveryRefresh(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
||||||
if a.refreshConsumer == nil {
|
if a.refreshConsumer == nil {
|
||||||
return response.Internal(a.logger, a.Name(), errors.New("discovery refresh consumer is not configured"))
|
return response.Internal(a.logger, a.Name(), merrors.Internal("discovery refresh consumer is not configured"))
|
||||||
}
|
}
|
||||||
|
|
||||||
orgRef, err := a.oph.GetRef(r)
|
orgRef, err := a.oph.GetRef(r)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package walletapiimp
|
package walletapiimp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/api/http/response"
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
@@ -23,7 +23,7 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
|
|||||||
}
|
}
|
||||||
walletRef := strings.TrimSpace(a.wph.GetID(r))
|
walletRef := strings.TrimSpace(a.wph.GetID(r))
|
||||||
if walletRef == "" {
|
if walletRef == "" {
|
||||||
return response.BadReference(a.logger, a.Name(), a.wph.Name(), a.wph.GetID(r), errors.New("wallet reference is required"))
|
return response.BadReference(a.logger, a.Name(), a.wph.Name(), a.wph.GetID(r), merrors.InvalidArgument("wallet reference is required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
@@ -37,7 +37,7 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
|
|||||||
return response.AccessDenied(a.logger, a.Name(), "wallet balance read permission denied")
|
return response.AccessDenied(a.logger, a.Name(), "wallet balance read permission denied")
|
||||||
}
|
}
|
||||||
if a.chainGateway == nil {
|
if a.chainGateway == nil {
|
||||||
return response.Internal(a.logger, mservice.ChainGateway, errors.New("chain gateway client is not configured"))
|
return response.Internal(a.logger, mservice.ChainGateway, merrors.Internal("chain gateway client is not configured"))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.chainGateway.GetWalletBalance(ctx, &chainv1.GetWalletBalanceRequest{WalletRef: walletRef})
|
resp, err := a.chainGateway.GetWalletBalance(ctx, &chainv1.GetWalletBalanceRequest{WalletRef: walletRef})
|
||||||
@@ -49,7 +49,7 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
|
|||||||
bal := resp.GetBalance()
|
bal := resp.GetBalance()
|
||||||
if bal == nil {
|
if bal == nil {
|
||||||
a.logger.Warn("Wallet balance missing in response", zap.String("wallet_ref", walletRef))
|
a.logger.Warn("Wallet balance missing in response", zap.String("wallet_ref", walletRef))
|
||||||
return response.Auto(a.logger, mservice.ChainGateway, errors.New("wallet balance not available"))
|
return response.Auto(a.logger, mservice.ChainGateway, merrors.Internal("wallet balance not available"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return sresponse.WalletBalance(a.logger, bal, token)
|
return sresponse.WalletBalance(a.logger, bal, token)
|
||||||
|
|||||||
98
api/server/internal/server/walletapiimp/create.go
Normal file
98
api/server/internal/server/walletapiimp/create.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package walletapiimp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
|
"github.com/tech/sendico/server/interface/api/srequest"
|
||||||
|
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||||
|
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||||
|
ast "github.com/tech/sendico/server/internal/mutil/proto"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *WalletAPI) create(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
|
||||||
|
orgRef, err := a.oph.GetRef(r)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to parse organization reference for wallet list", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r)))
|
||||||
|
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sr srequest.CreateWallet
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&sr); err != nil {
|
||||||
|
a.logger.Warn("Failed to decode wallet creation request request", zap.Error(err), mzap.StorableRef(account))
|
||||||
|
return response.BadPayload(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
res, err := a.enf.Enforce(ctx, a.walletsPermissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionCreate)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to check chain wallet access permissions", zap.Error(err), mutil.PLog(a.oph, r), mzap.StorableRef(account))
|
||||||
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
if !res {
|
||||||
|
a.logger.Debug("Access denied when listing organization wallets", mutil.PLog(a.oph, r), mzap.StorableRef(account))
|
||||||
|
return response.AccessDenied(a.logger, a.Name(), "wallets creation permission denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
asset, err := a.assets.Resolve(ctx, sr.Asset)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to resolve asset", zap.Error(err), mzap.StorableRef(account),
|
||||||
|
zap.String("chain", string(sr.Asset.Chain)), zap.String("token", sr.Asset.TokenSymbol))
|
||||||
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.chainGateway == nil {
|
||||||
|
return response.Internal(a.logger, mservice.ChainGateway, merrors.Internal("chain gateway client is not configured"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var ownerRef string
|
||||||
|
if !sr.IsOrgWallet {
|
||||||
|
ownerRef = account.ID.Hex()
|
||||||
|
}
|
||||||
|
passet, err := ast.Asset2Proto(&asset.Asset)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to convert asset to proto asset", zap.Error(err),
|
||||||
|
mzap.StorableRef(asset), mzap.StorableRef(account))
|
||||||
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &chainv1.CreateManagedWalletRequest{
|
||||||
|
IdempotencyKey: uuid.NewString(),
|
||||||
|
OrganizationRef: orgRef.Hex(),
|
||||||
|
OwnerRef: ownerRef,
|
||||||
|
Describable: &describablev1.Describable{
|
||||||
|
Name: sr.Description.Name,
|
||||||
|
Description: sr.Description.Description,
|
||||||
|
},
|
||||||
|
Asset: passet,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"source": "create",
|
||||||
|
"login": account.Login,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := a.chainGateway.CreateManagedWallet(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn("Failed to create managed wallet", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), mzap.StorableRef(account))
|
||||||
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
}
|
||||||
|
if resp == nil || resp.Wallet == nil || strings.TrimSpace(resp.Wallet.WalletRef) == "" {
|
||||||
|
return response.Auto(a.logger, a.Name(), merrors.Internal("chain gateway returned empty wallet reference"))
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logger.Info("Managed wallet created for organization", mzap.ObjRef("organization_ref", orgRef),
|
||||||
|
zap.String("wallet_ref", resp.Wallet.WalletRef), mzap.StorableRef(account))
|
||||||
|
|
||||||
|
return sresponse.Success(a.logger, token)
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package walletapiimp
|
package walletapiimp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/api/http/response"
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
@@ -33,7 +33,7 @@ func (a *WalletAPI) listWallets(r *http.Request, account *model.Account, token *
|
|||||||
return response.AccessDenied(a.logger, a.Name(), "wallets read permission denied")
|
return response.AccessDenied(a.logger, a.Name(), "wallets read permission denied")
|
||||||
}
|
}
|
||||||
if a.chainGateway == nil {
|
if a.chainGateway == nil {
|
||||||
return response.Internal(a.logger, mservice.ChainGateway, errors.New("chain gateway client is not configured"))
|
return response.Internal(a.logger, mservice.ChainGateway, merrors.Internal("chain gateway client is not configured"))
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &chainv1.ListManagedWalletsRequest{
|
req := &chainv1.ListManagedWalletsRequest{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
chaingatewayclient "github.com/tech/sendico/gateway/chain/client"
|
chaingatewayclient "github.com/tech/sendico/gateway/chain/client"
|
||||||
api "github.com/tech/sendico/pkg/api/http"
|
api "github.com/tech/sendico/pkg/api/http"
|
||||||
"github.com/tech/sendico/pkg/auth"
|
"github.com/tech/sendico/pkg/auth"
|
||||||
|
"github.com/tech/sendico/pkg/db/chainassets"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
@@ -28,9 +29,11 @@ type WalletAPI struct {
|
|||||||
wph mutil.ParamHelper
|
wph mutil.ParamHelper
|
||||||
walletsPermissionRef primitive.ObjectID
|
walletsPermissionRef primitive.ObjectID
|
||||||
balancesPermissionRef primitive.ObjectID
|
balancesPermissionRef primitive.ObjectID
|
||||||
|
assets chainassets.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
type chainWalletClient interface {
|
type chainWalletClient interface {
|
||||||
|
CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
|
||||||
ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
|
ListManagedWallets(ctx context.Context, req *chainv1.ListManagedWalletsRequest) (*chainv1.ListManagedWalletsResponse, error)
|
||||||
GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
|
GetWalletBalance(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error)
|
||||||
Close() error
|
Close() error
|
||||||
@@ -55,6 +58,12 @@ func CreateAPI(apiCtx eapi.API) (*WalletAPI, error) {
|
|||||||
wph: mutil.CreatePH(mservice.Wallets),
|
wph: mutil.CreatePH(mservice.Wallets),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if p.assets, err = apiCtx.DBFactory().NewChainAsstesDB(); err != nil {
|
||||||
|
p.logger.Warn("Failed to create asstes db", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
walletsPolicy, err := apiCtx.Permissions().GetPolicyDescription(context.Background(), mservice.ChainWallets)
|
walletsPolicy, err := apiCtx.Permissions().GetPolicyDescription(context.Background(), mservice.ChainWallets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logger.Warn("Failed to fetch chain wallets permission policy description", zap.Error(err))
|
p.logger.Warn("Failed to fetch chain wallets permission policy description", zap.Error(err))
|
||||||
@@ -81,6 +90,7 @@ func CreateAPI(apiCtx eapi.API) (*WalletAPI, error) {
|
|||||||
|
|
||||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listWallets)
|
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listWallets)
|
||||||
apiCtx.Register().AccountHandler(p.Name(), p.wph.AddRef(p.oph.AddRef("/"))+"/balance", api.Get, p.getWalletBalance)
|
apiCtx.Register().AccountHandler(p.Name(), p.wph.AddRef(p.oph.AddRef("/"))+"/balance", api.Get, p.getWalletBalance)
|
||||||
|
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Post, p.create)
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,18 @@ part 'signup.g.dart';
|
|||||||
class SignupRequest {
|
class SignupRequest {
|
||||||
final AccountData account;
|
final AccountData account;
|
||||||
final DescribableDTO organization;
|
final DescribableDTO organization;
|
||||||
final String organizationTimeZone;
|
|
||||||
final DescribableDTO ownerRole;
|
final DescribableDTO ownerRole;
|
||||||
|
final DescribableDTO cryptoWallet;
|
||||||
|
final DescribableDTO ledgerWallet;
|
||||||
|
final String organizationTimeZone;
|
||||||
|
|
||||||
const SignupRequest({
|
const SignupRequest({
|
||||||
required this.account,
|
required this.account,
|
||||||
required this.organization,
|
required this.organization,
|
||||||
required this.organizationTimeZone,
|
required this.organizationTimeZone,
|
||||||
required this.ownerRole,
|
required this.ownerRole,
|
||||||
|
required this.cryptoWallet,
|
||||||
|
required this.ledgerWallet,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory SignupRequest.build({
|
factory SignupRequest.build({
|
||||||
@@ -27,11 +31,15 @@ class SignupRequest {
|
|||||||
required Describable organization,
|
required Describable organization,
|
||||||
required String organizationTimeZone,
|
required String organizationTimeZone,
|
||||||
required Describable ownerRole,
|
required Describable ownerRole,
|
||||||
|
required Describable cryptoWallet,
|
||||||
|
required Describable ledgerWallet,
|
||||||
}) => SignupRequest(
|
}) => SignupRequest(
|
||||||
account: account,
|
account: account,
|
||||||
organization: organization.toDTO(),
|
organization: organization.toDTO(),
|
||||||
organizationTimeZone: organizationTimeZone,
|
organizationTimeZone: organizationTimeZone,
|
||||||
ownerRole: ownerRole.toDTO(),
|
ownerRole: ownerRole.toDTO(),
|
||||||
|
cryptoWallet: cryptoWallet.toDTO(),
|
||||||
|
ledgerWallet: ledgerWallet.toDTO(),
|
||||||
);
|
);
|
||||||
|
|
||||||
factory SignupRequest.fromJson(Map<String, dynamic> json) => _$SignupRequestFromJson(json);
|
factory SignupRequest.fromJson(Map<String, dynamic> json) => _$SignupRequestFromJson(json);
|
||||||
|
|||||||
@@ -165,8 +165,10 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
Future<void> signup({
|
Future<void> signup({
|
||||||
required AccountData account,
|
required AccountData account,
|
||||||
required Describable organization,
|
required Describable organization,
|
||||||
required String timezone,
|
|
||||||
required Describable ownerRole,
|
required Describable ownerRole,
|
||||||
|
required Describable cryptoWallet,
|
||||||
|
required Describable ledgerWallet,
|
||||||
|
required String timezone,
|
||||||
}) async {
|
}) async {
|
||||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||||
try {
|
try {
|
||||||
@@ -176,6 +178,8 @@ class AccountProvider extends ChangeNotifier {
|
|||||||
organization: organization,
|
organization: organization,
|
||||||
organizationTimeZone: timezone,
|
organizationTimeZone: timezone,
|
||||||
ownerRole: ownerRole,
|
ownerRole: ownerRole,
|
||||||
|
cryptoWallet: cryptoWallet,
|
||||||
|
ledgerWallet: ledgerWallet,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Signup might not automatically log in the user,
|
// Signup might not automatically log in the user,
|
||||||
|
|||||||
@@ -474,6 +474,10 @@
|
|||||||
"optional": "optional",
|
"optional": "optional",
|
||||||
"ownerRole": "Organization Owner",
|
"ownerRole": "Organization Owner",
|
||||||
"ownerRoleDescription": "This role is granted to the organization’s creator, providing full administrative privileges",
|
"ownerRoleDescription": "This role is granted to the organization’s creator, providing full administrative privileges",
|
||||||
|
"cryptoWallet": "Crypto",
|
||||||
|
"cryptoWalletDesc": "TRC-20 USDT",
|
||||||
|
"ledgerWallet": "Internal",
|
||||||
|
"ledgerWalletDesc": "RUB wallet for settlements",
|
||||||
"accountVerificationFailed": "Oops! We failed to verify your account. Please, contact support",
|
"accountVerificationFailed": "Oops! We failed to verify your account. Please, contact support",
|
||||||
"verifyAccount": "Account Verification",
|
"verifyAccount": "Account Verification",
|
||||||
"verificationFailed": "Verification Failed",
|
"verificationFailed": "Verification Failed",
|
||||||
|
|||||||
@@ -475,6 +475,10 @@
|
|||||||
|
|
||||||
"ownerRole": "Владелец организации",
|
"ownerRole": "Владелец организации",
|
||||||
"ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права",
|
"ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права",
|
||||||
|
"cryptoWallet": "Крипто",
|
||||||
|
"cryptoWalletDesc": "TRC-20 USDT",
|
||||||
|
"ledgerWallet": "Внутренний",
|
||||||
|
"ledgerWalletDesc": "RUB кошелек для расчетов",
|
||||||
"accountVerificationFailed": "Упс! Не удалось подтвердить ваш аккаунт. Пожалуйста, свяжитесь с поддержкой.",
|
"accountVerificationFailed": "Упс! Не удалось подтвердить ваш аккаунт. Пожалуйста, свяжитесь с поддержкой.",
|
||||||
"verifyAccount": "Подтвердить аккаунт",
|
"verifyAccount": "Подтвердить аккаунт",
|
||||||
"verificationFailed": "Ошибка подтверждения",
|
"verificationFailed": "Ошибка подтверждения",
|
||||||
|
|||||||
@@ -71,6 +71,14 @@ class SignUpFormState extends State<SignUpForm> {
|
|||||||
name: locs.ownerRole,
|
name: locs.ownerRole,
|
||||||
description: locs.ownerRoleDescription,
|
description: locs.ownerRoleDescription,
|
||||||
),
|
),
|
||||||
|
cryptoWallet: newDescribable(
|
||||||
|
name: locs.cryptoWallet,
|
||||||
|
description: locs.cryptoWalletDesc,
|
||||||
|
),
|
||||||
|
ledgerWallet: newDescribable(
|
||||||
|
name: locs.ledgerWallet,
|
||||||
|
description: locs.ledgerWalletDesc,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
onSignUp();
|
onSignUp();
|
||||||
return 'ok';
|
return 'ok';
|
||||||
|
|||||||
Reference in New Issue
Block a user