Skip to content

Commit 43fa8f2

Browse files
authored
feat: decode market data and extract prediction type (#177)
1 parent 63fb005 commit 43fa8f2

File tree

10 files changed

+358
-6
lines changed

10 files changed

+358
-6
lines changed

core/contractsapi/attestation_encoding.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/binary"
66
"fmt"
7+
"io"
78

89
"github.com/trufnetwork/kwil-db/core/types"
910
)
@@ -57,3 +58,57 @@ func EncodeActionArgs(args []any) ([]byte, error) {
5758

5859
return buf.Bytes(), nil
5960
}
61+
62+
// DecodeActionArgs decodes canonical bytes back into action arguments.
63+
// This is the inverse operation of EncodeActionArgs.
64+
//
65+
// Returns an error if:
66+
// - Data is too short to contain arg count
67+
// - Individual arguments fail to decode
68+
// - Type information is invalid
69+
func DecodeActionArgs(data []byte) ([]any, error) {
70+
if len(data) < 4 {
71+
return nil, fmt.Errorf("data too short for arg count")
72+
}
73+
74+
buf := bytes.NewReader(data)
75+
76+
// Read argument count (little-endian uint32)
77+
var argCount uint32
78+
if err := binary.Read(buf, binary.LittleEndian, &argCount); err != nil {
79+
return nil, fmt.Errorf("failed to read arg count: %w", err)
80+
}
81+
82+
args := make([]any, argCount)
83+
84+
// Decode each argument
85+
for i := uint32(0); i < argCount; i++ {
86+
// Read argument length (little-endian uint32)
87+
var argLen uint32
88+
if err := binary.Read(buf, binary.LittleEndian, &argLen); err != nil {
89+
return nil, fmt.Errorf("failed to read arg %d length: %w", i, err)
90+
}
91+
92+
// Read argument bytes
93+
argBytes := make([]byte, argLen)
94+
if _, err := io.ReadFull(buf, argBytes); err != nil {
95+
return nil, fmt.Errorf("failed to read arg %d bytes: %w", i, err)
96+
}
97+
98+
// Unmarshal EncodedValue
99+
var encodedVal types.EncodedValue
100+
if err := encodedVal.UnmarshalBinary(argBytes); err != nil {
101+
return nil, fmt.Errorf("failed to unmarshal arg %d: %w", i, err)
102+
}
103+
104+
// Decode to Go value
105+
decodedVal, err := encodedVal.Decode()
106+
if err != nil {
107+
return nil, fmt.Errorf("failed to decode arg %d value: %w", i, err)
108+
}
109+
110+
args[i] = decodedVal
111+
}
112+
113+
return args, nil
114+
}

core/contractsapi/order_book_markets_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,3 +371,63 @@ func TestDecodeQueryComponents(t *testing.T) {
371371
require.Equal(t, actionID, decodedActionID)
372372
require.Equal(t, args, decodedArgs)
373373
}
374+
375+
func TestDecodeMarketData(t *testing.T) {
376+
dataProvider := "0x1111111111111111111111111111111111111111"
377+
streamID := "stbtcusd000000000000000000000000"
378+
379+
tests := []struct {
380+
name string
381+
actionID string
382+
args []any
383+
expectType string
384+
expectThresh []string
385+
}{
386+
{
387+
name: "Price Above",
388+
actionID: "price_above_threshold",
389+
args: []any{"0x...", "stream", int64(123), "100000", nil},
390+
expectType: "above",
391+
expectThresh: []string{"100000"},
392+
},
393+
{
394+
name: "Price Below",
395+
actionID: "price_below_threshold",
396+
args: []any{"0x...", "stream", int64(123), "4.5", nil},
397+
expectType: "below",
398+
expectThresh: []string{"4.5"},
399+
},
400+
{
401+
name: "Value In Range",
402+
actionID: "value_in_range",
403+
args: []any{"0x...", "stream", int64(123), "90000", "110000", nil},
404+
expectType: "between",
405+
expectThresh: []string{"90000", "110000"},
406+
},
407+
{
408+
name: "Value Equals",
409+
actionID: "value_equals",
410+
args: []any{"0x...", "stream", int64(123), "5.25", "0.01", nil},
411+
expectType: "equals",
412+
expectThresh: []string{"5.25", "0.01"},
413+
},
414+
}
415+
416+
for _, tt := range tests {
417+
t.Run(tt.name, func(t *testing.T) {
418+
argsBytes, err := EncodeActionArgs(tt.args)
419+
require.NoError(t, err)
420+
421+
encoded, err := EncodeQueryComponents(dataProvider, streamID, tt.actionID, argsBytes)
422+
require.NoError(t, err)
423+
424+
market, err := DecodeMarketData(encoded)
425+
require.NoError(t, err)
426+
427+
require.Equal(t, tt.expectType, market.Type)
428+
require.Equal(t, tt.expectThresh, market.Thresholds)
429+
require.Equal(t, streamID, market.StreamID)
430+
require.Equal(t, tt.actionID, market.ActionID)
431+
})
432+
}
433+
}

core/contractsapi/query_components.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package contractsapi
22

33
import (
44
"fmt"
5+
"reflect"
56

67
"github.com/ethereum/go-ethereum/accounts/abi"
78
"github.com/ethereum/go-ethereum/common"
@@ -139,3 +140,88 @@ func DecodeQueryComponents(encoded []byte) (dataProvider, streamID, actionID str
139140

140141
return dataProvider, streamID, actionID, args, nil
141142
}
143+
144+
// MarketData represents the structured content of a prediction market's query components
145+
type MarketData struct {
146+
DataProvider string `json:"data_provider"`
147+
StreamID string `json:"stream_id"`
148+
ActionID string `json:"action_id"`
149+
Type string `json:"type"` // "above", "below", "between", "equals"
150+
Thresholds []string `json:"thresholds"` // Formatted numeric values
151+
}
152+
153+
// DecodeMarketData decodes ABI-encoded query_components into high-level MarketData
154+
func DecodeMarketData(encoded []byte) (*MarketData, error) {
155+
dataProvider, streamID, actionID, argsBytes, err := DecodeQueryComponents(encoded)
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
args, err := DecodeActionArgs(argsBytes)
161+
if err != nil {
162+
return nil, fmt.Errorf("failed to decode action args: %w", err)
163+
}
164+
165+
market := &MarketData{
166+
DataProvider: dataProvider,
167+
StreamID: streamID,
168+
ActionID: actionID,
169+
Thresholds: []string{},
170+
}
171+
172+
// Helper to format arguments (handling Decimal and pointer types)
173+
formatArg := func(arg any) string {
174+
if arg == nil {
175+
return ""
176+
}
177+
178+
// Handle *string directly (common in decoded results)
179+
if s, ok := arg.(*string); ok {
180+
if s == nil {
181+
return ""
182+
}
183+
return *s
184+
}
185+
186+
// Use reflection to find String() method (handles other pointer types like *Decimal)
187+
v := reflect.ValueOf(arg)
188+
method := v.MethodByName("String")
189+
if method.IsValid() && method.Type().NumIn() == 0 && method.Type().NumOut() == 1 {
190+
results := method.Call(nil)
191+
if s, ok := results[0].Interface().(string); ok {
192+
return s
193+
}
194+
}
195+
196+
return fmt.Sprint(arg)
197+
}
198+
199+
// Map action_id to market type and thresholds
200+
// Based on 040-binary-attestation-actions.sql
201+
switch actionID {
202+
case "price_above_threshold":
203+
market.Type = "above"
204+
if len(args) >= 4 {
205+
market.Thresholds = append(market.Thresholds, formatArg(args[3]))
206+
}
207+
case "price_below_threshold":
208+
market.Type = "below"
209+
if len(args) >= 4 {
210+
market.Thresholds = append(market.Thresholds, formatArg(args[3]))
211+
}
212+
case "value_in_range":
213+
market.Type = "between"
214+
if len(args) >= 5 {
215+
market.Thresholds = append(market.Thresholds, formatArg(args[3]), formatArg(args[4]))
216+
}
217+
case "value_equals":
218+
market.Type = "equals"
219+
if len(args) >= 5 {
220+
market.Thresholds = append(market.Thresholds, formatArg(args[3]), formatArg(args[4]))
221+
}
222+
default:
223+
market.Type = "unknown"
224+
}
225+
226+
return market, nil
227+
}

docs/api-reference.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1679,6 +1679,77 @@ if err != nil {
16791679
}
16801680
```
16811681

1682+
## Prediction Market Data Decoding
1683+
1684+
The `contractsapi` package provides high-level utilities for decoding prediction market query components. This is essential for extracting market types (above, below, between) and threshold values from `marketInfo.QueryComponents`.
1685+
1686+
### Core Methods
1687+
1688+
#### `DecodeMarketData`
1689+
1690+
Decodes ABI-encoded `query_components` into a structured `MarketData` object.
1691+
1692+
**Signature:**
1693+
```go
1694+
func DecodeMarketData(encoded []byte) (*MarketData, error)
1695+
```
1696+
1697+
**Parameters:**
1698+
- `encoded` ([]byte): The `QueryComponents` field from a `MarketInfo` object.
1699+
1700+
**Returns:**
1701+
- `*MarketData`: Structured market information.
1702+
- `error`: Error if decoding fails.
1703+
1704+
**Example:**
1705+
```go
1706+
import "github.com/trufnetwork/sdk-go/core/contractsapi"
1707+
1708+
// market.QueryComponents is []byte from the node
1709+
data, err := contractsapi.DecodeMarketData(market.QueryComponents)
1710+
if err != nil {
1711+
log.Fatal(err)
1712+
}
1713+
1714+
fmt.Printf("Type: %s\n", data.Type) // e.g. "above"
1715+
fmt.Printf("Thresholds: %v\n", data.Thresholds) // e.g. ["100000"]
1716+
```
1717+
1718+
---
1719+
1720+
#### `DecodeActionArgs`
1721+
1722+
Decodes Kwil-native canonical bytes back into a slice of action arguments.
1723+
1724+
**Signature:**
1725+
```go
1726+
func DecodeActionArgs(data []byte) ([]any, error) {
1727+
```
1728+
1729+
**Example:**
1730+
```go
1731+
args, err := contractsapi.DecodeActionArgs(argsBytes)
1732+
for i, arg := range args {
1733+
fmt.Printf("Arg %d: %v (Type: %T)\n", i, arg, arg)
1734+
}
1735+
```
1736+
1737+
### Types
1738+
1739+
#### `MarketData`
1740+
1741+
```go
1742+
type MarketData struct {
1743+
DataProvider string `json:"data_provider"`
1744+
StreamID string `json:"stream_id"`
1745+
ActionID string `json:"action_id"`
1746+
Type string `json:"type"` // "above", "below", "between", "equals" or "unknown"
1747+
Thresholds []string `json:"thresholds"` // Formatted numeric values as strings
1748+
}
1749+
```
1750+
1751+
---
1752+
16821753
## Attestation Actions Interface
16831754
16841755
### Overview
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
8+
"github.com/trufnetwork/kwil-db/core/crypto"
9+
"github.com/trufnetwork/kwil-db/core/crypto/auth"
10+
"github.com/trufnetwork/sdk-go/core/contractsapi"
11+
"github.com/trufnetwork/sdk-go/core/tnclient"
12+
"github.com/trufnetwork/sdk-go/core/types"
13+
)
14+
15+
func main() {
16+
// Configuration
17+
endpoint := "https://gateway.testnet.truf.network"
18+
// Use a dummy private key for read-only operation
19+
privateKeyHex := "0000000000000000000000000000000000000000000000000000000000000001"
20+
21+
fmt.Println("--- Prediction Market Decoding Example (Real Data) ---")
22+
fmt.Printf("Endpoint: %s\n\n", endpoint)
23+
24+
// 1. Initialize Client
25+
ctx := context.Background()
26+
pk, _ := crypto.Secp256k1PrivateKeyFromHex(privateKeyHex)
27+
signer := &auth.EthPersonalSigner{Key: *pk}
28+
29+
client, err := tnclient.NewClient(ctx, endpoint, tnclient.WithSigner(signer))
30+
if err != nil {
31+
log.Fatalf("Failed to create client: %v", err)
32+
}
33+
34+
// 2. Load Order Book
35+
orderBook, err := client.LoadOrderBook()
36+
if err != nil {
37+
log.Fatalf("Failed to load order book: %v", err)
38+
}
39+
40+
// 3. List Latest Markets
41+
limit := 3
42+
markets, err := orderBook.ListMarkets(ctx, types.ListMarketsInput{
43+
Limit: &limit,
44+
})
45+
if err != nil {
46+
log.Fatalf("Failed to list markets: %v", err)
47+
}
48+
49+
fmt.Printf("Found %d latest markets. Decoding details...\n\n", len(markets))
50+
51+
// 4. Fetch and Decode each market
52+
for _, m := range markets {
53+
fmt.Printf("Processing Market ID: %d\n", m.ID)
54+
55+
// Fetch full info (including queryComponents)
56+
marketInfo, err := orderBook.GetMarketInfo(ctx, types.GetMarketInfoInput{
57+
QueryID: m.ID,
58+
})
59+
if err != nil {
60+
fmt.Printf(" Error fetching info: %v\n\n", err)
61+
continue
62+
}
63+
64+
// Decode components
65+
data, err := contractsapi.DecodeMarketData(marketInfo.QueryComponents)
66+
if err != nil {
67+
fmt.Printf(" Error decoding data: %v\n\n", err)
68+
continue
69+
}
70+
71+
fmt.Printf(" Market Type: %s\n", data.Type)
72+
fmt.Printf(" Thresholds: %v\n", data.Thresholds)
73+
fmt.Printf(" Action: %s\n", data.ActionID)
74+
fmt.Printf(" Stream: %s\n\n", data.StreamID)
75+
}
76+
}

examples/history_example/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ func main() {
1818
ctx := context.Background()
1919

2020
// 1. Initialize Client
21-
// Use environment variable for private key to avoid hardcoding
21+
// Use environment variable for private key or default to test key
2222
privateKeyHex := os.Getenv("TN_PRIVATE_KEY")
2323
if privateKeyHex == "" {
24-
log.Fatal("TN_PRIVATE_KEY environment variable is required")
24+
privateKeyHex = "0000000000000000000000000000000000000000000000000000000000000001"
2525
}
2626

2727
pk, err := crypto.Secp256k1PrivateKeyFromHex(privateKeyHex)

0 commit comments

Comments
 (0)