Skip to content

storacha/go-ucanto

Repository files navigation

go-ucanto

Go Report Card

Ucanto UCAN RPC in Golang.

Install

go get github.com/storacha/go-ucanto

Usage

Client

package main

import (
  "..."
)

// service URL & DID
serviceURL, _ := url.Parse("https://up.web3.storage")
servicePrincipal, _ := did.Parse("did:web:web3.storage")

// HTTP transport and CAR encoding
channel := http.NewHTTPChannel(serviceURL)
conn, _ := client.NewConnection(servicePrincipal, channel)

// private key to sign UCANs with
priv, _ := ioutil.ReadFile("path/to/private.key")
signer, _ := ed25519.Parse(priv)

audience := servicePrincipal

type StoreAddCaveats struct {
	Link ipld.Link
	Size int
}

func (c StoreAddCaveats) ToIPLD() (datamodel.Node, error) {
	return ipld.WrapWithRecovery(&c, StoreAddType())
}

func StoreAddType() ipldschema.Type {
	ts, _ := ipldprime.LoadSchemaBytes([]byte(`
		type StoreAdd struct {
			link Link
			size Int
		}
	`))
	return ts.TypeByName("StoreAdd")
}

capability := ucan.NewCapability(
	"store/add",
	did.Parse("did:key:z6MkwDuRThQcyWjqNsK54yKAmzfsiH6BTkASyiucThMtHt1T").String(),
	StoreAddCaveats{
		// TODO
	},
)

// create invocation(s) to perform a task with granted capabilities
inv, _ := invocation.Invoke(signer, audience, capability, delegation.WithProofs(...))
invocations := []invocation.Invocation{inv}

// send the invocation(s) to the service
resp, _ := client.Execute(invocations, conn)

// define datamodels for ok and error outcome
type OkModel struct {
  Status string
}
type ErrModel struct {
	Message string
}

// create new receipt reader, passing the IPLD schema for the result and the
// ok and error types
reader, _ := receipt.NewReceiptReader[OkModel, ErrModel]([]byte(`
	type Result union {
		| Ok "ok"
		| Err "error"
	} representation keyed

	type Ok struct {
		status String
	}

	type Err struct {
		message String
	}
`))

// get the receipt link for the invocation from the response
rcptlnk, _ := resp.Get(invocations[0].Link())
// read the receipt for the invocation from the response
rcpt, _ := reader.Read(rcptlnk, res.Blocks())

fmt.Println(rcpt.Out().Ok())

Server

package main

import (
	"..."
)

type TestEcho struct {
	Echo string
}

func (c TestEcho) ToIPLD() (ipld.Node, error) {
	return ipld.WrapWithRecovery(&c, EchoType())
}

func EchoType() ipldschema.Type {
	ts, _ := ipldprime.LoadSchemaBytes([]byte(`
		type TestEcho struct {
			echo String
		}
	`))
	return ts.TypeByName("TestEcho")
}

func createServer(signer principal.Signer) (server.ServerView, error) {
	// Capability definition(s)
	testecho := validator.NewCapability(
		"test/echo",
		schema.DIDString(),
		schema.Struct[TestEcho](EchoType(), nil),
		validator.DefaultDerives,
	)

	return server.NewServer(
		signer,
		// Handler definitions
		server.WithServiceMethod(
			testecho.Can(),
			server.Provide(
				testecho,
				func(cap ucan.Capability[TestEcho], inv invocation.Invocation, ctx server.InvocationContext) (TestEcho, receipt.Effects, error) {
					return TestEcho{Echo: cap.Nb().Echo}, nil, nil
				},
			),
		),
	)
}

func main() {
	signer, _ := ed25519.Generate()
	server, _ := createServer(signer)

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		res, _ := server.Request(uhttp.NewHTTPRequest(r.Body, r.Header))

		for key, vals := range res.Headers() {
			for _, v := range vals {
				w.Header().Add(key, v)
			}
		}

		if res.Status() != 0 {
			w.WriteHeader(res.Status())
		}

		io.Copy(w, res.Body())
	})

	listener, _ := net.Listen("tcp", ":0")

	port := listener.Addr().(*net.TCPAddr).Port
	fmt.Printf("{\"id\":\"%s\",\"url\":\"http://127.0.0.1:%d\"}\n", signer.DID().String(), port)

	http.Serve(listener, nil)
}

API

pkg.go.dev Reference

Related

Contributing

Feel free to join in. All welcome. Please open an issue!

License

Dual-licensed under MIT + Apache 2.0