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
12 changes: 5 additions & 7 deletions internal/migrations/001-common-actions.prod.sql
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,10 @@ CREATE OR REPLACE ACTION create_streams(
}

-- ===== FEE COLLECTION =====
$num_streams INT := array_length($stream_ids);

-- Charge 6 TRUF per stream to every caller (no role-based exemption).
$fee_per_stream := 6000000000000000000::NUMERIC(78, 0); -- 6 TRUF with 18 decimals
$total_fee := $fee_per_stream * $num_streams::NUMERIC(78, 0);
-- Flat 1 TRUF per transaction (write-fee policy per issue #3805).
-- Cost is independent of $num_streams: batching N streams in one call
-- charges the same 1 TRUF as creating a single stream.
$total_fee := 1000000000000000000::NUMERIC(78, 0); -- 1 TRUF with 18 decimals

IF @leader_sender IS NULL {
ERROR('Leader address not available for fee transfer');
Expand All @@ -54,8 +53,7 @@ CREATE OR REPLACE ACTION create_streams(
$caller_balance := eth_truf.balance(@caller);

IF $caller_balance < $total_fee {
-- Derive human-readable fee from $total_fee
ERROR('Insufficient balance for stream creation. Required: ' || ($total_fee / 1000000000000000000::NUMERIC(78, 0))::TEXT || ' TRUF for ' || $num_streams::TEXT || ' stream(s)');
ERROR('Insufficient balance for stream creation. Required: 1 TRUF');
}

eth_truf.transfer($leader_hex, $total_fee);
Expand Down
14 changes: 6 additions & 8 deletions internal/migrations/001-common-actions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ CREATE OR REPLACE ACTION create_stream(

/**
* create_streams: Creates multiple streams at once.
* Fee: 6 TRUF per stream created (charged to every caller, no exemptions)
* Fee: 1 TRUF flat per transaction (charged to every caller, no exemptions)
Comment thread
MicBun marked this conversation as resolved.
* Validates stream_id format, data provider address, and stream type.
* Sets default metadata including type, owner, visibility, and readonly keys.
*
Expand Down Expand Up @@ -90,11 +90,10 @@ CREATE OR REPLACE ACTION create_streams(
}

-- ===== FEE COLLECTION =====
$num_streams INT := array_length($stream_ids);

-- Charge 6 TRUF per stream to every caller (no role-based exemption).
$fee_per_stream := 6000000000000000000::NUMERIC(78, 0); -- 6 TRUF with 18 decimals
$total_fee := $fee_per_stream * $num_streams::NUMERIC(78, 0);
-- Flat 1 TRUF per transaction (write-fee policy per issue #3805).
-- Cost is independent of $num_streams: batching N streams in one call
-- charges the same 1 TRUF as creating a single stream.
$total_fee := 1000000000000000000::NUMERIC(78, 0); -- 1 TRUF with 18 decimals

IF @leader_sender IS NULL {
ERROR('Leader address not available for fee transfer');
Expand All @@ -104,8 +103,7 @@ CREATE OR REPLACE ACTION create_streams(
$caller_balance := hoodi_tt.balance(@caller);

IF $caller_balance < $total_fee {
-- Derive human-readable fee from $total_fee
ERROR('Insufficient balance for stream creation. Required: ' || ($total_fee / 1000000000000000000::NUMERIC(78, 0))::TEXT || ' TRUF for ' || $num_streams::TEXT || ' stream(s)');
ERROR('Insufficient balance for stream creation. Required: 1 TRUF');
}

hoodi_tt.transfer($leader_hex, $total_fee);
Expand Down
2 changes: 1 addition & 1 deletion tests/streams/allow_zeros_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ func createStreamWithAllowZeros(ctx context.Context, platform *kwilTesting.Platf
}

// Fund for the universal create_stream fee — mirror setup.UntypedCreateStream.
if err := feefund.EnsureWalletFunded(ctx, platform, addr.Address(), feefund.PerStreamWei); err != nil {
if err := feefund.EnsureWalletFunded(ctx, platform, addr.Address(), feefund.WriteFeeWei); err != nil {
return errors.Wrap(err, "fund wallet for create_stream fee")
}

Expand Down
12 changes: 8 additions & 4 deletions tests/streams/attestation/request_attestation_fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ import (
"github.com/trufnetwork/sdk-go/core/util"
)

// Test constants for attestation fees
// Test constants for attestation fees — bridge configuration must match the
// `hoodi_tt` USE block in erc20-bridge/000-extension.sql since
// `request_attestation` charges its fee through `hoodi_tt.balance` /
// `hoodi_tt.transfer`. Crediting `sepolia_bridge` here would credit the wrong
// instance and the fee check would always see a zero balance.
const (
testAttestationChain = "sepolia"
testAttestationEscrow = "0x502430eD0BbE0f230215870c9C2853e126eE5Ae3"
testAttestationChain = "hoodi"
testAttestationEscrow = "0x878d6aaeb6e746033f50b8dc268d54b4631554e7"
testAttestationERC20 = "0x2222222222222222222222222222222222222222"
testAttestationExtensionName = "sepolia_bridge"
testAttestationExtensionName = "hoodi_tt"
attestationFeeAmount = "40000000000000000000" // 40 TRUF with 18 decimals
)

Expand Down
9 changes: 6 additions & 3 deletions tests/streams/attestation/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ const (
DefaultBlockHeight = 10
InvalidTxID = "0x0000000000000000000000000000000000000000000000000000000000000000"

// ERC20 instance constants for balance operations
testChain = "sepolia"
testEscrow = "0x502430eD0BbE0f230215870c9C2853e126eE5Ae3"
// ERC20 instance constants for balance operations — must match the
// `hoodi_tt` USE block in erc20-bridge/000-extension.sql so the balance
// credited via ForTestingCreditBalance lands on the same instance the
// fee-collection actions (001/003/004/024) read via `hoodi_tt.balance`.
testChain = "hoodi"
testEscrow = "0x878d6aaeb6e746033f50b8dc268d54b4631554e7"
testERC20 = "0x2222222222222222222222222222222222222222"
)

Expand Down
6 changes: 3 additions & 3 deletions tests/streams/primitive_batch_insert_alignment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ func testBatchAlignment(t *testing.T) func(ctx context.Context, platform *kwilTe
}

// Fund deployer for the per-record write fee (universal fee enforcement).
// Mirrors the migration 003 charge of feefund.PerStreamWei (6 TRUF) per record.
feePerRecord, ok := new(big.Int).SetString(feefund.PerStreamWei, 10)
// Mirrors the migration 003 charge of feefund.WriteFeeWei (6 TRUF) per record.
feePerRecord, ok := new(big.Int).SetString(feefund.WriteFeeWei, 10)
if !ok {
return errors.Errorf("invalid feefund.PerStreamWei: %s", feefund.PerStreamWei)
return errors.Errorf("invalid feefund.WriteFeeWei: %s", feefund.WriteFeeWei)
}
totalFee := new(big.Int).Mul(feePerRecord, big.NewInt(int64(len(eventTimes))))
if err := feefund.EnsureWalletFunded(ctx, platform, deployer.Address(), totalFee.String()); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion tests/streams/roles/permission_gates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestPermissionGates(t *testing.T) {

func testStreamCreationPermissionGates(t *testing.T, ctx context.Context, platform *kwilTesting.Platform) {
// Universal write-fee enforcement removed the role-based exemption: every caller
// pays 6 TRUF per stream regardless of network_writer membership. This test now
// pays a flat 1 TRUF per tx regardless of network_writer membership. This test now
// only covers the still-meaningful gate — that an authorized data provider can
// create streams when their wallet is funded (UntypedCreateStream tops up the
// balance via feefund). Cases that asserted role-based exemption or expected
Expand Down
75 changes: 40 additions & 35 deletions tests/streams/stream_creation_fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,24 @@ import (
"github.com/trufnetwork/sdk-go/core/util"
)

// Test constants - must match erc20-bridge/000-extension.sql configuration
// Test constants — must match the `hoodi_tt` USE block in
// erc20-bridge/000-extension.sql. The fee-collection actions (001/003/004)
// call hoodi_tt.balance / hoodi_tt.transfer directly, so this test helper
// credits balance into the matching bridge instance.
const (
testChain = "sepolia"
testEscrow = "0x502430eD0BbE0f230215870c9C2853e126eE5Ae3" // From erc20-bridge/000-extension.sql
testChain = "hoodi"
testEscrow = "0x878d6aaeb6e746033f50b8dc268d54b4631554e7"
testERC20 = "0x2222222222222222222222222222222222222222"
testExtensionName = "sepolia_bridge"
testExtensionName = "hoodi_tt"
)

var (
// sixTRUF is parsed from feefund.PerStreamWei — the same constant the
// oneTRUF is parsed from feefund.WriteFeeWei — the same constant the
// migration uses, so a fee-schedule change in one place can't drift
// from test assertions silently.
sixTRUF = mustParseBigInt(feefund.PerStreamWei)
// from test assertions silently. Per issue #3805 the write fee is now a
// flat 1 TRUF per transaction, regardless of how many streams the tx
// creates.
oneTRUF = mustParseBigInt(feefund.WriteFeeWei)
pointCounter int64 = 10 // Start from 10, increment for each balance injection
)

Expand All @@ -56,7 +61,7 @@ func TestStreamCreationFees(t *testing.T) {
testNonExemptWalletPaysFee(t),
testInsufficientBalance(t),
testFeeIndependentOfRole(t),
testBatchCreationPerStreamFee(t),
testBatchCreationChargesFlatFee(t),
testLeaderReceivesFees(t),
},
}, testutils.GetTestOptionsWithCache())
Expand Down Expand Up @@ -94,7 +99,7 @@ func setupTestEnvironment(t *testing.T) func(ctx context.Context, platform *kwil
}

// Test 1: Wallet with network_writer role still pays the create_streams fee.
// The role no longer carries an exemption — funded callers always pay 6 TRUF/stream.
// The role no longer carries an exemption — funded callers always pay 1 TRUF per tx.
func testWriterRolePaysFee(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error {
return func(ctx context.Context, platform *kwilTesting.Platform) error {
writerAddrVal := util.Unsafe_NewEthereumAddressFromString("0x1111111111111111111111111111111111111111")
Expand All @@ -117,15 +122,15 @@ func testWriterRolePaysFee(t *testing.T) func(ctx context.Context, platform *kwi
finalBalance, err := getBalance(ctx, platform, writerAddr.Address())
require.NoError(t, err, "failed to get final balance")

expectedBalance := new(big.Int).Sub(initialBalance, sixTRUF)
expectedBalance := new(big.Int).Sub(initialBalance, oneTRUF)
require.Equal(t, 0, expectedBalance.Cmp(finalBalance),
"network_writer should pay 6 TRUF, expected %s but got %s", expectedBalance, finalBalance)
"network_writer should pay 1 TRUF, expected %s but got %s", expectedBalance, finalBalance)

return nil
}
}

// Test 2: Non-exempt wallet pays 6 TRUF fee
// Test 2: Non-exempt wallet pays 1 TRUF fee
func testNonExemptWalletPaysFee(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error {
return func(ctx context.Context, platform *kwilTesting.Platform) error {
userAddrVal := util.Unsafe_NewEthereumAddressFromString("0x3333333333333333333333333333333333333333")
Expand All @@ -147,13 +152,13 @@ func testNonExemptWalletPaysFee(t *testing.T) func(ctx context.Context, platform
err = createStream(ctx, platform, userAddr, "st000000000000000000000000000002", "primitive")
require.NoError(t, err, "stream creation should succeed")

// Verify balance decreased by 6 TRUF
// Verify balance decreased by 1 TRUF
finalBalance, err := getBalance(ctx, platform, userAddr.Address())
require.NoError(t, err, "failed to get final balance")

expectedBalance := new(big.Int).Sub(initialBalance, sixTRUF)
expectedBalance := new(big.Int).Sub(initialBalance, oneTRUF)
require.Equal(t, 0, expectedBalance.Cmp(finalBalance),
"Balance should decrease by 6 TRUF, expected %s but got %s", expectedBalance, finalBalance)
"Balance should decrease by 1 TRUF, expected %s but got %s", expectedBalance, finalBalance)

return nil
}
Expand All @@ -169,8 +174,8 @@ func testInsufficientBalance(t *testing.T) func(ctx context.Context, platform *k
err := setup.CreateDataProviderWithoutRole(ctx, platform, userAddr.Address())
require.NoError(t, err, "failed to register data provider")

// Give user only 1 TRUF (insufficient)
err = giveBalance(ctx, platform, userAddr.Address(), "1000000000000000000")
// Give user 0.5 TRUF (insufficient for the flat 1 TRUF fee).
err = giveBalance(ctx, platform, userAddr.Address(), "500000000000000000")
require.NoError(t, err, "failed to give balance")

// Try to create stream (should fail)
Expand All @@ -184,7 +189,7 @@ func testInsufficientBalance(t *testing.T) func(ctx context.Context, platform *k
}

// Test 4: network_writer role grant/revoke does NOT change the create_streams fee.
// Every call charges 6 TRUF regardless of role membership.
// Every call charges 1 TRUF regardless of role membership.
func testFeeIndependentOfRole(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error {
return func(ctx context.Context, platform *kwilTesting.Platform) error {
userAddrVal := util.Unsafe_NewEthereumAddressFromString("0x5555555555555555555555555555555555555555")
Expand All @@ -199,15 +204,15 @@ func testFeeIndependentOfRole(t *testing.T) func(ctx context.Context, platform *
initialBalance, err := getBalance(ctx, platform, userAddr.Address())
require.NoError(t, err)

// Without role: charges 6 TRUF.
// Without role: charges 1 TRUF.
err = createStream(ctx, platform, userAddr, "st000000000000000000000000000004", "primitive")
require.NoError(t, err, "first stream creation should succeed")

balanceAfterFirst, err := getBalance(ctx, platform, userAddr.Address())
require.NoError(t, err)

expectedAfterFirst := new(big.Int).Sub(initialBalance, sixTRUF)
require.Equal(t, 0, expectedAfterFirst.Cmp(balanceAfterFirst), "first create should charge 6 TRUF")
expectedAfterFirst := new(big.Int).Sub(initialBalance, oneTRUF)
require.Equal(t, 0, expectedAfterFirst.Cmp(balanceAfterFirst), "first create should charge 1 TRUF")

// Grant role — must NOT exempt.
err = setup.AddMemberToRoleBypass(ctx, platform, "system", "network_writer", userAddr.Address())
Expand All @@ -218,9 +223,9 @@ func testFeeIndependentOfRole(t *testing.T) func(ctx context.Context, platform *

balanceAfterSecond, err := getBalance(ctx, platform, userAddr.Address())
require.NoError(t, err)
expectedAfterSecond := new(big.Int).Sub(balanceAfterFirst, sixTRUF)
expectedAfterSecond := new(big.Int).Sub(balanceAfterFirst, oneTRUF)
require.Equal(t, 0, expectedAfterSecond.Cmp(balanceAfterSecond),
"network_writer must still pay the 6 TRUF fee — exemption removed")
"network_writer must still pay the 1 TRUF fee — exemption removed")

// Revoke role — fee unchanged.
err = revokeRoleBypass(ctx, platform, "system", "network_writer", userAddr.Address())
Expand All @@ -232,16 +237,17 @@ func testFeeIndependentOfRole(t *testing.T) func(ctx context.Context, platform *
balanceAfterThird, err := getBalance(ctx, platform, userAddr.Address())
require.NoError(t, err)

expectedAfterThird := new(big.Int).Sub(balanceAfterSecond, sixTRUF)
expectedAfterThird := new(big.Int).Sub(balanceAfterSecond, oneTRUF)
require.Equal(t, 0, expectedAfterThird.Cmp(balanceAfterThird),
"third create should charge 6 TRUF (role revoked, fee unchanged)")
"third create should charge 1 TRUF (role revoked, fee unchanged)")

return nil
}
}

// Test 5: Batch stream creation charges fee per stream (not per call)
func testBatchCreationPerStreamFee(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error {
// Test 5: Batched create_streams charges a flat 1 TRUF (not 1 TRUF × N).
// This is the key invariant of issue #3805 — pricing is per-tx, not per-stream.
func testBatchCreationChargesFlatFee(t *testing.T) func(ctx context.Context, platform *kwilTesting.Platform) error {
return func(ctx context.Context, platform *kwilTesting.Platform) error {
userAddrVal := util.Unsafe_NewEthereumAddressFromString("0x6666666666666666666666666666666666666666")
userAddr := &userAddrVal
Expand Down Expand Up @@ -269,15 +275,14 @@ func testBatchCreationPerStreamFee(t *testing.T) func(ctx context.Context, platf
err = createStreams(ctx, platform, userAddr, streamIds, streamTypes)
require.NoError(t, err, "batch stream creation should succeed")

// Verify balance decreased by 18 TRUF (6 TRUF × 3 streams)
// Verify balance decreased by exactly 1 TRUF for the whole batch.
finalBalance, err := getBalance(ctx, platform, userAddr.Address())
require.NoError(t, err)

numStreams := int64(len(streamIds)) // 3 streams
expectedFee := new(big.Int).Mul(sixTRUF, big.NewInt(numStreams))
expectedBalance := new(big.Int).Sub(initialBalance, expectedFee)
expectedBalance := new(big.Int).Sub(initialBalance, oneTRUF)
require.Equal(t, 0, expectedBalance.Cmp(finalBalance),
"Batch should charge 6 TRUF per stream (3 streams = 18 TRUF), expected %s but got %s", expectedBalance, finalBalance)
"Batch of %d streams must still charge 1 TRUF flat (per-tx, not per-stream); expected %s but got %s",
len(streamIds), expectedBalance, finalBalance)

return nil
}
Expand Down Expand Up @@ -316,13 +321,13 @@ func testLeaderReceivesFees(t *testing.T) func(ctx context.Context, platform *kw
err = createStreamWithLeader(ctx, platform, userAddr, pub, "st00000000000000000000000000000a", "primitive")
require.NoError(t, err, "stream creation with leader should succeed")

// Verify leader balance increased by 6 TRUF
// Verify leader balance increased by 1 TRUF
finalLeaderBalance, err := getBalance(ctx, platform, leaderAddr)
require.NoError(t, err, "failed to get final leader balance")

expectedLeaderBalance := new(big.Int).Add(initialLeaderBalance, sixTRUF)
expectedLeaderBalance := new(big.Int).Add(initialLeaderBalance, oneTRUF)
require.Equal(t, 0, expectedLeaderBalance.Cmp(finalLeaderBalance),
"Leader should receive 6 TRUF fee, expected %s but got %s", expectedLeaderBalance, finalLeaderBalance)
"Leader should receive 1 TRUF fee, expected %s but got %s", expectedLeaderBalance, finalLeaderBalance)

return nil
}
Expand Down
2 changes: 1 addition & 1 deletion tests/streams/utils/feefund/feefund_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

// Constants kept identical to feefund_kwiltest.go so callers compile uniformly.
const (
PerStreamWei = "6000000000000000000" // 6 TRUF
WriteFeeWei = "1000000000000000000" // 1 TRUF (flat per-tx write fee)
AttestationFeeWei = "40000000000000000000" // 40 TRUF
)

Expand Down
9 changes: 6 additions & 3 deletions tests/streams/utils/feefund/feefund_kwiltest.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ import (
// hard-coded fee in its corresponding migration so that test funding stays
// in sync with on-chain charges. If a migration changes its fee, update here.
const (
// PerStreamWei mirrors the fee in 001-common-actions.sql `create_streams`
// AND in 003-primitive-insertion.sql / 004-composed-taxonomy.sql.
PerStreamWei = "6000000000000000000" // 6 TRUF
// WriteFeeWei mirrors the flat per-transaction write fee charged by
// 001-common-actions.sql `create_streams`, 003-primitive-insertion.sql
// `insert_records`, and 004-composed-taxonomy.sql `insert_taxonomy`.
// Per issue #3805 the fee is independent of batch size — one tx charges
// 1 TRUF regardless of how many streams/records/children it touches.
WriteFeeWei = "1000000000000000000" // 1 TRUF

// AttestationFeeWei mirrors the flat fee in 024-attestation-actions.sql.
AttestationFeeWei = "40000000000000000000" // 40 TRUF
Expand Down
14 changes: 4 additions & 10 deletions tests/streams/utils/procedure/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"math/big"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -435,15 +434,10 @@ func SetTaxonomy(ctx context.Context, input SetTaxonomyInput) error {
weightDecimals = append(weightDecimals, valueDecimal)
}

// insert_taxonomy charges feefund.PerStreamWei per child stream after the
// universal-fee migration removed the network_writer exemption.
feePerChild, ok := new(big.Int).SetString(feefund.PerStreamWei, 10)
if !ok {
return errors.Errorf("invalid feefund.PerStreamWei: %s", feefund.PerStreamWei)
}
totalFee := new(big.Int).Mul(feePerChild, big.NewInt(int64(len(primitiveStreamStrings))))
if err := feefund.EnsureWalletFunded(ctx, input.Platform, deployer.Address(), totalFee.String()); err != nil {
return errors.Wrap(err, "fund deployer for insert_taxonomy fees")
// insert_taxonomy charges a flat 1 TRUF per call regardless of child
// count, so fund a single feefund.WriteFeeWei before invoking it.
if err := feefund.EnsureWalletFunded(ctx, input.Platform, deployer.Address(), feefund.WriteFeeWei); err != nil {
return errors.Wrap(err, "fund deployer for insert_taxonomy fee")
}

engineContext := testctx.NewEngineContext(ctx, input.Platform, deployer, input.Height)
Expand Down
Loading
Loading