onchain balance getter implementation
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
This commit is contained in:
@@ -9,7 +9,9 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type getWalletBalanceCommand struct {
|
type getWalletBalanceCommand struct {
|
||||||
@@ -34,7 +36,7 @@ func (c *getWalletBalanceCommand) Execute(ctx context.Context, req *gatewayv1.Ge
|
|||||||
c.deps.Logger.Warn("wallet_ref missing")
|
c.deps.Logger.Warn("wallet_ref missing")
|
||||||
return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required"))
|
||||||
}
|
}
|
||||||
balance, err := c.deps.Storage.Wallets().GetBalance(ctx, walletRef)
|
wallet, err := c.deps.Storage.Wallets().Get(ctx, walletRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
c.deps.Logger.Warn("not found", zap.String("wallet_ref", walletRef))
|
c.deps.Logger.Warn("not found", zap.String("wallet_ref", walletRef))
|
||||||
@@ -43,5 +45,33 @@ func (c *getWalletBalanceCommand) Execute(ctx context.Context, req *gatewayv1.Ge
|
|||||||
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
c.deps.Logger.Warn("storage get failed", zap.Error(err), zap.String("wallet_ref", walletRef))
|
||||||
return gsresponse.Auto[gatewayv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
return gsresponse.Auto[gatewayv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
}
|
}
|
||||||
return gsresponse.Success(&gatewayv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(balance)})
|
|
||||||
|
balance, chainErr := onChainWalletBalance(ctx, c.deps, wallet)
|
||||||
|
if chainErr != nil {
|
||||||
|
c.deps.Logger.Warn("on-chain balance fetch failed, falling back to stored balance", zap.Error(chainErr), zap.String("wallet_ref", walletRef))
|
||||||
|
stored, err := c.deps.Storage.Wallets().GetBalance(ctx, walletRef)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
c.deps.Logger.Warn("stored balance not found", zap.String("wallet_ref", walletRef))
|
||||||
|
return gsresponse.NotFound[gatewayv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
return gsresponse.Auto[gatewayv1.GetWalletBalanceResponse](c.deps.Logger, mservice.ChainGateway, err)
|
||||||
|
}
|
||||||
|
return gsresponse.Success(&gatewayv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(stored)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return gsresponse.Success(&gatewayv1.GetWalletBalanceResponse{Balance: onChainBalanceToProto(balance)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func onChainBalanceToProto(balance *moneyv1.Money) *gatewayv1.WalletBalance {
|
||||||
|
if balance == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
zero := &moneyv1.Money{Currency: balance.Currency, Amount: "0"}
|
||||||
|
return &gatewayv1.WalletBalance{
|
||||||
|
Available: balance,
|
||||||
|
PendingInbound: zero,
|
||||||
|
PendingOutbound: zero,
|
||||||
|
CalculatedAt: timestamppb.Now(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"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/ethclient"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"github.com/tech/sendico/chain/gateway/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func onChainWalletBalance(ctx context.Context, deps Deps, wallet *model.ManagedWallet) (*moneyv1.Money, error) {
|
||||||
|
network := deps.Networks[strings.ToLower(strings.TrimSpace(wallet.Network))]
|
||||||
|
rpcURL := strings.TrimSpace(network.RPCURL)
|
||||||
|
if rpcURL == "" {
|
||||||
|
return nil, merrors.Internal("network rpc url is not configured")
|
||||||
|
}
|
||||||
|
contract := strings.TrimSpace(wallet.ContractAddress)
|
||||||
|
if contract == "" || !common.IsHexAddress(contract) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid contract address")
|
||||||
|
}
|
||||||
|
if wallet.DepositAddress == "" || !common.IsHexAddress(wallet.DepositAddress) {
|
||||||
|
return nil, merrors.InvalidArgument("invalid wallet address")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := ethclient.DialContext(ctx, rpcURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to connect rpc: " + err.Error())
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tokenABI, err := abi.JSON(strings.NewReader(erc20ABIJSON))
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.Internal("failed to parse erc20 abi: " + err.Error())
|
||||||
|
}
|
||||||
|
tokenAddr := common.HexToAddress(contract)
|
||||||
|
walletAddr := common.HexToAddress(wallet.DepositAddress)
|
||||||
|
|
||||||
|
decimals, err := readDecimals(timeoutCtx, client, tokenABI, tokenAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bal, err := readBalanceOf(timeoutCtx, client, tokenABI, tokenAddr, walletAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := decimal.NewFromBigInt(bal, 0).Shift(-int32(decimals))
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
msg := ethereum.CallMsg{To: &token, Data: data}
|
||||||
|
out, err := client.CallContract(ctx, msg, nil)
|
||||||
|
if 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")
|
||||||
|
}
|
||||||
|
if val, ok := values[0].(uint8); ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, merrors.Internal("decimals returned unexpected type")
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
msg := ethereum.CallMsg{To: &token, Data: data}
|
||||||
|
out, err := client.CallContract(ctx, msg, nil)
|
||||||
|
if 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")
|
||||||
|
}
|
||||||
|
raw, ok := values[0].(*big.Int)
|
||||||
|
if !ok {
|
||||||
|
return nil, merrors.Internal("balanceOf returned unexpected type")
|
||||||
|
}
|
||||||
|
return decimal.NewFromBigInt(raw, 0).BigInt(), 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"
|
||||||
|
}
|
||||||
|
]`
|
||||||
Reference in New Issue
Block a user