Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 69 additions & 14 deletions internal/scapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package scapi

import (
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"math/big"
Expand Down Expand Up @@ -164,37 +165,91 @@ func (s *PublicSmartContractAPI) Name(ctx context.Context, tokenAddress common.A

// BalanceOf returns the balance of a given token holder for a given token contract.
// It automatically decodes the uint256 response and converts it to a hexutil.Big.
func (s *PublicSmartContractAPI) BalanceOf(ctx context.Context, holderAddress, tokenAddress common.Address) (*hexutil.Big, error) {
// CBC20 balanceOf(address) function selector: 0x1d7976f3
selector := "0x1d7976f3" // standard CBC20 balanceOf()
// If unit is provided, it will first attempt to call balanceOf(address,string) and fall back
// to the canonical balanceOf(address) if the aliased call fails.
func (s *PublicSmartContractAPI) BalanceOf(ctx context.Context, holderAddress, tokenAddress common.Address, unit *string) (*hexutil.Big, error) {
if unit != nil {
unitValue := *unit
normalizedUnit := strings.ToLower(unitValue)
balance, err := s.balanceOfWithUnit(ctx, holderAddress, tokenAddress, normalizedUnit)
if err != nil {
log.Debug("balanceOf alias call failed",
"holder", holderAddress,
"token", tokenAddress,
"unit", unitValue,
"err", err,
)
return nil, fmt.Errorf("unit %s does not exist", unitValue)
}
return balance, nil
}

// Create the call data: selector + padded address (32 bytes)
data := hexutil.MustDecode(selector)
return s.balanceOfDefault(ctx, holderAddress, tokenAddress)
}

// balanceOfWithUnit calls balanceOf(address,string) and decodes the result.
func (s *PublicSmartContractAPI) balanceOfWithUnit(ctx context.Context, holderAddress, tokenAddress common.Address, unit string) (*hexutil.Big, error) {
selector := hexutil.MustDecode("0x5a805b98") // balanceOf(address,string)
data := append([]byte{}, selector...)

// Pad the holder address to 32 bytes (left-pad with zeros)
addressBytes := holderAddress.Bytes()
paddedAddress := make([]byte, 32)
copy(paddedAddress[32-len(addressBytes):], addressBytes)

// Append the padded address to the selector
data = append(data, paddedAddress...)

// Make the contract call with properly initialized CallMsg
offset := make([]byte, 32)
offset[31] = 0x40 // dynamic data starts right after the two static slots
data = append(data, offset...)

unitBytes := []byte(unit)
lengthBytes := make([]byte, 32)
binary.BigEndian.PutUint64(lengthBytes[24:], uint64(len(unitBytes)))
data = append(data, lengthBytes...)

paddedLen := ((len(unitBytes) + 31) / 32) * 32
paddedUnit := make([]byte, paddedLen)
copy(paddedUnit, unitBytes)
data = append(data, paddedUnit...)

result, err := s.b.CallContract(ctx, s.createViewCallMsg(tokenAddress, data), rpc.LatestBlockNumber)
if err != nil {
return nil, fmt.Errorf("failed to call balanceOf(address,string) on contract %s for address %s and unit %s: %w", tokenAddress.Hex(), holderAddress.Hex(), unit, err)
}

const MaxResponseSize = 1024 * 1024 // 1MB limit
if len(result) > MaxResponseSize {
return nil, fmt.Errorf("response too large: %d bytes exceeds limit of %d", len(result), MaxResponseSize)
}

if len(result) == 0 {
return nil, fmt.Errorf("empty response from balanceOf(address,string) call on contract %s for address %s and unit %s", tokenAddress.Hex(), holderAddress.Hex(), unit)
}

balance := new(big.Int).SetBytes(result)
return (*hexutil.Big)(balance), nil
}

// balanceOfDefault calls the canonical balanceOf(address) selector.
func (s *PublicSmartContractAPI) balanceOfDefault(ctx context.Context, holderAddress, tokenAddress common.Address) (*hexutil.Big, error) {
selector := "0x1d7976f3" // standard CBC20 balanceOf()
data := hexutil.MustDecode(selector)

addressBytes := holderAddress.Bytes()
paddedAddress := make([]byte, 32)
copy(paddedAddress[32-len(addressBytes):], addressBytes)
data = append(data, paddedAddress...)

result, err := s.b.CallContract(ctx, s.createViewCallMsg(tokenAddress, data), rpc.LatestBlockNumber)
if err != nil {
return nil, fmt.Errorf("failed to call balanceOf on contract %s for address %s: %v", tokenAddress.Hex(), holderAddress.Hex(), err)
}

// Add response size limit to prevent DoS attacks
const MaxResponseSize = 1024 * 1024 // 1MB limit
if len(result) > MaxResponseSize {
return nil, fmt.Errorf("response too large: %d bytes exceeds limit of %d", len(result), MaxResponseSize)
}

// If we got a result, decode it as uint256
if len(result) > 0 {
// Convert the 32-byte result to big.Int, then to hexutil.Big
balance := new(big.Int).SetBytes(result)
return (*hexutil.Big)(balance), nil
}
Expand Down Expand Up @@ -704,7 +759,7 @@ func (s *PublicSmartContractAPI) NameSubscription(ctx context.Context, tokenAddr

// BalanceOfSubscription provides real-time updates about token balances.
// This can be useful for monitoring balance changes for specific addresses.
func (s *PublicSmartContractAPI) BalanceOfSubscription(ctx context.Context, holderAddress, tokenAddress common.Address) (*rpc.Subscription, error) {
func (s *PublicSmartContractAPI) BalanceOfSubscription(ctx context.Context, holderAddress, tokenAddress common.Address, unit *string) (*rpc.Subscription, error) {
notifier, supported := rpc.NotifierFromContext(ctx)
if !supported {
return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported
Expand All @@ -713,7 +768,7 @@ func (s *PublicSmartContractAPI) BalanceOfSubscription(ctx context.Context, hold

go func() {
// Send initial balance
balance, err := s.BalanceOf(ctx, holderAddress, tokenAddress)
balance, err := s.BalanceOf(ctx, holderAddress, tokenAddress, unit)
if err == nil {
notifier.Notify(rpcSub.ID, balance)
}
Expand Down
17 changes: 13 additions & 4 deletions scclient/scclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,14 @@ func (sc *Client) Name(ctx context.Context, tokenAddress common.Address) (string
}

// BalanceOf returns the balance of a given token holder for a given token contract.
func (sc *Client) BalanceOf(ctx context.Context, holderAddress, tokenAddress common.Address) (*hexutil.Big, error) {
// Optional unit parameter enables querying human-readable units.
func (sc *Client) BalanceOf(ctx context.Context, holderAddress, tokenAddress common.Address, unit ...string) (*hexutil.Big, error) {
var result hexutil.Big
err := sc.c.CallContext(ctx, &result, "sc_balanceOf", holderAddress, tokenAddress)
args := []interface{}{holderAddress, tokenAddress}
if len(unit) > 0 {
args = append(args, unit[0])
}
err := sc.c.CallContext(ctx, &result, "sc_balanceOf", args...)
return &result, err
}

Expand Down Expand Up @@ -131,8 +136,12 @@ func (sc *Client) TokenURI(ctx context.Context, tokenAddress common.Address, tok

// BalanceOfSubscription subscribes to real-time updates about token balances.
// It returns a subscription that will notify when the balance changes.
func (sc *Client) BalanceOfSubscription(ctx context.Context, holderAddress, tokenAddress common.Address) (Subscription, error) {
return sc.c.XcbSubscribe(ctx, make(chan *big.Int), "balanceOf", holderAddress, tokenAddress)
func (sc *Client) BalanceOfSubscription(ctx context.Context, holderAddress, tokenAddress common.Address, unit ...string) (Subscription, error) {
args := []interface{}{"balanceOf", holderAddress, tokenAddress}
if len(unit) > 0 {
args = append(args, unit[0])
}
return sc.c.XcbSubscribe(ctx, make(chan *big.Int), args...)
}

// DecimalsSubscription subscribes to real-time updates about token decimal places.
Expand Down
Loading