refactored payment orchestration

This commit is contained in:
Stephan D
2026-02-03 00:40:46 +01:00
parent 05d998e0f7
commit 5e87e2f2f9
184 changed files with 3920 additions and 2219 deletions

View File

@@ -9,7 +9,7 @@ import (
chainasset "github.com/tech/sendico/pkg/chain"
"github.com/tech/sendico/pkg/merrors"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/model/account_role"
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
@@ -426,7 +426,7 @@ func operationFromTransfer(req *chainv1.SubmitTransferRequest) (*connectorv1.Ope
params := map[string]interface{}{
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
"client_reference": strings.TrimSpace(req.GetClientReference()),
"payment_ref": strings.TrimSpace(req.GetPaymentRef()),
}
if memo := strings.TrimSpace(req.GetDestination().GetMemo()); memo != "" {
params["destination_memo"] = memo
@@ -444,6 +444,8 @@ func operationFromTransfer(req *chainv1.SubmitTransferRequest) (*connectorv1.Ope
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: chainConnectorID, AccountId: strings.TrimSpace(req.GetSourceWalletRef())}}},
Money: req.GetAmount(),
Params: structFromMap(params),
IntentRef: strings.TrimSpace(req.GetIntentRef()),
OperationRef: strings.TrimSpace(req.GetOperationRef()),
}
to, err := destinationToParty(req.GetDestination())
if err != nil {
@@ -472,14 +474,14 @@ func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[strin
if op == nil || len(metadata) == 0 {
return
}
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyFromRole]); raw != "" {
if role, ok := pmodel.Parse(raw); ok && role != "" {
op.FromRole = pmodel.ToProto(role)
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyFromRole]); raw != "" {
if role, ok := account_role.Parse(raw); ok && role != "" {
op.FromRole = account_role.ToProto(role)
}
}
if raw := strings.TrimSpace(metadata[pmodel.MetadataKeyToRole]); raw != "" {
if role, ok := pmodel.Parse(raw); ok && role != "" {
op.ToRole = pmodel.ToProto(role)
if raw := strings.TrimSpace(metadata[account_role.MetadataKeyToRole]); raw != "" {
if role, ok := account_role.Parse(raw); ok && role != "" {
op.ToRole = account_role.ToProto(role)
}
}
}
@@ -619,7 +621,7 @@ func gasTopUpEnsureOperation(req *chainv1.EnsureGasTopUpRequest) (*connectorv1.O
"mode": "ensure",
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
"target_wallet_ref": strings.TrimSpace(req.GetTargetWalletRef()),
"client_reference": strings.TrimSpace(req.GetClientReference()),
"payment_ref": strings.TrimSpace(req.GetPaymentRef()),
"estimated_total_fee": map[string]interface{}{"amount": fee.GetAmount(), "currency": fee.GetCurrency()},
}
if len(req.GetMetadata()) > 0 {
@@ -765,28 +767,54 @@ func managedWalletStatusFromAccount(state connectorv1.AccountState) chainv1.Mana
}
}
func transferStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
switch status {
case connectorv1.OperationStatus_CONFIRMED:
return chainv1.TransferStatus_TRANSFER_CONFIRMED
case connectorv1.OperationStatus_FAILED:
return chainv1.TransferStatus_TRANSFER_FAILED
case connectorv1.OperationStatus_CANCELED:
return chainv1.TransferStatus_TRANSFER_CANCELLED
default:
return chainv1.TransferStatus_TRANSFER_PENDING
}
}
func operationStatusFromTransfer(status chainv1.TransferStatus) connectorv1.OperationStatus {
switch status {
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
return connectorv1.OperationStatus_CONFIRMED
case chainv1.TransferStatus_TRANSFER_CREATED:
return connectorv1.OperationStatus_OPERATION_CREATED
case chainv1.TransferStatus_TRANSFER_PROCESSING:
return connectorv1.OperationStatus_OPERATION_PROCESSING
case chainv1.TransferStatus_TRANSFER_WAITING:
return connectorv1.OperationStatus_OPERATION_WAITING
case chainv1.TransferStatus_TRANSFER_SUCCESS:
return connectorv1.OperationStatus_OPERATION_SUCCESS
case chainv1.TransferStatus_TRANSFER_FAILED:
return connectorv1.OperationStatus_FAILED
return connectorv1.OperationStatus_OPERATION_FAILED
case chainv1.TransferStatus_TRANSFER_CANCELLED:
return connectorv1.OperationStatus_CANCELED
return connectorv1.OperationStatus_OPERATION_CANCELLED
default:
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
}
}
func transferStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
switch status {
case connectorv1.OperationStatus_OPERATION_CREATED:
return chainv1.TransferStatus_TRANSFER_CREATED
case connectorv1.OperationStatus_OPERATION_PROCESSING:
return chainv1.TransferStatus_TRANSFER_PROCESSING
case connectorv1.OperationStatus_OPERATION_WAITING:
return chainv1.TransferStatus_TRANSFER_WAITING
case connectorv1.OperationStatus_OPERATION_SUCCESS:
return chainv1.TransferStatus_TRANSFER_SUCCESS
case connectorv1.OperationStatus_OPERATION_FAILED:
return chainv1.TransferStatus_TRANSFER_FAILED
case connectorv1.OperationStatus_OPERATION_CANCELLED:
return chainv1.TransferStatus_TRANSFER_CANCELLED
default:
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
}
}

View File

@@ -5,7 +5,7 @@ import (
"strings"
"github.com/tech/sendico/pkg/merrors"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/model/account_role"
"github.com/tech/sendico/pkg/payments/rail"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
@@ -102,13 +102,15 @@ func (g *chainRailGateway) Send(ctx context.Context, req rail.TransferRequest) (
OrganizationRef: orgRef,
SourceWalletRef: source,
Destination: dest,
IntentRef: strings.TrimSpace(req.IntentRef),
OperationRef: strings.TrimSpace(req.OperationRef),
PaymentRef: strings.TrimSpace(req.PaymentRef),
Amount: &moneyv1.Money{
Currency: currency,
Amount: amountValue,
},
Fees: fees,
Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
ClientReference: strings.TrimSpace(req.ClientReference),
Fees: fees,
Metadata: transferMetadataWithRoles(req.Metadata, req.FromRole, req.ToRole),
})
if err != nil {
return rail.RailResult{}, err
@@ -186,20 +188,29 @@ func (g *chainRailGateway) isManagedWallet(ctx context.Context, walletRef string
return true, nil
}
func statusFromTransfer(status chainv1.TransferStatus) string {
func statusFromTransfer(status chainv1.TransferStatus) rail.TransferStatus {
switch status {
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
case chainv1.TransferStatus_TRANSFER_CREATED:
return rail.TransferStatusCreated
case chainv1.TransferStatus_TRANSFER_PROCESSING:
return rail.TransferStatusProcessing
case chainv1.TransferStatus_TRANSFER_WAITING:
return rail.TransferStatusProcessing
case chainv1.TransferStatus_TRANSFER_SUCCESS:
return rail.TransferStatusSuccess
case chainv1.TransferStatus_TRANSFER_FAILED:
return rail.TransferStatusFailed
case chainv1.TransferStatus_TRANSFER_CANCELLED:
return rail.TransferStatusRejected
case chainv1.TransferStatus_TRANSFER_SIGNING,
chainv1.TransferStatus_TRANSFER_PENDING,
chainv1.TransferStatus_TRANSFER_SUBMITTED:
return rail.TransferStatusPending
return rail.TransferStatusCancelled
default:
return rail.TransferStatusPending
return rail.TransferStatusUnspecified
}
}
@@ -255,19 +266,19 @@ func railMoneyFromProto(m *moneyv1.Money) *rail.Money {
}
}
func transferMetadataWithRoles(metadata map[string]string, fromRole, toRole pmodel.AccountRole) map[string]string {
func transferMetadataWithRoles(metadata map[string]string, fromRole, toRole account_role.AccountRole) map[string]string {
result := cloneMetadata(metadata)
if strings.TrimSpace(string(fromRole)) != "" {
if result == nil {
result = map[string]string{}
}
result[pmodel.MetadataKeyFromRole] = strings.TrimSpace(string(fromRole))
result[account_role.MetadataKeyFromRole] = strings.TrimSpace(string(fromRole))
}
if strings.TrimSpace(string(toRole)) != "" {
if result == nil {
result = map[string]string{}
}
result[pmodel.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
result[account_role.MetadataKeyToRole] = strings.TrimSpace(string(toRole))
}
if len(result) == 0 {
return nil

View File

@@ -22,7 +22,7 @@ require (
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260131145833-e3fabd62fc61 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260201044653-ee82dce4af02 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
@@ -84,5 +84,5 @@ require (
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
)

View File

@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260131145833-e3fabd62fc61 h1:iLc9NjmJ3AdAl5VoiRSDXzEmmW8kvHp3E2vJ2eKKc7s=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260131145833-e3fabd62fc61/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260201044653-ee82dce4af02 h1:0uY5Ooun4eqGmP0IrQhiKVqeeEXoeEcL8KVRtug8+r8=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260201044653-ee82dce4af02/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -360,8 +360,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -5,6 +5,7 @@ import (
"math/big"
"github.com/ethereum/go-ethereum/core/types"
pmodel "github.com/tech/sendico/pkg/model"
)
// ManagedWalletKey captures information returned after provisioning a managed wallet key.
@@ -17,7 +18,7 @@ type ManagedWalletKey struct {
// Manager defines the contract for managing managed wallet keys.
type Manager interface {
// CreateManagedWalletKey provisions a new managed wallet key for the provided wallet reference and network.
CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*ManagedWalletKey, error)
CreateManagedWalletKey(ctx context.Context, walletRef string, network pmodel.ChainNetwork) (*ManagedWalletKey, error)
// SignTransaction signs the provided transaction using the identified key material.
SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/tech/sendico/gateway/chain/internal/keymanager"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
pmodel "github.com/tech/sendico/pkg/model"
)
// Config describes how to connect to Vault for managed wallet keys.
@@ -92,19 +93,19 @@ func New(logger mlogger.Logger, cfg Config) (*Manager, error) {
}
// CreateManagedWalletKey creates a new managed wallet key and stores it in Vault.
func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) {
func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network pmodel.ChainNetwork) (*keymanager.ManagedWalletKey, error) {
if strings.TrimSpace(walletRef) == "" {
m.logger.Warn("WalletRef missing for managed key creation", zap.String("network", network))
m.logger.Warn("WalletRef missing for managed key creation", zap.String("network", string(network)))
return nil, merrors.InvalidArgument("vault key manager: walletRef is required")
}
if strings.TrimSpace(network) == "" {
if network == pmodel.ChainNetworkUnspecified {
m.logger.Warn("Network missing for managed key creation", zap.String("wallet_ref", walletRef))
return nil, merrors.InvalidArgument("vault key manager: network is required")
}
privateKey, err := ecdsa.GenerateKey(secp256k1.S256(), rand.Reader)
if err != nil {
m.logger.Warn("Failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
m.logger.Warn("Failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", string(network)), zap.Error(err))
return nil, merrors.Internal("vault key manager: failed to generate key: " + err.Error())
}
privateKeyBytes := crypto.FromECDSA(privateKey)
@@ -113,9 +114,9 @@ func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string,
publicKeyHex := hex.EncodeToString(publicKeyBytes)
address := crypto.PubkeyToAddress(publicKey).Hex()
err = m.persistKey(ctx, walletRef, network, privateKeyBytes, publicKeyBytes, address)
err = m.persistKey(ctx, walletRef, string(network), privateKeyBytes, publicKeyBytes, address)
if err != nil {
m.logger.Warn("Failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err))
m.logger.Warn("Failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", string(network)), zap.Error(err))
zeroBytes(privateKeyBytes)
zeroBytes(publicKeyBytes)
return nil, err
@@ -125,12 +126,12 @@ func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string,
m.logger.Info("Managed wallet key created",
zap.String("wallet_ref", walletRef),
zap.String("network", network),
zap.String("network", string(network)),
zap.String("address", strings.ToLower(address)),
)
return &keymanager.ManagedWalletKey{
KeyID: m.buildKeyID(network, walletRef),
KeyID: m.buildKeyID(string(network), walletRef),
Address: strings.ToLower(address),
PublicKey: publicKeyHex,
}, nil

View File

@@ -22,6 +22,7 @@ import (
"github.com/tech/sendico/pkg/merrors"
msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/server/grpcapp"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
@@ -48,7 +49,7 @@ type config struct {
}
type chainConfig struct {
Name string `yaml:"name"`
Name pmodel.ChainNetwork `yaml:"name"`
RPCURLEnv string `yaml:"rpc_url_env"`
ChainID uint64 `yaml:"chain_id"`
NativeToken string `yaml:"native_token"`
@@ -57,10 +58,10 @@ type chainConfig struct {
}
type serviceWalletConfig struct {
Chain string `yaml:"chain"`
Address string `yaml:"address"`
AddressEnv string `yaml:"address_env"`
PrivateKeyEnv string `yaml:"private_key_env"`
Chain pmodel.ChainNetwork `yaml:"chain"`
Address string `yaml:"address"`
AddressEnv string `yaml:"address_env"`
PrivateKeyEnv string `yaml:"private_key_env"`
}
type tokenConfig struct {
@@ -209,20 +210,16 @@ func (i *Imp) loadConfig() (*config, error) {
func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatewayshared.Network, error) {
result := make([]gatewayshared.Network, 0, len(chains))
for _, chain := range chains {
if strings.TrimSpace(chain.Name) == "" {
logger.Warn("Skipping unnamed chain configuration")
continue
}
rpcURL := strings.TrimSpace(os.Getenv(chain.RPCURLEnv))
if rpcURL == "" {
logger.Error("RPC url not configured", zap.String("chain", chain.Name), zap.String("env", chain.RPCURLEnv))
logger.Error("RPC url not configured", zap.String("chain", string(chain.Name)), zap.String("env", chain.RPCURLEnv))
return nil, merrors.InvalidArgument(fmt.Sprintf("chain RPC endpoint not configured (chain=%s env=%s)", chain.Name, chain.RPCURLEnv))
}
contracts := make([]gatewayshared.TokenContract, 0, len(chain.Tokens))
for _, token := range chain.Tokens {
symbol := strings.TrimSpace(token.Symbol)
if symbol == "" {
logger.Warn("Skipping token with empty symbol", zap.String("chain", chain.Name))
logger.Warn("Skipping token with empty symbol", zap.String("chain", string(chain.Name)))
continue
}
addr := strings.TrimSpace(token.Contract)
@@ -232,9 +229,9 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
}
if addr == "" {
if env != "" {
logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("env", env), zap.String("chain", chain.Name))
logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("env", env), zap.String("chain", string(chain.Name)))
} else {
logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("chain", chain.Name))
logger.Warn("Token contract not configured", zap.String("token", symbol), zap.String("chain", string(chain.Name)))
}
continue
}
@@ -246,7 +243,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
gasPolicy, err := buildGasTopUpPolicy(chain.Name, chain.GasTopUpPolicy)
if err != nil {
logger.Error("Invalid gas top-up policy", zap.String("chain", chain.Name), zap.Error(err))
logger.Error("Invalid gas top-up policy", zap.String("chain", string(chain.Name)), zap.Error(err))
return nil, err
}
@@ -262,7 +259,7 @@ func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) ([]gatew
return result, nil
}
func buildGasTopUpPolicy(chainName string, cfg *gasTopUpPolicyConfig) (*gatewayshared.GasTopUpPolicy, error) {
func buildGasTopUpPolicy(chainName pmodel.ChainNetwork, cfg *gasTopUpPolicyConfig) (*gatewayshared.GasTopUpPolicy, error) {
if cfg == nil {
return nil, nil
}
@@ -300,7 +297,7 @@ func buildGasTopUpPolicy(chainName string, cfg *gasTopUpPolicyConfig) (*gateways
return policy, nil
}
func parseGasTopUpRule(chainName, label string, cfg gasTopUpRuleConfig) (gatewayshared.GasTopUpRule, bool, error) {
func parseGasTopUpRule(chainName pmodel.ChainNetwork, label string, cfg gasTopUpRuleConfig) (gatewayshared.GasTopUpRule, bool, error) {
if cfg.BufferPercent == 0 && cfg.MinNativeBalanceTRX == 0 && cfg.RoundingUnitTRX == 0 && cfg.MaxTopUpTRX == 0 {
return gatewayshared.GasTopUpRule{}, false, nil
}
@@ -336,7 +333,7 @@ func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewa
if cfg.AddressEnv != "" {
logger.Warn("Service wallet address not configured", zap.String("env", cfg.AddressEnv))
} else {
logger.Warn("Service wallet address not configured", zap.String("chain", cfg.Chain))
logger.Warn("Service wallet address not configured", zap.String("chain", string(cfg.Chain)))
}
}
if privateKey == "" {

View File

@@ -26,8 +26,8 @@ func resolveDestination(ctx context.Context, deps Deps, dest *chainv1.TransferDe
deps.Logger.Warn("Destination wallet lookup failed", zap.Error(err), zap.String("managed_wallet_ref", managedRef))
return model.TransferDestination{}, err
}
if !strings.EqualFold(wallet.Network, source.Network) {
deps.Logger.Warn("Destination wallet network mismatch", zap.String("source_network", source.Network), zap.String("dest_network", wallet.Network))
if wallet.Network != source.Network {
deps.Logger.Warn("Destination wallet network mismatch", zap.String("source_network", string(source.Network)), zap.String("dest_network", string(wallet.Network)))
return model.TransferDestination{}, merrors.InvalidArgument("destination wallet network mismatch")
}
if strings.TrimSpace(wallet.DepositAddress) == "" {
@@ -44,12 +44,12 @@ func resolveDestination(ctx context.Context, deps Deps, dest *chainv1.TransferDe
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
}
if deps.Drivers == nil {
deps.Logger.Warn("Chain drivers missing", zap.String("network", source.Network))
deps.Logger.Warn("Chain drivers missing", zap.String("network", string(source.Network)))
return model.TransferDestination{}, merrors.Internal("chain drivers not configured")
}
chainDriver, err := deps.Drivers.Driver(source.Network)
if err != nil {
deps.Logger.Warn("Unsupported chain driver", zap.String("network", source.Network), zap.Error(err))
deps.Logger.Warn("Unsupported chain driver", zap.String("network", string(source.Network)), zap.Error(err))
return model.TransferDestination{}, merrors.InvalidArgument("unsupported chain for wallet")
}
normalized, err := chainDriver.NormalizeAddress(external)

View File

@@ -53,19 +53,18 @@ func (c *estimateTransferFeeCommand) Execute(ctx context.Context, req *chainv1.E
return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
}
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
networkCfg, ok := c.deps.Networks.Network(networkKey)
networkCfg, ok := c.deps.Networks.Network(sourceWallet.Network)
if !ok {
c.deps.Logger.Warn("Unsupported chain", zap.String("network", networkKey))
c.deps.Logger.Warn("Unsupported chain", zap.String("network", string(sourceWallet.Network)))
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
}
if c.deps.Drivers == nil {
c.deps.Logger.Warn("Chain drivers missing", zap.String("network", networkKey))
c.deps.Logger.Warn("Chain drivers missing", zap.String("network", string(sourceWallet.Network)))
return gsresponse.Internal[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("chain drivers not configured"))
}
chainDriver, err := c.deps.Drivers.Driver(networkKey)
chainDriver, err := c.deps.Drivers.Driver(sourceWallet.Network)
if err != nil {
c.deps.Logger.Warn("Unsupported chain driver", zap.String("network", networkKey), zap.Error(err))
c.deps.Logger.Warn("Unsupported chain driver", zap.String("network", string(sourceWallet.Network)), zap.Error(err))
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
}

View File

@@ -125,9 +125,9 @@ func (c *ensureGasTopUpCommand) Execute(ctx context.Context, req *chainv1.Ensure
Destination: &chainv1.TransferDestination{
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: targetWalletRef},
},
Amount: topUp,
Metadata: shared.CloneMetadata(req.GetMetadata()),
ClientReference: strings.TrimSpace(req.GetClientReference()),
Amount: topUp,
Metadata: shared.CloneMetadata(req.GetMetadata()),
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
}
submitResponder := NewSubmitTransfer(c.deps.WithLogger("transfer.submit")).Execute(ctx, submitReq)
@@ -152,12 +152,7 @@ func computeGasTopUp(ctx context.Context, deps Deps, walletRef string, estimated
return nil, false, nil, nil, err
}
networkKey := strings.ToLower(strings.TrimSpace(walletModel.Network))
if strings.HasPrefix(networkKey, "tron") {
return nil, false, nil, nil, merrors.InvalidArgument("tron networks must use the tron gateway")
}
networkCfg, ok := deps.Networks.Network(networkKey)
networkCfg, ok := deps.Networks.Network(walletModel.Network)
if !ok {
return nil, false, nil, nil, merrors.InvalidArgument("unsupported chain for wallet")
}
@@ -248,7 +243,7 @@ func logDecision(logger mlogger.Logger, walletRef string, estimatedFee *moneyv1.
zap.Bool("cap_hit", capHit),
}
if walletModel != nil {
fields = append(fields, zap.String("network", strings.TrimSpace(walletModel.Network)))
fields = append(fields, zap.String("network", string(walletModel.Network)))
}
logger.Info("Gas top-up decision", fields...)
}

View File

@@ -41,8 +41,8 @@ func toProtoTransfer(transfer *model.Transfer) *chainv1.Transfer {
SourceWalletRef: transfer.SourceWalletRef,
Destination: destination,
Asset: asset,
RequestedAmount: shared.CloneMoney(transfer.RequestedAmount),
NetAmount: shared.CloneMoney(transfer.NetAmount),
RequestedAmount: shared.MonenyToProto(transfer.RequestedAmount),
NetAmount: shared.MonenyToProto(transfer.NetAmount),
Fees: protoFees,
Status: shared.TransferStatusToProto(transfer.Status),
TransactionHash: transfer.TxHash,

View File

@@ -77,10 +77,9 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
c.deps.Logger.Warn("Organization mismatch", zap.String("wallet_org", sourceWallet.OrganizationRef), zap.String("req_org", organizationRef))
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref mismatch with wallet"))
}
networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network))
networkCfg, ok := c.deps.Networks.Network(networkKey)
networkCfg, ok := c.deps.Networks.Network(sourceWallet.Network)
if !ok {
c.deps.Logger.Warn("Unsupported chain", zap.String("network", networkKey))
c.deps.Logger.Warn("Unsupported chain", zap.String("network", string(sourceWallet.Network)))
return gsresponse.InvalidArgument[chainv1.SubmitTransferResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
}
@@ -124,17 +123,19 @@ func (c *submitTransferCommand) Execute(ctx context.Context, req *chainv1.Submit
transfer := &model.Transfer{
IdempotencyKey: idempotencyKey,
TransferRef: shared.GenerateTransferRef(),
IntentRef: req.IntentRef,
OperationRef: req.OperationRef,
OrganizationRef: organizationRef,
SourceWalletRef: sourceWalletRef,
Destination: destination,
Network: sourceWallet.Network,
TokenSymbol: effectiveTokenSymbol,
ContractAddress: effectiveContractAddress,
RequestedAmount: shared.CloneMoney(amount),
NetAmount: netAmount,
RequestedAmount: shared.ProtoToMoney(amount),
NetAmount: shared.ProtoToMoney(netAmount),
Fees: fees,
Status: model.TransferStatusPending,
ClientReference: strings.TrimSpace(req.GetClientReference()),
Status: model.TransferStatusCreated,
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
LastStatusAt: c.deps.Clock.Now().UTC(),
}

View File

@@ -51,23 +51,19 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required"))
}
chainKey, _ := shared.ChainKeyFromEnum(asset.GetChain())
if chainKey == "" {
c.deps.Logger.Warn("Unsupported chain", zap.Any("chain", asset.GetChain()))
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
}
chainKey := shared.ChainKeyFromEnum(asset.GetChain())
networkCfg, ok := c.deps.Networks.Network(chainKey)
if !ok {
c.deps.Logger.Warn("Unsupported chain in config", zap.String("chain", chainKey))
c.deps.Logger.Warn("Unsupported chain in config", zap.String("chain", string(chainKey)))
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
}
if c.deps.Drivers == nil {
c.deps.Logger.Warn("Chain drivers missing", zap.String("chain", chainKey))
c.deps.Logger.Warn("Chain drivers missing", zap.String("chain", string(chainKey)))
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("chain drivers not configured"))
}
chainDriver, err := c.deps.Drivers.Driver(chainKey)
if err != nil {
c.deps.Logger.Warn("Unsupported chain driver", zap.String("chain", chainKey), zap.Error(err))
c.deps.Logger.Warn("Unsupported chain driver", zap.String("chain", string(chainKey)), zap.Error(err))
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
}
@@ -81,7 +77,7 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C
if !strings.EqualFold(tokenSymbol, networkCfg.NativeToken) {
contractAddress = shared.ResolveContractAddress(networkCfg.TokenConfigs, tokenSymbol)
if contractAddress == "" {
c.deps.Logger.Warn("Unsupported token", zap.String("token", tokenSymbol), zap.String("chain", chainKey))
c.deps.Logger.Warn("Unsupported token", zap.String("token", tokenSymbol), zap.String("chain", string(chainKey)))
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain"))
}
}

View File

@@ -34,7 +34,7 @@ func (c *listManagedWalletsCommand) Execute(ctx context.Context, req *chainv1.Li
filter.OwnerRefFilter = &ownerRef
}
if asset := req.GetAsset(); asset != nil {
filter.Network, _ = shared.ChainKeyFromEnum(asset.GetChain())
filter.Network = shared.ChainKeyFromEnum(asset.GetChain())
filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol())
}
if page := req.GetPage(); page != nil {

View File

@@ -3,7 +3,6 @@ package wallet
import (
"context"
"fmt"
"strings"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
"github.com/tech/sendico/gateway/chain/storage/model"
@@ -24,21 +23,20 @@ func OnChainWalletBalances(ctx context.Context, deps Deps, wallet *model.Managed
return nil, nil, merrors.Internal("chain drivers not configured")
}
networkKey := strings.ToLower(strings.TrimSpace(wallet.Network))
network, ok := deps.Networks.Network(networkKey)
network, ok := deps.Networks.Network(wallet.Network)
if !ok {
logger.Warn("Requested network is not configured",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", networkKey),
zap.String("network", string(wallet.Network)),
)
return nil, nil, merrors.Internal(fmt.Sprintf("Requested network '%s' is not configured", networkKey))
return nil, nil, merrors.Internal(fmt.Sprintf("Requested network '%s' is not configured", wallet.Network))
}
chainDriver, err := deps.Drivers.Driver(networkKey)
chainDriver, err := deps.Drivers.Driver(wallet.Network)
if err != nil {
logger.Warn("Chain driver not configured",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", networkKey),
zap.String("network", string(wallet.Network)),
zap.Error(err),
)
return nil, nil, merrors.InvalidArgument("unsupported chain")

View File

@@ -165,11 +165,13 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
OrganizationRef: orgRef,
SourceWalletRef: source,
IntentRef: strings.TrimSpace(op.GetIntentRef()),
OperationRef: op.GetOperationRef(),
Destination: dest,
Amount: amount,
Fees: parseChainFees(reader),
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
ClientReference: strings.TrimSpace(reader.String("client_reference")),
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
})
if err != nil {
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
@@ -208,7 +210,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
return &connectorv1.SubmitOperationResponse{
Receipt: &connectorv1.OperationReceipt{
OperationId: opID,
Status: connectorv1.OperationStatus_CONFIRMED,
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
Result: result,
},
}, nil
@@ -238,7 +240,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
return &connectorv1.SubmitOperationResponse{
Receipt: &connectorv1.OperationReceipt{
OperationId: opID,
Status: connectorv1.OperationStatus_CONFIRMED,
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), ""),
},
}, nil
@@ -256,12 +258,14 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
}
resp, err := s.EnsureGasTopUp(ctx, &chainv1.EnsureGasTopUpRequest{
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
IntentRef: strings.TrimSpace(op.GetIntentRef()),
OperationRef: strings.TrimSpace(op.GetOperationRef()),
OrganizationRef: orgRef,
SourceWalletRef: source,
TargetWalletRef: target,
EstimatedTotalFee: fee,
Metadata: shared.CloneMetadata(reader.StringMap("metadata")),
ClientReference: strings.TrimSpace(reader.String("client_reference")),
PaymentRef: strings.TrimSpace(reader.String("payment_ref")),
})
if err != nil {
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
@@ -273,7 +277,7 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
return &connectorv1.SubmitOperationResponse{
Receipt: &connectorv1.OperationReceipt{
OperationId: opID,
Status: connectorv1.OperationStatus_CONFIRMED,
Status: shared.СhainTransferStatusToOperation(resp.GetTransfer().GetStatus()),
Result: gasTopUpResult(resp.GetTopupAmount(), resp.GetCapHit(), transferRef),
},
}, nil
@@ -544,25 +548,51 @@ func chainTransferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation
func chainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
switch status {
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
return connectorv1.OperationStatus_CONFIRMED
case chainv1.TransferStatus_TRANSFER_CREATED:
return connectorv1.OperationStatus_OPERATION_CREATED
case chainv1.TransferStatus_TRANSFER_PROCESSING:
return connectorv1.OperationStatus_OPERATION_PROCESSING
case chainv1.TransferStatus_TRANSFER_WAITING:
return connectorv1.OperationStatus_OPERATION_WAITING
case chainv1.TransferStatus_TRANSFER_SUCCESS:
return connectorv1.OperationStatus_OPERATION_SUCCESS
case chainv1.TransferStatus_TRANSFER_FAILED:
return connectorv1.OperationStatus_FAILED
return connectorv1.OperationStatus_OPERATION_FAILED
case chainv1.TransferStatus_TRANSFER_CANCELLED:
return connectorv1.OperationStatus_CANCELED
return connectorv1.OperationStatus_OPERATION_CANCELLED
default:
return connectorv1.OperationStatus_PENDING
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
}
}
func chainStatusFromOperation(status connectorv1.OperationStatus) chainv1.TransferStatus {
switch status {
case connectorv1.OperationStatus_CONFIRMED:
return chainv1.TransferStatus_TRANSFER_CONFIRMED
case connectorv1.OperationStatus_FAILED:
case connectorv1.OperationStatus_OPERATION_CREATED:
return chainv1.TransferStatus_TRANSFER_CREATED
case connectorv1.OperationStatus_OPERATION_PROCESSING:
return chainv1.TransferStatus_TRANSFER_PROCESSING
case connectorv1.OperationStatus_OPERATION_WAITING:
return chainv1.TransferStatus_TRANSFER_WAITING
case connectorv1.OperationStatus_OPERATION_SUCCESS:
return chainv1.TransferStatus_TRANSFER_SUCCESS
case connectorv1.OperationStatus_OPERATION_FAILED:
return chainv1.TransferStatus_TRANSFER_FAILED
case connectorv1.OperationStatus_CANCELED:
case connectorv1.OperationStatus_OPERATION_CANCELLED:
return chainv1.TransferStatus_TRANSFER_CANCELLED
default:
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
}

View File

@@ -47,7 +47,7 @@ func (d *Driver) NormalizeAddress(address string) (string, error) {
func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
d.logger.Debug("Balance request",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
driverDeps := deps
driverDeps.Logger = d.logger
@@ -55,13 +55,13 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
if err != nil {
d.logger.Warn("Balance failed",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
} else if result != nil {
d.logger.Debug("Balance result",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("amount", result.Amount),
zap.String("currency", result.Currency),
)
@@ -72,7 +72,7 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
d.logger.Debug("Native balance request",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
driverDeps := deps
driverDeps.Logger = d.logger
@@ -80,13 +80,13 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
if err != nil {
d.logger.Warn("Native balance failed",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
} else if result != nil {
d.logger.Debug("Native balance result",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("amount", result.Amount),
zap.String("currency", result.Currency),
)
@@ -97,7 +97,7 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
d.logger.Debug("Estimate fee request",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("destination", destination),
)
driverDeps := deps
@@ -106,13 +106,13 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
if err != nil {
d.logger.Warn("Estimate fee failed",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
} else if result != nil {
d.logger.Debug("Estimate fee result",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("amount", result.Amount),
zap.String("currency", result.Currency),
)
@@ -123,7 +123,7 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, destination string) (string, error) {
d.logger.Debug("Submit transfer request",
zap.String("transfer_ref", transfer.TransferRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("destination", destination),
)
driverDeps := deps
@@ -132,13 +132,13 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
if err != nil {
d.logger.Warn("Submit transfer failed",
zap.String("transfer_ref", transfer.TransferRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
} else {
d.logger.Debug("Submit transfer result",
zap.String("transfer_ref", transfer.TransferRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("tx_hash", txHash),
)
}
@@ -148,7 +148,7 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Network, txHash string) (*types.Receipt, error) {
d.logger.Debug("Await confirmation",
zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
driverDeps := deps
driverDeps.Logger = d.logger
@@ -156,13 +156,13 @@ func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, networ
if err != nil {
d.logger.Warn("Await confirmation failed",
zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
} else if receipt != nil {
d.logger.Debug("Await confirmation result",
zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
zap.Uint64("status", receipt.Status),
)

View File

@@ -47,7 +47,7 @@ func (d *Driver) NormalizeAddress(address string) (string, error) {
func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
d.logger.Debug("Balance request",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
driverDeps := deps
driverDeps.Logger = d.logger
@@ -55,13 +55,13 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
if err != nil {
d.logger.Warn("Balance failed",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
} else if result != nil {
d.logger.Debug("Balance result",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("amount", result.Amount),
zap.String("currency", result.Currency),
)
@@ -72,7 +72,7 @@ func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.N
func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
d.logger.Debug("Native balance request",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
driverDeps := deps
driverDeps.Logger = d.logger
@@ -80,13 +80,13 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
if err != nil {
d.logger.Warn("Native balance failed",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
} else if result != nil {
d.logger.Debug("Native balance result",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("amount", result.Amount),
zap.String("currency", result.Currency),
)
@@ -97,7 +97,7 @@ func (d *Driver) NativeBalance(ctx context.Context, deps driver.Deps, network sh
func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
d.logger.Debug("Estimate fee request",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("destination", destination),
)
driverDeps := deps
@@ -106,13 +106,13 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
if err != nil {
d.logger.Warn("Estimate fee failed",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
} else if result != nil {
d.logger.Debug("Estimate fee result",
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("amount", result.Amount),
zap.String("currency", result.Currency),
)
@@ -123,7 +123,7 @@ func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shar
func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, destination string) (string, error) {
d.logger.Debug("Submit transfer request",
zap.String("transfer_ref", transfer.TransferRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("destination", destination),
)
driverDeps := deps
@@ -132,13 +132,13 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
if err != nil {
d.logger.Warn("Submit transfer failed",
zap.String("transfer_ref", transfer.TransferRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
} else {
d.logger.Debug("Submit transfer result",
zap.String("transfer_ref", transfer.TransferRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("tx_hash", txHash),
)
}
@@ -148,7 +148,7 @@ func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network s
func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Network, txHash string) (*types.Receipt, error) {
d.logger.Debug("Await confirmation",
zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
driverDeps := deps
driverDeps.Logger = d.logger
@@ -156,13 +156,13 @@ func (d *Driver) AwaitConfirmation(ctx context.Context, deps driver.Deps, networ
if err != nil {
d.logger.Warn("Await confirmation failed",
zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
} else if receipt != nil {
d.logger.Debug("Await confirmation result",
zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
zap.Uint64("status", receipt.Status),
)

View File

@@ -74,7 +74,7 @@ func NormalizeAddress(address string) (string, error) {
func nativeCurrency(network shared.Network) string {
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
if currency == "" {
currency = strings.ToUpper(network.Name)
currency = strings.ToUpper(string(network.Name))
}
return currency
}
@@ -114,7 +114,7 @@ func Balance(ctx context.Context, deps driver.Deps, network shared.Network, wall
rpcURL := strings.TrimSpace(network.RPCURL)
logFields := []zap.Field{
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", strings.ToLower(strings.TrimSpace(network.Name))),
zap.String("network", strings.ToLower(strings.TrimSpace(string(network.Name)))),
zap.String("token_symbol", strings.ToUpper(strings.TrimSpace(wallet.TokenSymbol))),
zap.String("contract", strings.ToLower(strings.TrimSpace(wallet.ContractAddress))),
zap.String("wallet_address", normalizedAddress),
@@ -194,7 +194,7 @@ func NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network
rpcURL := strings.TrimSpace(network.RPCURL)
logFields := []zap.Field{
zap.String("wallet_ref", wallet.WalletRef),
zap.String("network", strings.ToLower(strings.TrimSpace(network.Name))),
zap.String("network", string(network.Name)),
zap.String("wallet_address", normalizedAddress),
}
if rpcURL == "" {
@@ -260,12 +260,12 @@ func EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network,
client, err := registry.Client(network.Name)
if err != nil {
logger.Warn("Failed to resolve client", zap.Error(err), zap.String("network_name", network.Name))
logger.Warn("Failed to resolve client", zap.Error(err), zap.String("network_name", string(network.Name)))
return nil, err
}
rpcClient, err := registry.RPCClient(network.Name)
if err != nil {
logger.Warn("Failed to resolve RPC client", zap.Error(err), zap.String("network_name", network.Name))
logger.Warn("Failed to resolve RPC client", zap.Error(err), zap.String("network_name", string(network.Name)))
return nil, err
}
@@ -374,7 +374,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
}
rpcURL := strings.TrimSpace(network.RPCURL)
if rpcURL == "" {
logger.Warn("Network rpc url missing", zap.String("network", network.Name))
logger.Warn("Network rpc url missing", zap.String("network", string(network.Name)))
return "", executorInvalid("network rpc url is not configured")
}
if source == nil || transfer == nil {
@@ -397,18 +397,18 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
logger.Info("Submitting transfer",
zap.String("transfer_ref", transfer.TransferRef),
zap.String("source_wallet_ref", source.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("destination", strings.ToLower(destination)),
)
client, err := registry.Client(network.Name)
if err != nil {
logger.Warn("Failed to initialise RPC client", zap.Error(err), zap.String("network", network.Name))
logger.Warn("Failed to initialise RPC client", zap.Error(err), zap.String("network", string(network.Name)))
return "", err
}
rpcClient, err := registry.RPCClient(network.Name)
if err != nil {
logger.Warn("Failed to initialise RPC client", zap.String("network", network.Name))
logger.Warn("Failed to initialise RPC client", zap.String("network", string(network.Name)))
return "", err
}
@@ -429,7 +429,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
gasPrice, err := client.SuggestGasPrice(ctx)
if err != nil {
logger.Warn("Failed to suggest gas price", zap.Error(err),
zap.String("transfer_ref", transfer.TransferRef), zap.String("network", network.Name),
zap.String("transfer_ref", transfer.TransferRef), zap.String("network", string(network.Name)),
)
return "", executorInternal("failed to suggest gas price", err)
}
@@ -532,7 +532,7 @@ func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Networ
txHash := signedTx.Hash().Hex()
logger.Info("Transaction submitted", zap.String("transfer_ref", transfer.TransferRef),
zap.String("tx_hash", txHash), zap.String("network", network.Name),
zap.String("tx_hash", txHash), zap.String("network", string(network.Name)),
)
return txHash, nil
@@ -544,7 +544,7 @@ func AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Net
registry := deps.Registry
if strings.TrimSpace(txHash) == "" {
logger.Warn("Missing transaction hash for confirmation", zap.String("network", network.Name))
logger.Warn("Missing transaction hash for confirmation", zap.String("network", string(network.Name)))
return nil, executorInvalid("tx hash is required")
}
rpcURL := strings.TrimSpace(network.RPCURL)
@@ -572,23 +572,23 @@ func AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Net
select {
case <-ticker.C:
logger.Debug("Transaction not yet mined", zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
continue
case <-ctx.Done():
logger.Warn("Context cancelled while awaiting confirmation", zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
return nil, ctx.Err()
}
}
logger.Warn("Failed to fetch transaction receipt", zap.Error(err),
zap.String("tx_hash", txHash), zap.String("network", network.Name),
zap.String("tx_hash", txHash), zap.String("network", string(network.Name)),
)
return nil, executorInternal("failed to fetch transaction receipt", err)
}
logger.Info("Transaction confirmed", zap.String("tx_hash", txHash),
zap.String("network", network.Name), zap.Uint64("status", receipt.Status),
zap.String("network", string(network.Name)), zap.Uint64("status", receipt.Status),
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
)
return receipt, nil
@@ -654,12 +654,6 @@ type gasEstimator interface {
}
func estimateGas(ctx context.Context, network shared.Network, client gasEstimator, rpcClient *rpc.Client, callMsg ethereum.CallMsg) (uint64, error) {
if isTronNetwork(network) {
if rpcClient == nil {
return 0, merrors.Internal("rpc client not initialised")
}
return estimateGasTron(ctx, rpcClient, callMsg)
}
return client.EstimateGas(ctx, callMsg)
}
@@ -702,10 +696,6 @@ func tronEstimateCall(callMsg ethereum.CallMsg) map[string]string {
return call
}
func isTronNetwork(network shared.Network) bool {
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(network.Name)), "tron")
}
func toBaseUnits(amount string, decimals uint8) (*big.Int, error) {
value, err := decimal.NewFromString(strings.TrimSpace(amount))
if err != nil {

View File

@@ -29,9 +29,6 @@ func ComputeGasTopUp(network shared.Network, wallet *model.ManagedWallet, estima
}
nativeCurrency := strings.TrimSpace(network.NativeToken)
if nativeCurrency == "" {
nativeCurrency = strings.ToUpper(strings.TrimSpace(network.Name))
}
if !strings.EqualFold(nativeCurrency, estimatedFee.GetCurrency()) {
return nil, false, merrors.InvalidArgument(fmt.Sprintf("estimated fee currency mismatch (expected %s)", nativeCurrency))
}

View File

@@ -2,7 +2,6 @@ package drivers
import (
"fmt"
"strings"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/arbitrum"
@@ -10,12 +9,13 @@ import (
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
pmodel "github.com/tech/sendico/pkg/model"
"go.uber.org/zap"
)
// Registry maps configured network keys to chain drivers.
type Registry struct {
byNetwork map[string]driver.Driver
byNetwork map[pmodel.ChainNetwork]driver.Driver
}
// NewRegistry selects drivers for the configured networks.
@@ -23,18 +23,14 @@ func NewRegistry(logger mlogger.Logger, networks []shared.Network) (*Registry, e
if logger == nil {
return nil, merrors.InvalidArgument("driver registry: logger is required")
}
result := &Registry{byNetwork: map[string]driver.Driver{}}
result := &Registry{byNetwork: map[pmodel.ChainNetwork]driver.Driver{}}
for _, network := range networks {
name := strings.ToLower(strings.TrimSpace(network.Name))
if name == "" {
continue
}
chainDriver, err := resolveDriver(logger, name)
chainDriver, err := resolveDriver(logger, network.Name)
if err != nil {
logger.Error("Unsupported chain driver", zap.String("network", name), zap.Error(err))
logger.Error("Unsupported chain driver", zap.String("network", string(network.Name)), zap.Error(err))
return nil, err
}
result.byNetwork[name] = chainDriver
result.byNetwork[network.Name] = chainDriver
}
if len(result.byNetwork) == 0 {
return nil, merrors.InvalidArgument("driver registry: no supported networks configured")
@@ -44,30 +40,25 @@ func NewRegistry(logger mlogger.Logger, networks []shared.Network) (*Registry, e
}
// Driver resolves a driver for the provided network key.
func (r *Registry) Driver(network string) (driver.Driver, error) {
func (r *Registry) Driver(network pmodel.ChainNetwork) (driver.Driver, error) {
if r == nil || len(r.byNetwork) == 0 {
return nil, merrors.Internal("driver registry is not configured")
}
key := strings.ToLower(strings.TrimSpace(network))
if key == "" {
return nil, merrors.InvalidArgument("network is required")
}
chainDriver, ok := r.byNetwork[key]
chainDriver, ok := r.byNetwork[network]
if !ok {
return nil, merrors.InvalidArgument(fmt.Sprintf("unsupported chain network %s", key))
return nil, merrors.InvalidArgument(fmt.Sprintf("unsupported chain network %s", network))
}
return chainDriver, nil
}
func resolveDriver(logger mlogger.Logger, network string) (driver.Driver, error) {
switch {
case strings.HasPrefix(network, "tron"):
return nil, merrors.InvalidArgument("tron networks must use the tron gateway, not chain gateway")
case strings.HasPrefix(network, "arbitrum"):
func resolveDriver(logger mlogger.Logger, network pmodel.ChainNetwork) (driver.Driver, error) {
switch network {
case pmodel.ChainNetworkArbitrumOne:
case pmodel.ChainNetworkArbitrumSepolia:
return arbitrum.New(logger), nil
case strings.HasPrefix(network, "ethereum"):
case pmodel.ChainNetworkEthereumMainnet:
return ethereum.New(logger), nil
default:
return nil, merrors.InvalidArgument("unsupported chain network " + network)
}
return nil, merrors.InvalidArgument("unsupported chain network " + string(network))
}

View File

@@ -52,7 +52,7 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
}
rpcURL := strings.TrimSpace(network.RPCURL)
if rpcURL == "" {
o.logger.Warn("Network rpc url missing", zap.String("network", network.Name))
o.logger.Warn("Network rpc url missing", zap.String("network", string(network.Name)))
return "", executorInvalid("network rpc url is not configured")
}
if source == nil || transfer == nil {
@@ -75,19 +75,19 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
o.logger.Info("Submitting transfer",
zap.String("transfer_ref", transfer.TransferRef),
zap.String("source_wallet_ref", source.WalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.String("destination", strings.ToLower(destinationAddress)),
)
client, err := o.clients.Client(network.Name)
if err != nil {
o.logger.Warn("Failed to initialise RPC client", zap.Error(err), zap.String("network", network.Name))
o.logger.Warn("Failed to initialise RPC client", zap.Error(err), zap.String("network", string(network.Name)))
return "", err
}
rpcClient, err := o.clients.RPCClient(network.Name)
if err != nil {
o.logger.Warn("Failed to initialise RPC client",
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
return "", err
@@ -112,7 +112,7 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
if err != nil {
o.logger.Warn("Failed to suggest gas price",
zap.String("transfer_ref", transfer.TransferRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
return "", executorInternal("failed to suggest gas price", err)
@@ -206,7 +206,7 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
o.logger.Info("Transaction submitted",
zap.String("transfer_ref", transfer.TransferRef),
zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
return txHash, nil
@@ -214,7 +214,7 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr
func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network shared.Network, txHash string) (*types.Receipt, error) {
if strings.TrimSpace(txHash) == "" {
o.logger.Warn("Missing transaction hash for confirmation", zap.String("network", network.Name))
o.logger.Warn("Missing transaction hash for confirmation", zap.String("network", string(network.Name)))
return nil, executorInvalid("tx hash is required")
}
rpcURL := strings.TrimSpace(network.RPCURL)
@@ -240,27 +240,27 @@ func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network shared.
case <-ticker.C:
o.logger.Debug("Transaction not yet mined",
zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
continue
case <-ctx.Done():
o.logger.Warn("Context cancelled while awaiting confirmation",
zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
return nil, ctx.Err()
}
}
o.logger.Warn("Failed to fetch transaction receipt",
zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Error(err),
)
return nil, executorInternal("failed to fetch transaction receipt", err)
}
o.logger.Info("Transaction confirmed",
zap.String("tx_hash", txHash),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
zap.Uint64("status", receipt.Status),
)

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
clockpkg "github.com/tech/sendico/pkg/clock"
pmodel "github.com/tech/sendico/pkg/model"
)
// Option configures the Service.
@@ -34,7 +35,7 @@ func WithNetworks(networks []shared.Network) Option {
return
}
if s.networks == nil {
s.networks = make(map[string]shared.Network, len(networks))
s.networks = make(map[pmodel.ChainNetwork]shared.Network, len(networks))
}
for _, network := range networks {
if network.Name == "" {
@@ -48,7 +49,7 @@ func WithNetworks(networks []shared.Network) Option {
clone.TokenConfigs[i].Symbol = strings.ToUpper(strings.TrimSpace(clone.TokenConfigs[i].Symbol))
clone.TokenConfigs[i].ContractAddress = strings.ToLower(strings.TrimSpace(clone.TokenConfigs[i].ContractAddress))
}
clone.Name = strings.ToLower(strings.TrimSpace(clone.Name))
clone.Name = clone.Name
s.networks[clone.Name] = clone
}
}

View File

@@ -14,13 +14,14 @@ import (
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
pmodel "github.com/tech/sendico/pkg/model"
"go.uber.org/zap"
)
// Clients holds pre-initialised RPC clients keyed by network name.
type Clients struct {
logger mlogger.Logger
clients map[string]clientEntry
clients map[pmodel.ChainNetwork]clientEntry
}
type clientEntry struct {
@@ -36,25 +37,20 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
clientLogger := logger.Named("rpc_client")
result := &Clients{
logger: clientLogger,
clients: make(map[string]clientEntry),
clients: make(map[pmodel.ChainNetwork]clientEntry),
}
for _, network := range networks {
name := strings.ToLower(strings.TrimSpace(network.Name))
rpcURL := strings.TrimSpace(network.RPCURL)
if name == "" {
clientLogger.Warn("Skipping network with empty name during rpc client preparation")
continue
}
if rpcURL == "" {
result.Close()
err := merrors.InvalidArgument(fmt.Sprintf("rpc url not configured for network %s", name))
clientLogger.Warn("Rpc url missing", zap.String("network", name))
err := merrors.InvalidArgument(fmt.Sprintf("rpc url not configured for network %s", network.Name))
clientLogger.Warn("Rpc url missing", zap.String("network", string(network.Name)))
return nil, err
}
fields := []zap.Field{
zap.String("network", name),
zap.String("network", string(network.Name)),
}
clientLogger.Info("Initialising rpc client", fields...)
@@ -62,7 +58,7 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
httpClient := &http.Client{
Transport: &loggingRoundTripper{
logger: clientLogger,
network: name,
network: network.Name,
endpoint: rpcURL,
base: http.DefaultTransport,
},
@@ -72,10 +68,10 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
if err != nil {
result.Close()
clientLogger.Warn("Failed to dial rpc endpoint", append(fields, zap.Error(err))...)
return nil, merrors.Internal(fmt.Sprintf("rpc dial failed for %s: %s", name, err.Error()))
return nil, merrors.Internal(fmt.Sprintf("rpc dial failed for %s: %s", network.Name, err.Error()))
}
client := ethclient.NewClient(rpcCli)
result.clients[name] = clientEntry{
result.clients[network.Name] = clientEntry{
eth: client,
rpc: rpcCli,
}
@@ -93,27 +89,25 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo
}
// Client returns a prepared client for the given network name.
func (c *Clients) Client(network string) (*ethclient.Client, error) {
func (c *Clients) Client(network pmodel.ChainNetwork) (*ethclient.Client, error) {
if c == nil {
return nil, merrors.Internal("RPC clients not initialised")
}
name := strings.ToLower(strings.TrimSpace(network))
entry, ok := c.clients[name]
entry, ok := c.clients[network]
if !ok || entry.eth == nil {
return nil, merrors.InvalidArgument(fmt.Sprintf("RPC client not configured for network %s", name))
return nil, merrors.InvalidArgument(fmt.Sprintf("RPC client not configured for network %s", network))
}
return entry.eth, nil
}
// RPCClient returns the raw RPC client for low-level calls.
func (c *Clients) RPCClient(network string) (*rpc.Client, error) {
func (c *Clients) RPCClient(network pmodel.ChainNetwork) (*rpc.Client, error) {
if c == nil {
return nil, merrors.Internal("rpc clients not initialised")
}
name := strings.ToLower(strings.TrimSpace(network))
entry, ok := c.clients[name]
entry, ok := c.clients[network]
if !ok || entry.rpc == nil {
return nil, merrors.InvalidArgument(fmt.Sprintf("rpc client not configured for network %s", name))
return nil, merrors.InvalidArgument(fmt.Sprintf("rpc client not configured for network %s", network))
}
return entry.rpc, nil
}
@@ -130,14 +124,14 @@ func (c *Clients) Close() {
entry.eth.Close()
}
if c.logger != nil {
c.logger.Info("RPC client closed", zap.String("network", name))
c.logger.Info("RPC client closed", zap.String("network", string(name)))
}
}
}
type loggingRoundTripper struct {
logger mlogger.Logger
network string
network pmodel.ChainNetwork
endpoint string
base http.RoundTripper
}
@@ -155,7 +149,7 @@ func (l *loggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
}
fields := []zap.Field{
zap.String("network", l.network),
zap.String("network", string(l.network)),
}
if len(reqBody) > 0 {
fields = append(fields, zap.String("rpc_request", truncate(string(reqBody), 2048)))

View File

@@ -1,22 +1,21 @@
package rpcclient
import (
"strings"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/pkg/merrors"
pmodel "github.com/tech/sendico/pkg/model"
)
// Registry binds static network metadata with prepared RPC clients.
type Registry struct {
networks map[string]shared.Network
networks map[pmodel.ChainNetwork]shared.Network
clients *Clients
}
// NewRegistry constructs a registry keyed by lower-cased network name.
func NewRegistry(networks map[string]shared.Network, clients *Clients) *Registry {
func NewRegistry(networks map[pmodel.ChainNetwork]shared.Network, clients *Clients) *Registry {
return &Registry{
networks: networks,
clients: clients,
@@ -24,31 +23,31 @@ func NewRegistry(networks map[string]shared.Network, clients *Clients) *Registry
}
// Network fetches network metadata by key (case-insensitive).
func (r *Registry) Network(key string) (shared.Network, bool) {
func (r *Registry) Network(key pmodel.ChainNetwork) (shared.Network, bool) {
if r == nil || len(r.networks) == 0 {
return shared.Network{}, false
}
n, ok := r.networks[strings.ToLower(strings.TrimSpace(key))]
n, ok := r.networks[key]
return n, ok
}
// Client returns the prepared RPC client for the given network name.
func (r *Registry) Client(key string) (*ethclient.Client, error) {
func (r *Registry) Client(key pmodel.ChainNetwork) (*ethclient.Client, error) {
if r == nil || r.clients == nil {
return nil, merrors.Internal("rpc clients not initialised")
}
return r.clients.Client(strings.ToLower(strings.TrimSpace(key)))
return r.clients.Client(key)
}
// RPCClient returns the raw RPC client for low-level calls.
func (r *Registry) RPCClient(key string) (*rpc.Client, error) {
func (r *Registry) RPCClient(key pmodel.ChainNetwork) (*rpc.Client, error) {
if r == nil || r.clients == nil {
return nil, merrors.Internal("rpc clients not initialised")
}
return r.clients.RPCClient(strings.ToLower(strings.TrimSpace(key)))
return r.clients.RPCClient(key)
}
// Networks exposes the registry map for iteration when needed.
func (r *Registry) Networks() map[string]shared.Network {
func (r *Registry) Networks() map[pmodel.ChainNetwork]shared.Network {
return r.networks
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/tech/sendico/pkg/discovery"
msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
@@ -43,7 +44,7 @@ type Service struct {
settings CacheSettings
networks map[string]shared.Network
networks map[pmodel.ChainNetwork]shared.Network
serviceWallet shared.ServiceWallet
keyManager keymanager.Manager
rpcClients *rpcclient.Clients
@@ -64,7 +65,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
producer: producer,
clock: clockpkg.System{},
settings: defaultSettings(),
networks: map[string]shared.Network{},
networks: map[pmodel.ChainNetwork]shared.Network{},
}
initMetrics()
@@ -79,7 +80,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
svc.clock = clockpkg.System{}
}
if svc.networks == nil {
svc.networks = map[string]shared.Network{}
svc.networks = map[pmodel.ChainNetwork]shared.Network{}
}
svc.settings = svc.settings.withDefaults()
svc.networkRegistry = rpcclient.NewRegistry(svc.networks, svc.rpcClients)
@@ -207,7 +208,7 @@ func (s *Service) startDiscoveryAnnouncers() {
announce := discovery.Announcement{
Service: "CRYPTO_RAIL_GATEWAY",
Rail: "CRYPTO",
Network: network.Name,
Network: string(network.Name),
Operations: []string{"balance.read", "payin.crypto", "payout.crypto", "fee.send", "observe.confirm"},
Currencies: currencies,
InvokeURI: s.invokeURI,

View File

@@ -11,6 +11,7 @@ import (
"time"
"github.com/stretchr/testify/require"
pmodel "github.com/tech/sendico/pkg/model"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
ichainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.mongodb.org/mongo-driver/v2/bson"
@@ -170,6 +171,7 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"},
},
},
IntentRef: "intent-1",
})
require.NoError(t, err)
require.NotNil(t, transferResp.GetTransfer())
@@ -177,7 +179,7 @@ func TestSubmitTransfer_ManagedDestination(t *testing.T) {
stored := repo.transfers.get(transferResp.GetTransfer().GetTransferRef())
require.NotNil(t, stored)
require.Equal(t, model.TransferStatusPending, stored.Status)
require.Equal(t, model.TransferStatusCreated, stored.Status)
// GetTransfer
getResp, err := svc.GetTransfer(ctx, &ichainv1.GetTransferRequest{TransferRef: stored.TransferRef})
@@ -335,7 +337,7 @@ func (w *inMemoryWallets) List(ctx context.Context, filter model.ManagedWalletFi
continue
}
}
if filter.Network != "" && !strings.EqualFold(wallet.Network, filter.Network) {
if wallet.Network != filter.Network {
continue
}
if filter.TokenSymbol != "" && !strings.EqualFold(wallet.TokenSymbol, filter.TokenSymbol) {
@@ -644,9 +646,9 @@ func newTestService(t *testing.T) (*Service, *inMemoryRepository) {
type fakeKeyManager struct{}
func (f *fakeKeyManager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) {
func (f *fakeKeyManager) CreateManagedWalletKey(ctx context.Context, walletRef string, network pmodel.ChainNetwork) (*keymanager.ManagedWalletKey, error) {
return &keymanager.ManagedWalletKey{
KeyID: fmt.Sprintf("%s/%s", strings.ToLower(network), walletRef),
KeyID: fmt.Sprintf("%s/%s", network, walletRef),
Address: "0x" + strings.Repeat("a", 40),
PublicKey: strings.Repeat("b", 128),
}, nil

View File

@@ -1,6 +1,10 @@
package shared
import "github.com/shopspring/decimal"
import (
"github.com/shopspring/decimal"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
)
// GasTopUpRule defines buffer, minimum, rounding, and cap behavior for native gas top-ups.
type GasTopUpRule struct {
@@ -30,3 +34,20 @@ func (p *GasTopUpPolicy) Rule(contractTransfer bool) (GasTopUpRule, bool) {
}
return p.Default, true
}
func СhainTransferStatusToOperation(ts chainv1.TransferStatus) connectorv1.OperationStatus {
switch ts {
case chainv1.TransferStatus_TRANSFER_CANCELLED:
return connectorv1.OperationStatus_OPERATION_CANCELLED
case chainv1.TransferStatus_TRANSFER_CREATED:
return connectorv1.OperationStatus_OPERATION_CREATED
case chainv1.TransferStatus_TRANSFER_FAILED:
return connectorv1.OperationStatus_OPERATION_FAILED
case chainv1.TransferStatus_TRANSFER_SUCCESS:
return connectorv1.OperationStatus_OPERATION_SUCCESS
case chainv1.TransferStatus_TRANSFER_WAITING:
return connectorv1.OperationStatus_OPERATION_WAITING
default:
}
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
}

View File

@@ -4,8 +4,10 @@ import (
"strings"
"github.com/tech/sendico/gateway/chain/storage/model"
chainasset "github.com/tech/sendico/pkg/chain"
pmodel "github.com/tech/sendico/pkg/model"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.mongodb.org/mongo-driver/v2/bson"
)
@@ -49,16 +51,38 @@ func GenerateTransferRef() string {
return bson.NewObjectID().Hex()
}
func ChainKeyFromEnum(chain chainv1.ChainNetwork) (string, chainv1.ChainNetwork) {
if name, ok := chainv1.ChainNetwork_name[int32(chain)]; ok {
key := strings.ToLower(strings.TrimPrefix(name, "CHAIN_NETWORK_"))
return key, chain
func ChainKeyFromEnum(chain chainv1.ChainNetwork) pmodel.ChainNetwork {
switch chain {
case chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE:
return pmodel.ChainNetworkArbitrumOne
case chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_SEPOLIA:
return pmodel.ChainNetworkArbitrumSepolia
case chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET:
return pmodel.ChainNetworkEthereumMainnet
case chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET:
return pmodel.ChainNetworkTronMainnet
case chainv1.ChainNetwork_CHAIN_NETWORK_TRON_NILE:
return pmodel.ChainNetworkTronNile
default:
return pmodel.ChainNetworkUnspecified
}
return "", chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
}
func ChainEnumFromName(name string) chainv1.ChainNetwork {
return chainasset.NetworkFromString(name)
func ChainEnumFromName(name pmodel.ChainNetwork) chainv1.ChainNetwork {
switch name {
case pmodel.ChainNetworkArbitrumOne:
return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE
case pmodel.ChainNetworkArbitrumSepolia:
return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_SEPOLIA
case pmodel.ChainNetworkEthereumMainnet:
return chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET
case pmodel.ChainNetworkTronMainnet:
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET
case pmodel.ChainNetworkTronNile:
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_NILE
default:
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED
}
}
func ManagedWalletStatusToProto(status model.ManagedWalletStatus) chainv1.ManagedWalletStatus {
@@ -76,54 +100,94 @@ func ManagedWalletStatusToProto(status model.ManagedWalletStatus) chainv1.Manage
func TransferStatusToModel(status chainv1.TransferStatus) model.TransferStatus {
switch status {
case chainv1.TransferStatus_TRANSFER_PENDING:
return model.TransferStatusPending
case chainv1.TransferStatus_TRANSFER_SIGNING:
return model.TransferStatusSigning
case chainv1.TransferStatus_TRANSFER_SUBMITTED:
return model.TransferStatusSubmitted
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
return model.TransferStatusConfirmed
case chainv1.TransferStatus_TRANSFER_CREATED:
return model.TransferStatusCreated
case chainv1.TransferStatus_TRANSFER_PROCESSING:
return model.TransferStatusProcessing
case chainv1.TransferStatus_TRANSFER_WAITING:
return model.TransferStatusWaiting
case chainv1.TransferStatus_TRANSFER_SUCCESS:
return model.TransferStatusSuccess
case chainv1.TransferStatus_TRANSFER_FAILED:
return model.TransferStatusFailed
case chainv1.TransferStatus_TRANSFER_CANCELLED:
return model.TransferStatusCancelled
default:
return ""
return model.TransferStatus("")
}
}
func TransferStatusToProto(status model.TransferStatus) chainv1.TransferStatus {
switch status {
case model.TransferStatusPending:
return chainv1.TransferStatus_TRANSFER_PENDING
case model.TransferStatusSigning:
return chainv1.TransferStatus_TRANSFER_SIGNING
case model.TransferStatusSubmitted:
return chainv1.TransferStatus_TRANSFER_SUBMITTED
case model.TransferStatusConfirmed:
return chainv1.TransferStatus_TRANSFER_CONFIRMED
case model.TransferStatusCreated:
return chainv1.TransferStatus_TRANSFER_CREATED
case model.TransferStatusProcessing:
return chainv1.TransferStatus_TRANSFER_PROCESSING
case model.TransferStatusWaiting:
return chainv1.TransferStatus_TRANSFER_WAITING
case model.TransferStatusSuccess:
return chainv1.TransferStatus_TRANSFER_SUCCESS
case model.TransferStatusFailed:
return chainv1.TransferStatus_TRANSFER_FAILED
case model.TransferStatusCancelled:
return chainv1.TransferStatus_TRANSFER_CANCELLED
default:
return chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED
}
}
func ChainTransferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
switch status {
case chainv1.TransferStatus_TRANSFER_CREATED:
return connectorv1.OperationStatus_OPERATION_CREATED
case chainv1.TransferStatus_TRANSFER_PROCESSING:
return connectorv1.OperationStatus_OPERATION_PROCESSING
case chainv1.TransferStatus_TRANSFER_WAITING:
return connectorv1.OperationStatus_OPERATION_WAITING
case chainv1.TransferStatus_TRANSFER_SUCCESS:
return connectorv1.OperationStatus_OPERATION_SUCCESS
case chainv1.TransferStatus_TRANSFER_FAILED:
return connectorv1.OperationStatus_OPERATION_FAILED
case chainv1.TransferStatus_TRANSFER_CANCELLED:
return connectorv1.OperationStatus_OPERATION_CANCELLED
default:
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
}
}
// NativeCurrency returns the canonical native token symbol for a network.
func NativeCurrency(network Network) string {
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
if currency == "" {
currency = strings.ToUpper(strings.TrimSpace(network.Name))
currency = strings.ToUpper(network.Name.String())
}
return currency
}
// Network describes a supported blockchain network and known token contracts.
type Network struct {
Name string
Name pmodel.ChainNetwork
RPCURL string
ChainID uint64
NativeToken string
@@ -139,7 +203,27 @@ type TokenContract struct {
// ServiceWallet captures the managed service wallet configuration.
type ServiceWallet struct {
Network string
Network pmodel.ChainNetwork
Address string
PrivateKey string
}
func ProtoToMoney(money *moneyv1.Money) *paymenttypes.Money {
if money == nil {
return &paymenttypes.Money{}
}
return &paymenttypes.Money{
Amount: money.GetAmount(),
Currency: money.GetCurrency(),
}
}
func MonenyToProto(money *paymenttypes.Money) *moneyv1.Money {
if money == nil {
return &moneyv1.Money{}
}
return &moneyv1.Money{
Amount: money.Amount,
Currency: money.Currency,
}
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/merrors"
pmodel "github.com/tech/sendico/pkg/model"
"go.uber.org/zap"
)
@@ -40,35 +41,35 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
return err
}
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSigning, "", ""); err != nil {
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusProcessing, "", ""); err != nil {
s.logger.Warn("Failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
}
driverDeps := s.driverDeps()
chainDriver, err := s.driverForNetwork(network.Name)
if err != nil {
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
return err
}
destinationAddress, err := s.destinationAddress(ctx, chainDriver, transfer.Destination)
if err != nil {
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
return err
}
sourceAddress, err := chainDriver.NormalizeAddress(sourceWallet.DepositAddress)
if err != nil {
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
_, _ = s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
return err
}
if chainDriver.Name() == "tron" && sourceAddress == destinationAddress {
s.logger.Info("Self transfer detected; skipping submission",
zap.String("transfer_ref", transferRef),
zap.String("wallet_ref", sourceWalletRef),
zap.String("network", network.Name),
zap.String("network", string(network.Name)),
)
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", ""); err != nil {
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusSuccess, "", ""); err != nil {
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
}
return nil
@@ -76,11 +77,14 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
txHash, err := chainDriver.SubmitTransfer(ctx, driverDeps, network, transfer, sourceWallet, destinationAddress)
if err != nil {
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
s.logger.Warn("Failed to submit transfer", zap.String("transfer_ref", transferRef), zap.Error(err))
if _, e := s.updateTransferStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), ""); e != nil {
s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(e))
}
return err
}
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSubmitted, "", txHash); err != nil {
if _, err := s.updateTransferStatus(ctx, transferRef, model.TransferStatusWaiting, "", txHash); err != nil {
s.logger.Warn("Failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err))
}
@@ -94,15 +98,15 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
return err
}
if receipt != nil && receipt.Status == types.ReceiptStatusSuccessful {
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", txHash); err != nil {
s.logger.Warn("Failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err))
}
return nil
failureReason := ""
pStatus := model.TransferStatusSuccess
if receipt != nil && receipt.Status != types.ReceiptStatusSuccessful {
failureReason = "transaction reverted"
pStatus = model.TransferStatusFailed
}
if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, "transaction reverted", txHash); err != nil {
s.logger.Warn("Failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(err))
if _, err := s.updateTransferStatus(ctx, transferRef, pStatus, failureReason, txHash); err != nil {
s.logger.Warn("Failed to update transfer status", zap.Error(err),
zap.String("transfer_ref", transferRef), zap.String("status", string(pStatus)))
}
return nil
}
@@ -133,7 +137,7 @@ func (s *Service) driverDeps() driver.Deps {
}
}
func (s *Service) driverForNetwork(network string) (driver.Driver, error) {
func (s *Service) driverForNetwork(network pmodel.ChainNetwork) (driver.Driver, error) {
if s.drivers == nil {
return nil, merrors.Internal("chain drivers not configured")
}

View File

@@ -0,0 +1,77 @@
package gateway
import (
"context"
"fmt"
"github.com/tech/sendico/gateway/chain/storage/model"
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/payments/rail"
"go.uber.org/zap"
)
func isFinalStatus(t *model.Transfer) bool {
switch t.Status {
case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled:
return true
default:
return false
}
}
func toOpStatus(t *model.Transfer) rail.OperationResult {
switch t.Status {
case model.TransferStatusFailed:
return rail.OperationResultFailed
case model.TransferStatusSuccess:
return rail.OperationResultSuccess
case model.TransferStatusCancelled:
return rail.OperationResultCancelled
default:
panic(fmt.Sprintf("toOpStatus: unexpected transfer status: %s", t.Status))
}
}
func toError(t *model.Transfer) string {
if t == nil {
return ""
}
if t.Status == model.TransferStatusSuccess {
return ""
}
return t.FailureReason
}
func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) (*model.Transfer, error) {
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
if err != nil {
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
}
if isFinalStatus(transfer) {
s.emitTransferStatusEvent(transfer)
}
return transfer, err
}
func (s *Service) emitTransferStatusEvent(transfer *model.Transfer) {
if s == nil || s.producer == nil || transfer == nil {
return
}
exec := pmodel.PaymentGatewayExecution{
PaymentIntentID: transfer.IntentRef,
IdempotencyKey: transfer.IdempotencyKey,
ExecutedMoney: transfer.NetAmount,
PaymentRef: transfer.PaymentRef,
Status: toOpStatus(transfer),
OperationRef: transfer.OperationRef,
Error: toError(transfer),
TransferRef: transfer.TransferRef,
}
env := paymentgateway.PaymentGatewayExecution(mservice.ChainGateway, &exec)
if err := s.producer.SendMessage(env); err != nil {
s.logger.Warn("Failed to publish transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
}
}

View File

@@ -5,19 +5,22 @@ import (
"time"
"github.com/tech/sendico/pkg/db/storable"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
paytypes "github.com/tech/sendico/pkg/payments/types"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)
type TransferStatus string
const (
TransferStatusPending TransferStatus = "pending"
TransferStatusSigning TransferStatus = "signing"
TransferStatusSubmitted TransferStatus = "submitted"
TransferStatusConfirmed TransferStatus = "confirmed"
TransferStatusFailed TransferStatus = "failed"
TransferStatusCancelled TransferStatus = "cancelled"
TransferStatusCreated TransferStatus = "created" // record exists, not started
TransferStatusProcessing TransferStatus = "processing" // we are working on it
TransferStatusWaiting TransferStatus = "waiting" // waiting external world
TransferStatusSuccess TransferStatus = "success" // final success
TransferStatusFailed TransferStatus = "failed" // final failure
TransferStatusCancelled TransferStatus = "cancelled" // final cancelled
)
// ServiceFee represents a fee component applied to a transfer.
@@ -38,21 +41,23 @@ type TransferDestination struct {
type Transfer struct {
storable.Base `bson:",inline" json:",inline"`
OperationRef string `bson:"operationRef" json:"operationRef"`
TransferRef string `bson:"transferRef" json:"transferRef"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
IdempotencyKey string `bson:"intentRef" json:"intentRef"`
IntentRef string `bson:"idempotencyKey" json:"idempotencyKey"`
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
SourceWalletRef string `bson:"sourceWalletRef" json:"sourceWalletRef"`
Destination TransferDestination `bson:"destination" json:"destination"`
Network string `bson:"network" json:"network"`
Network pmodel.ChainNetwork `bson:"network" json:"network"`
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
RequestedAmount *moneyv1.Money `bson:"requestedAmount" json:"requestedAmount"`
NetAmount *moneyv1.Money `bson:"netAmount" json:"netAmount"`
RequestedAmount *paytypes.Money `bson:"requestedAmount" json:"requestedAmount"`
NetAmount *paytypes.Money `bson:"netAmount" json:"netAmount"`
Fees []ServiceFee `bson:"fees,omitempty" json:"fees,omitempty"`
Status TransferStatus `bson:"status" json:"status"`
TxHash string `bson:"txHash,omitempty" json:"txHash,omitempty"`
FailureReason string `bson:"failureReason,omitempty" json:"failureReason,omitempty"`
ClientReference string `bson:"clientReference,omitempty" json:"clientReference,omitempty"`
PaymentRef string `bson:"paymentRef,omitempty" json:"paymentRef,omitempty"`
LastStatusAt time.Time `bson:"lastStatusAt" json:"lastStatusAt"`
}
@@ -82,12 +87,11 @@ func (t *Transfer) Normalize() {
t.IdempotencyKey = strings.TrimSpace(t.IdempotencyKey)
t.OrganizationRef = strings.TrimSpace(t.OrganizationRef)
t.SourceWalletRef = strings.TrimSpace(t.SourceWalletRef)
t.Network = strings.TrimSpace(strings.ToLower(t.Network))
t.TokenSymbol = strings.TrimSpace(strings.ToUpper(t.TokenSymbol))
t.ContractAddress = strings.TrimSpace(strings.ToLower(t.ContractAddress))
t.Destination.ManagedWalletRef = strings.TrimSpace(t.Destination.ManagedWalletRef)
t.Destination.ExternalAddress = normalizeWalletAddress(t.Destination.ExternalAddress)
t.Destination.ExternalAddressOriginal = strings.TrimSpace(t.Destination.ExternalAddressOriginal)
t.Destination.Memo = strings.TrimSpace(t.Destination.Memo)
t.ClientReference = strings.TrimSpace(t.ClientReference)
t.PaymentRef = strings.TrimSpace(t.PaymentRef)
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/tech/sendico/pkg/db/storable"
pkgmodel "github.com/tech/sendico/pkg/model"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)
@@ -23,17 +24,17 @@ type ManagedWallet struct {
storable.Base `bson:",inline" json:",inline"`
pkgmodel.Describable `bson:",inline" json:",inline"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
WalletRef string `bson:"walletRef" json:"walletRef"`
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
OwnerRef string `bson:"ownerRef" json:"ownerRef"`
Network string `bson:"network" json:"network"`
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
DepositAddress string `bson:"depositAddress" json:"depositAddress"`
KeyReference string `bson:"keyReference,omitempty" json:"keyReference,omitempty"`
Status ManagedWalletStatus `bson:"status" json:"status"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
WalletRef string `bson:"walletRef" json:"walletRef"`
OrganizationRef string `bson:"organizationRef" json:"organizationRef"`
OwnerRef string `bson:"ownerRef" json:"ownerRef"`
Network pkgmodel.ChainNetwork `bson:"network" json:"network"`
TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"`
ContractAddress string `bson:"contractAddress" json:"contractAddress"`
DepositAddress string `bson:"depositAddress" json:"depositAddress"`
KeyReference string `bson:"keyReference,omitempty" json:"keyReference,omitempty"`
Status ManagedWalletStatus `bson:"status" json:"status"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
}
// Collection implements storable.Storable.
@@ -66,7 +67,7 @@ type ManagedWalletFilter struct {
// - pointer to empty string: filter for wallets where owner_ref is empty
// - pointer to a value: filter for wallets where owner_ref matches
OwnerRefFilter *string
Network string
Network pmodel.ChainNetwork
TokenSymbol string
Cursor string
Limit int32
@@ -93,7 +94,6 @@ func (m *ManagedWallet) Normalize() {
m.Description = &desc
}
}
m.Network = strings.TrimSpace(strings.ToLower(m.Network))
m.TokenSymbol = strings.TrimSpace(strings.ToUpper(m.TokenSymbol))
m.ContractAddress = strings.TrimSpace(strings.ToLower(m.ContractAddress))
m.DepositAddress = normalizeWalletAddress(m.DepositAddress)

View File

@@ -79,7 +79,7 @@ func (t *Transfers) Create(ctx context.Context, transfer *model.Transfer) (*mode
return nil, merrors.InvalidArgument("transfersStore: empty idempotencyKey")
}
if transfer.Status == "" {
transfer.Status = model.TransferStatusPending
transfer.Status = model.TransferStatusCreated
}
if transfer.LastStatusAt.IsZero() {
transfer.LastStatusAt = time.Now().UTC()

View File

@@ -111,7 +111,7 @@ func (w *Wallets) Create(ctx context.Context, wallet *model.ManagedWallet) (*mod
fields = append(fields, zap.String("owner_ref", wallet.OwnerRef))
}
if wallet.Network != "" {
fields = append(fields, zap.String("network", wallet.Network))
fields = append(fields, zap.String("network", string(wallet.Network)))
}
if wallet.TokenSymbol != "" {
fields = append(fields, zap.String("token_symbol", wallet.TokenSymbol))
@@ -161,11 +161,7 @@ func (w *Wallets) List(ctx context.Context, filter model.ManagedWalletFilter) (*
query = query.Filter(repository.Field("ownerRef"), ownerRef)
fields = append(fields, zap.String("owner_ref_filter", ownerRef))
}
if network := strings.TrimSpace(filter.Network); network != "" {
normalized := strings.ToLower(network)
query = query.Filter(repository.Field("network"), normalized)
fields = append(fields, zap.String("network", normalized))
}
fields = append(fields, zap.String("network", string(filter.Network)))
if token := strings.TrimSpace(filter.TokenSymbol); token != "" {
normalized := strings.ToUpper(token)
query = query.Filter(repository.Field("tokenSymbol"), normalized)