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
82 changes: 82 additions & 0 deletions internal/migrations/erc20-bridge/002-public-transfer-actions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,85 @@ CREATE OR REPLACE ACTION ethereum_transfer($to_address TEXT, $amount TEXT) PUBLI
NULL
);
};


-- HOODI TEST TOKEN (TT) TRANSFERS
CREATE OR REPLACE ACTION hoodi_tt_transfer($to_address TEXT, $amount TEXT) PUBLIC {
$recipient_lower TEXT := LOWER($to_address);

-- Validate Ethereum address format
if NOT check_ethereum_address($recipient_lower) {
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');
}

-- Fee
$fee := 1000000000000000000::NUMERIC(78, 0); -- 1 TT with 18 decimals
$caller_balance := COALESCE(hoodi_tt.balance(@caller), 0::NUMERIC(78, 0));

IF @leader_sender IS NULL {
ERROR('Leader address not available for fee transfer');
}
$leader_hex TEXT := encode(@leader_sender, 'hex')::TEXT;

IF ($caller_balance < ($amount::NUMERIC(78, 0) + $fee)) {
ERROR('Insufficient balance for transfer. Requires an extra 1 TT fee on top of the transfer amount');
}

hoodi_tt.transfer($leader_hex, $fee);

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

record_transaction_event(
4,
$fee,
'0x' || $leader_hex,
NULL
);
};


-- HOODI TEST TOKEN 2 (TT2) TRANSFERS
CREATE OR REPLACE ACTION hoodi_tt2_transfer($to_address TEXT, $amount TEXT) PUBLIC {
$recipient_lower TEXT := LOWER($to_address);

-- Validate Ethereum address format
if NOT check_ethereum_address($recipient_lower) {
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');
}

-- Fee
$fee := 1000000000000000000::NUMERIC(78, 0); -- 1 TT2 with 18 decimals
$caller_balance := COALESCE(hoodi_tt2.balance(@caller), 0::NUMERIC(78, 0));

IF @leader_sender IS NULL {
ERROR('Leader address not available for fee transfer');
}
$leader_hex TEXT := encode(@leader_sender, 'hex')::TEXT;

IF ($caller_balance < ($amount::NUMERIC(78, 0) + $fee)) {
ERROR('Insufficient balance for transfer. Requires an extra 1 TT2 fee on top of the transfer amount');
}

hoodi_tt2.transfer($leader_hex, $fee);

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

record_transaction_event(
4,
$fee,
'0x' || $leader_hex,
NULL
);
};
97 changes: 97 additions & 0 deletions tests/extensions/erc20/erc20_bridge_transfer_actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,100 @@ func getBridgeEscrowAddress(ctx context.Context, platform *kwilTesting.Platform)
}
return escrow, nil
}

// hoodi_tt bridge constants (chain + lowercase escrow + erc20), matching erc20-bridge/000-extension.sql.
// The exact lowercase escrow matches the instance id the migration registered, so InjectERC20Transfer
// targets that same instance (a different-cased escrow would derive a different id).
const (
hoodiTTChain = "hoodi"
hoodiTTEscrow = "0x878d6aaeb6e746033f50b8dc268d54b4631554e7"
hoodiTTERC20 = "0x263ce78fef26600e4e428cebc91c2a52484b4fbf"
)

// TestHoodiTransferActions tests the hoodi_tt_transfer() wrapper against the hoodi_tt bridge that
// testnet actually uses. hoodi_tt is registered by erc20-bridge/000-extension.sql and activated in the
// singleton by ForTestingInitializeExtension. This guards the wrapper added so agent-wallet funding
// (owner -> MAA) works without a main-namespace transfer action having to be hand-seeded per bridge.
func TestHoodiTransferActions(t *testing.T) {
seedAndRun(t, "hoodi_tt_transfer_actions", func(ctx context.Context, platform *kwilTesting.Platform) error {
// Activate the bridge singleton (loads + syncs the migration-registered instances, incl. hoodi_tt).
require.NoError(t, erc20shim.ForTestingInitializeExtension(ctx, platform))

// Credit UserA on hoodi_tt: 3.0 TT (1.0 transfer + 1.0 fee + 1.0 remaining). Single deposit -> nil prev.
initialAmount := "3000000000000000000"
require.NoError(t, testerc20.InjectERC20Transfer(ctx, platform,
hoodiTTChain, hoodiTTEscrow, hoodiTTERC20, TestUserA, TestUserA, initialAmount, 10, nil))

balanceA, err := callHoodiWalletBalance(ctx, platform, TestUserA)
require.NoError(t, err)
require.Equal(t, initialAmount, balanceA, "UserA should have initial deposit")

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

// Transfer 1.0 TT (+ 1.0 fee) from A to B.
err = callHoodiTransfer(ctx, platform, TestUserA, TestUserB, TestAmount1)
require.NoError(t, err)

// UserA: 3.0 - 1.0 transfer - 1.0 fee = 1.0
balanceA, err = callHoodiWalletBalance(ctx, platform, TestUserA)
require.NoError(t, err)
require.Equal(t, TestAmount1, balanceA, "UserA should have remaining amount after transfer + fee")

// UserB: received exactly the transfer amount (not the fee).
balanceB, err = callHoodiWalletBalance(ctx, platform, TestUserB)
require.NoError(t, err)
require.Equal(t, TestAmount1, balanceB, "UserB should have received transferred amount")

// Validation: B has 1.0, a 1.0 transfer needs 2.0 (1.0 + 1.0 fee) -> insufficient.
err = callHoodiTransfer(ctx, platform, TestUserB, TestUserA, TestAmount1)
require.Error(t, err)
require.Contains(t, err.Error(), "Insufficient balance for transfer")
require.Contains(t, err.Error(), "Requires an extra 1 TT fee")

// Validation: bad recipient address.
err = callHoodiTransfer(ctx, platform, TestUserA, "invalid_address", TestAmount1)
require.Error(t, err)
require.Contains(t, err.Error(), "Invalid Ethereum address format")

return nil
})
}

// Helper function to call hoodi_tt_transfer action
func callHoodiTransfer(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, "", "hoodi_tt_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 hoodi_tt_wallet_balance action
func callHoodiWalletBalance(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, "", "hoodi_tt_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
}
Loading