-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
lanyard/client: add Golang API client for lanyard (#58)
Co-authored-by: Ryan Smith <[email protected]>
- Loading branch information
1 parent
76c9695
commit 4aa5c30
Showing
7 changed files
with
430 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
name: tests | ||
|
||
on: | ||
push: | ||
branches: [main] | ||
paths: | ||
- "cmd/api/**" | ||
- "api/**" | ||
- ".github/**" | ||
- "**.go" | ||
pull_request: | ||
paths: | ||
- "clients/**" | ||
|
||
jobs: | ||
build: | ||
name: integration | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/setup-go@v3 | ||
with: | ||
go-version: "1.19" | ||
- uses: actions/cache@v3 | ||
with: | ||
path: | | ||
~/.cache/go-build | ||
~/go/pkg/mod | ||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} | ||
restore-keys: | | ||
${{ runner.os }}-go- | ||
- name: Check out code into the Go module directory | ||
uses: actions/checkout@v2 | ||
- name: test | ||
run: go test ./... --tags=integration |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
// An API client for [lanyard.org]. | ||
// | ||
// [lanyard.org]: https://lanyard.org | ||
package lanyard | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
"github.com/ethereum/go-ethereum/common/hexutil" | ||
"golang.org/x/xerrors" | ||
) | ||
|
||
var ErrNotFound error = xerrors.New("resource not found") | ||
|
||
type Client struct { | ||
httpClient *http.Client | ||
url string | ||
} | ||
|
||
type ClientOpt func(*Client) | ||
|
||
func WithURL(url string) ClientOpt { | ||
return func(c *Client) { | ||
c.url = url | ||
} | ||
} | ||
|
||
func WithClient(hc *http.Client) ClientOpt { | ||
return func(c *Client) { | ||
c.httpClient = hc | ||
} | ||
} | ||
|
||
// Uses https://lanyard.org/api/v1 for a default url | ||
// and http.Client with a 30s timeout unless specified | ||
// using [WithURL] or [WithClient] | ||
func New(opts ...ClientOpt) *Client { | ||
const url = "https://lanyard.org/api/v1" | ||
c := &Client{ | ||
url: url, | ||
httpClient: &http.Client{ | ||
Timeout: 30 * time.Second, | ||
}, | ||
} | ||
for _, opt := range opts { | ||
opt(c) | ||
} | ||
return c | ||
} | ||
|
||
func (c *Client) sendRequest( | ||
ctx context.Context, | ||
method, path string, | ||
body, destination any, | ||
) error { | ||
var ( | ||
url string = c.url + path | ||
jsonb []byte | ||
err error | ||
) | ||
|
||
if body != nil { | ||
jsonb, err = json.Marshal(body) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(jsonb)) | ||
if err != nil { | ||
return xerrors.Errorf("error creating request: %w", err) | ||
} | ||
|
||
req.Header.Set("Content-Type", "application/json") | ||
req.Header.Set("User-Agent", "lanyard-go+v1.0.0") | ||
|
||
resp, err := c.httpClient.Do(req) | ||
if err != nil { | ||
return xerrors.Errorf("failed to send request: %w", err) | ||
} | ||
|
||
if resp.StatusCode >= 400 { | ||
// special case 404s to make consuming client API easier | ||
if resp.StatusCode == http.StatusNotFound { | ||
return ErrNotFound | ||
} | ||
|
||
return xerrors.Errorf("error making http request: %s", resp.Status) | ||
} | ||
|
||
defer resp.Body.Close() | ||
|
||
if err := json.NewDecoder(resp.Body).Decode(&destination); err != nil { | ||
return xerrors.Errorf("failed to decode response: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type createTreeRequest struct { | ||
UnhashedLeaves []hexutil.Bytes `json:"unhashedLeaves"` | ||
LeafTypeDescriptor []string `json:"leafTypeDescriptor,omitempty"` | ||
PackedEncoding bool `json:"packedEncoding"` | ||
} | ||
|
||
type CreateResponse struct { | ||
// MerkleRoot is the root of the created merkle tree | ||
MerkleRoot hexutil.Bytes `json:"merkleRoot"` | ||
} | ||
|
||
// If you have a list of addresses for an allowlist, you can | ||
// create a Merkle tree using CreateTree. Any Merkle tree | ||
// published on Lanyard will be publicly available to any | ||
// user of Lanyard’s API, including minting interfaces such | ||
// as Zora or mint.fun. | ||
func (c *Client) CreateTree( | ||
ctx context.Context, | ||
addresses []hexutil.Bytes, | ||
) (*CreateResponse, error) { | ||
req := &createTreeRequest{ | ||
UnhashedLeaves: addresses, | ||
PackedEncoding: true, | ||
} | ||
|
||
resp := &CreateResponse{} | ||
err := c.sendRequest(ctx, http.MethodPost, "/tree", req, resp) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return resp, nil | ||
} | ||
|
||
// CreateTypedTree is a more advanced way of creating a tree. | ||
// Useful if your tree has ABI encoded data, such as quantity | ||
// or other values. | ||
// unhashedLeaves is a slice of addresses or ABI encoded types. | ||
// leafTypeDescriptor describes the abi-encoded types of the leaves, and | ||
// is required if leaves are not address types. | ||
// Set packedEncoding to true if your arguments are packed/encoded | ||
func (c *Client) CreateTypedTree( | ||
ctx context.Context, | ||
unhashedLeaves []hexutil.Bytes, | ||
leafTypeDescriptor []string, | ||
packedEncoding bool, | ||
) (*CreateResponse, error) { | ||
req := &createTreeRequest{ | ||
UnhashedLeaves: unhashedLeaves, | ||
LeafTypeDescriptor: leafTypeDescriptor, | ||
PackedEncoding: packedEncoding, | ||
} | ||
|
||
resp := &CreateResponse{} | ||
|
||
err := c.sendRequest(ctx, http.MethodPost, "/tree", req, resp) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return resp, nil | ||
} | ||
|
||
type TreeResponse struct { | ||
// UnhashedLeaves is a slice of addresses or ABI encoded types | ||
UnhashedLeaves []hexutil.Bytes `json:"unhashedLeaves"` | ||
|
||
// LeafTypeDescriptor describes the abi-encoded types of the leaves, and | ||
// is required if leaves are not address types | ||
LeafTypeDescriptor []string `json:"leafTypeDescriptor"` | ||
|
||
// PackedEncoding is true by default | ||
PackedEncoding bool `json:"packedEncoding"` | ||
|
||
LeafCount int `json:"leafCount"` | ||
} | ||
|
||
// If a Merkle tree has been published to Lanyard, GetTreeFromRoot | ||
// will return the entire tree based on the root. | ||
// This endpoint will return ErrNotFound if the tree | ||
// associated with the root has not been published. | ||
func (c *Client) GetTreeFromRoot( | ||
ctx context.Context, | ||
root hexutil.Bytes, | ||
) (*TreeResponse, error) { | ||
resp := &TreeResponse{} | ||
|
||
err := c.sendRequest( | ||
ctx, http.MethodGet, | ||
fmt.Sprintf("/tree?root=%s", root.String()), | ||
nil, resp, | ||
) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return resp, nil | ||
} | ||
|
||
type ProofResponse struct { | ||
UnhashedLeaf hexutil.Bytes `json:"unhashedLeaf"` | ||
Proof []hexutil.Bytes `json:"proof"` | ||
} | ||
|
||
// If the tree has been published to Lanyard, GetProof will | ||
// return the proof associated with an unHashedLeaf. | ||
// This endpoint will return ErrNotFound if the tree | ||
// associated with the root has not been published. | ||
func (c *Client) GetProofFromLeaf( | ||
ctx context.Context, | ||
root hexutil.Bytes, | ||
unhashedLeaf hexutil.Bytes, | ||
) (*ProofResponse, error) { | ||
resp := &ProofResponse{} | ||
|
||
err := c.sendRequest( | ||
ctx, http.MethodGet, | ||
fmt.Sprintf("/proof?root=%s&unhashedLeaf=%s", | ||
root.String(), unhashedLeaf.String(), | ||
), | ||
nil, resp, | ||
) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return resp, nil | ||
} | ||
|
||
type RootResponse struct { | ||
Root hexutil.Bytes `json:"root"` | ||
} | ||
|
||
// If a Merkle tree has been published to Lanyard, | ||
// GetRootFromLeaf will return the root of the tree | ||
// based on a proof of a leaf. This endpoint will return | ||
// ErrNotFound if the tree associated with the | ||
// leaf has not been published. | ||
func (c *Client) GetRootFromProof( | ||
ctx context.Context, | ||
proof []hexutil.Bytes, | ||
) (*RootResponse, error) { | ||
resp := &RootResponse{} | ||
|
||
if len(proof) == 0 { | ||
return nil, xerrors.New("proof must not be empty") | ||
} | ||
|
||
var pq []string | ||
for _, p := range proof { | ||
pq = append(pq, p.String()) | ||
} | ||
|
||
err := c.sendRequest( | ||
ctx, http.MethodGet, | ||
fmt.Sprintf("/root?proof=%s", | ||
strings.Join(pq, ","), | ||
), | ||
nil, resp, | ||
) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return resp, nil | ||
} |
Oops, something went wrong.
4aa5c30
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
lanyard – ./
lanyard.context.wtf
lanyard-git-main.context.wtf
allowlist.context.wtf