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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.37.0
github.com/trufnetwork/kwil-db v0.10.3-0.20250911225741-d6cb2b2747ff
github.com/trufnetwork/kwil-db/core v0.4.3-0.20250911225741-d6cb2b2747ff
github.com/trufnetwork/kwil-db v0.10.3-0.20250915124855-c60f28b113d1
github.com/trufnetwork/kwil-db/core v0.4.3-0.20250915124855-c60f28b113d1
github.com/trufnetwork/sdk-go v0.3.2-0.20250630062504-841b40cdb709
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1212,10 +1212,10 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/trufnetwork/kwil-db v0.10.3-0.20250911225741-d6cb2b2747ff h1:TeVummmhXdwNAuRDst/aWQPfvHZh3TUl7Nw6Q2Oogao=
github.com/trufnetwork/kwil-db v0.10.3-0.20250911225741-d6cb2b2747ff/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo=
github.com/trufnetwork/kwil-db/core v0.4.3-0.20250911225741-d6cb2b2747ff h1:2hQ3ChOBM76eh10Ix0GItIfK5HNwfqrGWhoiQIH/P60=
github.com/trufnetwork/kwil-db/core v0.4.3-0.20250911225741-d6cb2b2747ff/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ=
github.com/trufnetwork/kwil-db v0.10.3-0.20250915124855-c60f28b113d1 h1:NQ1HD0kf61QtNhcaCJ0jDqTaMw7WipBEZzn7lb0Mios=
github.com/trufnetwork/kwil-db v0.10.3-0.20250915124855-c60f28b113d1/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo=
github.com/trufnetwork/kwil-db/core v0.4.3-0.20250915124855-c60f28b113d1 h1:FJ/dHHviqqx4wyH5ucA2z2JRmo4+k1eOCy6d9ye5djA=
github.com/trufnetwork/kwil-db/core v0.4.3-0.20250915124855-c60f28b113d1/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ=
github.com/trufnetwork/openzeppelin-merkle-tree-go v0.0.2 h1:DCq8MzbWH0wZmICNmMVsSzUHUPl+2vqRhluEABjxl88=
github.com/trufnetwork/openzeppelin-merkle-tree-go v0.0.2/go.mod h1:Y0MJpPp9QXU5vC6Gpoilql2NkgmGNcbHm9HYC2v2N8s=
github.com/trufnetwork/sdk-go v0.3.2-0.20250630062504-841b40cdb709 h1:d9EqPXIjbq/atzEncK5dM3Z9oStx1BxCGuL/sjefeCw=
Expand Down
36 changes: 36 additions & 0 deletions internal/migrations/erc20-bridge/002-public-transfer-actions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- Public Transfer Actions for TRUF Token Operations
-- Enables users to transfer TRUF tokens between addresses on TN

-- SEPOLIA TESTNET TRANSFERS
CREATE OR REPLACE ACTION sepolia_transfer($to_address TEXT, $amount TEXT) PUBLIC {
-- Validate Ethereum address format
if NOT check_ethereum_address($to_address) {
ERROR('Invalid Ethereum address format. Must be a valid Ethereum address: ' || $to_address);
}

-- Validate amount is positive
if $amount::NUMERIC(78, 0) <= 0::NUMERIC(78, 0) {
ERROR('Transfer amount must be positive');
}

-- Execute transfer using the bridge extension
sepolia_bridge.transfer($to_address, $amount::NUMERIC(78, 0));
};


-- MAINNET TRANSFERS
CREATE OR REPLACE ACTION mainnet_transfer($to_address TEXT, $amount TEXT) PUBLIC {
-- Validate Ethereum address format
if NOT check_ethereum_address($to_address) {
ERROR('Invalid Ethereum address format. Must be a valid Ethereum address: ' || $to_address);
}

-- Validate amount is positive
if $amount::NUMERIC(78, 0) <= 0::NUMERIC(78, 0) {
ERROR('Transfer amount must be positive');
}

-- Execute transfer using the bridge extension
mainnet_bridge.transfer($to_address, $amount::NUMERIC(78, 0));
};

13 changes: 12 additions & 1 deletion internal/migrations/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"strings"
)

//go:embed *.sql test_only/*.sql
//go:embed *.sql test_only/*.sql erc20-bridge/*.sql
var seedFiles embed.FS

func GetSeedScriptPaths() []string {
Expand Down Expand Up @@ -42,6 +42,17 @@ func GetSeedScriptPaths() []string {
}
}

// process erc20-bridge directory
entries, err = seedFiles.ReadDir("erc20-bridge")
if err != nil {
panic(err)
}
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".sql") {
seedsFiles = append(seedsFiles, filepath.Join(dir, "erc20-bridge", entry.Name()))
}
}

if len(seedsFiles) == 0 {
panic("no seeds files found in embedded directory")
}
Expand Down
265 changes: 265 additions & 0 deletions tests/extensions/erc20/erc20_bridge_transfer_actions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
//go:build kwiltest

package tests

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/require"
"github.com/trufnetwork/kwil-db/common"
"github.com/trufnetwork/kwil-db/core/types"
kwilTesting "github.com/trufnetwork/kwil-db/testing"
testerc20 "github.com/trufnetwork/node/tests/streams/utils/erc20"

erc20shim "github.com/trufnetwork/kwil-db/node/exts/erc20-bridge/erc20"
)

// TestSepoliaTransferActions tests the new sepolia_transfer() and sepolia_balance() public actions.
// This validates that users can transfer TRUF tokens directly using the new SQL actions
// without needing to use the extension methods directly.
func TestSepoliaTransferActions(t *testing.T) {
seedAndRun(t, "sepolia_transfer_actions", func(ctx context.Context, platform *kwilTesting.Platform) error {
// Initialize the extension to load the sepolia_bridge instance
err := erc20shim.ForTestingInitializeExtension(ctx, platform)
require.NoError(t, err)

// Get the configured escrow address from bridge info
configuredEscrow, err := getBridgeEscrowAddress(ctx, platform)
require.NoError(t, err)

// Credit initial balance to TestUserA using configured escrow
err = testerc20.InjectERC20Transfer(ctx, platform,
TestChain, configuredEscrow, TestERC20, configuredEscrow, TestUserA, TestAmount2, 10, nil)
require.NoError(t, err)

// Verify initial balance via sepolia_wallet_balance action
balanceA, err := callSepoliaWalletBalance(ctx, platform, TestUserA)
require.NoError(t, err)
require.Equal(t, TestAmount2, balanceA, "UserA should have initial deposit")

// Verify TestUserB has zero balance initially
balanceB, err := callSepoliaWalletBalance(ctx, platform, TestUserB)
require.NoError(t, err)
require.Equal(t, "0", balanceB, "UserB should have zero balance initially")

// Execute transfer via sepolia_transfer action
err = callSepoliaTransfer(ctx, platform, TestUserA, TestUserB, TestAmount1)
require.NoError(t, err)

// Verify balances after transfer
balanceA, err = callSepoliaWalletBalance(ctx, platform, TestUserA)
require.NoError(t, err)
require.Equal(t, TestAmount1, balanceA, "UserA should have remaining amount after transfer")

balanceB, err = callSepoliaWalletBalance(ctx, platform, TestUserB)
require.NoError(t, err)
require.Equal(t, TestAmount1, balanceB, "UserB should have received transferred amount")

return nil
})
}

// TestTransferActionValidation tests the validation logic in the new transfer actions.
func TestTransferActionValidation(t *testing.T) {
seedAndRun(t, "transfer_action_validation", func(ctx context.Context, platform *kwilTesting.Platform) error {
// Initialize the extension to load the sepolia_bridge instance
err := erc20shim.ForTestingInitializeExtension(ctx, platform)
require.NoError(t, err)

// Get the configured escrow address from bridge info
configuredEscrow, err := getBridgeEscrowAddress(ctx, platform)
require.NoError(t, err)

// Test Case 1: Invalid address format
t.Log("Testing invalid address format...")
err = callSepoliaTransfer(ctx, platform, TestUserA, "invalid_address", TestAmount1)
if err == nil {
t.Log("ERROR: Expected error for invalid address format but got none")
}
require.Error(t, err)
require.Contains(t, err.Error(), "Invalid Ethereum address format")

// Test Case 2: Address without 0x prefix
t.Log("Testing address without 0x prefix...")
err = callSepoliaTransfer(ctx, platform, TestUserA, "1111111111111111111111111111111111111111", TestAmount1)
if err == nil {
t.Log("ERROR: Expected error for address without 0x prefix but got none")
}
require.Error(t, err)
require.Contains(t, err.Error(), "Invalid Ethereum address format")

// Test Case 3: Zero amount
t.Log("Testing zero amount...")
err = callSepoliaTransfer(ctx, platform, TestUserA, TestUserB, "0")
if err == nil {
t.Log("ERROR: Expected error for zero amount but got none")
}
require.Error(t, err)
require.Contains(t, err.Error(), "Transfer amount must be positive")

// Test Case 4: Negative amount (string parsing will fail at conversion)
t.Log("Testing negative amount...")
err = callSepoliaTransfer(ctx, platform, TestUserA, TestUserB, "-10")
if err == nil {
t.Log("ERROR: Expected error for negative amount but got none")
}
require.Error(t, err)

// Test Case 5: Insufficient balance (user has no balance record - nil balance)
t.Log("Testing insufficient balance with nil balance...")
err = callSepoliaTransfer(ctx, platform, TestUserA, TestUserB, TestAmount1)
if err == nil {
t.Log("ERROR: Expected error for insufficient balance (nil) but got none")
}
require.Error(t, err)
require.Contains(t, err.Error(), "insufficient balance")

// Test Case 6: Insufficient balance (user has some balance but not enough)
t.Log("Testing insufficient balance with partial balance...")

// Give TestUserA a small balance (half of what they'll try to transfer)
smallAmount := "500000000000000000" // 0.5 tokens (half of TestAmount1 which is 1.0)
err = testerc20.InjectERC20Transfer(ctx, platform,
TestChain, configuredEscrow, TestERC20, configuredEscrow, TestUserA, smallAmount, 10, nil)
require.NoError(t, err)

// Verify they have the small balance
balance, err := callSepoliaWalletBalance(ctx, platform, TestUserA)
require.NoError(t, err)
require.Equal(t, smallAmount, balance, "TestUserA should have small balance")

// Try to transfer more than they have (TestAmount1 = 1.0 tokens > smallAmount = 0.5 tokens)
err = callSepoliaTransfer(ctx, platform, TestUserA, TestUserB, TestAmount1)
if err == nil {
t.Log("ERROR: Expected error for insufficient balance (partial) but got none")
}
require.Error(t, err)
require.Contains(t, err.Error(), "insufficient balance")
require.Contains(t, err.Error(), smallAmount) // Should show actual balance they have

// Test balance query with invalid address
t.Log("Testing balance query with invalid address...")
_, err = callSepoliaWalletBalance(ctx, platform, "invalid_address")
if err == nil {
t.Log("ERROR: Expected error for invalid address in balance query but got none")
}
require.Error(t, err)
require.Contains(t, err.Error(), "invalid ethereum address")

return nil
})
}

// TestMultipleTransferActions tests multiple sequential transfers using the new actions.
func TestMultipleTransferActions(t *testing.T) {
seedAndRun(t, "multiple_transfer_actions", func(ctx context.Context, platform *kwilTesting.Platform) error {
// Initialize the extension to load the sepolia_bridge instance
err := erc20shim.ForTestingInitializeExtension(ctx, platform)
require.NoError(t, err)

// Get the configured escrow address from bridge info
configuredEscrow, err := getBridgeEscrowAddress(ctx, platform)
require.NoError(t, err)

// Define test users
userA := TestUserA
userB := TestUserB
userC := "0xabc0000000000000000000000000000000000003"

// Credit large initial balance to userA
initialAmount := "10000000000000000000" // 10.0 tokens
err = testerc20.InjectERC20Transfer(ctx, platform,
TestChain, configuredEscrow, TestERC20, configuredEscrow, userA, initialAmount, 10, nil)
require.NoError(t, err)

// Transfer A -> B (3 tokens)
err = callSepoliaTransfer(ctx, platform, userA, userB, "3000000000000000000")
require.NoError(t, err)

// Transfer A -> C (2 tokens)
err = callSepoliaTransfer(ctx, platform, userA, userC, TestAmount2)
require.NoError(t, err)

// Transfer B -> C (1 token)
err = callSepoliaTransfer(ctx, platform, userB, userC, TestAmount1)
require.NoError(t, err)

// Verify final balances
// UserA: 10 - 3 - 2 = 5
balanceA, err := callSepoliaWalletBalance(ctx, platform, userA)
require.NoError(t, err)
require.Equal(t, "5000000000000000000", balanceA, "UserA should have 5 tokens remaining")

// UserB: 3 - 1 = 2
balanceB, err := callSepoliaWalletBalance(ctx, platform, userB)
require.NoError(t, err)
require.Equal(t, TestAmount2, balanceB, "UserB should have 2 tokens remaining")

// UserC: 2 + 1 = 3
balanceC, err := callSepoliaWalletBalance(ctx, platform, userC)
require.NoError(t, err)
require.Equal(t, "3000000000000000000", balanceC, "UserC should have 3 tokens total")

return nil
})
}

// Helper function to call sepolia_transfer action
func callSepoliaTransfer(ctx context.Context, platform *kwilTesting.Platform, from, to, amount string) error {
engineCtx := engCtx(ctx, platform, from, 1, false)

res, err := platform.Engine.Call(engineCtx, platform.DB, "", "sepolia_transfer", []any{to, amount}, func(row *common.Row) error {
return nil
})
if err != nil {
return err
}
if res != nil && res.Error != nil {
return res.Error
}
return nil
}

// Helper function to call sepolia_wallet_balance action
func callSepoliaWalletBalance(ctx context.Context, platform *kwilTesting.Platform, userAddr string) (string, error) {
engineCtx := engCtx(ctx, platform, "0x0000000000000000000000000000000000000000", 1, false)

var balance string
res, err := platform.Engine.Call(engineCtx, platform.DB, "", "sepolia_wallet_balance", []any{userAddr}, func(row *common.Row) error {
if len(row.Values) != 1 {
return fmt.Errorf("expected 1 column, got %d", len(row.Values))
}
balance = row.Values[0].(*types.Decimal).String()
return nil
})
if err != nil {
return "", err
}
if res != nil && res.Error != nil {
return "", res.Error
}
return balance, nil
}

// Helper function to get the configured escrow address from bridge info
func getBridgeEscrowAddress(ctx context.Context, platform *kwilTesting.Platform) (string, error) {
engineCtx := engCtx(ctx, platform, "0x0000000000000000000000000000000000000000", 1, false)

var escrow string
res, err := platform.Engine.Call(engineCtx, platform.DB, "", "get_erc20_bridge_info", []any{}, func(row *common.Row) error {
if len(row.Values) < 2 {
return fmt.Errorf("expected at least 2 columns, got %d", len(row.Values))
}
escrow = row.Values[1].(string) // escrow is the second column
return nil
})
if err != nil {
return "", err
}
if res != nil && res.Error != nil {
return "", res.Error
}
return escrow, nil
}
Loading