Skip to content

Commit

Permalink
feat: Add CCIP Support (#1896)
Browse files Browse the repository at this point in the history
## Relevant issue(s)

Resolves #1894

## Description

This PR adds an HTTP handler that enables DefraDB to act as an off-chain
data oracle for smart contracts.

Here is an end-to-end example of how this feature works:
https://github.com/sourcenetwork/defradb-example-ccip

## Tasks

- [x] I made sure the code is well commented, particularly
hard-to-understand areas.
- [x] I made sure the repository-held documentation is changed
accordingly.
- [x] I made sure the pull request title adheres to the conventional
commit style (the subset used in the project can be found in
[tools/configs/chglog/config.yml](tools/configs/chglog/config.yml)).
- [x] I made sure to discuss its limitations such as threats to
validity, vulnerability to mistake and misuse, robustness to
invalidation of assumptions, resource requirements, ...

## How has this been tested?

Unit tests

Specify the platform(s) on which this was tested:
- MacOS
  • Loading branch information
nasdf authored Sep 22, 2023
1 parent 84331e9 commit 9e3b4e7
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 0 deletions.
74 changes: 74 additions & 0 deletions http/handler_ccip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2023 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package http

import (
"encoding/hex"
"encoding/json"
"net/http"
"strings"

"github.com/go-chi/chi/v5"

"github.com/sourcenetwork/defradb/client"
)

type ccipHandler struct{}

type CCIPRequest struct {
Sender string `json:"sender"`
Data string `json:"data"`
}

type CCIPResponse struct {
Data string `json:"data"`
}

// ExecCCIP handles GraphQL over Cross Chain Interoperability Protocol requests.
func (c *ccipHandler) ExecCCIP(rw http.ResponseWriter, req *http.Request) {
store := req.Context().Value(storeContextKey).(client.Store)

var ccipReq CCIPRequest
switch req.Method {
case http.MethodGet:
ccipReq.Sender = chi.URLParam(req, "sender")
ccipReq.Data = chi.URLParam(req, "data")
case http.MethodPost:
if err := requestJSON(req, &ccipReq); err != nil {
responseJSON(rw, http.StatusBadRequest, errorResponse{err})
return
}
}

data, err := hex.DecodeString(strings.TrimPrefix(ccipReq.Data, "0x"))
if err != nil {
responseJSON(rw, http.StatusBadRequest, errorResponse{err})
return
}
var request GraphQLRequest
if err := json.Unmarshal(data, &request); err != nil {
responseJSON(rw, http.StatusBadRequest, errorResponse{err})
return
}

result := store.ExecRequest(req.Context(), request.Query)
if result.Pub != nil {
responseJSON(rw, http.StatusBadRequest, errorResponse{ErrStreamingNotSupported})
return
}
resultJSON, err := json.Marshal(result.GQL)
if err != nil {
responseJSON(rw, http.StatusBadRequest, errorResponse{err})
return
}
resultHex := "0x" + hex.EncodeToString(resultJSON)
responseJSON(rw, http.StatusOK, CCIPResponse{Data: resultHex})
}
207 changes: 207 additions & 0 deletions http/handler_ccip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Copyright 2023 Democratized Data Foundation
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package http

import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"path"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/sourcenetwork/defradb/client"
"github.com/sourcenetwork/defradb/datastore/memory"
"github.com/sourcenetwork/defradb/db"
)

func TestCCIPGet_WithValidData(t *testing.T) {
cdb := setupDatabase(t)

gqlData, err := json.Marshal(&GraphQLRequest{
Query: `query {
User {
name
}
}`,
})
require.NoError(t, err)

data := "0x" + hex.EncodeToString([]byte(gqlData))
sender := "0x0000000000000000000000000000000000000000"
url := "http://localhost:9181/api/v0/ccip/" + path.Join(sender, data)

req := httptest.NewRequest(http.MethodGet, url, nil)
rec := httptest.NewRecorder()

handler := NewServer(cdb)
handler.ServeHTTP(rec, req)

res := rec.Result()
require.NotNil(t, res.Body)

resData, err := io.ReadAll(res.Body)
require.NoError(t, err)

var ccipRes CCIPResponse
err = json.Unmarshal(resData, &ccipRes)
require.NoError(t, err)

resHex, err := hex.DecodeString(strings.TrimPrefix(ccipRes.Data, "0x"))
require.NoError(t, err)

assert.JSONEq(t, `{"data": [{"name": "bob"}]}`, string(resHex))
}

func TestCCIPGet_WithSubscription(t *testing.T) {
cdb := setupDatabase(t)

gqlData, err := json.Marshal(&GraphQLRequest{
Query: `subscription {
User {
name
}
}`,
})
require.NoError(t, err)

data := "0x" + hex.EncodeToString([]byte(gqlData))
sender := "0x0000000000000000000000000000000000000000"
url := "http://localhost:9181/api/v0/ccip/" + path.Join(sender, data)

req := httptest.NewRequest(http.MethodGet, url, nil)
rec := httptest.NewRecorder()

handler := NewServer(cdb)
handler.ServeHTTP(rec, req)

res := rec.Result()
assert.Equal(t, 400, res.StatusCode)
}

func TestCCIPGet_WithInvalidData(t *testing.T) {
cdb := setupDatabase(t)

data := "invalid_hex_data"
sender := "0x0000000000000000000000000000000000000000"
url := "http://localhost:9181/api/v0/ccip/" + path.Join(sender, data)

req := httptest.NewRequest(http.MethodGet, url, nil)
rec := httptest.NewRecorder()

handler := NewServer(cdb)
handler.ServeHTTP(rec, req)

res := rec.Result()
assert.Equal(t, 400, res.StatusCode)
}

func TestCCIPPost_WithValidData(t *testing.T) {
cdb := setupDatabase(t)

gqlJSON, err := json.Marshal(&GraphQLRequest{
Query: `query {
User {
name
}
}`,
})
require.NoError(t, err)

body, err := json.Marshal(&CCIPRequest{
Data: "0x" + hex.EncodeToString([]byte(gqlJSON)),
Sender: "0x0000000000000000000000000000000000000000",
})
require.NoError(t, err)

req := httptest.NewRequest(http.MethodPost, "http://localhost:9181/api/v0/ccip", bytes.NewBuffer(body))
rec := httptest.NewRecorder()

handler := NewServer(cdb)
handler.ServeHTTP(rec, req)

res := rec.Result()
require.NotNil(t, res.Body)

resData, err := io.ReadAll(res.Body)
require.NoError(t, err)

var ccipRes CCIPResponse
err = json.Unmarshal(resData, &ccipRes)
require.NoError(t, err)

resHex, err := hex.DecodeString(strings.TrimPrefix(ccipRes.Data, "0x"))
require.NoError(t, err)

assert.JSONEq(t, `{"data": [{"name": "bob"}]}`, string(resHex))
}

func TestCCIPPost_WithInvalidGraphQLRequest(t *testing.T) {
cdb := setupDatabase(t)

body, err := json.Marshal(&CCIPRequest{
Data: "0x" + hex.EncodeToString([]byte("invalid_graphql_request")),
Sender: "0x0000000000000000000000000000000000000000",
})
require.NoError(t, err)

req := httptest.NewRequest(http.MethodPost, "http://localhost:9181/api/v0/ccip", bytes.NewBuffer(body))
rec := httptest.NewRecorder()

handler := NewServer(cdb)
handler.ServeHTTP(rec, req)

res := rec.Result()
assert.Equal(t, 400, res.StatusCode)
}

func TestCCIPPost_WithInvalidBody(t *testing.T) {
cdb := setupDatabase(t)

req := httptest.NewRequest(http.MethodPost, "http://localhost:9181/api/v0/ccip", nil)
rec := httptest.NewRecorder()

handler := NewServer(cdb)
handler.ServeHTTP(rec, req)

res := rec.Result()
assert.Equal(t, 400, res.StatusCode)
}

func setupDatabase(t *testing.T) client.DB {
ctx := context.Background()

cdb, err := db.NewDB(ctx, memory.NewDatastore(ctx), db.WithUpdateEvents())
require.NoError(t, err)

_, err = cdb.AddSchema(ctx, `type User {
name: String
}`)
require.NoError(t, err)

col, err := cdb.GetCollectionByName(ctx, "User")
require.NoError(t, err)

doc, err := client.NewDocFromJSON([]byte(`{"name": "bob"}`))
require.NoError(t, err)

err = col.Create(ctx, doc)
require.NoError(t, err)

return cdb
}
5 changes: 5 additions & 0 deletions http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func NewServer(db client.DB) *Server {
store_handler := &storeHandler{}
collection_handler := &collectionHandler{}
lens_handler := &lensHandler{}
ccip_handler := &ccipHandler{}

router := chi.NewRouter()
router.Use(middleware.RequestLogger(&logFormatter{}))
Expand Down Expand Up @@ -82,6 +83,10 @@ func NewServer(db client.DB) *Server {
graphQL.Get("/", store_handler.ExecRequest)
graphQL.Post("/", store_handler.ExecRequest)
})
api.Route("/ccip", func(ccip chi.Router) {
ccip.Get("/{sender}/{data}", ccip_handler.ExecCCIP)
ccip.Post("/", ccip_handler.ExecCCIP)
})
api.Route("/p2p", func(p2p chi.Router) {
p2p.Route("/replicators", func(p2p_replicators chi.Router) {
p2p_replicators.Get("/", store_handler.GetAllReplicators)
Expand Down

0 comments on commit 9e3b4e7

Please sign in to comment.