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
17 changes: 17 additions & 0 deletions service/dealpusher/ddo_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package dealpusher
import (
"context"
"errors"
"math/big"
"time"

"github.com/data-preservation-programs/go-synapse/signer"
"github.com/ethereum/go-ethereum/common"
"github.com/ipfs/go-cid"
)

Expand Down Expand Up @@ -52,6 +54,11 @@ type DDODealManager interface {
// DDO contract, and returns its on-chain config.
ValidateSP(ctx context.Context, providerActorID uint64) (*DDOSPConfig, error)

// CheckBalance queries the wallet's native FIL and payment token balances,
// as well as the payments contract account status. Returns a summary that
// the scheduler uses for pre-flight logging and low-balance warnings.
CheckBalance(ctx context.Context, walletAddr common.Address) (*DDOBalanceStatus, error)

// EnsurePayments checks account balance and operator approval, deposits
// and approves if needed. Takes the actual pieces (not aggregated totals)
// because the SDK computes per-piece lockup: allocationLockupAmount * len(pieces).
Expand Down Expand Up @@ -95,3 +102,13 @@ type DDOTransactionReceipt struct {
GasUsed uint64
Status uint64
}

// DDOBalanceStatus summarizes a wallet's balance state for DDO deal-making.
type DDOBalanceStatus struct {
WalletAddr common.Address
NativeFIL *big.Int // native FIL balance (for gas)
TokenBalance *big.Int // payment token balance held in wallet
DepositedFunds *big.Int // funds already deposited in payments contract
LockupCurrent *big.Int // current lockup in payments contract
Available *big.Int // deposited - lockup (spendable for new deals)
}
67 changes: 67 additions & 0 deletions service/dealpusher/ddo_onchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,73 @@ func (o *OnChainDDO) Close() {
}
}

func (o *OnChainDDO) CheckBalance(ctx context.Context, walletAddr common.Address) (*DDOBalanceStatus, error) {
ethClient := o.ddoClient.GetEthClient()

// Native FIL balance (for gas)
nativeBal, err := ethClient.BalanceAt(ctx, walletAddr, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to query native FIL balance")
}

// Payment token (USDFC) balance via balanceOf call
tokenBal, err := o.queryERC20Balance(ctx, o.paymentToken, walletAddr)
if err != nil {
return nil, errors.Wrap(err, "failed to query payment token balance")
}

// Payments contract account status — use transactor constructor with
// read-only auth (we only call GetAccount, no writes).
paymentsClient, err := paymentscontract.NewClientWithTransactor(
ethClient,
o.paymentsContractAddr.Hex(),
&bind.TransactOpts{From: walletAddr},
)
if err != nil {
return nil, errors.Wrap(err, "failed to create payments client for balance query")
}

account, err := paymentsClient.GetAccount(o.paymentToken, walletAddr)
if err != nil {
return nil, errors.Wrap(err, "failed to query payments contract account")
}

available := new(big.Int).Sub(account.Funds, account.LockupCurrent)
if available.Sign() < 0 {
available = big.NewInt(0)
}

return &DDOBalanceStatus{
WalletAddr: walletAddr,
NativeFIL: nativeBal,
TokenBalance: tokenBal,
DepositedFunds: account.Funds,
LockupCurrent: account.LockupCurrent,
Available: available,
}, nil
}

// queryERC20Balance calls balanceOf on an ERC20 token contract.
func (o *OnChainDDO) queryERC20Balance(ctx context.Context, tokenAddr, account common.Address) (*big.Int, error) {
// balanceOf(address) selector = 0x70a08231
selector := common.FromHex("0x70a08231")
data := make([]byte, 4+32)
copy(data[0:4], selector)
copy(data[4+12:4+32], account.Bytes())

result, err := o.ddoClient.GetEthClient().CallContract(ctx, ethereum.CallMsg{
To: &tokenAddr,
Data: data,
}, nil)
if err != nil {
return nil, err
}
if len(result) < 32 {
return big.NewInt(0), nil
}
return new(big.Int).SetBytes(result), nil
}

func (o *OnChainDDO) ValidateSP(ctx context.Context, providerActorID uint64) (*DDOSPConfig, error) {
cfg, err := o.ddoClient.GetSPConfig(providerActorID)
if err != nil {
Expand Down
35 changes: 35 additions & 0 deletions service/dealpusher/ddo_schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dealpusher
import (
"context"
"fmt"
"math/big"
"strconv"
"strings"
"time"
Expand All @@ -17,6 +18,10 @@ import (
"gorm.io/gorm"
)

// minGasBalance is the minimum native FIL balance before a low-gas warning
// is emitted. 0.1 FIL = 10^17 attoFIL.
var minGasBalance = new(big.Int).Exp(big.NewInt(10), big.NewInt(17), nil)

func defaultDDOSchedulingConfig() DDOSchedulingConfig {
return DDOSchedulingConfig{
BatchSize: 10,
Expand Down Expand Up @@ -98,6 +103,36 @@ func (d *DealPusher) runDDOSchedule(ctx context.Context, schedule *model.Schedul
return model.ScheduleError, fmt.Errorf("provider %s (actor %d) is not active in the DDO contract", schedule.Provider, providerActorID)
}

// Pre-flight balance check — log wallet state for observability
// and warn if gas balance is dangerously low.
if balStatus, balErr := d.ddoDealManager.CheckBalance(ctx, evmSigner.EVMAddress()); balErr != nil {
Logger.Warnw("failed to check wallet balance before DDO scheduling",
"schedule", schedule.ID, "wallet", evmSigner.EVMAddress().Hex(), "error", balErr)
} else {
Logger.Infow("DDO wallet balance",
"schedule", schedule.ID,
"wallet", evmSigner.EVMAddress().Hex(),
"nativeFIL", balStatus.NativeFIL.String(),
"paymentToken", balStatus.TokenBalance.String(),
"deposited", balStatus.DepositedFunds.String(),
"available", balStatus.Available.String(),
)
if balStatus.NativeFIL.Cmp(minGasBalance) < 0 {
Logger.Warnw("wallet FIL balance low — may not have enough gas for DDO transactions",
"schedule", schedule.ID,
"wallet", evmSigner.EVMAddress().Hex(),
"balance", balStatus.NativeFIL.String(),
"threshold", minGasBalance.String(),
)
}
if balStatus.TokenBalance.Sign() == 0 && balStatus.Available.Sign() == 0 {
Logger.Warnw("wallet has no payment tokens and no deposited funds — EnsurePayments will fail",
"schedule", schedule.ID,
"wallet", evmSigner.EVMAddress().Hex(),
)
}
}

var timer *time.Timer
current := sumResult{}
for {
Expand Down
12 changes: 12 additions & 0 deletions service/dealpusher/ddo_schedule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package dealpusher

import (
"context"
"math/big"
"testing"
"time"

"github.com/data-preservation-programs/go-synapse/signer"
"github.com/data-preservation-programs/singularity/model"
"github.com/data-preservation-programs/singularity/util/keystore"
"github.com/data-preservation-programs/singularity/util/testutil"
"github.com/ethereum/go-ethereum/common"
"github.com/filecoin-project/go-address"
"github.com/ipfs/go-cid"
"github.com/stretchr/testify/require"
Expand All @@ -26,6 +28,16 @@ func (m *ddoDealManagerMock) ValidateSP(_ context.Context, _ uint64) (*DDOSPConf
return m.spConfig, nil
}

func (m *ddoDealManagerMock) CheckBalance(_ context.Context, _ common.Address) (*DDOBalanceStatus, error) {
return &DDOBalanceStatus{
NativeFIL: new(big.Int).Mul(big.NewInt(10), big.NewInt(1e18)),
TokenBalance: new(big.Int).Mul(big.NewInt(1000), big.NewInt(1e18)),
DepositedFunds: big.NewInt(0),
LockupCurrent: big.NewInt(0),
Available: big.NewInt(0),
}, nil
}

func (m *ddoDealManagerMock) EnsurePayments(_ context.Context, _ signer.EVMSigner, _ []DDOPieceSubmission, _ DDOSchedulingConfig) error {
m.ensureCalled = true
return nil
Expand Down
Loading