Skip to content

Commit cf96fd1

Browse files
committed
feat: request data attestation
1 parent 27eabcb commit cf96fd1

File tree

12 files changed

+1325
-5
lines changed

12 files changed

+1325
-5
lines changed
Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
package contractsapi
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"fmt"
7+
8+
"github.com/pkg/errors"
9+
"github.com/trufnetwork/kwil-db/core/gatewayclient"
10+
"github.com/trufnetwork/sdk-go/core/types"
11+
)
12+
13+
// AttestationAction implements attestation-related actions
14+
type AttestationAction struct {
15+
_client *gatewayclient.GatewayClient
16+
}
17+
18+
var _ types.IAttestationAction = (*AttestationAction)(nil)
19+
20+
// AttestationActionOptions contains options for creating an AttestationAction
21+
type AttestationActionOptions struct {
22+
Client *gatewayclient.GatewayClient
23+
}
24+
25+
// LoadAttestationActions creates a new attestation action handler
26+
func LoadAttestationActions(opts AttestationActionOptions) (types.IAttestationAction, error) {
27+
if opts.Client == nil {
28+
return nil, fmt.Errorf("kwil client is required")
29+
}
30+
return &AttestationAction{
31+
_client: opts.Client,
32+
}, nil
33+
}
34+
35+
// RequestAttestation submits a request for a signed attestation of query results
36+
func (a *AttestationAction) RequestAttestation(
37+
ctx context.Context,
38+
input types.RequestAttestationInput,
39+
) (*types.RequestAttestationResult, error) {
40+
// Validate inputs
41+
if err := input.Validate(); err != nil {
42+
return nil, errors.WithStack(err)
43+
}
44+
45+
// Encode arguments using the canonical format
46+
argsBytes, err := EncodeActionArgs(input.Args)
47+
if err != nil {
48+
return nil, errors.Wrap(err, "failed to encode action args")
49+
}
50+
51+
// Prepare execute arguments
52+
// The request_attestation action expects:
53+
// ($data_provider TEXT, $stream_id TEXT, $action_name TEXT, $args_bytes BYTEA, $encrypt_sig BOOLEAN, $max_fee INT8)
54+
args := [][]any{
55+
{
56+
input.DataProvider,
57+
input.StreamID,
58+
input.ActionName,
59+
argsBytes,
60+
input.EncryptSig,
61+
input.MaxFee,
62+
},
63+
}
64+
65+
// Execute the action
66+
txHash, err := a._client.Execute(ctx, "", "request_attestation", args)
67+
if err != nil {
68+
return nil, errors.Wrap(err, "failed to execute request_attestation")
69+
}
70+
71+
// Wait for the transaction to be mined to get the result
72+
// Note: The actual implementation should wait for the transaction result
73+
// and extract the request_tx_id and attestation_hash from the result
74+
// For now, we return the transaction hash as the request_tx_id
75+
// The attestation_hash would need to be extracted from the transaction receipt
76+
77+
result := &types.RequestAttestationResult{
78+
RequestTxID: txHash.String(),
79+
AttestationHash: nil, // TODO: Extract from transaction result
80+
}
81+
82+
return result, nil
83+
}
84+
85+
// GetSignedAttestation retrieves a complete signed attestation payload
86+
func (a *AttestationAction) GetSignedAttestation(
87+
ctx context.Context,
88+
input types.GetSignedAttestationInput,
89+
) (*types.SignedAttestationResult, error) {
90+
if input.RequestTxID == "" {
91+
return nil, fmt.Errorf("request_tx_id cannot be empty")
92+
}
93+
94+
// Call the get_signed_attestation view action
95+
// The action expects: ($request_tx_id TEXT)
96+
args := []any{input.RequestTxID}
97+
98+
callResult, err := a._client.Call(ctx, "", "get_signed_attestation", args)
99+
if err != nil {
100+
return nil, errors.Wrap(err, "failed to call get_signed_attestation")
101+
}
102+
103+
if callResult.Error != nil {
104+
return nil, errors.Errorf("get_signed_attestation returned error: %s", *callResult.Error)
105+
}
106+
107+
// Extract the payload from the result
108+
// The action returns a single row with a single column (payload BYTEA)
109+
if len(callResult.QueryResult.Values) == 0 {
110+
return nil, fmt.Errorf("no attestation found for request_tx_id: %s", input.RequestTxID)
111+
}
112+
113+
row := callResult.QueryResult.Values[0]
114+
if len(row) == 0 {
115+
return nil, fmt.Errorf("empty result row")
116+
}
117+
118+
// Extract the payload bytes
119+
payload, ok := row[0].([]byte)
120+
if !ok {
121+
// Try pointer to bytes
122+
if payloadPtr, ok := row[0].(*[]byte); ok && payloadPtr != nil {
123+
payload = *payloadPtr
124+
} else {
125+
return nil, fmt.Errorf("unexpected payload type: %T", row[0])
126+
}
127+
}
128+
129+
return &types.SignedAttestationResult{
130+
Payload: payload,
131+
}, nil
132+
}
133+
134+
// ListAttestations returns metadata for attestations, optionally filtered
135+
func (a *AttestationAction) ListAttestations(
136+
ctx context.Context,
137+
input types.ListAttestationsInput,
138+
) ([]types.AttestationMetadata, error) {
139+
// Set defaults
140+
limit := 5000
141+
if input.Limit != nil {
142+
if *input.Limit <= 0 || *input.Limit > 5000 {
143+
return nil, fmt.Errorf("limit must be between 1 and 5000")
144+
}
145+
limit = *input.Limit
146+
}
147+
148+
offset := 0
149+
if input.Offset != nil {
150+
if *input.Offset < 0 {
151+
return nil, fmt.Errorf("offset must be non-negative")
152+
}
153+
offset = *input.Offset
154+
}
155+
156+
// Prepare call arguments
157+
// The action expects: ($requester BYTEA, $limit INT, $offset INT, $order_by TEXT)
158+
args := []any{
159+
input.Requester, // Can be nil
160+
limit,
161+
offset,
162+
nil, // orderBy - use default
163+
}
164+
165+
if input.OrderBy != nil {
166+
args[3] = *input.OrderBy
167+
}
168+
169+
// Call the view action
170+
callResult, err := a._client.Call(ctx, "", "list_attestations", args)
171+
if err != nil {
172+
return nil, errors.Wrap(err, "failed to call list_attestations")
173+
}
174+
175+
if callResult.Error != nil {
176+
return nil, errors.Errorf("list_attestations returned error: %s", *callResult.Error)
177+
}
178+
179+
// Parse the result rows
180+
// Expected columns: request_tx_id, attestation_hash, requester, created_height, signed_height, encrypt_sig
181+
results := make([]types.AttestationMetadata, 0, len(callResult.QueryResult.Values))
182+
183+
for i, row := range callResult.QueryResult.Values {
184+
if len(row) < 6 {
185+
return nil, fmt.Errorf("row %d has insufficient columns: expected 6, got %d", i, len(row))
186+
}
187+
188+
metadata := types.AttestationMetadata{}
189+
190+
// Column 0: request_tx_id (TEXT)
191+
if requestTxID, ok := row[0].(string); ok {
192+
metadata.RequestTxID = requestTxID
193+
} else if requestTxIDPtr, ok := row[0].(*string); ok && requestTxIDPtr != nil {
194+
metadata.RequestTxID = *requestTxIDPtr
195+
} else {
196+
return nil, fmt.Errorf("row %d: unexpected request_tx_id type: %T", i, row[0])
197+
}
198+
199+
// Column 1: attestation_hash (BYTEA)
200+
if err := extractBytesColumn(row[1], &metadata.AttestationHash, i, "attestation_hash"); err != nil {
201+
return nil, err
202+
}
203+
204+
// Column 2: requester (BYTEA)
205+
if err := extractBytesColumn(row[2], &metadata.Requester, i, "requester"); err != nil {
206+
return nil, err
207+
}
208+
209+
// Column 3: created_height (INT8)
210+
if err := extractInt64Column(row[3], &metadata.CreatedHeight, i, "created_height"); err != nil {
211+
return nil, err
212+
}
213+
214+
// Column 4: signed_height (INT8, nullable)
215+
if row[4] != nil {
216+
var signedHeight int64
217+
if err := extractInt64Column(row[4], &signedHeight, i, "signed_height"); err != nil {
218+
return nil, err
219+
}
220+
metadata.SignedHeight = &signedHeight
221+
}
222+
223+
// Column 5: encrypt_sig (BOOLEAN)
224+
if encryptSig, ok := row[5].(bool); ok {
225+
metadata.EncryptSig = encryptSig
226+
} else if encryptSigPtr, ok := row[5].(*bool); ok && encryptSigPtr != nil {
227+
metadata.EncryptSig = *encryptSigPtr
228+
} else {
229+
return nil, fmt.Errorf("row %d: unexpected encrypt_sig type: %T", i, row[5])
230+
}
231+
232+
results = append(results, metadata)
233+
}
234+
235+
return results, nil
236+
}
237+
238+
// Helper function to extract bytes from a column
239+
func extractBytesColumn(value any, dest *[]byte, rowIdx int, colName string) error {
240+
if value == nil {
241+
*dest = nil
242+
return nil
243+
}
244+
245+
switch v := value.(type) {
246+
case []byte:
247+
*dest = v
248+
case *[]byte:
249+
if v != nil {
250+
*dest = *v
251+
} else {
252+
*dest = nil
253+
}
254+
case string:
255+
// Handle PostgreSQL BYTEA hex format (\xHHHH...)
256+
if len(v) >= 2 && v[0:2] == "\\x" {
257+
// Remove \x prefix and decode hex
258+
decoded, err := hex.DecodeString(v[2:])
259+
if err != nil {
260+
return fmt.Errorf("row %d: failed to decode %s as hex (\\x format): %w", rowIdx, colName, err)
261+
}
262+
*dest = decoded
263+
} else {
264+
// Try plain hex decode
265+
decoded, err := hex.DecodeString(v)
266+
if err != nil {
267+
// If not hex, might already be raw bytes as string - just convert
268+
*dest = []byte(v)
269+
} else {
270+
*dest = decoded
271+
}
272+
}
273+
case *string:
274+
if v != nil {
275+
// Handle PostgreSQL BYTEA hex format (\xHHHH...)
276+
if len(*v) >= 2 && (*v)[0:2] == "\\x" {
277+
// Remove \x prefix and decode hex
278+
decoded, err := hex.DecodeString((*v)[2:])
279+
if err != nil {
280+
return fmt.Errorf("row %d: failed to decode %s as hex (\\x format): %w", rowIdx, colName, err)
281+
}
282+
*dest = decoded
283+
} else {
284+
// Try plain hex decode
285+
decoded, err := hex.DecodeString(*v)
286+
if err != nil {
287+
// If not hex, might already be raw bytes as string - just convert
288+
*dest = []byte(*v)
289+
} else {
290+
*dest = decoded
291+
}
292+
}
293+
} else {
294+
*dest = nil
295+
}
296+
default:
297+
return fmt.Errorf("row %d: unexpected %s type: %T", rowIdx, colName, value)
298+
}
299+
300+
return nil
301+
}
302+
303+
// Helper function to extract int64 from a column
304+
func extractInt64Column(value any, dest *int64, rowIdx int, colName string) error {
305+
switch v := value.(type) {
306+
case int64:
307+
*dest = v
308+
case *int64:
309+
if v != nil {
310+
*dest = *v
311+
} else {
312+
return fmt.Errorf("row %d: %s is null", rowIdx, colName)
313+
}
314+
case int:
315+
*dest = int64(v)
316+
case *int:
317+
if v != nil {
318+
*dest = int64(*v)
319+
} else {
320+
return fmt.Errorf("row %d: %s is null", rowIdx, colName)
321+
}
322+
case int32:
323+
*dest = int64(v)
324+
case *int32:
325+
if v != nil {
326+
*dest = int64(*v)
327+
} else {
328+
return fmt.Errorf("row %d: %s is null", rowIdx, colName)
329+
}
330+
case string:
331+
// Parse string as int64
332+
parsed, err := fmt.Sscanf(v, "%d", dest)
333+
if err != nil || parsed != 1 {
334+
return fmt.Errorf("row %d: failed to parse %s as int64: %w", rowIdx, colName, err)
335+
}
336+
case *string:
337+
if v != nil {
338+
parsed, err := fmt.Sscanf(*v, "%d", dest)
339+
if err != nil || parsed != 1 {
340+
return fmt.Errorf("row %d: failed to parse %s as int64: %w", rowIdx, colName, err)
341+
}
342+
} else {
343+
return fmt.Errorf("row %d: %s is null", rowIdx, colName)
344+
}
345+
default:
346+
return fmt.Errorf("row %d: unexpected %s type: %T", rowIdx, colName, value)
347+
}
348+
349+
return nil
350+
}

0 commit comments

Comments
 (0)