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
121 changes: 121 additions & 0 deletions service/dealpusher/ddo_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package dealpusher

import (
"context"
"testing"
"time"

"github.com/data-preservation-programs/singularity/util/testutil"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
)

func startCalibnetFork(t *testing.T) *testutil.AnvilInstance {
t.Helper()
return testutil.StartAnvil(t, testutil.CalibnetRPC)
}

// TestIntegration_DDOClientConnectivity verifies that OnChainDDO can connect
// to a calibnet fork and detect the correct chain ID.
func TestIntegration_DDOClientConnectivity(t *testing.T) {
anvil := startCalibnetFork(t)
rpcURL := anvil.RPCURL

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Verify raw RPC connectivity and chain ID
ethClient, err := ethclient.DialContext(ctx, rpcURL)
require.NoError(t, err)
defer ethClient.Close()

chainID, err := ethClient.ChainID(ctx)
require.NoError(t, err)
require.EqualValues(t, testutil.CalibnetChainID, chainID.Int64())

// Initialize OnChainDDO client with calibnet contract addresses
ddo, err := NewOnChainDDO(ctx, rpcURL,
testutil.CalibnetDDOContract,
testutil.CalibnetPaymentsContract,
testutil.CalibnetUSDFC,
)
require.NoError(t, err)
defer ddo.Close()

require.EqualValues(t, testutil.CalibnetChainID, ddo.chainID.Int64())
t.Logf("DDO client connected: chainID=%d, ddo=%s, payments=%s",
ddo.chainID, ddo.ddoContractAddr.Hex(), ddo.paymentsContractAddr.Hex())
}

// TestIntegration_DDOWalletFunding verifies the testutil wallet funding helper
// works against an Anvil fork.
func TestIntegration_DDOWalletFunding(t *testing.T) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be under testutil tests probably

anvil := startCalibnetFork(t)
rpcURL := anvil.RPCURL

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Generate a fresh test wallet
_, addr := testutil.GenerateTestKey(t)

// Fund it with 10 ETH (FIL on calibnet)
testutil.FundEVMWallet(t, rpcURL, addr, testutil.OneEther)

// Verify the balance
client, err := ethclient.DialContext(ctx, rpcURL)
require.NoError(t, err)
defer client.Close()

balance, err := client.BalanceAt(ctx, addr, nil)
require.NoError(t, err)
require.Equal(t, testutil.OneEther, balance)
t.Logf("funded wallet %s with %s wei", addr.Hex(), balance.String())
}

// TestIntegration_DDOValidateSP attempts to validate an SP on calibnet.
// This test exercises the contract read path. Without a registered SP,
// ValidateSP returns an empty (inactive) config — we verify the call
// succeeds and returns a well-formed response.
func TestIntegration_DDOValidateSP(t *testing.T) {
anvil := startCalibnetFork(t)
rpcURL := anvil.RPCURL

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

ddo, err := NewOnChainDDO(ctx, rpcURL,
testutil.CalibnetDDOContract,
testutil.CalibnetPaymentsContract,
testutil.CalibnetUSDFC,
)
require.NoError(t, err)
defer ddo.Close()

// Use a known-invalid provider ID — should return inactive, not error.
// When a real calibnet SP is registered, this test should be updated
// to use that provider ID and assert IsActive=true.
cfg, err := ddo.ValidateSP(ctx, 99999)
require.NoError(t, err)
require.NotNil(t, cfg)
t.Logf("ValidateSP(99999): active=%v, minPiece=%d, maxPiece=%d",
cfg.IsActive, cfg.MinPieceSize, cfg.MaxPieceSize)

// TODO: Once a calibnet SP is registered in the DDO contract, add a
// test here with the real provider actor ID and assert:
// require.True(t, cfg.IsActive)
// require.Greater(t, cfg.MaxPieceSize, uint64(0))
}

// TODO: TestIntegration_DDOFullDealFlow
// This test requires a registered, active SP on calibnet. Once FF provides
// the SP and it's registered in the DDO contract:
//
// 1. Fork calibnet via Anvil
// 2. Fund a test wallet with FIL
// 3. Create a test preparation with a piece in the database
// 4. Create a DDO schedule pointing to the funded wallet and the SP
// 5. Run the deal pusher schedule
// 6. Verify allocation was created on-chain
// 7. Initialize DDOTrackingClient
// 8. Verify allocation tracking returns the correct status
37 changes: 37 additions & 0 deletions service/dealtracker/ddo_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package dealtracker

import (
"context"
"testing"
"time"

"github.com/data-preservation-programs/singularity/util/testutil"
"github.com/stretchr/testify/require"
)

func startCalibnetFork(t *testing.T) *testutil.AnvilInstance {
t.Helper()
return testutil.StartAnvil(t, testutil.CalibnetRPC)
}

// TestIntegration_DDOTrackingClientConnectivity verifies that the DDO tracking
// client can connect to a calibnet fork and query allocation info.
func TestIntegration_DDOTrackingClientConnectivity(t *testing.T) {
anvil := startCalibnetFork(t)
rpcURL := anvil.RPCURL

client, err := NewDDOTrackingClient(rpcURL, testutil.CalibnetDDOContract)
require.NoError(t, err)
defer client.Close()

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Query a non-existent allocation — should return non-nil status, not error.
status, err := client.GetAllocationInfo(ctx, 0)
require.NoError(t, err)
require.NotNil(t, status)
require.False(t, status.Activated)
t.Logf("GetAllocationInfo(0): activated=%v, sectorNumber=%d",
status.Activated, status.SectorNumber)
}
26 changes: 15 additions & 11 deletions service/pdptracker/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,14 @@ import (
"gorm.io/gorm"
)

const calibnetRPC = "https://api.calibration.node.glif.io/rpc/v1"

func startCalibnetFork(t *testing.T) string {
func startCalibnetFork(t *testing.T) *testutil.AnvilInstance {
t.Helper()
anvil := testutil.StartAnvil(t, calibnetRPC)
return anvil.RPCURL
return testutil.StartAnvil(t, testutil.CalibnetRPC)
}

func TestIntegration_NetworkDetection(t *testing.T) {
rpcURL := startCalibnetFork(t)
anvil := startCalibnetFork(t)
rpcURL := anvil.RPCURL

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
Expand All @@ -48,7 +46,7 @@ func TestIntegration_ShovelConfig(t *testing.T) {
contractAddr := constants.GetPDPVerifierAddress(constants.NetworkCalibration)
conf := buildShovelConfig(
"postgres://localhost/test",
calibnetRPC,
testutil.CalibnetRPC,
uint64(constants.ChainIDCalibration),
contractAddr,
0,
Expand All @@ -60,7 +58,8 @@ func TestIntegration_ShovelConfig(t *testing.T) {
}

func TestIntegration_ShovelIndexer(t *testing.T) {
rpcURL := startCalibnetFork(t)
anvil := startCalibnetFork(t)
rpcURL := anvil.RPCURL

testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) {
if db.Dialector.Name() != "postgres" {
Expand All @@ -83,7 +82,7 @@ func TestIntegration_ShovelIndexer(t *testing.T) {
err = indexer.Start(indexCtx, exitErr)
require.NoError(t, err)

time.Sleep(10 * time.Second)
anvil.MineBlock(t)

var schemaExists bool
err = db.Raw("SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = 'shovel')").Scan(&schemaExists).Error
Expand Down Expand Up @@ -117,7 +116,8 @@ func TestIntegration_ShovelIndexer(t *testing.T) {
}

func TestIntegration_FullResync(t *testing.T) {
rpcURL := startCalibnetFork(t)
anvil := startCalibnetFork(t)
rpcURL := anvil.RPCURL

testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) {
if db.Dialector.Name() != "postgres" {
Expand All @@ -139,6 +139,9 @@ func TestIntegration_FullResync(t *testing.T) {
exitErr := make(chan error, 1)
require.NoError(t, indexer.Start(indexCtx, exitErr))

// produce a block so shovel has something to index
anvil.MineBlock(t)

// poll until shovel has indexed at least one block
require.Eventually(t, func() bool {
var count int64
Expand Down Expand Up @@ -210,7 +213,8 @@ func TestIntegration_FullResync(t *testing.T) {
}

func TestIntegration_RPCClient(t *testing.T) {
rpcURL := startCalibnetFork(t)
anvil := startCalibnetFork(t)
rpcURL := anvil.RPCURL

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
Expand Down
16 changes: 15 additions & 1 deletion util/testutil/anvil.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"context"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"strings"
"testing"
"time"

"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
)

type AnvilInstance struct {
Expand Down Expand Up @@ -41,7 +44,6 @@ func StartAnvil(t *testing.T, upstreamRPC string) *AnvilInstance {
cmd := exec.Command("anvil",
"--fork-url", upstreamRPC,
"--port", fmt.Sprintf("%d", port),
"--block-time", "1",
"--silent",
)
cmd.Stdout = os.Stderr
Expand All @@ -61,6 +63,18 @@ func StartAnvil(t *testing.T, upstreamRPC string) *AnvilInstance {
return inst
}

// MineBlock forces anvil to produce a block. In automine mode (default),
// blocks are mined on tx submission, but consumers that only read (e.g.
// Shovel) need an explicit nudge to see chain progress.
func (a *AnvilInstance) MineBlock(t *testing.T) {
t.Helper()
resp, err := http.Post(a.RPCURL, "application/json",
strings.NewReader(`{"jsonrpc":"2.0","method":"evm_mine","params":[],"id":1}`))
require.NoError(t, err)
resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
}

func freePort() (int, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
Expand Down
10 changes: 10 additions & 0 deletions util/testutil/calibnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package testutil

// calibnet constants shared by integration tests
const (
CalibnetRPC = "https://api.calibration.node.glif.io/rpc/v1"
CalibnetChainID = 314159
CalibnetDDOContract = "0x889fD50196BE300D06dc4b8F0F17fdB0af587095"
CalibnetPaymentsContract = "0x09a0fDc2723fAd1A7b8e3e00eE5DF73841df55a0"
CalibnetUSDFC = "0xb3042734b608a1B16e9e86B374A3f3e389B4cDf0"
)
97 changes: 97 additions & 0 deletions util/testutil/fund.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package testutil

import (
"context"
"crypto/ecdsa"
"errors"
"math/big"
"testing"
"time"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
)

// anvilAccount0Key is the private key for Anvil's first pre-funded account
// (0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, 10000 ETH).
// This is a well-known deterministic test key — never use in production.
const anvilAccount0Key = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"

// AnvilFunderKey returns the parsed private key for Anvil's first pre-funded account.
func AnvilFunderKey(t *testing.T) *ecdsa.PrivateKey {
t.Helper()
key, err := crypto.HexToECDSA(anvilAccount0Key)
require.NoError(t, err)
return key
}

// FundEVMWallet sends ETH from Anvil's pre-funded account 0 to the given address.
// amount is in wei. Anvil automines the tx immediately.
func FundEVMWallet(t *testing.T, rpcURL string, to common.Address, amount *big.Int) common.Hash {
t.Helper()

ctx := context.Background()
client, err := ethclient.DialContext(ctx, rpcURL)
require.NoError(t, err)
defer client.Close()

funderKey := AnvilFunderKey(t)
funderAddr := crypto.PubkeyToAddress(funderKey.PublicKey)

nonce, err := client.PendingNonceAt(ctx, funderAddr)
require.NoError(t, err)

chainID, err := client.ChainID(ctx)
require.NoError(t, err)

gasPrice, err := client.SuggestGasPrice(ctx)
require.NoError(t, err)

tx := types.NewTx(&types.LegacyTx{
Nonce: nonce,
To: &to,
Value: amount,
Gas: 21000,
GasPrice: gasPrice,
})

signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), funderKey)
require.NoError(t, err)

err = client.SendTransaction(ctx, signedTx)
require.NoError(t, err)

// automine should be instant but some anvil versions need a moment
var receipt *types.Receipt
for range 20 {
receipt, err = client.TransactionReceipt(ctx, signedTx.Hash())
if err == nil {
break
}
if !errors.Is(err, ethereum.NotFound) {
require.NoError(t, err)
}
time.Sleep(100 * time.Millisecond)
}
require.NoError(t, err, "receipt not available after automine")
require.EqualValues(t, 1, receipt.Status, "funding transaction failed")

return signedTx.Hash()
}

// GenerateTestKey creates a fresh ECDSA key pair for testing and returns
// the private key and its corresponding EVM address.
func GenerateTestKey(t *testing.T) (*ecdsa.PrivateKey, common.Address) {
t.Helper()
key, err := crypto.GenerateKey()
require.NoError(t, err)
addr := crypto.PubkeyToAddress(key.PublicKey)
return key, addr
}

// OneEther is 1e18 wei, useful as a unit for test funding amounts.
var OneEther = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)
Loading