From 77c205f9b2800bed13c652bc6d1f24339aa627f7 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 24 Dec 2025 02:57:15 +0100 Subject: [PATCH] fixed proto message --- .../service/gateway/commands/transfer/fee.go | 51 +++++----- .../commands/wallet/onchain_balance.go | 96 ++++++------------- .../internal/service/gateway/executor.go | 55 +++++------ .../service/gateway/rpcclient/clients.go | 49 +++++++--- .../service/gateway/rpcclient/registry.go | 9 ++ 5 files changed, 123 insertions(+), 137 deletions(-) diff --git a/api/gateway/chain/internal/service/gateway/commands/transfer/fee.go b/api/gateway/chain/internal/service/gateway/commands/transfer/fee.go index cdf69bf..992e152 100644 --- a/api/gateway/chain/internal/service/gateway/commands/transfer/fee.go +++ b/api/gateway/chain/internal/service/gateway/commands/transfer/fee.go @@ -10,8 +10,10 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/common/hexutil" + "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" @@ -85,7 +87,7 @@ func (c *estimateTransferFeeCommand) Execute(ctx context.Context, req *chainv1.E return gsresponse.InvalidArgument[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err) } - feeMoney, err := estimateNetworkFee(ctx, c.deps.Logger, networkCfg, c.deps.RPCTimeout, sourceWallet, destinationAddress, amount) + feeMoney, err := estimateNetworkFee(ctx, c.deps.Logger, c.deps.Networks, networkCfg, c.deps.RPCTimeout, sourceWallet, destinationAddress, amount) if err != nil { c.deps.Logger.Warn("fee estimation failed", zap.Error(err)) return gsresponse.Auto[chainv1.EstimateTransferFeeResponse](c.deps.Logger, mservice.ChainGateway, err) @@ -98,11 +100,14 @@ func (c *estimateTransferFeeCommand) Execute(ctx context.Context, req *chainv1.E return gsresponse.Success(resp) } -func estimateNetworkFee(ctx context.Context, logger mlogger.Logger, network shared.Network, timeout time.Duration, wallet *model.ManagedWallet, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) { +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") } @@ -116,11 +121,14 @@ func estimateNetworkFee(ctx context.Context, logger mlogger.Logger, network shar return nil, merrors.InvalidArgument("invalid destination address") } - client, err := ethclient.DialContext(ctx, rpcURL) + client, err := registry.Client(network.Name) if err != nil { - return nil, merrors.Internal("failed to connect to rpc: " + err.Error()) + return nil, err + } + rpcClient, err := registry.RPCClient(network.Name) + if err != nil { + return nil, err } - defer client.Close() if timeout <= 0 { timeout = 15 * time.Second @@ -136,7 +144,7 @@ func estimateNetworkFee(ctx context.Context, logger mlogger.Logger, network shar toAddr := common.HexToAddress(destination) fromAddr := common.HexToAddress(wallet.DepositAddress) - decimals, err := erc20Decimals(timeoutCtx, client, tokenABI, tokenAddr) + decimals, err := erc20Decimals(timeoutCtx, rpcClient, tokenAddr) if err != nil { logger.Warn("failed to read token decimals", zap.Error(err)) return nil, err @@ -182,31 +190,20 @@ func estimateNetworkFee(ctx context.Context, logger mlogger.Logger, network shar }, nil } -func erc20Decimals(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address) (uint8, error) { - callData, err := tokenABI.Pack("decimals") - if err != nil { - return 0, merrors.Internal("failed to encode decimals call: " + err.Error()) +func erc20Decimals(ctx context.Context, client *rpc.Client, token common.Address) (uint8, error) { + call := map[string]string{ + "to": strings.ToLower(token.Hex()), + "data": "0x313ce567", } - msg := ethereum.CallMsg{ - To: &token, - Data: callData, - } - output, err := client.CallContract(ctx, msg, nil) - if err != nil { + var hexResp string + if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil { return 0, merrors.Internal("decimals call failed: " + err.Error()) } - values, err := tokenABI.Unpack("decimals", output) + val, err := hexutil.DecodeUint64(hexResp) if err != nil { - return 0, merrors.Internal("failed to unpack decimals: " + err.Error()) + return 0, merrors.Internal("decimals decode failed: " + err.Error()) } - if len(values) == 0 { - return 0, merrors.Internal("decimals call returned no data") - } - decimals, ok := values[0].(uint8) - if !ok { - return 0, merrors.Internal("decimals call returned unexpected type") - } - return decimals, nil + return uint8(val), nil } func toBaseUnits(amount string, decimals uint8) (*big.Int, error) { diff --git a/api/gateway/chain/internal/service/gateway/commands/wallet/onchain_balance.go b/api/gateway/chain/internal/service/gateway/commands/wallet/onchain_balance.go index 23192f8..8452237 100644 --- a/api/gateway/chain/internal/service/gateway/commands/wallet/onchain_balance.go +++ b/api/gateway/chain/internal/service/gateway/commands/wallet/onchain_balance.go @@ -7,10 +7,9 @@ import ( "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/ethclient" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rpc" "github.com/shopspring/decimal" "github.com/tech/sendico/gateway/chain/storage/model" "github.com/tech/sendico/pkg/merrors" @@ -58,7 +57,7 @@ func onChainWalletBalance(ctx context.Context, deps Deps, wallet *model.ManagedW logger.Info("Fetching on-chain wallet balance", logFields...) - client, err := registry.Client(networkKey) + rpcClient, err := registry.RPCClient(networkKey) if err != nil { logger.Warn("Failed to fetch rpc client", append(logFields, zap.Error(err))...) return nil, err @@ -71,23 +70,15 @@ func onChainWalletBalance(ctx context.Context, deps Deps, wallet *model.ManagedW timeoutCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - tokenABI, err := abi.JSON(strings.NewReader(erc20ABIJSON)) - if err != nil { - logger.Warn("Failed to parse erc20 abi", append(logFields, zap.Error(err))...) - return nil, merrors.Internal("failed to parse erc20 abi: " + err.Error()) - } - tokenAddr := common.HexToAddress(contract) - walletAddr := common.HexToAddress(wallet.DepositAddress) - logger.Debug("Calling token decimals", logFields...) - decimals, err := readDecimals(timeoutCtx, client, tokenABI, tokenAddr) + 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, client, tokenABI, tokenAddr, walletAddr) + 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 @@ -104,65 +95,40 @@ func onChainWalletBalance(ctx context.Context, deps Deps, wallet *model.ManagedW return &moneyv1.Money{Currency: wallet.TokenSymbol, Amount: dec.String()}, nil } -func readDecimals(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address) (uint8, error) { - data, err := tokenABI.Pack("decimals") - if err != nil { - return 0, merrors.Internal("failed to encode decimals call: " + err.Error()) +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", } - msg := ethereum.CallMsg{To: &token, Data: data} - out, err := client.CallContract(ctx, msg, nil) - if err != nil { + var hexResp string + if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil { return 0, merrors.Internal("decimals call failed: " + err.Error()) } - values, err := tokenABI.Unpack("decimals", out) - if err != nil || len(values) == 0 { - return 0, merrors.Internal("failed to unpack decimals") + val, err := hexutil.DecodeUint64(hexResp) + if err != nil { + return 0, merrors.Internal("decimals decode failed: " + err.Error()) } - if val, ok := values[0].(uint8); ok { - return val, nil - } - return 0, merrors.Internal("decimals returned unexpected type") + return uint8(val), nil } -func readBalanceOf(ctx context.Context, client *ethclient.Client, tokenABI abi.ABI, token common.Address, wallet common.Address) (*big.Int, error) { - data, err := tokenABI.Pack("balanceOf", wallet) - if err != nil { - return nil, merrors.Internal("failed to encode balanceOf: " + err.Error()) +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 } - msg := ethereum.CallMsg{To: &token, Data: data} - out, err := client.CallContract(ctx, msg, nil) - if err != nil { + 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()) } - values, err := tokenABI.Unpack("balanceOf", out) - if err != nil || len(values) == 0 { - return nil, merrors.Internal("failed to unpack balanceOf") + bigVal, err := hexutil.DecodeBig(hexResp) + if err != nil { + return nil, merrors.Internal("balanceOf decode failed: " + err.Error()) } - raw, ok := values[0].(*big.Int) - if !ok { - return nil, merrors.Internal("balanceOf returned unexpected type") - } - return decimal.NewFromBigInt(raw, 0).BigInt(), nil + return bigVal, nil } - -const erc20ABIJSON = ` -[ - { - "constant": true, - "inputs": [], - "name": "decimals", - "outputs": [{ "name": "", "type": "uint8" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "_owner", "type": "address" }], - "name": "balanceOf", - "outputs": [{ "name": "balance", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - } -]` diff --git a/api/gateway/chain/internal/service/gateway/executor.go b/api/gateway/chain/internal/service/gateway/executor.go index c705456..bc5e25d 100644 --- a/api/gateway/chain/internal/service/gateway/executor.go +++ b/api/gateway/chain/internal/service/gateway/executor.go @@ -10,8 +10,9 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" + "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" @@ -80,10 +81,14 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr ) client, err := o.clients.Client(network.Name) + if err != nil { + o.logger.Warn("failed to initialise rpc client", zap.Error(err), zap.String("network", network.Name)) + return "", err + } + rpcClient, err := o.clients.RPCClient(network.Name) if err != nil { o.logger.Warn("failed to initialise rpc client", zap.String("network", network.Name), - zap.String("rpc_url", rpcURL), zap.Error(err), ) return "", err @@ -97,10 +102,9 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr nonce, err := client.PendingNonceAt(ctx, sourceAddress) if err != nil { - o.logger.Warn("failed to fetch nonce", + o.logger.Warn("failed to fetch nonce", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef), zap.String("wallet_ref", source.WalletRef), - zap.Error(err), ) return "", executorInternal("failed to fetch nonce", err) } @@ -134,12 +138,11 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr } tokenAddress := common.HexToAddress(transfer.ContractAddress) - decimals, err := erc20Decimals(ctx, client, tokenAddress) + decimals, err := erc20Decimals(ctx, rpcClient, tokenAddress) if err != nil { - o.logger.Warn("failed to read token decimals", + o.logger.Warn("failed to read token decimals", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef), zap.String("contract", transfer.ContractAddress), - zap.Error(err), ) return "", err } @@ -151,10 +154,9 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr } amountInt, err := toBaseUnits(amount.Amount, decimals) if err != nil { - o.logger.Warn("failed to convert amount to base units", + o.logger.Warn("failed to convert amount to base units", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef), zap.String("amount", amount.Amount), - zap.Error(err), ) return "", err } @@ -187,18 +189,16 @@ func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Tr signedTx, err := o.keyManager.SignTransaction(ctx, source.KeyReference, tx, chainID) if err != nil { - o.logger.Warn("failed to sign transaction", + o.logger.Warn("failed to sign transaction", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef), zap.String("wallet_ref", source.WalletRef), - zap.Error(err), ) return "", err } if err := client.SendTransaction(ctx, signedTx); err != nil { - o.logger.Warn("failed to send transaction", + o.logger.Warn("failed to send transaction", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef), - zap.Error(err), ) return "", executorInternal("failed to send transaction", err) } @@ -306,31 +306,20 @@ const erc20ABIJSON = ` } ]` -func erc20Decimals(ctx context.Context, client *ethclient.Client, token common.Address) (uint8, error) { - callData, err := erc20ABI.Pack("decimals") - if err != nil { - return 0, executorInternal("failed to encode decimals call", err) +func erc20Decimals(ctx context.Context, client *rpc.Client, token common.Address) (uint8, error) { + call := map[string]string{ + "to": strings.ToLower(token.Hex()), + "data": "0x313ce567", } - msg := ethereum.CallMsg{ - To: &token, - Data: callData, - } - output, err := client.CallContract(ctx, msg, nil) - if err != nil { + var hexResp string + if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil { return 0, executorInternal("decimals call failed", err) } - values, err := erc20ABI.Unpack("decimals", output) + val, err := hexutil.DecodeUint64(hexResp) if err != nil { - return 0, executorInternal("failed to unpack decimals", err) + return 0, executorInternal("decimals decode failed", err) } - if len(values) == 0 { - return 0, executorInternal("decimals call returned no data", nil) - } - decimals, ok := values[0].(uint8) - if !ok { - return 0, executorInternal("decimals call returned unexpected type", nil) - } - return decimals, nil + return uint8(val), nil } func toBaseUnits(amount string, decimals uint8) (*big.Int, error) { diff --git a/api/gateway/chain/internal/service/gateway/rpcclient/clients.go b/api/gateway/chain/internal/service/gateway/rpcclient/clients.go index 2049384..5e873da 100644 --- a/api/gateway/chain/internal/service/gateway/rpcclient/clients.go +++ b/api/gateway/chain/internal/service/gateway/rpcclient/clients.go @@ -19,7 +19,12 @@ import ( // Clients holds pre-initialised RPC clients keyed by network name. type Clients struct { logger mlogger.Logger - clients map[string]*ethclient.Client + clients map[string]clientEntry +} + +type clientEntry struct { + eth *ethclient.Client + rpc *rpc.Client } // Prepare dials all configured networks up front and returns a ready-to-use client set. @@ -30,7 +35,7 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo clientLogger := logger.Named("rpc_client") result := &Clients{ logger: clientLogger, - clients: make(map[string]*ethclient.Client), + clients: make(map[string]clientEntry), } for _, network := range networks { @@ -55,9 +60,10 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo dialCtx, cancel := context.WithTimeout(ctx, 15*time.Second) httpClient := &http.Client{ Transport: &loggingRoundTripper{ - logger: clientLogger, - network: name, - base: http.DefaultTransport, + logger: clientLogger, + network: name, + endpoint: rpcURL, + base: http.DefaultTransport, }, } rpcCli, err := rpc.DialOptions(dialCtx, rpcURL, rpc.WithHTTPClient(httpClient)) @@ -68,8 +74,10 @@ func Prepare(ctx context.Context, logger mlogger.Logger, networks []shared.Netwo return nil, merrors.Internal(fmt.Sprintf("rpc dial failed for %s: %s", name, err.Error())) } client := ethclient.NewClient(rpcCli) - - result.clients[name] = client + result.clients[name] = clientEntry{ + eth: client, + rpc: rpcCli, + } clientLogger.Info("rpc client ready", fields...) } @@ -89,11 +97,24 @@ func (c *Clients) Client(network string) (*ethclient.Client, error) { return nil, merrors.Internal("rpc clients not initialised") } name := strings.ToLower(strings.TrimSpace(network)) - client, ok := c.clients[name] - if !ok { + entry, ok := c.clients[name] + if !ok || entry.eth == nil { return nil, merrors.InvalidArgument(fmt.Sprintf("rpc client not configured for network %s", name)) } - return client, nil + return entry.eth, nil +} + +// RPCClient returns the raw RPC client for low-level calls. +func (c *Clients) RPCClient(network string) (*rpc.Client, error) { + if c == nil { + return nil, merrors.Internal("rpc clients not initialised") + } + name := strings.ToLower(strings.TrimSpace(network)) + entry, ok := c.clients[name] + if !ok || entry.rpc == nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("rpc client not configured for network %s", name)) + } + return entry.rpc, nil } // Close tears down all RPC clients, logging each close. @@ -101,8 +122,12 @@ func (c *Clients) Close() { if c == nil { return } - for name, client := range c.clients { - client.Close() + for name, entry := range c.clients { + if entry.rpc != nil { + entry.rpc.Close() + } else if entry.eth != nil { + entry.eth.Close() + } if c.logger != nil { c.logger.Info("rpc client closed", zap.String("network", name)) } diff --git a/api/gateway/chain/internal/service/gateway/rpcclient/registry.go b/api/gateway/chain/internal/service/gateway/rpcclient/registry.go index 8ccf4c4..185477f 100644 --- a/api/gateway/chain/internal/service/gateway/rpcclient/registry.go +++ b/api/gateway/chain/internal/service/gateway/rpcclient/registry.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared" "github.com/tech/sendico/pkg/merrors" ) @@ -39,6 +40,14 @@ func (r *Registry) Client(key string) (*ethclient.Client, error) { return r.clients.Client(strings.ToLower(strings.TrimSpace(key))) } +// RPCClient returns the raw RPC client for low-level calls. +func (r *Registry) RPCClient(key string) (*rpc.Client, error) { + if r == nil || r.clients == nil { + return nil, merrors.Internal("rpc clients not initialised") + } + return r.clients.RPCClient(strings.ToLower(strings.TrimSpace(key))) +} + // Networks exposes the registry map for iteration when needed. func (r *Registry) Networks() map[string]shared.Network { return r.networks