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
14 changes: 13 additions & 1 deletion extensions/tn_settlement/settlement_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ func testSettleMarketViaAction(t *testing.T) func(context.Context, *kwilTesting.
return nil
})
require.NoError(t, err)
for _, log := range createRes.Logs {
t.Logf("Engine log (create_market): %s", log)
}
if createRes.Error != nil {
t.Fatalf("create_market failed: %v", createRes.Error)
}
Expand All @@ -269,6 +272,9 @@ func testSettleMarketViaAction(t *testing.T) func(context.Context, *kwilTesting.
settleRes, err := platform.Engine.Call(engineCtx, platform.DB, "", "settle_market",
[]any{queryID}, nil)
require.NoError(t, err)
for _, log := range settleRes.Logs {
t.Logf("Engine log (settle_market): %s", log)
}
require.Nil(t, settleRes.Error)

// Verify settlement
Expand Down Expand Up @@ -313,7 +319,7 @@ func testLoadSettlementConfig(t *testing.T) func(context.Context, *kwilTesting.P
enabled, schedule, maxMarkets, retries, err := ops.LoadSettlementConfig(ctx)
require.NoError(t, err)
require.True(t, enabled, "should be true (enabled by migration 041)")
require.Equal(t, "0,30 * * * *", schedule, "should be 30-minute schedule from migration 041")
require.Equal(t, "*/5 * * * *", schedule, "should be 5-minute schedule from migration 041")
require.Equal(t, 1000, maxMarkets, "should be 1000 from migration 041")
require.Equal(t, 3, retries)

Expand Down Expand Up @@ -460,6 +466,9 @@ func testMultipleMarketsProcessing(t *testing.T) func(context.Context, *kwilTest
return nil
})
require.NoError(t, err)
for _, log := range createRes.Logs {
t.Logf("Engine log (create_market %d): %s", i, log)
}
if createRes.Error != nil {
t.Fatalf("create_market %d failed: %v", i, createRes.Error)
}
Expand All @@ -486,6 +495,9 @@ func testMultipleMarketsProcessing(t *testing.T) func(context.Context, *kwilTest
settleRes, err := platform.Engine.Call(engineCtx, platform.DB, "", "settle_market",
[]any{queryID}, nil)
require.NoError(t, err)
for _, log := range settleRes.Logs {
t.Logf("Engine log (settle_market %d): %s", queryID, log)
}
require.Nil(t, settleRes.Error)
}

Expand Down
139 changes: 139 additions & 0 deletions extensions/tn_utils/precompiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"math"
"math/big"
"strings"

"github.com/trufnetwork/kwil-db/common"
"github.com/trufnetwork/kwil-db/core/crypto"
"github.com/trufnetwork/kwil-db/core/types"
"github.com/trufnetwork/kwil-db/extensions/precompiles"
"github.com/trufnetwork/sdk-go/core/contractsapi"
Expand All @@ -34,6 +37,142 @@ func buildPrecompile() precompiles.Precompile {
parseAttestationBooleanMethod(),
computeAttestationHashMethod(),
unpackQueryComponentsMethod(),
getCallerHexMethod(),
getCallerBytesMethod(),
getLeaderHexMethod(),
getLeaderBytesMethod(),
},
}
}

// getCallerHexMethod returns the current transaction caller as a 0x-prefixed hex string.
func getCallerHexMethod() precompiles.Method {
return precompiles.Method{
Name: "get_caller_hex",
AccessModifiers: []precompiles.Modifier{precompiles.VIEW, precompiles.PUBLIC},
Parameters: []precompiles.PrecompileValue{},
Returns: &precompiles.MethodReturn{
IsTable: false,
Fields: []precompiles.PrecompileValue{
precompiles.NewPrecompileValue("caller_hex", types.TextType, false),
},
},
Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error {
if ctx == nil || ctx.TxContext == nil {
return resultFn([]any{""})
}
// If Caller is already a hex string (common in EVM), return it with 0x prefix if missing
caller := ctx.TxContext.Caller
if !strings.HasPrefix(caller, "0x") && !strings.HasPrefix(caller, "0X") {
// If it's a raw identifier, try to see if it's hex
if _, err := hex.DecodeString(caller); err == nil && len(caller) == 40 {
caller = "0x" + caller
}
}
return resultFn([]any{strings.ToLower(caller)})
},
}
}

// getCallerBytesMethod returns the current transaction caller as raw bytes (normalized address).
func getCallerBytesMethod() precompiles.Method {
return precompiles.Method{
Name: "get_caller_bytes",
AccessModifiers: []precompiles.Modifier{precompiles.VIEW, precompiles.PUBLIC},
Parameters: []precompiles.PrecompileValue{},
Returns: &precompiles.MethodReturn{
IsTable: false,
Fields: []precompiles.PrecompileValue{
precompiles.NewPrecompileValue("caller_bytes", types.ByteaType, false),
},
},
Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error {
if ctx == nil || ctx.TxContext == nil {
return resultFn([]any{[]byte{}})
}

// Return normalized address bytes instead of raw public key bytes.
// Caller is the string identifier (hex address for EVM).
caller := ctx.TxContext.Caller
if strings.HasPrefix(caller, "0x") || strings.HasPrefix(caller, "0X") {
caller = caller[2:]
}

if b, err := hex.DecodeString(caller); err == nil && len(b) == 20 {
return resultFn([]any{b})
}

// Fallback to Signer (public key) if not a hex address
return resultFn([]any{ctx.TxContext.Signer})
},
}
}

// getLeaderHexMethod returns the current block leader as a 0x-prefixed hex string.
func getLeaderHexMethod() precompiles.Method {
return precompiles.Method{
Name: "get_leader_hex",
AccessModifiers: []precompiles.Modifier{precompiles.VIEW, precompiles.PUBLIC},
Parameters: []precompiles.PrecompileValue{},
Returns: &precompiles.MethodReturn{
IsTable: false,
Fields: []precompiles.PrecompileValue{
precompiles.NewPrecompileValue("leader_hex", types.TextType, false),
},
},
Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error {
if ctx == nil || ctx.TxContext == nil || ctx.TxContext.BlockContext == nil || ctx.TxContext.BlockContext.Proposer == nil {
return resultFn([]any{""})
}

// For prediction markets, we usually want the Ethereum address of the leader
// to transfer fees via the bridge.
pubkey := ctx.TxContext.BlockContext.Proposer

if pubkey.Type() == crypto.KeyTypeSecp256k1 {
// Manually unmarshal to ensure we have the concrete type
secp, err := crypto.UnmarshalSecp256k1PublicKey(pubkey.Bytes())
if err == nil {
addr := crypto.EthereumAddressFromPubKey(secp)
return resultFn([]any{"0x" + hex.EncodeToString(addr)})
}
}

// Fallback to raw hex of the public key
return resultFn([]any{"0x" + hex.EncodeToString(pubkey.Bytes())})
},
}
}

// getLeaderBytesMethod returns the current block leader as raw bytes (normalized address).
func getLeaderBytesMethod() precompiles.Method {
return precompiles.Method{
Name: "get_leader_bytes",
AccessModifiers: []precompiles.Modifier{precompiles.VIEW, precompiles.PUBLIC},
Parameters: []precompiles.PrecompileValue{},
Returns: &precompiles.MethodReturn{
IsTable: false,
Fields: []precompiles.PrecompileValue{
precompiles.NewPrecompileValue("leader_bytes", types.ByteaType, false),
},
},
Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error {
if ctx == nil || ctx.TxContext == nil || ctx.TxContext.BlockContext == nil || ctx.TxContext.BlockContext.Proposer == nil {
return resultFn([]any{[]byte{}})
}

pubkey := ctx.TxContext.BlockContext.Proposer
if pubkey.Type() == crypto.KeyTypeSecp256k1 {
// Manually unmarshal to ensure we have the concrete type
secp, err := crypto.UnmarshalSecp256k1PublicKey(pubkey.Bytes())
if err == nil {
addr := crypto.EthereumAddressFromPubKey(secp)
return resultFn([]any{addr})
}
}

// Fallback to raw bytes of the public key
return resultFn([]any{pubkey.Bytes()})
},
}
}
Expand Down
18 changes: 10 additions & 8 deletions internal/migrations/024-attestation-actions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@ CREATE OR REPLACE ACTION request_attestation(
$caller_balance NUMERIC(78, 0);
$leader_addr TEXT;

-- Normalizing caller and leader safely using precompiles
$caller_bytes BYTEA := tn_utils.get_caller_bytes();
$lower_caller TEXT := tn_utils.get_caller_hex();

-- Check if caller is exempt (has system:network_writer role)
$is_exempt BOOL := FALSE;
$lower_caller TEXT := LOWER(@caller);
FOR $row IN are_members_of('system', 'network_writer', ARRAY[$lower_caller]) {
IF $row.wallet = $lower_caller AND $row.is_member {
$is_exempt := TRUE;
Expand All @@ -69,22 +72,21 @@ CREATE OR REPLACE ACTION request_attestation(
ERROR('Insufficient balance for attestation. Required: 40 TRUF');
}

-- Verify leader address is available
IF @leader_sender IS NULL {
-- Safe leader address conversion
$leader_addr := tn_utils.get_leader_hex();
IF $leader_addr = '' {
ERROR('Leader address not available for fee transfer');
}

$leader_addr := encode(@leader_sender, 'hex')::TEXT;
ethereum_bridge.transfer($leader_addr, $attestation_fee);
}
-- ===== END FEE COLLECTION =====

-- Get current block height
$created_height := @height;

-- Normalize caller address to bytes for storage
$caller_hex := LOWER(substring(@caller, 3, 40));
$caller_bytes := decode($caller_hex, 'hex');
-- Normalize caller address to bytes for storage (re-use safe normalization)
$caller_bytes := $caller_bytes; -- Already normalized above

-- Normalize provider input and enforce length
$provider_lower := LOWER($data_provider);
Expand Down Expand Up @@ -161,7 +163,7 @@ CREATE OR REPLACE ACTION request_attestation(
record_transaction_event(
6,
$attestation_fee,
'0x' || $leader_addr,
$leader_addr,
NULL
);

Expand Down
1 change: 0 additions & 1 deletion internal/migrations/031-order-book-vault.sql
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ PRIVATE RETURNS (participant_id INT) {

-- =============================================================================
-- ob_get_participant_id: Get participant ID without creating (for lookups)
-- =============================================================================
-- Returns the participant_id for a wallet address, or NULL if not found.
-- Use this for read-only operations where we don't want to create records.
CREATE OR REPLACE ACTION ob_get_participant_id($wallet_address TEXT)
Expand Down
Loading