diff --git a/internal/migrations/024-attestation-actions.sql b/internal/migrations/024-attestation-actions.sql index 25abe5669..b22d923e4 100644 --- a/internal/migrations/024-attestation-actions.sql +++ b/internal/migrations/024-attestation-actions.sql @@ -48,6 +48,24 @@ $max_fee INT8 if $action_id = 0 { ERROR('Action not allowed for attestation: ' || $action_name); } + + -- ===== FEE COLLECTION ===== + -- Collect 40 TRUF flat fee for attestation request + $attestation_fee := '40000000000000000000'::NUMERIC(78, 0); -- 40 TRUF with 18 decimals + $caller_balance := ethereum_bridge.balance(@caller); + + IF $caller_balance < $attestation_fee { + ERROR('Insufficient balance for attestation. Required: 40 TRUF'); + } + + -- Verify leader address is available + IF @leader_sender IS NULL { + ERROR('Leader address not available for fee transfer'); + } + + $leader_addr TEXT := encode(@leader_sender, 'hex')::TEXT; + ethereum_bridge.transfer($leader_addr, $attestation_fee); + -- ===== END FEE COLLECTION ===== -- Get current block height $created_height := @height; diff --git a/internal/migrations/024-get-database-size-v2.sql b/internal/migrations/025-get-database-size-v2.sql similarity index 100% rename from internal/migrations/024-get-database-size-v2.sql rename to internal/migrations/025-get-database-size-v2.sql diff --git a/tests/streams/attestation/attestation_request_test.go b/tests/streams/attestation/attestation_request_test.go index 3c280c50b..bd8916042 100644 --- a/tests/streams/attestation/attestation_request_test.go +++ b/tests/streams/attestation/attestation_request_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/trufnetwork/kwil-db/common" + "github.com/trufnetwork/kwil-db/core/crypto/auth" "github.com/trufnetwork/kwil-db/core/types" kwilTesting "github.com/trufnetwork/kwil-db/testing" attestation "github.com/trufnetwork/node/extensions/tn_attestation" @@ -144,11 +145,13 @@ func runAttestationUnauthorizedBlocked(t *testing.T, ctx context.Context, platfo TxContext: &common.TxContext{ Ctx: ctx, BlockContext: &common.BlockContext{ - Height: 1, + Height: 1, + Proposer: helper.leaderPub, // Required for @leader_sender }, - Signer: unauthorizedAddr.Bytes(), - Caller: unauthorizedAddr.Address(), - TxID: platform.Txid(), + Signer: unauthorizedAddr.Bytes(), + Caller: unauthorizedAddr.Address(), + TxID: platform.Txid(), + Authenticator: auth.EthPersonalSignAuth, // Required for balance operations }, } diff --git a/tests/streams/attestation/request_attestation_fee_test.go b/tests/streams/attestation/request_attestation_fee_test.go new file mode 100644 index 000000000..47b85080c --- /dev/null +++ b/tests/streams/attestation/request_attestation_fee_test.go @@ -0,0 +1,455 @@ +//go:build kwiltest + +package tests + +import ( + "context" + "fmt" + "math/big" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/trufnetwork/kwil-db/common" + "github.com/trufnetwork/kwil-db/core/crypto" + coreauth "github.com/trufnetwork/kwil-db/core/crypto/auth" + kwilTesting "github.com/trufnetwork/kwil-db/testing" + "github.com/trufnetwork/node/extensions/tn_utils" + "github.com/trufnetwork/node/internal/migrations" + testutils "github.com/trufnetwork/node/tests/streams/utils" + testerc20 "github.com/trufnetwork/node/tests/streams/utils/erc20" + "github.com/trufnetwork/node/tests/streams/utils/setup" + "github.com/trufnetwork/sdk-go/core/types" + "github.com/trufnetwork/sdk-go/core/util" +) + +// Test constants for attestation fees +const ( + testAttestationChain = "sepolia" + testAttestationEscrow = "0x502430eD0BbE0f230215870c9C2853e126eE5Ae3" + testAttestationERC20 = "0x2222222222222222222222222222222222222222" + testAttestationExtensionName = "sepolia_bridge" + attestationFeeAmount = "40000000000000000000" // 40 TRUF with 18 decimals +) + +var ( + fortyTRUFAttest = mustParseAttestationBigInt(attestationFeeAmount) // 40 TRUF as big.Int + attestationPointCounter int64 = 10000 // Start from 10000, increment for each balance injection +) + +func mustParseAttestationBigInt(s string) *big.Int { + val := new(big.Int) + if _, ok := val.SetString(s, 10); !ok { + panic(fmt.Sprintf("failed to parse big int: %s", s)) + } + return val +} + +// TestRequestAttestationFees is the main test suite for request_attestation transaction fees +func TestRequestAttestationFees(t *testing.T) { + testutils.RunSchemaTest(t, kwilTesting.SchemaTest{ + Name: "ATTESTATION_FEE01_RequestAttestationFees", + SeedStatements: migrations.GetSeedScriptStatements(), + FunctionTests: []kwilTesting.TestFunc{ + setupAttestationTestEnvironment(t), + testAttestationNetworkWriterPaysFee(t), + testAttestationInsufficientBalance(t), + testAttestationMultipleRequestsChargeFees(t), + testAttestationLeaderReceivesFees(t), + testAttestationBalanceCorrectlyDeducted(t), + }, + }, testutils.GetTestOptionsWithCache()) +} + +// setupAttestationTestEnvironment creates system admin, registers test action, and creates test stream +func setupAttestationTestEnvironment(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + // Use the system admin address (derived from private key 0x00...01) + systemAdmin := util.Unsafe_NewEthereumAddressFromString("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf") + platform.Deployer = systemAdmin.Bytes() + + // Grant network_writers_manager role so system admin can manage network_writer roles + err := setup.AddMemberToRoleBypass(ctx, platform, "system", "network_writers_manager", systemAdmin.Address()) + if err != nil { + return fmt.Errorf("failed to grant network_writers_manager to system admin: %w", err) + } + + // Register system admin as data provider (this also grants network_writer role) + err = setup.CreateDataProvider(ctx, platform, systemAdmin.Address()) + if err != nil { + return fmt.Errorf("failed to register system admin as data provider: %w", err) + } + + // Note: get_record is already registered in 023-attestation-schema.sql with ID=1 + // No need to register it again + + // Create a test stream for attestations + streamID := "st000000000000000000000000000000" + streamLocator := types.StreamLocator{ + StreamId: util.GenerateStreamId(streamID), + DataProvider: systemAdmin, + } + err = setup.CreateStream(ctx, platform, setup.StreamInfo{ + Type: setup.ContractTypePrimitive, + Locator: streamLocator, + }) + if err != nil { + return fmt.Errorf("failed to create test stream: %w", err) + } + + return nil + } +} + +// Test 1: Network writer member pays 40 TRUF fee per attestation request +func testAttestationNetworkWriterPaysFee(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + requesterAddrVal := util.Unsafe_NewEthereumAddressFromString("0xa111111111111111111111111111111111111111") + requesterAddr := &requesterAddrVal + + // Register as data provider with network_writer role + err := setup.CreateDataProvider(ctx, platform, requesterAddr.Address()) + require.NoError(t, err, "failed to register data provider") + + // Give requester 100 TRUF + err = giveAttestationBalance(ctx, platform, requesterAddr.Address(), "100000000000000000000") + require.NoError(t, err, "failed to give balance") + + // Get initial balance + initialBalance, err := getAttestationBalance(ctx, platform, requesterAddr.Address()) + require.NoError(t, err, "failed to get initial balance") + + // Request attestation (should pay 40 TRUF) + systemAdmin := util.Unsafe_NewEthereumAddressFromString("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf") + streamID := "st000000000000000000000000000000" + err = requestAttestation(ctx, platform, requesterAddr, systemAdmin.Address(), streamID, "get_record") + require.NoError(t, err, "attestation request should succeed") + + // Verify balance decreased by 40 TRUF + finalBalance, err := getAttestationBalance(ctx, platform, requesterAddr.Address()) + require.NoError(t, err, "failed to get final balance") + + expectedBalance := new(big.Int).Sub(initialBalance, fortyTRUFAttest) + require.Equal(t, 0, expectedBalance.Cmp(finalBalance), + "Balance should decrease by 40 TRUF, expected %s but got %s", expectedBalance, finalBalance) + + return nil + } +} + +// Test 2: Insufficient balance causes error +func testAttestationInsufficientBalance(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + requesterAddrVal := util.Unsafe_NewEthereumAddressFromString("0xa222222222222222222222222222222222222222") + requesterAddr := &requesterAddrVal + + // Register as data provider with network_writer role + err := setup.CreateDataProvider(ctx, platform, requesterAddr.Address()) + require.NoError(t, err, "failed to register data provider") + + // Give requester only 10 TRUF (insufficient for 40 TRUF fee) + err = giveAttestationBalance(ctx, platform, requesterAddr.Address(), "10000000000000000000") + require.NoError(t, err, "failed to give balance") + + // Try to request attestation (should fail) + systemAdmin := util.Unsafe_NewEthereumAddressFromString("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf") + streamID := "st000000000000000000000000000000" + err = requestAttestation(ctx, platform, requesterAddr, systemAdmin.Address(), streamID, "get_record") + require.Error(t, err, "attestation request should fail with insufficient balance") + require.Contains(t, err.Error(), "Insufficient balance for attestation", + "error should mention insufficient balance") + + return nil + } +} + +// Test 3: Multiple attestation requests charge 40 TRUF each +func testAttestationMultipleRequestsChargeFees(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + requesterAddrVal := util.Unsafe_NewEthereumAddressFromString("0xa333333333333333333333333333333333333333") + requesterAddr := &requesterAddrVal + + // Register as data provider with network_writer role + err := setup.CreateDataProvider(ctx, platform, requesterAddr.Address()) + require.NoError(t, err, "failed to register data provider") + + // Give requester 200 TRUF (enough for 5 attestations) + err = giveAttestationBalance(ctx, platform, requesterAddr.Address(), "200000000000000000000") + require.NoError(t, err, "failed to give balance") + + // Get initial balance + initialBalance, err := getAttestationBalance(ctx, platform, requesterAddr.Address()) + require.NoError(t, err, "failed to get initial balance") + + // Request 3 attestations with different time ranges (to get different attestation hashes) + systemAdmin := util.Unsafe_NewEthereumAddressFromString("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf") + streamID := "st000000000000000000000000000000" + for i := 0; i < 3; i++ { + // Vary the time range for each request to get different attestation hashes + from := int64(i * 1000) + to := int64(i*1000 + 999) + err = requestAttestationWithTimeRange(ctx, platform, requesterAddr, systemAdmin.Address(), streamID, "get_record", from, to) + require.NoError(t, err, "attestation request %d should succeed", i+1) + } + + // Verify balance decreased by 120 TRUF (3 × 40) + finalBalance, err := getAttestationBalance(ctx, platform, requesterAddr.Address()) + require.NoError(t, err, "failed to get final balance") + + expectedFee := new(big.Int).Mul(fortyTRUFAttest, big.NewInt(3)) + expectedBalance := new(big.Int).Sub(initialBalance, expectedFee) + require.Equal(t, 0, expectedBalance.Cmp(finalBalance), + "Balance should decrease by 120 TRUF (3 × 40), expected %s but got %s", expectedBalance, finalBalance) + + return nil + } +} + +// Test 4: Leader receives attestation fees correctly +func testAttestationLeaderReceivesFees(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + requesterAddrVal := util.Unsafe_NewEthereumAddressFromString("0xa444444444444444444444444444444444444444") + requesterAddr := &requesterAddrVal + + // Register as data provider with network_writer role + err := setup.CreateDataProvider(ctx, platform, requesterAddr.Address()) + require.NoError(t, err, "failed to register data provider") + + // Give requester 100 TRUF + err = giveAttestationBalance(ctx, platform, requesterAddr.Address(), "100000000000000000000") + require.NoError(t, err, "failed to give balance") + + // Generate leader keys + _, pubGeneric, err := crypto.GenerateSecp256k1Key(nil) + require.NoError(t, err, "failed to generate leader key") + pub := pubGeneric.(*crypto.Secp256k1PublicKey) + + // Get leader address + leaderSigner := crypto.EthereumAddressFromPubKey(pub) + leaderAddr := fmt.Sprintf("0x%x", leaderSigner) + + // Give leader initial balance + err = giveAttestationBalance(ctx, platform, leaderAddr, "10000000000000000000") + require.NoError(t, err, "failed to give leader balance") + + // Get initial leader balance + initialLeaderBalance, err := getAttestationBalance(ctx, platform, leaderAddr) + require.NoError(t, err, "failed to get initial leader balance") + + // Request attestation with specific leader + systemAdmin := util.Unsafe_NewEthereumAddressFromString("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf") + streamID := "st000000000000000000000000000000" + err = requestAttestationWithLeader(ctx, platform, requesterAddr, pub, systemAdmin.Address(), streamID, "get_record") + require.NoError(t, err, "attestation request with leader should succeed") + + // Verify leader balance increased by 40 TRUF + finalLeaderBalance, err := getAttestationBalance(ctx, platform, leaderAddr) + require.NoError(t, err, "failed to get final leader balance") + + expectedLeaderBalance := new(big.Int).Add(initialLeaderBalance, fortyTRUFAttest) + require.Equal(t, 0, expectedLeaderBalance.Cmp(finalLeaderBalance), + "Leader should receive 40 TRUF fee, expected %s but got %s", expectedLeaderBalance, finalLeaderBalance) + + return nil + } +} + +// Test 5: Balance is correctly deducted after fee payment +func testAttestationBalanceCorrectlyDeducted(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error { + return func(ctx context.Context, platform *kwilTesting.Platform) error { + requesterAddrVal := util.Unsafe_NewEthereumAddressFromString("0xa555555555555555555555555555555555555555") + requesterAddr := &requesterAddrVal + + // Register as data provider with network_writer role + err := setup.CreateDataProvider(ctx, platform, requesterAddr.Address()) + require.NoError(t, err, "failed to register data provider") + + // Give requester exactly 80 TRUF (enough for 2 attestations) + err = giveAttestationBalance(ctx, platform, requesterAddr.Address(), "80000000000000000000") + require.NoError(t, err, "failed to give balance") + + systemAdmin := util.Unsafe_NewEthereumAddressFromString("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf") + streamID := "st000000000000000000000000000000" + + // First attestation should succeed (time range 0-999) + err = requestAttestationWithTimeRange(ctx, platform, requesterAddr, systemAdmin.Address(), streamID, "get_record", 0, 999) + require.NoError(t, err, "first attestation should succeed") + + // Second attestation should succeed (time range 1000-1999) + err = requestAttestationWithTimeRange(ctx, platform, requesterAddr, systemAdmin.Address(), streamID, "get_record", 1000, 1999) + require.NoError(t, err, "second attestation should succeed") + + // Third attestation should fail (insufficient balance) (time range 2000-2999) + err = requestAttestationWithTimeRange(ctx, platform, requesterAddr, systemAdmin.Address(), streamID, "get_record", 2000, 2999) + require.Error(t, err, "third attestation should fail with insufficient balance") + require.Contains(t, err.Error(), "Insufficient balance for attestation", + "error should mention insufficient balance") + + // Verify final balance is 0 + finalBalance, err := getAttestationBalance(ctx, platform, requesterAddr.Address()) + require.NoError(t, err, "failed to get final balance") + require.Equal(t, 0, finalBalance.Cmp(big.NewInt(0)), + "Balance should be 0 after 2 attestations, but got %s", finalBalance) + + return nil + } +} + +// ===== HELPER FUNCTIONS ===== + +// registerAttestationAction registers an action in attestation_actions table +func registerAttestationAction(ctx context.Context, platform *kwilTesting.Platform, actionName string, actionID int) error { + systemAdmin := util.Unsafe_NewEthereumAddressFromString("0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf") + + txContext := &common.TxContext{ + Ctx: ctx, + BlockContext: &common.BlockContext{Height: 1}, + TxID: platform.Txid(), + Signer: systemAdmin.Bytes(), + Caller: systemAdmin.Address(), + } + + engineContext := &common.EngineContext{ + TxContext: txContext, + OverrideAuthz: true, + } + + // Register action in attestation_actions table + insertSQL := `INSERT INTO attestation_actions (action_id, action_name) + VALUES ($action_id, $action_name) + ON CONFLICT (action_id) DO NOTHING` + + err := platform.Engine.Execute(engineContext, platform.DB, insertSQL, map[string]any{ + "$action_id": actionID, + "$action_name": actionName, + }, func(row *common.Row) error { + return nil + }) + if err != nil { + return fmt.Errorf("failed to register attestation action: %w", err) + } + + return nil +} + +// giveAttestationBalance credits TRUF balance to a wallet using ERC20 inject +func giveAttestationBalance(ctx context.Context, platform *kwilTesting.Platform, wallet string, amountStr string) error { + attestationPointCounter++ + return testerc20.InjectERC20Transfer( + ctx, + platform, + testAttestationChain, + testAttestationEscrow, + testAttestationERC20, + wallet, + wallet, + amountStr, + attestationPointCounter, + nil, + ) +} + +// getAttestationBalance retrieves the TRUF balance for a wallet +func getAttestationBalance(ctx context.Context, platform *kwilTesting.Platform, wallet string) (*big.Int, error) { + balanceStr, err := testerc20.GetUserBalance(ctx, platform, testAttestationExtensionName, wallet) + if err != nil { + return nil, err + } + + balance := new(big.Int) + if _, ok := balance.SetString(balanceStr, 10); !ok { + return nil, fmt.Errorf("invalid balance string: %s", balanceStr) + } + + return balance, nil +} + +// callRequestAttestationAction is the base implementation - calls the request_attestation action +func callRequestAttestationAction(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, leaderPub *crypto.Secp256k1PublicKey, dataProvider string, streamID string, actionName string) error { + return callRequestAttestationActionWithTimeRange(ctx, platform, signer, leaderPub, dataProvider, streamID, actionName, int64(0), int64(99999)) +} + +// callRequestAttestationActionWithTimeRange is the base implementation with custom time range +func callRequestAttestationActionWithTimeRange(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, leaderPub *crypto.Secp256k1PublicKey, dataProvider string, streamID string, actionName string, from int64, to int64) error { + // Prepare arguments for get_record action + // get_record($data_provider TEXT, $stream_id TEXT, $from INT8, $to INT8, $frozen_at INT8, $use_cache BOOL) + actionArgs := []any{ + dataProvider, + streamID, + from, + to, + int64(99999), // frozen_at + false, // use_cache (will be forced to false by request_attestation) + } + + // Encode action arguments + argsBytes, err := tn_utils.EncodeActionArgs(actionArgs) + if err != nil { + return fmt.Errorf("failed to encode action args: %w", err) + } + + tx := &common.TxContext{ + Ctx: ctx, + BlockContext: &common.BlockContext{ + Height: 1, + Proposer: leaderPub, + }, + Signer: signer.Bytes(), + Caller: signer.Address(), + TxID: platform.Txid(), + Authenticator: coreauth.EthPersonalSignAuth, + } + engineCtx := &common.EngineContext{TxContext: tx} + + res, err := platform.Engine.Call( + engineCtx, + platform.DB, + "", + "request_attestation", + []any{ + strings.ToLower(dataProvider), + streamID, + actionName, + argsBytes, + false, // encrypt_sig + int64(0), // max_fee (unused) + }, + func(row *common.Row) error { return nil }, + ) + if err != nil { + return err + } + if res != nil && res.Error != nil { + return res.Error + } + return nil +} + +// requestAttestation requests attestation with a randomly generated leader +func requestAttestation(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, dataProvider string, streamID string, actionName string) error { + // Generate random leader + _, pubGeneric, err := crypto.GenerateSecp256k1Key(nil) + if err != nil { + return err + } + pub := pubGeneric.(*crypto.Secp256k1PublicKey) + + return callRequestAttestationAction(ctx, platform, signer, pub, dataProvider, streamID, actionName) +} + +// requestAttestationWithTimeRange requests attestation with specific time range and randomly generated leader +func requestAttestationWithTimeRange(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, dataProvider string, streamID string, actionName string, from int64, to int64) error { + // Generate random leader + _, pubGeneric, err := crypto.GenerateSecp256k1Key(nil) + if err != nil { + return err + } + pub := pubGeneric.(*crypto.Secp256k1PublicKey) + + return callRequestAttestationActionWithTimeRange(ctx, platform, signer, pub, dataProvider, streamID, actionName, from, to) +} + +// requestAttestationWithLeader requests attestation with a specific leader +func requestAttestationWithLeader(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, leaderPub *crypto.Secp256k1PublicKey, dataProvider string, streamID string, actionName string) error { + return callRequestAttestationAction(ctx, platform, signer, leaderPub, dataProvider, streamID, actionName) +} diff --git a/tests/streams/attestation/test_helpers.go b/tests/streams/attestation/test_helpers.go index fb5f9c5eb..b764efb1e 100644 --- a/tests/streams/attestation/test_helpers.go +++ b/tests/streams/attestation/test_helpers.go @@ -14,6 +14,8 @@ import ( "github.com/trufnetwork/kwil-db/common" kcrypto "github.com/trufnetwork/kwil-db/core/crypto" "github.com/trufnetwork/kwil-db/core/crypto/auth" + "github.com/trufnetwork/kwil-db/core/types" + erc20bridge "github.com/trufnetwork/kwil-db/node/exts/erc20-bridge/erc20" kwilTesting "github.com/trufnetwork/kwil-db/testing" "github.com/trufnetwork/node/extensions/tn_utils" testsetup "github.com/trufnetwork/node/tests/streams/utils/setup" @@ -31,10 +33,16 @@ const ( MinCanonicalLength = 20 DefaultBlockHeight = 10 InvalidTxID = "0x0000000000000000000000000000000000000000000000000000000000000000" + + // ERC20 instance constants for balance operations + testChain = "sepolia" + testEscrow = "0x502430eD0BbE0f230215870c9C2853e126eE5Ae3" + testERC20 = "0x2222222222222222222222222222222222222222" ) var ( TestDataProviderBytes = mustHexToBytes(TestDataProviderHex) + erc20InstanceID *types.UUID // Set during NewAttestationTestHelper ) func mustHexToBytes(input string) []byte { @@ -67,17 +75,31 @@ func NewTestAddresses() *TestAddresses { // AttestationTestHelper encapsulates common attestation test operations type AttestationTestHelper struct { - t *testing.T - ctx context.Context - platform *kwilTesting.Platform + t *testing.T + ctx context.Context + platform *kwilTesting.Platform + leaderPub kcrypto.PublicKey // Leader public key for fee transfers } // NewAttestationTestHelper creates a new helper func NewAttestationTestHelper(t *testing.T, ctx context.Context, platform *kwilTesting.Platform) *AttestationTestHelper { + // Generate a leader key for fee transfers (reused across all contexts) + _, leaderPub, err := kcrypto.GenerateSecp256k1Key(nil) + require.NoError(t, err, "generate leader key") + helper := &AttestationTestHelper{ - t: t, - ctx: ctx, - platform: platform, + t: t, + ctx: ctx, + platform: platform, + leaderPub: leaderPub, + } + + // Set up ERC20 instance for balance operations (once per test suite) + if erc20InstanceID == nil { + instanceID, err := erc20bridge.ForTestingForceSyncInstance(ctx, platform, + testChain, testEscrow, testERC20, 18) + require.NoError(t, err, "setup ERC20 instance") + erc20InstanceID = instanceID } // Grant network_writer role to deployer by default @@ -85,6 +107,10 @@ func NewAttestationTestHelper(t *testing.T, ctx context.Context, platform *kwilT require.NoError(t, err, "create deployer address") helper.GrantNetworkWriterRole(deployer.Address()) + // Give deployer a large balance upfront to cover all attestation fees in tests + // 1000 TRUF should be enough for 25 attestation requests (40 TRUF each) + helper.GiveBalance(deployer.Address(), "1000000000000000000000") // 1000 TRUF in wei + return helper } @@ -97,11 +123,13 @@ func (h *AttestationTestHelper) NewEngineContext() *common.EngineContext { TxContext: &common.TxContext{ Ctx: h.ctx, BlockContext: &common.BlockContext{ - Height: 1, + Height: 1, + Proposer: h.leaderPub, // Required for @leader_sender in fee transfers }, - Signer: h.platform.Deployer, - Caller: deployer.Address(), - TxID: h.platform.Txid(), + Signer: h.platform.Deployer, + Caller: deployer.Address(), + TxID: h.platform.Txid(), + Authenticator: auth.EthPersonalSignAuth, // Required for balance operations }, } } @@ -134,11 +162,13 @@ func (h *AttestationTestHelper) NewRequesterContext(requester *util.EthereumAddr TxContext: &common.TxContext{ Ctx: h.ctx, BlockContext: &common.BlockContext{ - Height: 1, + Height: 1, + Proposer: h.leaderPub, // Required for @leader_sender in fee transfers }, - Signer: requester.Bytes(), - Caller: requester.Address(), - TxID: h.platform.Txid(), + Signer: requester.Bytes(), + Caller: requester.Address(), + TxID: h.platform.Txid(), + Authenticator: auth.EthPersonalSignAuth, // Required for balance operations }, } } @@ -220,14 +250,18 @@ func (h *AttestationTestHelper) GrantNetworkWriterRole(walletAddr string) { // CreateAttestationForRequester creates an attestation for a specific requester func (h *AttestationTestHelper) CreateAttestationForRequester(actionName string, requester *util.EthereumAddress, value int64) { - // Grant network_writer role to requester first + // Grant network_writer role to requester h.GrantNetworkWriterRole(requester.Address()) + // Give requester balance to pay for attestation fees (using proper kwil-db API) + h.GiveBalance(requester.Address(), "1000000000000000000000") // 1000 TRUF in wei + argsBytes, err := tn_utils.EncodeActionArgs([]any{value}) require.NoError(h.t, err, "encode args") + // Use requester context (now that balance injection works properly) requesterCtx := h.NewRequesterContext(requester) - _, err = h.platform.Engine.Call(requesterCtx, h.platform.DB, "", "request_attestation", + res, err := h.platform.Engine.Call(requesterCtx, h.platform.DB, "", "request_attestation", []any{ TestDataProviderHex, TestStreamID, @@ -237,7 +271,10 @@ func (h *AttestationTestHelper) CreateAttestationForRequester(actionName string, int64(0), }, func(row *common.Row) error { return nil }) - require.NoError(h.t, err, "request_attestation") + require.NoError(h.t, err, "request_attestation engine call") + if res.Error != nil { + require.NoError(h.t, res.Error, "request_attestation action failed: %v", res.Error) + } } // CountRows counts the number of rows returned by an action @@ -288,3 +325,12 @@ ON CONFLICT (action_name) DO UPDATE SET action_id = EXCLUDED.action_id;` func ComputeDigest(canonical []byte) [32]byte { return sha256.Sum256(canonical) } + +// GiveBalance credits TRUF balance to a wallet using ForTestingCreditBalance (proper kwil-db test API) +func (h *AttestationTestHelper) GiveBalance(walletAddr string, amountWei string) { + app := &common.App{DB: h.platform.DB, Engine: h.platform.Engine} + amount := types.MustParseDecimalExplicit(amountWei, 78, 0) + + err := erc20bridge.ForTestingCreditBalance(h.ctx, app, erc20InstanceID, walletAddr, amount) + require.NoError(h.t, err, "credit balance %s wei to %s", amountWei, walletAddr) +}