tron refactoring
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
vaultmanager "github.com/tech/sendico/gateway/chain/internal/keymanager/vault"
|
vaultmanager "github.com/tech/sendico/gateway/chain/internal/keymanager/vault"
|
||||||
gatewayservice "github.com/tech/sendico/gateway/chain/internal/service/gateway"
|
gatewayservice "github.com/tech/sendico/gateway/chain/internal/service/gateway"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/drivers"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
||||||
gatewayshared "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
gatewayshared "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/gateway/chain/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
@@ -121,15 +122,18 @@ func (i *Imp) Start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
driverRegistry, err := drivers.NewRegistry(i.logger.Named("drivers"), networkConfigs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) {
|
serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) {
|
||||||
executor := gatewayservice.NewOnChainExecutor(logger, keyManager, rpcClients)
|
|
||||||
opts := []gatewayservice.Option{
|
opts := []gatewayservice.Option{
|
||||||
gatewayservice.WithNetworks(networkConfigs),
|
gatewayservice.WithNetworks(networkConfigs),
|
||||||
gatewayservice.WithServiceWallet(walletConfig),
|
gatewayservice.WithServiceWallet(walletConfig),
|
||||||
gatewayservice.WithKeyManager(keyManager),
|
gatewayservice.WithKeyManager(keyManager),
|
||||||
gatewayservice.WithTransferExecutor(executor),
|
|
||||||
gatewayservice.WithRPCClients(rpcClients),
|
gatewayservice.WithRPCClients(rpcClients),
|
||||||
|
gatewayservice.WithDriverRegistry(driverRegistry),
|
||||||
gatewayservice.WithSettings(cfg.Settings),
|
gatewayservice.WithSettings(cfg.Settings),
|
||||||
}
|
}
|
||||||
return gatewayservice.NewService(logger, repo, producer, opts...), nil
|
return gatewayservice.NewService(logger, repo, producer, opts...), nil
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/drivers"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/gateway/chain/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
|
|
||||||
type Deps struct {
|
type Deps struct {
|
||||||
Logger mlogger.Logger
|
Logger mlogger.Logger
|
||||||
|
Drivers *drivers.Registry
|
||||||
Networks *rpcclient.Registry
|
Networks *rpcclient.Registry
|
||||||
Storage storage.Repository
|
Storage storage.Repository
|
||||||
Clock clockpkg.Clock
|
Clock clockpkg.Clock
|
||||||
|
|||||||
@@ -43,8 +43,22 @@ func resolveDestination(ctx context.Context, deps Deps, dest *chainv1.TransferDe
|
|||||||
deps.Logger.Warn("destination external address missing")
|
deps.Logger.Warn("destination external address missing")
|
||||||
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
return model.TransferDestination{}, merrors.InvalidArgument("destination is required")
|
||||||
}
|
}
|
||||||
|
if deps.Drivers == nil {
|
||||||
|
deps.Logger.Warn("chain drivers missing", zap.String("network", 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))
|
||||||
|
return model.TransferDestination{}, merrors.InvalidArgument("unsupported chain for wallet")
|
||||||
|
}
|
||||||
|
normalized, err := chainDriver.NormalizeAddress(external)
|
||||||
|
if err != nil {
|
||||||
|
deps.Logger.Warn("invalid external address", zap.Error(err))
|
||||||
|
return model.TransferDestination{}, err
|
||||||
|
}
|
||||||
return model.TransferDestination{
|
return model.TransferDestination{
|
||||||
ExternalAddress: strings.ToLower(external),
|
ExternalAddress: normalized,
|
||||||
Memo: strings.TrimSpace(dest.GetMemo()),
|
Memo: strings.TrimSpace(dest.GetMemo()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||||
"github.com/tech/sendico/gateway/chain/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func destinationAddress(ctx context.Context, deps Deps, dest model.TransferDestination) (string, error) {
|
func destinationAddress(ctx context.Context, deps Deps, chainDriver driver.Driver, dest model.TransferDestination) (string, error) {
|
||||||
if ref := strings.TrimSpace(dest.ManagedWalletRef); ref != "" {
|
if ref := strings.TrimSpace(dest.ManagedWalletRef); ref != "" {
|
||||||
wallet, err := deps.Storage.Wallets().Get(ctx, ref)
|
wallet, err := deps.Storage.Wallets().Get(ctx, ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -17,10 +18,10 @@ func destinationAddress(ctx context.Context, deps Deps, dest model.TransferDesti
|
|||||||
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
||||||
return "", merrors.Internal("destination wallet missing deposit address")
|
return "", merrors.Internal("destination wallet missing deposit address")
|
||||||
}
|
}
|
||||||
return wallet.DepositAddress, nil
|
return chainDriver.NormalizeAddress(wallet.DepositAddress)
|
||||||
}
|
}
|
||||||
if addr := strings.TrimSpace(dest.ExternalAddress); addr != "" {
|
if addr := strings.TrimSpace(dest.ExternalAddress); addr != "" {
|
||||||
return strings.ToLower(addr), nil
|
return chainDriver.NormalizeAddress(addr)
|
||||||
}
|
}
|
||||||
return "", merrors.InvalidArgument("transfer destination address not resolved")
|
return "", merrors.InvalidArgument("transfer destination address not resolved")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,12 @@ package transfer
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"math/big"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
|
||||||
"github.com/tech/sendico/gateway/chain/storage/model"
|
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
||||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -69,6 +58,15 @@ func (c *estimateTransferFeeCommand) Execute(ctx context.Context, req *chainv1.E
|
|||||||
c.deps.Logger.Warn("unsupported chain", zap.String("network", networkKey))
|
c.deps.Logger.Warn("unsupported chain", zap.String("network", networkKey))
|
||||||
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
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))
|
||||||
|
return gsresponse.Internal[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("chain drivers not configured"))
|
||||||
|
}
|
||||||
|
chainDriver, err := c.deps.Drivers.Driver(networkKey)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("unsupported chain driver", zap.String("network", networkKey), zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet"))
|
||||||
|
}
|
||||||
|
|
||||||
dest, err := resolveDestination(ctx, c.deps, req.GetDestination(), sourceWallet)
|
dest, err := resolveDestination(ctx, c.deps, req.GetDestination(), sourceWallet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,13 +78,18 @@ func (c *estimateTransferFeeCommand) Execute(ctx context.Context, req *chainv1.E
|
|||||||
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
destinationAddress, err := destinationAddress(ctx, c.deps, dest)
|
destinationAddress, err := destinationAddress(ctx, c.deps, chainDriver, dest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.deps.Logger.Warn("failed to resolve destination address", zap.Error(err))
|
c.deps.Logger.Warn("failed to resolve destination address", zap.Error(err))
|
||||||
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
feeMoney, err := estimateNetworkFee(ctx, c.deps.Logger, c.deps.Networks, networkCfg, c.deps.RPCTimeout, sourceWallet, destinationAddress, amount)
|
driverDeps := driver.Deps{
|
||||||
|
Logger: c.deps.Logger,
|
||||||
|
Registry: c.deps.Networks,
|
||||||
|
RPCTimeout: c.deps.RPCTimeout,
|
||||||
|
}
|
||||||
|
feeMoney, err := chainDriver.EstimateFee(ctx, driverDeps, networkCfg, sourceWallet, destinationAddress, amount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.deps.Logger.Warn("fee estimation failed", zap.Error(err))
|
c.deps.Logger.Warn("fee estimation failed", zap.Error(err))
|
||||||
return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
@@ -98,150 +101,3 @@ func (c *estimateTransferFeeCommand) Execute(ctx context.Context, req *chainv1.E
|
|||||||
}
|
}
|
||||||
return gsresponse.Success(resp)
|
return gsresponse.Success(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func estimateNetworkFee(ctx context.Context, logger mlogger.Logger, registry *rpcclient.Registry, network shared.Network, timeout time.Duration, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
|
|
||||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
|
||||||
if rpcURL == "" {
|
|
||||||
return nil, merrors.InvalidArgument("network rpc url not configured")
|
|
||||||
}
|
|
||||||
if registry == nil {
|
|
||||||
return nil, merrors.Internal("rpc clients not initialised")
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(wallet.ContractAddress) == "" {
|
|
||||||
return nil, merrors.NotImplemented("native token transfers not supported")
|
|
||||||
}
|
|
||||||
if !common.IsHexAddress(wallet.ContractAddress) {
|
|
||||||
return nil, merrors.InvalidArgument("invalid token contract address")
|
|
||||||
}
|
|
||||||
if !common.IsHexAddress(wallet.DepositAddress) {
|
|
||||||
return nil, merrors.InvalidArgument("invalid source wallet address")
|
|
||||||
}
|
|
||||||
if !common.IsHexAddress(destination) {
|
|
||||||
return nil, merrors.InvalidArgument("invalid destination address")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := registry.Client(network.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rpcClient, err := registry.RPCClient(network.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if timeout <= 0 {
|
|
||||||
timeout = 15 * time.Second
|
|
||||||
}
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tokenABI, err := abi.JSON(strings.NewReader(erc20TransferABI))
|
|
||||||
if err != nil {
|
|
||||||
return nil, merrors.Internal("failed to parse erc20 abi: " + err.Error())
|
|
||||||
}
|
|
||||||
tokenAddr := common.HexToAddress(wallet.ContractAddress)
|
|
||||||
toAddr := common.HexToAddress(destination)
|
|
||||||
fromAddr := common.HexToAddress(wallet.DepositAddress)
|
|
||||||
|
|
||||||
decimals, err := erc20Decimals(timeoutCtx, rpcClient, tokenAddr)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("failed to read token decimals", zap.Error(err))
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
amountBase, err := toBaseUnits(strings.TrimSpace(amount.GetAmount()), decimals)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
input, err := tokenABI.Pack("transfer", toAddr, amountBase)
|
|
||||||
if err != nil {
|
|
||||||
return nil, merrors.Internal("failed to encode transfer call: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
gasPrice, err := client.SuggestGasPrice(timeoutCtx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, merrors.Internal("failed to suggest gas price: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
callMsg := ethereum.CallMsg{
|
|
||||||
From: fromAddr,
|
|
||||||
To: &tokenAddr,
|
|
||||||
GasPrice: gasPrice,
|
|
||||||
Data: input,
|
|
||||||
}
|
|
||||||
gasLimit, err := client.EstimateGas(timeoutCtx, callMsg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, merrors.Internal("failed to estimate gas: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
fee := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasLimit))
|
|
||||||
feeDec := decimal.NewFromBigInt(fee, 0)
|
|
||||||
|
|
||||||
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
|
||||||
if currency == "" {
|
|
||||||
currency = strings.ToUpper(network.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &moneyv1.Money{
|
|
||||||
Currency: currency,
|
|
||||||
Amount: feeDec.String(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func erc20Decimals(ctx context.Context, client *rpc.Client, token common.Address) (uint8, error) {
|
|
||||||
call := map[string]string{
|
|
||||||
"to": strings.ToLower(token.Hex()),
|
|
||||||
"data": "0x313ce567",
|
|
||||||
}
|
|
||||||
var hexResp string
|
|
||||||
if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil {
|
|
||||||
return 0, merrors.Internal("decimals call failed: " + err.Error())
|
|
||||||
}
|
|
||||||
val, err := shared.DecodeHexUint8(hexResp)
|
|
||||||
if err != nil {
|
|
||||||
return 0, merrors.Internal("decimals decode failed: " + err.Error())
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func toBaseUnits(amount string, decimals uint8) (*big.Int, error) {
|
|
||||||
value, err := decimal.NewFromString(strings.TrimSpace(amount))
|
|
||||||
if err != nil {
|
|
||||||
return nil, merrors.InvalidArgument("invalid amount " + amount + ": " + err.Error())
|
|
||||||
}
|
|
||||||
if value.IsNegative() {
|
|
||||||
return nil, merrors.InvalidArgument("amount must be positive")
|
|
||||||
}
|
|
||||||
multiplier := decimal.NewFromInt(1).Shift(int32(decimals))
|
|
||||||
scaled := value.Mul(multiplier)
|
|
||||||
if !scaled.Equal(scaled.Truncate(0)) {
|
|
||||||
return nil, merrors.InvalidArgument("amount " + amount + " exceeds token precision")
|
|
||||||
}
|
|
||||||
return scaled.BigInt(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const erc20TransferABI = `
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"constant": true,
|
|
||||||
"inputs": [],
|
|
||||||
"name": "decimals",
|
|
||||||
"outputs": [{ "name": "", "type": "uint8" }],
|
|
||||||
"payable": false,
|
|
||||||
"stateMutability": "view",
|
|
||||||
"type": "function"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"constant": false,
|
|
||||||
"inputs": [
|
|
||||||
{ "name": "_to", "type": "address" },
|
|
||||||
{ "name": "_value", "type": "uint256" }
|
|
||||||
],
|
|
||||||
"name": "transfer",
|
|
||||||
"outputs": [{ "name": "", "type": "bool" }],
|
|
||||||
"payable": false,
|
|
||||||
"stateMutability": "nonpayable",
|
|
||||||
"type": "function"
|
|
||||||
}
|
|
||||||
]`
|
|
||||||
|
|||||||
@@ -65,6 +65,15 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C
|
|||||||
c.deps.Logger.Warn("unsupported chain in config", zap.String("chain", chainKey))
|
c.deps.Logger.Warn("unsupported chain in config", zap.String("chain", chainKey))
|
||||||
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
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))
|
||||||
|
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))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain"))
|
||||||
|
}
|
||||||
|
|
||||||
tokenSymbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
|
tokenSymbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
|
||||||
if tokenSymbol == "" {
|
if tokenSymbol == "" {
|
||||||
@@ -95,6 +104,11 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C
|
|||||||
c.deps.Logger.Warn("key manager returned empty address")
|
c.deps.Logger.Warn("key manager returned empty address")
|
||||||
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address"))
|
return gsresponse.Internal[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address"))
|
||||||
}
|
}
|
||||||
|
depositAddress, err := chainDriver.FormatAddress(keyInfo.Address)
|
||||||
|
if err != nil {
|
||||||
|
c.deps.Logger.Warn("invalid derived deposit address", zap.String("wallet_ref", walletRef), zap.Error(err))
|
||||||
|
return gsresponse.InvalidArgument[chainv1.CreateManagedWalletResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
|
||||||
metadata := shared.CloneMetadata(req.GetMetadata())
|
metadata := shared.CloneMetadata(req.GetMetadata())
|
||||||
desc := req.GetDescribable()
|
desc := req.GetDescribable()
|
||||||
@@ -128,7 +142,7 @@ func (c *createManagedWalletCommand) Execute(ctx context.Context, req *chainv1.C
|
|||||||
Network: chainKey,
|
Network: chainKey,
|
||||||
TokenSymbol: tokenSymbol,
|
TokenSymbol: tokenSymbol,
|
||||||
ContractAddress: contractAddress,
|
ContractAddress: contractAddress,
|
||||||
DepositAddress: strings.ToLower(keyInfo.Address),
|
DepositAddress: depositAddress,
|
||||||
KeyReference: keyInfo.KeyID,
|
KeyReference: keyInfo.KeyID,
|
||||||
Status: model.ManagedWalletStatusActive,
|
Status: model.ManagedWalletStatusActive,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/drivers"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
||||||
"github.com/tech/sendico/gateway/chain/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
|
|
||||||
type Deps struct {
|
type Deps struct {
|
||||||
Logger mlogger.Logger
|
Logger mlogger.Logger
|
||||||
|
Drivers *drivers.Registry
|
||||||
Networks *rpcclient.Registry
|
Networks *rpcclient.Registry
|
||||||
KeyManager keymanager.Manager
|
KeyManager keymanager.Manager
|
||||||
Storage storage.Repository
|
Storage storage.Repository
|
||||||
|
|||||||
@@ -3,14 +3,9 @@ package wallet
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
|
||||||
"github.com/tech/sendico/gateway/chain/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
@@ -19,10 +14,18 @@ import (
|
|||||||
|
|
||||||
func onChainWalletBalance(ctx context.Context, deps Deps, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
func onChainWalletBalance(ctx context.Context, deps Deps, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||||
logger := deps.Logger
|
logger := deps.Logger
|
||||||
registry := deps.Networks
|
if wallet == nil {
|
||||||
|
return nil, merrors.InvalidArgument("wallet is required")
|
||||||
|
}
|
||||||
|
if deps.Networks == nil {
|
||||||
|
return nil, merrors.Internal("rpc clients not initialised")
|
||||||
|
}
|
||||||
|
if deps.Drivers == nil {
|
||||||
|
return nil, merrors.Internal("chain drivers not configured")
|
||||||
|
}
|
||||||
|
|
||||||
networkKey := strings.ToLower(strings.TrimSpace(wallet.Network))
|
networkKey := strings.ToLower(strings.TrimSpace(wallet.Network))
|
||||||
network, ok := registry.Network(networkKey)
|
network, ok := deps.Networks.Network(networkKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Warn("Requested network is not configured",
|
logger.Warn("Requested network is not configured",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
@@ -31,104 +34,21 @@ func onChainWalletBalance(ctx context.Context, deps Deps, wallet *model.ManagedW
|
|||||||
return nil, merrors.Internal(fmt.Sprintf("Requested network '%s' is not configured", networkKey))
|
return nil, merrors.Internal(fmt.Sprintf("Requested network '%s' is not configured", networkKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
rpcURL := strings.TrimSpace(network.RPCURL)
|
chainDriver, err := deps.Drivers.Driver(networkKey)
|
||||||
|
if err != nil {
|
||||||
logFields := []zap.Field{
|
logger.Warn("Chain driver not configured",
|
||||||
zap.String("wallet_ref", wallet.WalletRef),
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
zap.String("network", networkKey),
|
zap.String("network", networkKey),
|
||||||
zap.String("token_symbol", strings.ToUpper(strings.TrimSpace(wallet.TokenSymbol))),
|
zap.Error(err),
|
||||||
zap.String("contract", strings.ToLower(strings.TrimSpace(wallet.ContractAddress))),
|
|
||||||
zap.String("wallet_address", strings.ToLower(strings.TrimSpace(wallet.DepositAddress))),
|
|
||||||
}
|
|
||||||
|
|
||||||
if rpcURL == "" {
|
|
||||||
logger.Warn("Network rpc url is not configured", logFields...)
|
|
||||||
return nil, merrors.Internal("network rpc url is not configured")
|
|
||||||
}
|
|
||||||
contract := strings.TrimSpace(wallet.ContractAddress)
|
|
||||||
if contract == "" || !common.IsHexAddress(contract) {
|
|
||||||
logger.Warn("Invalid contract address for balance fetch", logFields...)
|
|
||||||
return nil, merrors.InvalidArgument("invalid contract address")
|
|
||||||
}
|
|
||||||
if wallet.DepositAddress == "" || !common.IsHexAddress(wallet.DepositAddress) {
|
|
||||||
logger.Warn("Invalid wallet address for balance fetch", logFields...)
|
|
||||||
return nil, merrors.InvalidArgument("invalid wallet address")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Fetching on-chain wallet balance", logFields...)
|
|
||||||
|
|
||||||
rpcClient, err := registry.RPCClient(networkKey)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("Failed to fetch rpc client", append(logFields, zap.Error(err))...)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
timeout := deps.RPCTimeout
|
|
||||||
if timeout <= 0 {
|
|
||||||
timeout = 10 * time.Second
|
|
||||||
}
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
logger.Debug("Calling token decimals", logFields...)
|
|
||||||
decimals, err := readDecimals(timeoutCtx, rpcClient, contract)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("Token decimals call failed", append(logFields, zap.Error(err))...)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug("Calling token balanceOf", append(logFields, zap.Uint8("decimals", decimals))...)
|
|
||||||
bal, err := readBalanceOf(timeoutCtx, rpcClient, contract, wallet.DepositAddress)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("Token balanceOf call failed", append(logFields, zap.Uint8("decimals", decimals), zap.Error(err))...)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dec := decimal.NewFromBigInt(bal, 0).Shift(-int32(decimals))
|
|
||||||
logger.Info("On-chain wallet balance fetched",
|
|
||||||
append(logFields,
|
|
||||||
zap.Uint8("decimals", decimals),
|
|
||||||
zap.String("balance_raw", bal.String()),
|
|
||||||
zap.String("balance", dec.String()),
|
|
||||||
)...,
|
|
||||||
)
|
)
|
||||||
return &moneyv1.Money{Currency: wallet.TokenSymbol, Amount: dec.String()}, nil
|
return nil, merrors.InvalidArgument("unsupported chain")
|
||||||
}
|
}
|
||||||
|
|
||||||
func readDecimals(ctx context.Context, client *rpc.Client, token string) (uint8, error) {
|
driverDeps := driver.Deps{
|
||||||
call := map[string]string{
|
Logger: deps.Logger,
|
||||||
"to": strings.ToLower(common.HexToAddress(token).Hex()),
|
Registry: deps.Networks,
|
||||||
"data": "0x313ce567",
|
KeyManager: deps.KeyManager,
|
||||||
|
RPCTimeout: deps.RPCTimeout,
|
||||||
}
|
}
|
||||||
var hexResp string
|
return chainDriver.Balance(ctx, driverDeps, network, wallet)
|
||||||
if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil {
|
|
||||||
return 0, merrors.Internal("decimals call failed: " + err.Error())
|
|
||||||
}
|
|
||||||
val, err := shared.DecodeHexUint8(hexResp)
|
|
||||||
if err != nil {
|
|
||||||
return 0, merrors.Internal("decimals decode failed: " + err.Error())
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBalanceOf(ctx context.Context, client *rpc.Client, token string, wallet string) (*big.Int, error) {
|
|
||||||
tokenAddr := common.HexToAddress(token)
|
|
||||||
walletAddr := common.HexToAddress(wallet)
|
|
||||||
addr := strings.TrimPrefix(walletAddr.Hex(), "0x")
|
|
||||||
if len(addr) < 64 {
|
|
||||||
addr = strings.Repeat("0", 64-len(addr)) + addr
|
|
||||||
}
|
|
||||||
call := map[string]string{
|
|
||||||
"to": strings.ToLower(tokenAddr.Hex()),
|
|
||||||
"data": "0x70a08231" + addr,
|
|
||||||
}
|
|
||||||
var hexResp string
|
|
||||||
if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil {
|
|
||||||
return nil, merrors.Internal("balanceOf call failed: " + err.Error())
|
|
||||||
}
|
|
||||||
bigVal, err := shared.DecodeHexBig(hexResp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, merrors.Internal("balanceOf decode failed: " + err.Error())
|
|
||||||
}
|
|
||||||
return bigVal, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package arbitrum
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/evm"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Driver implements Arbitrum-specific behavior using the shared EVM logic.
|
||||||
|
type Driver struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(logger mlogger.Logger) *Driver {
|
||||||
|
return &Driver{logger: logger.Named("arbitrum")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Name() string {
|
||||||
|
return "arbitrum"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) FormatAddress(address string) (string, error) {
|
||||||
|
d.logger.Debug("format address", zap.String("address", address))
|
||||||
|
normalized, err := evm.NormalizeAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("format address failed", zap.String("address", address), zap.Error(err))
|
||||||
|
}
|
||||||
|
return normalized, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) NormalizeAddress(address string) (string, error) {
|
||||||
|
d.logger.Debug("normalize address", zap.String("address", address))
|
||||||
|
normalized, err := evm.NormalizeAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("normalize address failed", zap.String("address", address), zap.Error(err))
|
||||||
|
}
|
||||||
|
return normalized, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
result, err := evm.Balance(ctx, driverDeps, network, wallet, wallet.DepositAddress)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("balance failed",
|
||||||
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
|
zap.String("network", 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("amount", result.Amount),
|
||||||
|
zap.String("currency", result.Currency),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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("destination", destination),
|
||||||
|
)
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
result, err := evm.EstimateFee(ctx, driverDeps, network, wallet, wallet.DepositAddress, destination, amount)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("estimate fee failed",
|
||||||
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
|
zap.String("network", 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("amount", result.Amount),
|
||||||
|
zap.String("currency", result.Currency),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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("destination", destination),
|
||||||
|
)
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
txHash, err := evm.SubmitTransfer(ctx, driverDeps, network, transfer, source, source.DepositAddress, destination)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("submit transfer failed",
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("network", 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("tx_hash", txHash),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return txHash, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
receipt, err := evm.AwaitConfirmation(ctx, driverDeps, network, txHash)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("await confirmation failed",
|
||||||
|
zap.String("tx_hash", txHash),
|
||||||
|
zap.String("network", 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.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
||||||
|
zap.Uint64("status", receipt.Status),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return receipt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*Driver)(nil)
|
||||||
33
api/gateway/chain/internal/service/gateway/driver/driver.go
Normal file
33
api/gateway/chain/internal/service/gateway/driver/driver.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package driver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deps bundles dependencies shared across chain drivers.
|
||||||
|
type Deps struct {
|
||||||
|
Logger mlogger.Logger
|
||||||
|
Registry *rpcclient.Registry
|
||||||
|
KeyManager keymanager.Manager
|
||||||
|
RPCTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Driver defines chain-specific behavior for wallet and transfer operations.
|
||||||
|
type Driver interface {
|
||||||
|
Name() string
|
||||||
|
FormatAddress(address string) (string, error)
|
||||||
|
NormalizeAddress(address string) (string, error)
|
||||||
|
Balance(ctx context.Context, deps Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error)
|
||||||
|
EstimateFee(ctx context.Context, deps Deps, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error)
|
||||||
|
SubmitTransfer(ctx context.Context, deps Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, destination string) (string, error)
|
||||||
|
AwaitConfirmation(ctx context.Context, deps Deps, network shared.Network, txHash string) (*types.Receipt, error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package ethereum
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/evm"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Driver implements Ethereum-specific behavior using the shared EVM logic.
|
||||||
|
type Driver struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(logger mlogger.Logger) *Driver {
|
||||||
|
return &Driver{logger: logger.Named("ethereum")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Name() string {
|
||||||
|
return "ethereum"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) FormatAddress(address string) (string, error) {
|
||||||
|
d.logger.Debug("format address", zap.String("address", address))
|
||||||
|
normalized, err := evm.NormalizeAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("format address failed", zap.String("address", address), zap.Error(err))
|
||||||
|
}
|
||||||
|
return normalized, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) NormalizeAddress(address string) (string, error) {
|
||||||
|
d.logger.Debug("normalize address", zap.String("address", address))
|
||||||
|
normalized, err := evm.NormalizeAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("normalize address failed", zap.String("address", address), zap.Error(err))
|
||||||
|
}
|
||||||
|
return normalized, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
result, err := evm.Balance(ctx, driverDeps, network, wallet, wallet.DepositAddress)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("balance failed",
|
||||||
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
|
zap.String("network", 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("amount", result.Amount),
|
||||||
|
zap.String("currency", result.Currency),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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("destination", destination),
|
||||||
|
)
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
result, err := evm.EstimateFee(ctx, driverDeps, network, wallet, wallet.DepositAddress, destination, amount)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("estimate fee failed",
|
||||||
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
|
zap.String("network", 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("amount", result.Amount),
|
||||||
|
zap.String("currency", result.Currency),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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("destination", destination),
|
||||||
|
)
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
txHash, err := evm.SubmitTransfer(ctx, driverDeps, network, transfer, source, source.DepositAddress, destination)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("submit transfer failed",
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("network", 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("tx_hash", txHash),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return txHash, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
receipt, err := evm.AwaitConfirmation(ctx, driverDeps, network, txHash)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("await confirmation failed",
|
||||||
|
zap.String("tx_hash", txHash),
|
||||||
|
zap.String("network", 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.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
||||||
|
zap.Uint64("status", receipt.Status),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return receipt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*Driver)(nil)
|
||||||
554
api/gateway/chain/internal/service/gateway/driver/evm/evm.go
Normal file
554
api/gateway/chain/internal/service/gateway/driver/evm/evm.go
Normal file
@@ -0,0 +1,554 @@
|
|||||||
|
package evm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum"
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
erc20ABI abi.ABI
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
erc20ABI, err = abi.JSON(strings.NewReader(erc20ABIJSON))
|
||||||
|
if err != nil {
|
||||||
|
panic("evm driver: failed to parse erc20 abi: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const erc20ABIJSON = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{ "name": "_to", "type": "address" },
|
||||||
|
{ "name": "_value", "type": "uint256" }
|
||||||
|
],
|
||||||
|
"name": "transfer",
|
||||||
|
"outputs": [{ "name": "", "type": "bool" }],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "decimals",
|
||||||
|
"outputs": [{ "name": "", "type": "uint8" }],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
]`
|
||||||
|
|
||||||
|
// NormalizeAddress validates and normalizes EVM hex addresses.
|
||||||
|
func NormalizeAddress(address string) (string, error) {
|
||||||
|
trimmed := strings.TrimSpace(address)
|
||||||
|
if trimmed == "" {
|
||||||
|
return "", merrors.InvalidArgument("address is required")
|
||||||
|
}
|
||||||
|
if !common.IsHexAddress(trimmed) {
|
||||||
|
return "", merrors.InvalidArgument("invalid hex address")
|
||||||
|
}
|
||||||
|
return strings.ToLower(common.HexToAddress(trimmed).Hex()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Balance fetches ERC20 token balance for the provided address.
|
||||||
|
func Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, address string) (*moneyv1.Money, error) {
|
||||||
|
logger := deps.Logger
|
||||||
|
registry := deps.Registry
|
||||||
|
|
||||||
|
if registry == nil {
|
||||||
|
return nil, merrors.Internal("rpc clients not initialised")
|
||||||
|
}
|
||||||
|
if wallet == nil {
|
||||||
|
return nil, merrors.InvalidArgument("wallet is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedAddress, err := NormalizeAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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("token_symbol", strings.ToUpper(strings.TrimSpace(wallet.TokenSymbol))),
|
||||||
|
zap.String("contract", strings.ToLower(strings.TrimSpace(wallet.ContractAddress))),
|
||||||
|
zap.String("wallet_address", normalizedAddress),
|
||||||
|
}
|
||||||
|
if rpcURL == "" {
|
||||||
|
logger.Warn("Network rpc url is not configured", logFields...)
|
||||||
|
return nil, merrors.Internal("network rpc url is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
contract := strings.TrimSpace(wallet.ContractAddress)
|
||||||
|
if contract == "" || !common.IsHexAddress(contract) {
|
||||||
|
logger.Warn("Invalid contract address for balance fetch", logFields...)
|
||||||
|
return nil, merrors.InvalidArgument("invalid contract address")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Fetching on-chain wallet balance", logFields...)
|
||||||
|
|
||||||
|
rpcClient, err := registry.RPCClient(network.Name)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to fetch rpc client", append(logFields, zap.Error(err))...)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := deps.RPCTimeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
logger.Debug("Calling token decimals", logFields...)
|
||||||
|
decimals, err := readDecimals(timeoutCtx, rpcClient, contract)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Token decimals call failed", append(logFields, zap.Error(err))...)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("Calling token balanceOf", append(logFields, zap.Uint8("decimals", decimals))...)
|
||||||
|
bal, err := readBalanceOf(timeoutCtx, rpcClient, contract, normalizedAddress)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Token balanceOf call failed", append(logFields, zap.Uint8("decimals", decimals), zap.Error(err))...)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := decimal.NewFromBigInt(bal, 0).Shift(-int32(decimals))
|
||||||
|
logger.Info("On-chain wallet balance fetched",
|
||||||
|
append(logFields,
|
||||||
|
zap.Uint8("decimals", decimals),
|
||||||
|
zap.String("balance_raw", bal.String()),
|
||||||
|
zap.String("balance", dec.String()),
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
return &moneyv1.Money{Currency: wallet.TokenSymbol, Amount: dec.String()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstimateFee estimates ERC20 transfer fees for the given parameters.
|
||||||
|
func EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, fromAddress, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
|
||||||
|
logger := deps.Logger
|
||||||
|
registry := deps.Registry
|
||||||
|
|
||||||
|
if registry == nil {
|
||||||
|
return nil, merrors.Internal("rpc clients not initialised")
|
||||||
|
}
|
||||||
|
if wallet == nil {
|
||||||
|
return nil, merrors.InvalidArgument("wallet is required")
|
||||||
|
}
|
||||||
|
if amount == nil {
|
||||||
|
return nil, merrors.InvalidArgument("amount is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
|
if rpcURL == "" {
|
||||||
|
return nil, merrors.InvalidArgument("network rpc url not configured")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(wallet.ContractAddress) == "" {
|
||||||
|
return nil, merrors.NotImplemented("native token transfers not supported")
|
||||||
|
}
|
||||||
|
if !common.IsHexAddress(wallet.ContractAddress) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid token contract address")
|
||||||
|
}
|
||||||
|
if _, err := NormalizeAddress(fromAddress); err != nil {
|
||||||
|
return nil, merrors.InvalidArgument("invalid source wallet address")
|
||||||
|
}
|
||||||
|
if _, err := NormalizeAddress(destination); err != nil {
|
||||||
|
return nil, merrors.InvalidArgument("invalid destination address")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := registry.Client(network.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rpcClient, err := registry.RPCClient(network.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := deps.RPCTimeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 15 * time.Second
|
||||||
|
}
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tokenAddr := common.HexToAddress(wallet.ContractAddress)
|
||||||
|
toAddr := common.HexToAddress(destination)
|
||||||
|
fromAddr := common.HexToAddress(fromAddress)
|
||||||
|
|
||||||
|
decimals, err := erc20Decimals(timeoutCtx, rpcClient, tokenAddr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to read token decimals", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
amountBase, err := toBaseUnits(strings.TrimSpace(amount.GetAmount()), decimals)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err := erc20ABI.Pack("transfer", toAddr, amountBase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to encode transfer call: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
gasPrice, err := client.SuggestGasPrice(timeoutCtx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to suggest gas price: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
callMsg := ethereum.CallMsg{
|
||||||
|
From: fromAddr,
|
||||||
|
To: &tokenAddr,
|
||||||
|
GasPrice: gasPrice,
|
||||||
|
Data: input,
|
||||||
|
}
|
||||||
|
gasLimit, err := client.EstimateGas(timeoutCtx, callMsg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to estimate gas: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fee := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasLimit))
|
||||||
|
feeDec := decimal.NewFromBigInt(fee, 0)
|
||||||
|
|
||||||
|
currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
|
||||||
|
if currency == "" {
|
||||||
|
currency = strings.ToUpper(network.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &moneyv1.Money{
|
||||||
|
Currency: currency,
|
||||||
|
Amount: feeDec.String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitTransfer submits an ERC20 transfer on an EVM-compatible chain.
|
||||||
|
func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, fromAddress, destination string) (string, error) {
|
||||||
|
logger := deps.Logger
|
||||||
|
registry := deps.Registry
|
||||||
|
|
||||||
|
if deps.KeyManager == nil {
|
||||||
|
logger.Warn("Key manager not configured")
|
||||||
|
return "", executorInternal("key manager is not configured", nil)
|
||||||
|
}
|
||||||
|
if registry == nil {
|
||||||
|
return "", executorInternal("rpc clients not initialised", nil)
|
||||||
|
}
|
||||||
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
|
if rpcURL == "" {
|
||||||
|
logger.Warn("Network rpc url missing", zap.String("network", network.Name))
|
||||||
|
return "", executorInvalid("network rpc url is not configured")
|
||||||
|
}
|
||||||
|
if source == nil || transfer == nil {
|
||||||
|
logger.Warn("Transfer context missing")
|
||||||
|
return "", executorInvalid("transfer context missing")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(source.KeyReference) == "" {
|
||||||
|
logger.Warn("Source wallet missing key reference", zap.String("wallet_ref", source.WalletRef))
|
||||||
|
return "", executorInvalid("source wallet missing key reference")
|
||||||
|
}
|
||||||
|
if _, err := NormalizeAddress(fromAddress); err != nil {
|
||||||
|
logger.Warn("Invalid source wallet address", zap.String("wallet_ref", source.WalletRef))
|
||||||
|
return "", executorInvalid("invalid source wallet address")
|
||||||
|
}
|
||||||
|
if _, err := NormalizeAddress(destination); err != nil {
|
||||||
|
logger.Warn("Invalid destination address", zap.String("transfer_ref", transfer.TransferRef), zap.String("address", destination))
|
||||||
|
return "", executorInvalid("invalid destination address " + destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("submitting transfer",
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("source_wallet_ref", source.WalletRef),
|
||||||
|
zap.String("network", 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))
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
rpcClient, err := registry.RPCClient(network.Name)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("failed to initialise rpc client", zap.String("network", network.Name))
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceAddress := common.HexToAddress(fromAddress)
|
||||||
|
destinationAddr := common.HexToAddress(destination)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
nonce, err := client.PendingNonceAt(ctx, sourceAddress)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to fetch nonce", zap.Error(err),
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("wallet_ref", source.WalletRef),
|
||||||
|
)
|
||||||
|
return "", executorInternal("failed to fetch nonce", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
return "", executorInternal("failed to suggest gas price", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
chainID := new(big.Int).SetUint64(network.ChainID)
|
||||||
|
|
||||||
|
if strings.TrimSpace(transfer.ContractAddress) == "" {
|
||||||
|
logger.Warn("Native token transfer requested but not supported", zap.String("transfer_ref", transfer.TransferRef))
|
||||||
|
return "", merrors.NotImplemented("executor: native token transfers not yet supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !common.IsHexAddress(transfer.ContractAddress) {
|
||||||
|
logger.Warn("Invalid token contract address",
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("contract", transfer.ContractAddress),
|
||||||
|
)
|
||||||
|
return "", executorInvalid("invalid token contract address " + transfer.ContractAddress)
|
||||||
|
}
|
||||||
|
tokenAddress := common.HexToAddress(transfer.ContractAddress)
|
||||||
|
|
||||||
|
decimals, err := erc20Decimals(ctx, rpcClient, tokenAddress)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to read token decimals", zap.Error(err),
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("contract", transfer.ContractAddress),
|
||||||
|
)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
amount := transfer.NetAmount
|
||||||
|
if amount == nil || strings.TrimSpace(amount.Amount) == "" {
|
||||||
|
logger.Warn("Transfer missing net amount", zap.String("transfer_ref", transfer.TransferRef))
|
||||||
|
return "", executorInvalid("transfer missing net amount")
|
||||||
|
}
|
||||||
|
amountInt, err := toBaseUnits(amount.Amount, decimals)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to convert amount to base units", zap.Error(err),
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("amount", amount.Amount),
|
||||||
|
)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err := erc20ABI.Pack("transfer", destinationAddr, amountInt)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to encode transfer call", zap.Error(err),
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
)
|
||||||
|
return "", executorInternal("failed to encode transfer call", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
callMsg := ethereum.CallMsg{
|
||||||
|
From: sourceAddress,
|
||||||
|
To: &tokenAddress,
|
||||||
|
GasPrice: gasPrice,
|
||||||
|
Data: input,
|
||||||
|
}
|
||||||
|
gasLimit, err := client.EstimateGas(ctx, callMsg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to estimate gas", zap.Error(err),
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
)
|
||||||
|
return "", executorInternal("failed to estimate gas", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := types.NewTransaction(nonce, tokenAddress, big.NewInt(0), gasLimit, gasPrice, input)
|
||||||
|
|
||||||
|
signedTx, err := deps.KeyManager.SignTransaction(ctx, source.KeyReference, tx, chainID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to sign transaction", zap.Error(err),
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("wallet_ref", source.WalletRef),
|
||||||
|
)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.SendTransaction(ctx, signedTx); err != nil {
|
||||||
|
logger.Warn("Failed to send transaction", zap.Error(err),
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
)
|
||||||
|
return "", executorInternal("failed to send transaction", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
txHash := signedTx.Hash().Hex()
|
||||||
|
logger.Info("Transaction submitted",
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("tx_hash", txHash),
|
||||||
|
zap.String("network", network.Name),
|
||||||
|
)
|
||||||
|
|
||||||
|
return txHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AwaitConfirmation waits for the transaction receipt.
|
||||||
|
func AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Network, txHash string) (*types.Receipt, error) {
|
||||||
|
logger := deps.Logger
|
||||||
|
registry := deps.Registry
|
||||||
|
|
||||||
|
if strings.TrimSpace(txHash) == "" {
|
||||||
|
logger.Warn("Missing transaction hash for confirmation", zap.String("network", network.Name))
|
||||||
|
return nil, executorInvalid("tx hash is required")
|
||||||
|
}
|
||||||
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
|
if rpcURL == "" {
|
||||||
|
logger.Warn("Network rpc url missing while awaiting confirmation", zap.String("tx_hash", txHash))
|
||||||
|
return nil, executorInvalid("network rpc url is not configured")
|
||||||
|
}
|
||||||
|
if registry == nil {
|
||||||
|
return nil, executorInternal("rpc clients not initialised", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := registry.Client(network.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := common.HexToHash(txHash)
|
||||||
|
ticker := time.NewTicker(3 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
receipt, err := client.TransactionReceipt(ctx, hash)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ethereum.NotFound) {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
logger.Debug("Transaction not yet mined",
|
||||||
|
zap.String("tx_hash", txHash),
|
||||||
|
zap.String("network", network.Name),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Warn("Context cancelled while awaiting confirmation",
|
||||||
|
zap.String("tx_hash", txHash),
|
||||||
|
zap.String("network", network.Name),
|
||||||
|
)
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Warn("Failed to fetch transaction receipt",
|
||||||
|
zap.String("tx_hash", txHash),
|
||||||
|
zap.String("network", network.Name),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
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("block_number", receipt.BlockNumber.Uint64()),
|
||||||
|
zap.Uint64("status", receipt.Status),
|
||||||
|
)
|
||||||
|
return receipt, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDecimals(ctx context.Context, client *rpc.Client, token string) (uint8, error) {
|
||||||
|
call := map[string]string{
|
||||||
|
"to": strings.ToLower(common.HexToAddress(token).Hex()),
|
||||||
|
"data": "0x313ce567",
|
||||||
|
}
|
||||||
|
var hexResp string
|
||||||
|
if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil {
|
||||||
|
return 0, merrors.Internal("decimals call failed: " + err.Error())
|
||||||
|
}
|
||||||
|
val, err := shared.DecodeHexUint8(hexResp)
|
||||||
|
if err != nil {
|
||||||
|
return 0, merrors.Internal("decimals decode failed: " + err.Error())
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBalanceOf(ctx context.Context, client *rpc.Client, token string, wallet string) (*big.Int, error) {
|
||||||
|
tokenAddr := common.HexToAddress(token)
|
||||||
|
walletAddr := common.HexToAddress(wallet)
|
||||||
|
addr := strings.TrimPrefix(walletAddr.Hex(), "0x")
|
||||||
|
if len(addr) < 64 {
|
||||||
|
addr = strings.Repeat("0", 64-len(addr)) + addr
|
||||||
|
}
|
||||||
|
call := map[string]string{
|
||||||
|
"to": strings.ToLower(tokenAddr.Hex()),
|
||||||
|
"data": "0x70a08231" + addr,
|
||||||
|
}
|
||||||
|
var hexResp string
|
||||||
|
if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil {
|
||||||
|
return nil, merrors.Internal("balanceOf call failed: " + err.Error())
|
||||||
|
}
|
||||||
|
bigVal, err := shared.DecodeHexBig(hexResp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("balanceOf decode failed: " + err.Error())
|
||||||
|
}
|
||||||
|
return bigVal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func erc20Decimals(ctx context.Context, client *rpc.Client, token common.Address) (uint8, error) {
|
||||||
|
call := map[string]string{
|
||||||
|
"to": strings.ToLower(token.Hex()),
|
||||||
|
"data": "0x313ce567",
|
||||||
|
}
|
||||||
|
var hexResp string
|
||||||
|
if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil {
|
||||||
|
return 0, executorInternal("decimals call failed", err)
|
||||||
|
}
|
||||||
|
val, err := shared.DecodeHexUint8(hexResp)
|
||||||
|
if err != nil {
|
||||||
|
return 0, executorInternal("decimals decode failed", err)
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBaseUnits(amount string, decimals uint8) (*big.Int, error) {
|
||||||
|
value, err := decimal.NewFromString(strings.TrimSpace(amount))
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.InvalidArgument("invalid amount " + amount + ": " + err.Error())
|
||||||
|
}
|
||||||
|
if value.IsNegative() {
|
||||||
|
return nil, merrors.InvalidArgument("amount must be positive")
|
||||||
|
}
|
||||||
|
multiplier := decimal.NewFromInt(1).Shift(int32(decimals))
|
||||||
|
scaled := value.Mul(multiplier)
|
||||||
|
if !scaled.Equal(scaled.Truncate(0)) {
|
||||||
|
return nil, merrors.InvalidArgument("amount " + amount + " exceeds token precision")
|
||||||
|
}
|
||||||
|
return scaled.BigInt(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func executorInvalid(msg string) error {
|
||||||
|
return merrors.InvalidArgument("executor: " + msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executorInternal(msg string, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
msg = msg + ": " + err.Error()
|
||||||
|
}
|
||||||
|
return merrors.Internal("executor: " + msg)
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tronHexPrefix = "0x"
|
||||||
|
|
||||||
|
var base58Alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
|
||||||
|
|
||||||
|
func normalizeAddress(address string) (string, error) {
|
||||||
|
trimmed := strings.TrimSpace(address)
|
||||||
|
if trimmed == "" {
|
||||||
|
return "", merrors.InvalidArgument("address is required")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, tronHexPrefix) || isHexString(trimmed) {
|
||||||
|
return hexToBase58(trimmed)
|
||||||
|
}
|
||||||
|
decoded, err := base58Decode(trimmed)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := validateChecksum(decoded); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base58Encode(decoded), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcAddress(address string) (string, error) {
|
||||||
|
trimmed := strings.TrimSpace(address)
|
||||||
|
if trimmed == "" {
|
||||||
|
return "", merrors.InvalidArgument("address is required")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, tronHexPrefix) || isHexString(trimmed) {
|
||||||
|
return normalizeHexRPC(trimmed)
|
||||||
|
}
|
||||||
|
return base58ToHex(trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexToBase58(address string) (string, error) {
|
||||||
|
bytesAddr, err := parseHexAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
payload := append(bytesAddr, checksum(bytesAddr)...)
|
||||||
|
return base58Encode(payload), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func base58ToHex(address string) (string, error) {
|
||||||
|
decoded, err := base58Decode(address)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := validateChecksum(decoded); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tronHexPrefix + hex.EncodeToString(decoded[1:21]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHexAddress(address string) ([]byte, error) {
|
||||||
|
trimmed := strings.TrimPrefix(strings.TrimSpace(address), tronHexPrefix)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil, merrors.InvalidArgument("address is required")
|
||||||
|
}
|
||||||
|
if len(trimmed)%2 == 1 {
|
||||||
|
trimmed = "0" + trimmed
|
||||||
|
}
|
||||||
|
decoded, err := hex.DecodeString(trimmed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.InvalidArgument("invalid hex address")
|
||||||
|
}
|
||||||
|
switch len(decoded) {
|
||||||
|
case 20:
|
||||||
|
prefixed := make([]byte, 21)
|
||||||
|
prefixed[0] = 0x41
|
||||||
|
copy(prefixed[1:], decoded)
|
||||||
|
return prefixed, nil
|
||||||
|
case 21:
|
||||||
|
if decoded[0] != 0x41 {
|
||||||
|
return nil, merrors.InvalidArgument("invalid tron address prefix")
|
||||||
|
}
|
||||||
|
return decoded, nil
|
||||||
|
default:
|
||||||
|
return nil, merrors.InvalidArgument(fmt.Sprintf("invalid tron address length %d", len(decoded)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeHexRPC(address string) (string, error) {
|
||||||
|
decoded, err := parseHexAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tronHexPrefix + hex.EncodeToString(decoded[1:21]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateChecksum(decoded []byte) error {
|
||||||
|
if len(decoded) != 25 {
|
||||||
|
return merrors.InvalidArgument("invalid tron address length")
|
||||||
|
}
|
||||||
|
payload := decoded[:21]
|
||||||
|
expected := checksum(payload)
|
||||||
|
if !bytes.Equal(expected, decoded[21:]) {
|
||||||
|
return merrors.InvalidArgument("invalid tron address checksum")
|
||||||
|
}
|
||||||
|
if payload[0] != 0x41 {
|
||||||
|
return merrors.InvalidArgument("invalid tron address prefix")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checksum(payload []byte) []byte {
|
||||||
|
first := sha256.Sum256(payload)
|
||||||
|
second := sha256.Sum256(first[:])
|
||||||
|
return second[:4]
|
||||||
|
}
|
||||||
|
|
||||||
|
func base58Encode(input []byte) string {
|
||||||
|
if len(input) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
x := new(big.Int).SetBytes(input)
|
||||||
|
base := big.NewInt(58)
|
||||||
|
zero := big.NewInt(0)
|
||||||
|
mod := new(big.Int)
|
||||||
|
|
||||||
|
encoded := make([]byte, 0, len(input))
|
||||||
|
for x.Cmp(zero) > 0 {
|
||||||
|
x.DivMod(x, base, mod)
|
||||||
|
encoded = append(encoded, base58Alphabet[mod.Int64()])
|
||||||
|
}
|
||||||
|
for _, b := range input {
|
||||||
|
if b != 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
encoded = append(encoded, base58Alphabet[0])
|
||||||
|
}
|
||||||
|
reverse(encoded)
|
||||||
|
return string(encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func base58Decode(input string) ([]byte, error) {
|
||||||
|
result := big.NewInt(0)
|
||||||
|
base := big.NewInt(58)
|
||||||
|
|
||||||
|
for i := 0; i < len(input); i++ {
|
||||||
|
idx := bytes.IndexByte(base58Alphabet, input[i])
|
||||||
|
if idx < 0 {
|
||||||
|
return nil, merrors.InvalidArgument("invalid base58 address")
|
||||||
|
}
|
||||||
|
result.Mul(result, base)
|
||||||
|
result.Add(result, big.NewInt(int64(idx)))
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded := result.Bytes()
|
||||||
|
zeroCount := 0
|
||||||
|
for zeroCount < len(input) && input[zeroCount] == base58Alphabet[0] {
|
||||||
|
zeroCount++
|
||||||
|
}
|
||||||
|
if zeroCount > 0 {
|
||||||
|
decoded = append(make([]byte, zeroCount), decoded...)
|
||||||
|
}
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reverse(data []byte) {
|
||||||
|
for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
data[i], data[j] = data[j], data[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHexString(value string) bool {
|
||||||
|
trimmed := strings.TrimPrefix(strings.TrimSpace(value), tronHexPrefix)
|
||||||
|
if trimmed == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range trimmed {
|
||||||
|
switch {
|
||||||
|
case r >= '0' && r <= '9':
|
||||||
|
case r >= 'a' && r <= 'f':
|
||||||
|
case r >= 'A' && r <= 'F':
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
203
api/gateway/chain/internal/service/gateway/driver/tron/driver.go
Normal file
203
api/gateway/chain/internal/service/gateway/driver/tron/driver.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/evm"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Driver implements Tron-specific behavior, including address conversion.
|
||||||
|
type Driver struct {
|
||||||
|
logger mlogger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(logger mlogger.Logger) *Driver {
|
||||||
|
return &Driver{logger: logger.Named("tron")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Name() string {
|
||||||
|
return "tron"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) FormatAddress(address string) (string, error) {
|
||||||
|
d.logger.Debug("format address", zap.String("address", address))
|
||||||
|
normalized, err := normalizeAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("format address failed", zap.String("address", address), zap.Error(err))
|
||||||
|
}
|
||||||
|
return normalized, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) NormalizeAddress(address string) (string, error) {
|
||||||
|
d.logger.Debug("normalize address", zap.String("address", address))
|
||||||
|
normalized, err := normalizeAddress(address)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("normalize address failed", zap.String("address", address), zap.Error(err))
|
||||||
|
}
|
||||||
|
return normalized, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||||
|
if wallet == nil {
|
||||||
|
return nil, merrors.InvalidArgument("wallet is required")
|
||||||
|
}
|
||||||
|
d.logger.Debug("balance request",
|
||||||
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
|
zap.String("network", network.Name),
|
||||||
|
)
|
||||||
|
rpcAddr, err := rpcAddress(wallet.DepositAddress)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("balance address conversion failed",
|
||||||
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
|
zap.String("address", wallet.DepositAddress),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
result, err := evm.Balance(ctx, driverDeps, network, wallet, rpcAddr)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("balance failed",
|
||||||
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
|
zap.String("network", 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("amount", result.Amount),
|
||||||
|
zap.String("currency", result.Currency),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
|
||||||
|
if wallet == nil {
|
||||||
|
return nil, merrors.InvalidArgument("wallet is required")
|
||||||
|
}
|
||||||
|
d.logger.Debug("estimate fee request",
|
||||||
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
|
zap.String("network", network.Name),
|
||||||
|
zap.String("destination", destination),
|
||||||
|
)
|
||||||
|
rpcFrom, err := rpcAddress(wallet.DepositAddress)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("estimate fee address conversion failed",
|
||||||
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
|
zap.String("address", wallet.DepositAddress),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rpcTo, err := rpcAddress(destination)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("estimate fee destination conversion failed",
|
||||||
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
|
zap.String("destination", destination),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
result, err := evm.EstimateFee(ctx, driverDeps, network, wallet, rpcFrom, rpcTo, amount)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("estimate fee failed",
|
||||||
|
zap.String("wallet_ref", wallet.WalletRef),
|
||||||
|
zap.String("network", 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("amount", result.Amount),
|
||||||
|
zap.String("currency", result.Currency),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, destination string) (string, error) {
|
||||||
|
if source == nil {
|
||||||
|
return "", merrors.InvalidArgument("source wallet is required")
|
||||||
|
}
|
||||||
|
d.logger.Debug("submit transfer request",
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("network", network.Name),
|
||||||
|
zap.String("destination", destination),
|
||||||
|
)
|
||||||
|
rpcFrom, err := rpcAddress(source.DepositAddress)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("submit transfer address conversion failed",
|
||||||
|
zap.String("wallet_ref", source.WalletRef),
|
||||||
|
zap.String("address", source.DepositAddress),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
rpcTo, err := rpcAddress(destination)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("submit transfer destination conversion failed",
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("destination", destination),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
txHash, err := evm.SubmitTransfer(ctx, driverDeps, network, transfer, source, rpcFrom, rpcTo)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("submit transfer failed",
|
||||||
|
zap.String("transfer_ref", transfer.TransferRef),
|
||||||
|
zap.String("network", 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("tx_hash", txHash),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return txHash, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
driverDeps := deps
|
||||||
|
driverDeps.Logger = d.logger
|
||||||
|
receipt, err := evm.AwaitConfirmation(ctx, driverDeps, network, txHash)
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Warn("await confirmation failed",
|
||||||
|
zap.String("tx_hash", txHash),
|
||||||
|
zap.String("network", 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.Uint64("block_number", receipt.BlockNumber.Uint64()),
|
||||||
|
zap.Uint64("status", receipt.Status),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return receipt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ driver.Driver = (*Driver)(nil)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/ethereum"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver/tron"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Registry maps configured network keys to chain drivers.
|
||||||
|
type Registry struct {
|
||||||
|
byNetwork map[string]driver.Driver
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRegistry selects drivers for the configured networks.
|
||||||
|
func NewRegistry(logger mlogger.Logger, networks []shared.Network) (*Registry, error) {
|
||||||
|
if logger == nil {
|
||||||
|
return nil, merrors.InvalidArgument("driver registry: logger is required")
|
||||||
|
}
|
||||||
|
result := &Registry{byNetwork: map[string]driver.Driver{}}
|
||||||
|
for _, network := range networks {
|
||||||
|
name := strings.ToLower(strings.TrimSpace(network.Name))
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
chainDriver, err := resolveDriver(logger, name)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unsupported chain driver", zap.String("network", name), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.byNetwork[name] = chainDriver
|
||||||
|
}
|
||||||
|
if len(result.byNetwork) == 0 {
|
||||||
|
return nil, merrors.InvalidArgument("driver registry: no supported networks configured")
|
||||||
|
}
|
||||||
|
logger.Info("chain drivers configured", zap.Int("count", len(result.byNetwork)))
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Driver resolves a driver for the provided network key.
|
||||||
|
func (r *Registry) Driver(network string) (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]
|
||||||
|
if !ok {
|
||||||
|
return nil, merrors.InvalidArgument(fmt.Sprintf("unsupported chain network %s", key))
|
||||||
|
}
|
||||||
|
return chainDriver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDriver(logger mlogger.Logger, network string) (driver.Driver, error) {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(network, "tron"):
|
||||||
|
return tron.New(logger), nil
|
||||||
|
case strings.HasPrefix(network, "arbitrum"):
|
||||||
|
return arbitrum.New(logger), nil
|
||||||
|
case strings.HasPrefix(network, "ethereum"):
|
||||||
|
return ethereum.New(logger), nil
|
||||||
|
default:
|
||||||
|
return nil, merrors.InvalidArgument("unsupported chain network " + network)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/drivers"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||||
@@ -19,13 +20,6 @@ func WithKeyManager(manager keymanager.Manager) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithTransferExecutor configures the executor responsible for on-chain submissions.
|
|
||||||
func WithTransferExecutor(executor TransferExecutor) Option {
|
|
||||||
return func(s *Service) {
|
|
||||||
s.executor = executor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRPCClients configures pre-initialised RPC clients.
|
// WithRPCClients configures pre-initialised RPC clients.
|
||||||
func WithRPCClients(clients *rpcclient.Clients) Option {
|
func WithRPCClients(clients *rpcclient.Clients) Option {
|
||||||
return func(s *Service) {
|
return func(s *Service) {
|
||||||
@@ -67,6 +61,13 @@ func WithServiceWallet(wallet shared.ServiceWallet) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithDriverRegistry configures the chain driver registry.
|
||||||
|
func WithDriverRegistry(registry *drivers.Registry) Option {
|
||||||
|
return func(s *Service) {
|
||||||
|
s.drivers = registry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithClock overrides the service clock.
|
// WithClock overrides the service clock.
|
||||||
func WithClock(clk clockpkg.Clock) Option {
|
func WithClock(clk clockpkg.Clock) Option {
|
||||||
return func(s *Service) {
|
return func(s *Service) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/transfer"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/transfer"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/wallet"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/commands/wallet"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/drivers"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/gateway/chain/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
@@ -42,9 +43,9 @@ type Service struct {
|
|||||||
networks map[string]shared.Network
|
networks map[string]shared.Network
|
||||||
serviceWallet shared.ServiceWallet
|
serviceWallet shared.ServiceWallet
|
||||||
keyManager keymanager.Manager
|
keyManager keymanager.Manager
|
||||||
executor TransferExecutor
|
|
||||||
rpcClients *rpcclient.Clients
|
rpcClients *rpcclient.Clients
|
||||||
networkRegistry *rpcclient.Registry
|
networkRegistry *rpcclient.Registry
|
||||||
|
drivers *drivers.Registry
|
||||||
commands commands.Registry
|
commands commands.Registry
|
||||||
|
|
||||||
chainv1.UnimplementedChainGatewayServiceServer
|
chainv1.UnimplementedChainGatewayServiceServer
|
||||||
@@ -135,6 +136,7 @@ func (s *Service) ensureRepository(ctx context.Context) error {
|
|||||||
func commandsWalletDeps(s *Service) wallet.Deps {
|
func commandsWalletDeps(s *Service) wallet.Deps {
|
||||||
return wallet.Deps{
|
return wallet.Deps{
|
||||||
Logger: s.logger.Named("command"),
|
Logger: s.logger.Named("command"),
|
||||||
|
Drivers: s.drivers,
|
||||||
Networks: s.networkRegistry,
|
Networks: s.networkRegistry,
|
||||||
KeyManager: s.keyManager,
|
KeyManager: s.keyManager,
|
||||||
Storage: s.storage,
|
Storage: s.storage,
|
||||||
@@ -148,6 +150,7 @@ func commandsWalletDeps(s *Service) wallet.Deps {
|
|||||||
func commandsTransferDeps(s *Service) transfer.Deps {
|
func commandsTransferDeps(s *Service) transfer.Deps {
|
||||||
return transfer.Deps{
|
return transfer.Deps{
|
||||||
Logger: s.logger.Named("transfer_cmd"),
|
Logger: s.logger.Named("transfer_cmd"),
|
||||||
|
Drivers: s.drivers,
|
||||||
Networks: s.networkRegistry,
|
Networks: s.networkRegistry,
|
||||||
Storage: s.storage,
|
Storage: s.storage,
|
||||||
Clock: s.clock,
|
Clock: s.clock,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
"github.com/tech/sendico/gateway/chain/internal/keymanager"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/drivers"
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/gateway/chain/storage"
|
"github.com/tech/sendico/gateway/chain/storage"
|
||||||
"github.com/tech/sendico/gateway/chain/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
@@ -526,18 +527,22 @@ func sanitizeLimit(requested int32, def, max int64) int64 {
|
|||||||
return int64(requested)
|
return int64(requested)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestService(_ *testing.T) (*Service, *inMemoryRepository) {
|
func newTestService(t *testing.T) (*Service, *inMemoryRepository) {
|
||||||
repo := newInMemoryRepository()
|
repo := newInMemoryRepository()
|
||||||
logger := zap.NewNop()
|
logger := zap.NewNop()
|
||||||
svc := NewService(logger, repo, nil,
|
networks := []shared.Network{{
|
||||||
WithKeyManager(&fakeKeyManager{}),
|
|
||||||
WithNetworks([]shared.Network{{
|
|
||||||
Name: "ethereum_mainnet",
|
Name: "ethereum_mainnet",
|
||||||
TokenConfigs: []shared.TokenContract{
|
TokenConfigs: []shared.TokenContract{
|
||||||
{Symbol: "USDC", ContractAddress: "0xusdc"},
|
{Symbol: "USDC", ContractAddress: "0xusdc"},
|
||||||
},
|
},
|
||||||
}}),
|
}}
|
||||||
|
driverRegistry, err := drivers.NewRegistry(logger.Named("drivers"), networks)
|
||||||
|
require.NoError(t, err)
|
||||||
|
svc := NewService(logger, repo, nil,
|
||||||
|
WithKeyManager(&fakeKeyManager{}),
|
||||||
|
WithNetworks(networks),
|
||||||
WithServiceWallet(shared.ServiceWallet{Network: "ethereum_mainnet", Address: "0xservice"}),
|
WithServiceWallet(shared.ServiceWallet{Network: "ethereum_mainnet", Address: "0xservice"}),
|
||||||
|
WithDriverRegistry(driverRegistry),
|
||||||
)
|
)
|
||||||
return svc, repo
|
return svc, repo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/driver"
|
||||||
|
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
||||||
"github.com/tech/sendico/gateway/chain/storage/model"
|
"github.com/tech/sendico/gateway/chain/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, network shared.Network) {
|
func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, network shared.Network) {
|
||||||
if s.executor == nil {
|
if s.drivers == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,13 +44,20 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
|||||||
s.logger.Warn("failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
|
s.logger.Warn("failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
destinationAddress, err := s.destinationAddress(ctx, transfer.Destination)
|
driverDeps := s.driverDeps()
|
||||||
|
chainDriver, err := s.driverForNetwork(network.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
txHash, err := s.executor.SubmitTransfer(ctx, transfer, sourceWallet, destinationAddress, network)
|
destinationAddress, err := s.destinationAddress(ctx, chainDriver, transfer.Destination)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
txHash, err := chainDriver.SubmitTransfer(ctx, driverDeps, network, transfer, sourceWallet, destinationAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
_, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "")
|
||||||
return err
|
return err
|
||||||
@@ -62,7 +69,7 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
|||||||
|
|
||||||
receiptCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
receiptCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
receipt, err := s.executor.AwaitConfirmation(receiptCtx, network, txHash)
|
receipt, err := chainDriver.AwaitConfirmation(receiptCtx, driverDeps, network, txHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
|
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
|
||||||
s.logger.Warn("failed to await transfer confirmation", zap.String("transfer_ref", transferRef), zap.Error(err))
|
s.logger.Warn("failed to await transfer confirmation", zap.String("transfer_ref", transferRef), zap.Error(err))
|
||||||
@@ -83,7 +90,7 @@ func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWallet
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) destinationAddress(ctx context.Context, dest model.TransferDestination) (string, error) {
|
func (s *Service) destinationAddress(ctx context.Context, chainDriver driver.Driver, dest model.TransferDestination) (string, error) {
|
||||||
if ref := strings.TrimSpace(dest.ManagedWalletRef); ref != "" {
|
if ref := strings.TrimSpace(dest.ManagedWalletRef); ref != "" {
|
||||||
wallet, err := s.storage.Wallets().Get(ctx, ref)
|
wallet, err := s.storage.Wallets().Get(ctx, ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,10 +99,26 @@ func (s *Service) destinationAddress(ctx context.Context, dest model.TransferDes
|
|||||||
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
if strings.TrimSpace(wallet.DepositAddress) == "" {
|
||||||
return "", merrors.Internal("destination wallet missing deposit address")
|
return "", merrors.Internal("destination wallet missing deposit address")
|
||||||
}
|
}
|
||||||
return wallet.DepositAddress, nil
|
return chainDriver.NormalizeAddress(wallet.DepositAddress)
|
||||||
}
|
}
|
||||||
if addr := strings.TrimSpace(dest.ExternalAddress); addr != "" {
|
if addr := strings.TrimSpace(dest.ExternalAddress); addr != "" {
|
||||||
return strings.ToLower(addr), nil
|
return chainDriver.NormalizeAddress(addr)
|
||||||
}
|
}
|
||||||
return "", merrors.InvalidArgument("transfer destination address not resolved")
|
return "", merrors.InvalidArgument("transfer destination address not resolved")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) driverDeps() driver.Deps {
|
||||||
|
return driver.Deps{
|
||||||
|
Logger: s.logger.Named("driver"),
|
||||||
|
Registry: s.networkRegistry,
|
||||||
|
KeyManager: s.keyManager,
|
||||||
|
RPCTimeout: s.settings.rpcTimeout(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) driverForNetwork(network string) (driver.Driver, error) {
|
||||||
|
if s.drivers == nil {
|
||||||
|
return nil, merrors.Internal("chain drivers not configured")
|
||||||
|
}
|
||||||
|
return s.drivers.Driver(network)
|
||||||
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func (m *ManagedWallet) Normalize() {
|
|||||||
m.Network = strings.TrimSpace(strings.ToLower(m.Network))
|
m.Network = strings.TrimSpace(strings.ToLower(m.Network))
|
||||||
m.TokenSymbol = strings.TrimSpace(strings.ToUpper(m.TokenSymbol))
|
m.TokenSymbol = strings.TrimSpace(strings.ToUpper(m.TokenSymbol))
|
||||||
m.ContractAddress = strings.TrimSpace(strings.ToLower(m.ContractAddress))
|
m.ContractAddress = strings.TrimSpace(strings.ToLower(m.ContractAddress))
|
||||||
m.DepositAddress = strings.TrimSpace(strings.ToLower(m.DepositAddress))
|
m.DepositAddress = normalizeWalletAddress(m.DepositAddress)
|
||||||
m.KeyReference = strings.TrimSpace(m.KeyReference)
|
m.KeyReference = strings.TrimSpace(m.KeyReference)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,3 +99,31 @@ func (m *ManagedWallet) Normalize() {
|
|||||||
func (b *WalletBalance) Normalize() {
|
func (b *WalletBalance) Normalize() {
|
||||||
b.WalletRef = strings.TrimSpace(b.WalletRef)
|
b.WalletRef = strings.TrimSpace(b.WalletRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeWalletAddress(address string) string {
|
||||||
|
trimmed := strings.TrimSpace(address)
|
||||||
|
if trimmed == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if isHexAddress(trimmed) {
|
||||||
|
return strings.ToLower(trimmed)
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHexAddress(value string) bool {
|
||||||
|
trimmed := strings.TrimPrefix(strings.TrimSpace(value), "0x")
|
||||||
|
if len(trimmed) != 40 && len(trimmed) != 42 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range trimmed {
|
||||||
|
switch {
|
||||||
|
case r >= '0' && r <= '9':
|
||||||
|
case r >= 'a' && r <= 'f':
|
||||||
|
case r >= 'A' && r <= 'F':
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ enum ChainNetwork {
|
|||||||
CHAIN_NETWORK_UNSPECIFIED = 0;
|
CHAIN_NETWORK_UNSPECIFIED = 0;
|
||||||
CHAIN_NETWORK_ETHEREUM_MAINNET = 1;
|
CHAIN_NETWORK_ETHEREUM_MAINNET = 1;
|
||||||
CHAIN_NETWORK_ARBITRUM_ONE = 2;
|
CHAIN_NETWORK_ARBITRUM_ONE = 2;
|
||||||
CHAIN_NETWORK_OTHER_EVM = 3;
|
|
||||||
CHAIN_NETWORK_TRON_MAINNET = 4;
|
CHAIN_NETWORK_TRON_MAINNET = 4;
|
||||||
CHAIN_NETWORK_TRON_NILE = 5;
|
CHAIN_NETWORK_TRON_NILE = 5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ const (
|
|||||||
ChainNetworkUnspecified ChainNetwork = "unspecified"
|
ChainNetworkUnspecified ChainNetwork = "unspecified"
|
||||||
ChainNetworkEthereumMainnet ChainNetwork = "ethereum_mainnet"
|
ChainNetworkEthereumMainnet ChainNetwork = "ethereum_mainnet"
|
||||||
ChainNetworkArbitrumOne ChainNetwork = "arbitrum_one"
|
ChainNetworkArbitrumOne ChainNetwork = "arbitrum_one"
|
||||||
ChainNetworkOtherEVM ChainNetwork = "other_evm"
|
|
||||||
ChainNetworkTronMainnet ChainNetwork = "tron_mainnet"
|
ChainNetworkTronMainnet ChainNetwork = "tron_mainnet"
|
||||||
ChainNetworkTronNile ChainNetwork = "tron_nile"
|
ChainNetworkTronNile ChainNetwork = "tron_nile"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func TestEndpointDTOBuildersAndDecoders(t *testing.T) {
|
|||||||
t.Run("external chain", func(t *testing.T) {
|
t.Run("external chain", func(t *testing.T) {
|
||||||
payload := ExternalChainEndpoint{
|
payload := ExternalChainEndpoint{
|
||||||
Asset: &Asset{
|
Asset: &Asset{
|
||||||
Chain: ChainNetworkOtherEVM,
|
Chain: ChainNetworkEthereumMainnet,
|
||||||
TokenSymbol: "ETH",
|
TokenSymbol: "ETH",
|
||||||
},
|
},
|
||||||
Address: "0x123",
|
Address: "0x123",
|
||||||
@@ -364,7 +364,7 @@ func TestPaymentIntentMinimalRoundTrip(t *testing.T) {
|
|||||||
func TestLegacyEndpointRoundTrip(t *testing.T) {
|
func TestLegacyEndpointRoundTrip(t *testing.T) {
|
||||||
legacy := &LegacyPaymentEndpoint{
|
legacy := &LegacyPaymentEndpoint{
|
||||||
ExternalChain: &ExternalChainEndpoint{
|
ExternalChain: &ExternalChainEndpoint{
|
||||||
Asset: &Asset{Chain: ChainNetworkOtherEVM, TokenSymbol: "DAI", ContractAddress: "0xdef"},
|
Asset: &Asset{Chain: ChainNetworkEthereumMainnet, TokenSymbol: "DAI", ContractAddress: "0xdef"},
|
||||||
Address: "0x123",
|
Address: "0x123",
|
||||||
Memo: "memo",
|
Memo: "memo",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -214,8 +214,6 @@ func parseChainNetwork(value string) (chainv1.ChainNetwork, error) {
|
|||||||
return chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, nil
|
return chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, nil
|
||||||
case "ARBITRUM_ONE", "CHAIN_NETWORK_ARBITRUM_ONE":
|
case "ARBITRUM_ONE", "CHAIN_NETWORK_ARBITRUM_ONE":
|
||||||
return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE, nil
|
return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE, nil
|
||||||
case "OTHER_EVM", "CHAIN_NETWORK_OTHER_EVM":
|
|
||||||
return chainv1.ChainNetwork_CHAIN_NETWORK_OTHER_EVM, nil
|
|
||||||
case "TRON_MAINNET", "CHAIN_NETWORK_TRON_MAINNET":
|
case "TRON_MAINNET", "CHAIN_NETWORK_TRON_MAINNET":
|
||||||
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET, nil
|
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET, nil
|
||||||
case "TRON_NILE", "CHAIN_NETWORK_TRON_NILE":
|
case "TRON_NILE", "CHAIN_NETWORK_TRON_NILE":
|
||||||
|
|||||||
@@ -286,8 +286,6 @@ func mapChainNetwork(chain srequest.ChainNetwork) (chainv1.ChainNetwork, error)
|
|||||||
return chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, nil
|
return chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, nil
|
||||||
case string(srequest.ChainNetworkArbitrumOne):
|
case string(srequest.ChainNetworkArbitrumOne):
|
||||||
return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE, nil
|
return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE, nil
|
||||||
case string(srequest.ChainNetworkOtherEVM):
|
|
||||||
return chainv1.ChainNetwork_CHAIN_NETWORK_OTHER_EVM, nil
|
|
||||||
case string(srequest.ChainNetworkTronMainnet):
|
case string(srequest.ChainNetworkTronMainnet):
|
||||||
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET, nil
|
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET, nil
|
||||||
case string(srequest.ChainNetworkTronNile):
|
case string(srequest.ChainNetworkTronNile):
|
||||||
|
|||||||
@@ -88,8 +88,6 @@ ChainNetwork chainNetworkFromValue(String? value) {
|
|||||||
return ChainNetwork.ethereumMainnet;
|
return ChainNetwork.ethereumMainnet;
|
||||||
case 'arbitrum_one':
|
case 'arbitrum_one':
|
||||||
return ChainNetwork.arbitrumOne;
|
return ChainNetwork.arbitrumOne;
|
||||||
case 'other_evm':
|
|
||||||
return ChainNetwork.otherEvm;
|
|
||||||
case 'tron_mainnet':
|
case 'tron_mainnet':
|
||||||
return ChainNetwork.tronMainnet;
|
return ChainNetwork.tronMainnet;
|
||||||
case 'tron_nile':
|
case 'tron_nile':
|
||||||
@@ -107,8 +105,6 @@ String chainNetworkToValue(ChainNetwork chain) {
|
|||||||
return 'ethereum_mainnet';
|
return 'ethereum_mainnet';
|
||||||
case ChainNetwork.arbitrumOne:
|
case ChainNetwork.arbitrumOne:
|
||||||
return 'arbitrum_one';
|
return 'arbitrum_one';
|
||||||
case ChainNetwork.otherEvm:
|
|
||||||
return 'other_evm';
|
|
||||||
case ChainNetwork.tronMainnet:
|
case ChainNetwork.tronMainnet:
|
||||||
return 'tron_mainnet';
|
return 'tron_mainnet';
|
||||||
case ChainNetwork.tronNile:
|
case ChainNetwork.tronNile:
|
||||||
|
|||||||
@@ -46,11 +46,6 @@
|
|||||||
"description": "Label for the Arbitrum One network"
|
"description": "Label for the Arbitrum One network"
|
||||||
},
|
},
|
||||||
|
|
||||||
"chainNetworkOtherEvm": "Other EVM chain",
|
|
||||||
"@chainNetworkOtherEvm": {
|
|
||||||
"description": "Label for any other EVM-compatible network"
|
|
||||||
},
|
|
||||||
|
|
||||||
"chainNetworkTronMainnet": "Tron Mainnet",
|
"chainNetworkTronMainnet": "Tron Mainnet",
|
||||||
"@chainNetworkTronMainnet": {
|
"@chainNetworkTronMainnet": {
|
||||||
"description": "Label for the Tron mainnet network"
|
"description": "Label for the Tron mainnet network"
|
||||||
|
|||||||
@@ -46,11 +46,6 @@
|
|||||||
"description": "Label for the Arbitrum One network"
|
"description": "Label for the Arbitrum One network"
|
||||||
},
|
},
|
||||||
|
|
||||||
"chainNetworkOtherEvm": "Другая EVM сеть",
|
|
||||||
"@chainNetworkOtherEvm": {
|
|
||||||
"description": "Label for any other EVM-compatible network"
|
|
||||||
},
|
|
||||||
|
|
||||||
"chainNetworkTronMainnet": "Tron Mainnet",
|
"chainNetworkTronMainnet": "Tron Mainnet",
|
||||||
"@chainNetworkTronMainnet": {
|
"@chainNetworkTronMainnet": {
|
||||||
"description": "Label for the Tron mainnet network"
|
"description": "Label for the Tron mainnet network"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ enum ChainNetwork {
|
|||||||
unspecified,
|
unspecified,
|
||||||
ethereumMainnet,
|
ethereumMainnet,
|
||||||
arbitrumOne,
|
arbitrumOne,
|
||||||
otherEvm,
|
|
||||||
tronMainnet,
|
tronMainnet,
|
||||||
tronNile
|
tronNile
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ extension ChainNetworkL10n on ChainNetwork {
|
|||||||
return l10n.chainNetworkEthereumMainnet;
|
return l10n.chainNetworkEthereumMainnet;
|
||||||
case ChainNetwork.arbitrumOne:
|
case ChainNetwork.arbitrumOne:
|
||||||
return l10n.chainNetworkArbitrumOne;
|
return l10n.chainNetworkArbitrumOne;
|
||||||
case ChainNetwork.otherEvm:
|
|
||||||
return l10n.chainNetworkOtherEvm;
|
|
||||||
case ChainNetwork.tronMainnet:
|
case ChainNetwork.tronMainnet:
|
||||||
return l10n.chainNetworkTronMainnet;
|
return l10n.chainNetworkTronMainnet;
|
||||||
case ChainNetwork.tronNile:
|
case ChainNetwork.tronNile:
|
||||||
|
|||||||
Reference in New Issue
Block a user