Skip to content

feat: create prediction markets#1262

Merged
MicBun merged 3 commits intomainfrom
createMarket
Nov 25, 2025
Merged

feat: create prediction markets#1262
MicBun merged 3 commits intomainfrom
createMarket

Conversation

@MicBun
Copy link
Member

@MicBun MicBun commented Nov 25, 2025

resolves: https://github.com/truflation/website/issues/2915

Summary by CodeRabbit

  • New Features

    • Prediction market support: create markets with configurable spread, min order size and settlement time; retrieve by ID or hash; list with pagination/filtering; lightweight existence check.
    • Collateral vault actions: lock/unlock collateral and participant lookup/creation.
  • Tests

    • End-to-end tests covering market creation, validations, duplicate/insufficient-balance scenarios, listing and retrieval.
  • Chores

    • .gitignore updated to ignore an additional file.

✏️ Tip: You can customize this high-level summary in your review settings.

@MicBun MicBun self-assigned this Nov 25, 2025
@MicBun MicBun added the type: feat New feature or request label Nov 25, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 25, 2025

Walkthrough

Adds an on-chain order book: database schema (queries, participants, positions), vault helper actions for collateral management, public market actions (create, query, list, exists) including fee handling, tests for market creation flows, and a .gitignore entry update.

Changes

Cohort / File(s) Summary
Ignore list update
\.gitignore``
Added CLAUDE.md to ignored files.
Order book schema & metadata
internal/migrations/030-order-book-schema.sql
New tables ob_queries, ob_participants, ob_positions with PKs, FKs (ON DELETE CASCADE), CHECK constraints (price, spread, min order), indexes for matching/lookup, and an insert into transaction_methods for createMarket.
Vault operations
internal/migrations/031-order-book-vault.sql
Added actions: ob_lock_collateral($amount), ob_unlock_collateral($user_address,$amount), ob_get_or_create_participant(), ob_get_participant_id($wallet_address) with address/amount validation and bridge placeholders.
Market management actions
internal/migrations/032-order-book-actions.sql
Added public actions: create_market(...) (validation + 2 TRUF fee handling), get_market_info, get_market_by_hash, list_markets (paginated, settled filter), and market_exists.
Market creation tests
tests/streams/order_book/market_creation_test.go
New Go test suite (kwiltest) exercising market creation happy path, validation errors, duplicate hash, insufficient balance, and retrieval/listing/existence checks with helper utilities.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Engine as Market Engine
    participant Vault as Collateral Vault
    participant DB as Order Book DB

    User->>Engine: create_market(hash, settle_time, max_spread, min_order_size)
    Engine->>Vault: check balance for 2 TRUF
    Vault-->>Engine: balance OK / insufficient
    alt balance OK
        Engine->>Vault: lock 2 TRUF fee
        Vault-->>Engine: fee locked
        Engine->>DB: insert into ob_queries (generate id)
        DB-->>Engine: query_id
        Engine-->>User: return query_id
    else insufficient
        Engine-->>User: error (insufficient funds)
    end
Loading
sequenceDiagram
    participant User
    participant Engine as Market Engine
    participant Participants as ob_participants
    participant Positions as ob_positions
    participant Vault as Collateral Vault

    User->>Engine: ob_lock_collateral(amount)
    Engine->>Vault: escrow via bridge
    Vault-->>Engine: confirmed

    User->>Engine: trade/order
    Engine->>Participants: ob_get_or_create_participant()
    Participants-->>Engine: participant_id
    Engine->>Positions: insert/update position (query_id, participant_id, outcome, price, amount)
    Positions-->>Engine: confirmed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus areas:
    • create_market: fee transfer flow, leader resolution, validations (settle_time, spread, min size).
    • ob_get_or_create_participant: ID generation via MAX(id)+1 and potential concurrency/race concerns.
    • SQL constraints and index correctness for matching and performance.
    • Vault actions: address validation, numeric precision, and bridge call placeholders.
    • Tests: determinism, seed migrations, and coverage of edge cases.

Poem

🐰 In tunnels of SQL and TRUF so bright,
I plant a market by moonlit byte,
I lock a coin and crown a bet,
Orders hop where prices set—
A rabbit cheers: the book takes flight! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: create prediction markets' directly and clearly summarizes the main changes: introducing market creation functionality including schema, vault operations, and actions for a prediction market system.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch createMarket

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@holdex
Copy link

holdex bot commented Nov 25, 2025

Time Submission Status

Member Status Time Action Last Update
MicBun ✅ Submitted 10h Update time Nov 25, 2025, 6:01 PM

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
internal/migrations/031-order-book-vault.sql (2)

48-49: Consider stricter address validation.

The current validation checks length and 0x prefix but doesn't verify that the remaining 40 characters are valid hex. While decode() will fail on invalid hex later, explicit validation here would provide clearer error messages.

Apply this diff to add hex validation:

-    if $user_address IS NULL OR length($user_address) != 42 OR substring(LOWER($user_address), 1, 2) != '0x' {
-        ERROR('Invalid user address format (expected 0x-prefixed hex, 42 chars)');
+    if $user_address IS NULL OR length($user_address) != 42 OR substring(LOWER($user_address), 1, 2) != '0x' {
+        ERROR('Invalid user address format (expected 0x-prefixed hex, 42 chars)');
+    }
+    
+    -- Validate hex characters (will throw if invalid)
+    BEGIN
+        $test_decode BYTEA := decode(substring(LOWER($user_address), 3, 40), 'hex');
+    EXCEPTION WHEN OTHERS THEN
+        ERROR('Invalid hex characters in address');
+    END;
+    
+    if false {  -- Remove the old check
+        ERROR('Invalid user address format (expected 0x-prefixed hex, 42 chars)');
     }

Note: If Kuneiform doesn't support exception handling, the current approach is acceptable since decode() will fail with an error anyway.


83-94: Consider using RETURNING clause for efficiency.

The current implementation performs a separate SELECT after INSERT to retrieve the generated ID. If Kuneiform supports the RETURNING clause, it would be more efficient to combine these operations.

If RETURNING is supported, apply this diff:

-    -- Create new participant with MAX(id) + 1 pattern
-    INSERT INTO ob_participants (id, wallet_address)
-    SELECT COALESCE(MAX(id), 0) + 1, $caller_bytes
-    FROM ob_participants;
-
-    -- Get the ID we just inserted
-    for $row in SELECT id FROM ob_participants WHERE wallet_address = $caller_bytes {
-        RETURN $row.id;
-    }
+    -- Create new participant with MAX(id) + 1 pattern
+    for $row in
+        INSERT INTO ob_participants (id, wallet_address)
+        SELECT COALESCE(MAX(id), 0) + 1, $caller_bytes
+        FROM ob_participants
+        RETURNING id
+    {
+        RETURN $row.id;
+    }

If RETURNING is not supported in Kuneiform, the current approach is acceptable but slightly less efficient.

internal/migrations/032-order-book-actions.sql (2)

116-120: Consider using RETURNING clause for efficiency.

Similar to the participant creation in 031-order-book-vault.sql, this pattern performs a separate SELECT after INSERT. If Kuneiform supports RETURNING, it would be more efficient.

If supported, apply this diff:

-    -- Get the ID we just inserted
     $query_id INT;
-    for $row in SELECT id FROM ob_queries WHERE hash = $query_hash {
+    for $row in
+        INSERT INTO ob_queries (...)
+        SELECT ...
+        FROM ob_queries
+        RETURNING id
+    {
         $query_id := $row.id;
     }

256-272: Consider reducing query duplication.

The two query branches differ only by the WHERE clause. While the current implementation is clear, a more maintainable approach would reduce duplication.

If Kuneiform supports dynamic WHERE clauses, consider:

-- Build query dynamically
$where_clause TEXT := '';
if $settled_filter IS NOT NULL {
    $where_clause := 'WHERE settled = ' || $settled_filter::TEXT;
}

-- Execute single query
RETURN SELECT id, hash, settle_time, settled, winning_outcome,
              max_spread, min_order_size, created_at
       FROM ob_queries
       || $where_clause ||
       ORDER BY created_at DESC
       LIMIT $effective_limit OFFSET $effective_offset;

However, the current explicit approach is acceptable and may be clearer for maintenance.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1bbdf4 and c409a46.

📒 Files selected for processing (5)
  • .gitignore (1 hunks)
  • internal/migrations/030-order-book-schema.sql (1 hunks)
  • internal/migrations/031-order-book-vault.sql (1 hunks)
  • internal/migrations/032-order-book-actions.sql (1 hunks)
  • tests/streams/order_book/market_creation_test.go (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: acceptance-test
🔇 Additional comments (16)
.gitignore (1)

34-35: LGTM!

The addition of CLAUDE.md to the ignore list is appropriate for auxiliary documentation files.

internal/migrations/031-order-book-vault.sql (2)

24-34: LGTM!

The amount validation is correct, and the ethereum_bridge placeholder is appropriately documented with a TODO for future USDC bridge migration.


101-115: LGTM!

The read-only participant lookup is correctly implemented with appropriate NULL handling for both invalid addresses and missing records.

internal/migrations/030-order-book-schema.sql (2)

56-69: LGTM!

The positions table design appropriately models both holdings (price=0) and orders (price≠0) with proper constraints and cascading deletes for referential integrity.


100-102: No issues identified—method_id=8 is unique and available.

Verification confirms that method_id=8 is not already in use in any migration, and 'createMarket' is a new unique method name. The INSERT with ON CONFLICT (method_id) DO NOTHING is safe and idiomatic.

tests/streams/order_book/market_creation_test.go (7)

25-44: LGTM!

Test constants correctly match the market creation fee, and the helper function provides clear error messages for invalid inputs.


47-61: LGTM!

The test suite provides comprehensive coverage of market creation flows with good organization and proper test isolation.


64-118: LGTM!

The happy path test comprehensively validates market creation, fee collection, and data integrity with appropriate assertions.


132-134: Note: BYTEA length validation limitation.

The comment correctly identifies that Kuneiform's length() function doesn't support BYTEA, which is why hash length validation must rely on table constraints or external validation. This aligns with the concerns raised in the schema review.


159-317: LGTM!

The test suite provides excellent coverage of validation, duplicate handling, balance checks, and all retrieval operations with appropriate assertions and error handling.


351-388: LGTM!

The helper function properly constructs the transaction context with all required fields including the leader key for fee transfers.


390-472: LGTM!

The helper functions consistently wrap engine calls with proper context construction and error handling.

internal/migrations/032-order-book-actions.sql (4)

146-174: LGTM!

The action correctly retrieves market information with proper validation and error handling.


188-216: LGTM!

The hash-based lookup is correctly implemented with appropriate error handling.


285-296: LGTM!

The existence check is efficiently implemented with appropriate NULL handling and LIMIT optimization.


77-85: The @leader_sender NULL check is intentional and correct.

Verification shows this is not a defensive precaution but a consistent architectural pattern across the entire codebase. Every PUBLIC state-modifying action that requires fees (in migrations 001, 003, 004, 024, 032, erc20 001, erc20 002, etc.) checks if @leader_sender IS NULL before use. By contrast, PRIVATE actions do not check—they assume @leader_sender exists. The tests confirm @leader_sender is populated from BlockContext.Proposer during normal block execution, but the codebase design intentionally allows it to be NULL in certain contexts. The current validation (check + ERROR) is the appropriate pattern: if the action cannot execute without a leader address for fee transfer, failing fast is correct behavior.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
internal/migrations/030-order-book-schema.sql (2)

20-34: Hash length constraint addressed in actions layer.

The past review comment about adding a hash length constraint has been addressed in 032-order-book-actions.sql (lines 45-47) which validates length(encode($query_hash, 'hex')) != 64 before insertion. This is the appropriate approach given Kuneiform's limitations with BYTEA length constraints.


44-47: Wallet address length constraint should be validated at insertion.

Past review noted that Ethereum addresses are 20 bytes, but the column doesn't enforce this. Verify that ob_get_or_create_participant (in 031-order-book-vault.sql) validates wallet address length before insertion, similar to how hash length is validated in create_market.

#!/bin/bash
# Check if wallet address length validation exists in vault actions
rg -n "wallet" internal/migrations/031-order-book-vault.sql | head -30
rg -n "length|octet" internal/migrations/031-order-book-vault.sql
internal/migrations/032-order-book-actions.sql (1)

39-47: Hash length validation now implemented.

The past review comment about adding hash length validation has been addressed. Using length(encode($query_hash, 'hex')) != 64 is a creative workaround for Kuneiform's lack of octet_length() support on BYTEA.

🧹 Nitpick comments (5)
tests/streams/order_book/market_creation_test.go (3)

33-36: Global pointCounter may cause race conditions in parallel test execution.

While tests in this suite run sequentially within the schema test framework, the global pointCounter variable could cause data races if other test files in the package access it concurrently. Consider making this a local counter passed through test setup, or protect with a mutex if parallelism is expected.


225-253: Commented-out fields in testGetMarketInfo reduce clarity.

Lines 233-235 contain commented-out field assignments. If these fields are intentionally unused, consider removing the comments or documenting why they're skipped.

 err = callGetMarketInfo(ctx, platform, &userAddr, int(queryID), func(row *common.Row) error {
     hash = row.Values[0].([]byte)
     storedSettleTime = row.Values[1].(int64)
-    // settled = row.Values[2].(bool)
-    // winningOutcome = row.Values[3]
-    // settledAt = row.Values[4]
+    // Indices 2-4 (settled, winningOutcome, settledAt) intentionally skipped for this test
     maxSpread = row.Values[5].(int64)
     minOrderSize = row.Values[6].(int64)
     return nil
 })

354-391: Generated key in callCreateMarket is unused beyond Proposer.

A new key is generated for each call but only the public key is used for BlockContext.Proposer. This works but may cause confusion about its purpose.

Consider documenting the purpose or reusing a constant test key if the specific key doesn't matter:

 func callCreateMarket(ctx context.Context, platform *kwilTesting.Platform, signer *util.EthereumAddress, queryHash []byte, settleTime int64, maxSpread int64, minOrderSize int64, resultFn func(*common.Row) error) error {
-	// Generate leader key for fee transfers
+	// Generate a block proposer key for the test context (key value doesn't affect test outcomes)
 	_, pubGeneric, err := crypto.GenerateSecp256k1Key(nil)
internal/migrations/032-order-book-actions.sql (2)

120-124: ID retrieval after INSERT could be simplified.

The pattern of inserting then querying by hash to get the ID works but adds an extra query. If Kuneiform supports RETURNING, that would be more efficient. Otherwise, this approach is acceptable given the sequential transaction processing.


235-277: Consider consolidating duplicate SQL in list_markets.

The two branches (lines 263-267 and 270-275) duplicate most of the SQL. If Kuneiform supports conditional WHERE clauses or dynamic SQL, this could be simplified. However, the current approach is explicit and may be necessary given language constraints.

If Kuneiform supports WHERE ($settled_filter IS NULL OR settled = $settled_filter):

RETURN SELECT id, hash, settle_time, settled, winning_outcome,
              max_spread, min_order_size, created_at
       FROM ob_queries
       WHERE $settled_filter IS NULL OR settled = $settled_filter
       ORDER BY created_at DESC
       LIMIT $effective_limit OFFSET $effective_offset;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c409a46 and 5c1f83d.

📒 Files selected for processing (3)
  • internal/migrations/030-order-book-schema.sql (1 hunks)
  • internal/migrations/032-order-book-actions.sql (1 hunks)
  • tests/streams/order_book/market_creation_test.go (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-10T13:00:14.189Z
Learnt from: outerlook
Repo: trufnetwork/node PR: 1207
File: internal/migrations/024-attestation-actions.sql:58-76
Timestamp: 2025-10-10T13:00:14.189Z
Learning: In the attestation system for internal/migrations/024-attestation-actions.sql, the attestation_hash is computed from (version|algo|data_provider|stream_id|action_id|args) and intentionally excludes created_height. This design ensures the hash is deterministic based only on user input, not network state like block height.

Applied to files:

  • internal/migrations/030-order-book-schema.sql
📚 Learning: 2025-10-10T13:00:13.731Z
Learnt from: outerlook
Repo: trufnetwork/node PR: 1207
File: internal/migrations/023-attestation-schema.sql:20-21
Timestamp: 2025-10-10T13:00:13.731Z
Learning: In the attestations table (internal/migrations/023-attestation-schema.sql), the primary key is (requester, created_height, attestation_hash) because attestation_hash is computed deterministically from user input only (version|algo|data_provider|stream_id|action_id|args) and does not include created_height. This allows the same user to request the same attestation at different block heights.

Applied to files:

  • internal/migrations/030-order-book-schema.sql
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: acceptance-test
🔇 Additional comments (10)
tests/streams/order_book/market_creation_test.go (4)

46-61: Well-structured test suite with comprehensive coverage.

The test suite covers key scenarios: happy path, input validation, duplicate rejection, insufficient balance, and CRUD operations. The use of SchemaTest with seed migrations ensures proper database state.


63-118: Solid happy path test with proper balance verification.

The test correctly validates:

  • Market creation returns a positive query ID
  • Balance decreases by the expected 2 TRUF fee
  • Market metadata is retrievable and matches input

120-159: Good validation test coverage.

The test covers multiple validation scenarios: invalid hash length, past settlement time, max_spread boundaries, and min_order_size validation. Error message assertions help ensure meaningful error feedback.


393-419: Consistent helper function patterns.

The callGetMarketInfo, callListMarkets, and callMarketExists helpers follow a clean, consistent pattern for setting up transaction context and handling results. Good use of the common error handling pattern.

Also applies to: 421-447, 449-475

internal/migrations/030-order-book-schema.sql (3)

56-69: Well-designed unified positions table.

The design combining orders and holdings with price semantics (0=holding, positive=sell, negative=buy) is elegant. The CHECK constraints appropriately bound price (-99 to 99) and require positive amounts.


77-91: Appropriate indexes for query patterns.

The indexes align well with expected query patterns:

  • idx_ob_pos_order_match for order book matching (FIFO via last_updated)
  • idx_ob_positions_participant for portfolio queries
  • idx_ob_queries_settle_time for settlement processing
  • idx_ob_queries_settled for filtering active/settled markets

Note: Past comment about redundant idx_ob_participants_wallet appears to have been addressed as it's not present in this version.


97-100: Transaction method registration looks correct.

Using ON CONFLICT DO NOTHING ensures idempotency for migration reruns.

internal/migrations/032-order-book-actions.sql (3)

150-178: get_market_info action is clean and correct.

Proper validation, clear error messages, and efficient single-row lookup by primary key.


289-300: Lightweight market_exists is well-implemented.

Efficient pattern using SELECT 1 ... LIMIT 1 and returning boolean. Returns false for NULL input rather than erroring, which is a sensible default for an existence check.


70-89: Balance validation already provides explicit error handling; clarify bridge error assumptions.

The code at lines 74-79 includes explicit balance validation with ERROR() thrown if insufficient funds. This satisfies the request for explicit error handling on the primary failure mode. However, the comment's assumption that ethereum_bridge.transfer() throws ERROR on all failure types is undocumented in this repository. Since Kwil's ERROR() function halts and rolls back the transaction, the balance check does prevent market creation without fee collection for insufficient balance. For other potential bridge failures beyond balance checks, either verify that the bridge extension reliably throws errors, or add inline documentation confirming this is the intended error semantics from the external bridge extension.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
internal/migrations/032-order-book-actions.sql (3)

88-88: Verify @leader_sender type and encode() compatibility.

Line 88 encodes @leader_sender as hex, which assumes it's BYTEA or a BYTEA-like type. Confirm that:

  1. @leader_sender is always provided and in the expected format (BYTEA or compatible).
  2. If @leader_sender can be NULL or TEXT, the encode() call will fail or produce unexpected results.

Consider adding a format validation check similar to the @caller validation on lines 95–100.


129-131: Optimize ID retrieval after INSERT.

After inserting a market record, the code queries back to retrieve the generated ID. This requires an additional SELECT query. Since the ID is computed as COALESCE(MAX(id), 0) + 1, you already know the value; consider returning it directly or using a returning clause (if Kuneiform supports it) to avoid the second query.

-- Current pattern:
INSERT INTO ob_queries (...) SELECT COALESCE(MAX(id), 0) + 1, ... FROM ob_queries;
for $row in SELECT id FROM ob_queries WHERE hash = $query_hash { ... }

-- Suggested: compute and return the ID upfront
$query_id INT := (SELECT COALESCE(MAX(id), 0) + 1 FROM ob_queries);
INSERT INTO ob_queries (id, ...) VALUES ($query_id, ...);
RETURN $query_id;

136-141: Document the transaction event type constant.

Line 137 passes a hardcoded action type 8 to record_transaction_event(). Clarify:

  1. What does action type 8 represent?
  2. Should this be a named constant defined in a shared location?
  3. Is there validation that this value is correct?

Add a comment above the call explaining the meaning, or extract it to a named constant.

-- Record market creation event (type: CREATE_MARKET or similar)
$transaction_type INT := 8;  -- Document the meaning of 8
record_transaction_event($transaction_type, $market_creation_fee, '0x' || $leader_hex, NULL);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c1f83d and f7df79b.

📒 Files selected for processing (1)
  • internal/migrations/032-order-book-actions.sql (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: acceptance-test
🔇 Additional comments (4)
internal/migrations/032-order-book-actions.sql (4)

157-185: LGTM: get_market_info.

Clear input validation and straightforward query logic. Error handling is defensive.


199-227: LGTM: get_market_by_hash.

Mirrors get_market_info with consistent validation and error handling. Useful for checking market existence before creation.


242-284: LGTM: list_markets.

Sensible pagination defaults (limit 100, offset 0) and clear conditional logic for the settled filter. Query is efficient.


296-307: LGTM: market_exists.

Lightweight existence check with clear semantics. Returning false for NULL input is reasonable defensive behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feat New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant