Skip to content

Commit 4aa5c30

Browse files
lanyard/client: add Golang API client for lanyard (#58)
Co-authored-by: Ryan Smith <[email protected]>
1 parent 76c9695 commit 4aa5c30

File tree

7 files changed

+430
-9
lines changed

7 files changed

+430
-9
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "cmd/api/**"
8+
- "api/**"
9+
- ".github/**"
10+
- "**.go"
11+
pull_request:
12+
paths:
13+
- "clients/**"
14+
15+
jobs:
16+
build:
17+
name: integration
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/setup-go@v3
21+
with:
22+
go-version: "1.19"
23+
- uses: actions/cache@v3
24+
with:
25+
path: |
26+
~/.cache/go-build
27+
~/go/pkg/mod
28+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
29+
restore-keys: |
30+
${{ runner.os }}-go-
31+
- name: Check out code into the Go module directory
32+
uses: actions/checkout@v2
33+
- name: test
34+
run: go test ./... --tags=integration

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ on: [pull_request]
44

55
jobs:
66
build:
7-
name: all
7+
name: packages
88
runs-on: ubuntu-latest
99
steps:
1010
- uses: actions/setup-go@v3
1111
with:
12-
go-version: '1.19'
12+
go-version: "1.19"
1313
- uses: actions/cache@v3
1414
with:
1515
path: |

clients/go/lanyard/client.go

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
// An API client for [lanyard.org].
2+
//
3+
// [lanyard.org]: https://lanyard.org
4+
package lanyard
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"encoding/json"
10+
"fmt"
11+
"net/http"
12+
"strings"
13+
"time"
14+
15+
"github.com/ethereum/go-ethereum/common/hexutil"
16+
"golang.org/x/xerrors"
17+
)
18+
19+
var ErrNotFound error = xerrors.New("resource not found")
20+
21+
type Client struct {
22+
httpClient *http.Client
23+
url string
24+
}
25+
26+
type ClientOpt func(*Client)
27+
28+
func WithURL(url string) ClientOpt {
29+
return func(c *Client) {
30+
c.url = url
31+
}
32+
}
33+
34+
func WithClient(hc *http.Client) ClientOpt {
35+
return func(c *Client) {
36+
c.httpClient = hc
37+
}
38+
}
39+
40+
// Uses https://lanyard.org/api/v1 for a default url
41+
// and http.Client with a 30s timeout unless specified
42+
// using [WithURL] or [WithClient]
43+
func New(opts ...ClientOpt) *Client {
44+
const url = "https://lanyard.org/api/v1"
45+
c := &Client{
46+
url: url,
47+
httpClient: &http.Client{
48+
Timeout: 30 * time.Second,
49+
},
50+
}
51+
for _, opt := range opts {
52+
opt(c)
53+
}
54+
return c
55+
}
56+
57+
func (c *Client) sendRequest(
58+
ctx context.Context,
59+
method, path string,
60+
body, destination any,
61+
) error {
62+
var (
63+
url string = c.url + path
64+
jsonb []byte
65+
err error
66+
)
67+
68+
if body != nil {
69+
jsonb, err = json.Marshal(body)
70+
if err != nil {
71+
return err
72+
}
73+
}
74+
75+
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(jsonb))
76+
if err != nil {
77+
return xerrors.Errorf("error creating request: %w", err)
78+
}
79+
80+
req.Header.Set("Content-Type", "application/json")
81+
req.Header.Set("User-Agent", "lanyard-go+v1.0.0")
82+
83+
resp, err := c.httpClient.Do(req)
84+
if err != nil {
85+
return xerrors.Errorf("failed to send request: %w", err)
86+
}
87+
88+
if resp.StatusCode >= 400 {
89+
// special case 404s to make consuming client API easier
90+
if resp.StatusCode == http.StatusNotFound {
91+
return ErrNotFound
92+
}
93+
94+
return xerrors.Errorf("error making http request: %s", resp.Status)
95+
}
96+
97+
defer resp.Body.Close()
98+
99+
if err := json.NewDecoder(resp.Body).Decode(&destination); err != nil {
100+
return xerrors.Errorf("failed to decode response: %w", err)
101+
}
102+
103+
return nil
104+
}
105+
106+
type createTreeRequest struct {
107+
UnhashedLeaves []hexutil.Bytes `json:"unhashedLeaves"`
108+
LeafTypeDescriptor []string `json:"leafTypeDescriptor,omitempty"`
109+
PackedEncoding bool `json:"packedEncoding"`
110+
}
111+
112+
type CreateResponse struct {
113+
// MerkleRoot is the root of the created merkle tree
114+
MerkleRoot hexutil.Bytes `json:"merkleRoot"`
115+
}
116+
117+
// If you have a list of addresses for an allowlist, you can
118+
// create a Merkle tree using CreateTree. Any Merkle tree
119+
// published on Lanyard will be publicly available to any
120+
// user of Lanyard’s API, including minting interfaces such
121+
// as Zora or mint.fun.
122+
func (c *Client) CreateTree(
123+
ctx context.Context,
124+
addresses []hexutil.Bytes,
125+
) (*CreateResponse, error) {
126+
req := &createTreeRequest{
127+
UnhashedLeaves: addresses,
128+
PackedEncoding: true,
129+
}
130+
131+
resp := &CreateResponse{}
132+
err := c.sendRequest(ctx, http.MethodPost, "/tree", req, resp)
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
return resp, nil
138+
}
139+
140+
// CreateTypedTree is a more advanced way of creating a tree.
141+
// Useful if your tree has ABI encoded data, such as quantity
142+
// or other values.
143+
// unhashedLeaves is a slice of addresses or ABI encoded types.
144+
// leafTypeDescriptor describes the abi-encoded types of the leaves, and
145+
// is required if leaves are not address types.
146+
// Set packedEncoding to true if your arguments are packed/encoded
147+
func (c *Client) CreateTypedTree(
148+
ctx context.Context,
149+
unhashedLeaves []hexutil.Bytes,
150+
leafTypeDescriptor []string,
151+
packedEncoding bool,
152+
) (*CreateResponse, error) {
153+
req := &createTreeRequest{
154+
UnhashedLeaves: unhashedLeaves,
155+
LeafTypeDescriptor: leafTypeDescriptor,
156+
PackedEncoding: packedEncoding,
157+
}
158+
159+
resp := &CreateResponse{}
160+
161+
err := c.sendRequest(ctx, http.MethodPost, "/tree", req, resp)
162+
if err != nil {
163+
return nil, err
164+
}
165+
166+
return resp, nil
167+
}
168+
169+
type TreeResponse struct {
170+
// UnhashedLeaves is a slice of addresses or ABI encoded types
171+
UnhashedLeaves []hexutil.Bytes `json:"unhashedLeaves"`
172+
173+
// LeafTypeDescriptor describes the abi-encoded types of the leaves, and
174+
// is required if leaves are not address types
175+
LeafTypeDescriptor []string `json:"leafTypeDescriptor"`
176+
177+
// PackedEncoding is true by default
178+
PackedEncoding bool `json:"packedEncoding"`
179+
180+
LeafCount int `json:"leafCount"`
181+
}
182+
183+
// If a Merkle tree has been published to Lanyard, GetTreeFromRoot
184+
// will return the entire tree based on the root.
185+
// This endpoint will return ErrNotFound if the tree
186+
// associated with the root has not been published.
187+
func (c *Client) GetTreeFromRoot(
188+
ctx context.Context,
189+
root hexutil.Bytes,
190+
) (*TreeResponse, error) {
191+
resp := &TreeResponse{}
192+
193+
err := c.sendRequest(
194+
ctx, http.MethodGet,
195+
fmt.Sprintf("/tree?root=%s", root.String()),
196+
nil, resp,
197+
)
198+
199+
if err != nil {
200+
return nil, err
201+
}
202+
203+
return resp, nil
204+
}
205+
206+
type ProofResponse struct {
207+
UnhashedLeaf hexutil.Bytes `json:"unhashedLeaf"`
208+
Proof []hexutil.Bytes `json:"proof"`
209+
}
210+
211+
// If the tree has been published to Lanyard, GetProof will
212+
// return the proof associated with an unHashedLeaf.
213+
// This endpoint will return ErrNotFound if the tree
214+
// associated with the root has not been published.
215+
func (c *Client) GetProofFromLeaf(
216+
ctx context.Context,
217+
root hexutil.Bytes,
218+
unhashedLeaf hexutil.Bytes,
219+
) (*ProofResponse, error) {
220+
resp := &ProofResponse{}
221+
222+
err := c.sendRequest(
223+
ctx, http.MethodGet,
224+
fmt.Sprintf("/proof?root=%s&unhashedLeaf=%s",
225+
root.String(), unhashedLeaf.String(),
226+
),
227+
nil, resp,
228+
)
229+
230+
if err != nil {
231+
return nil, err
232+
}
233+
234+
return resp, nil
235+
}
236+
237+
type RootResponse struct {
238+
Root hexutil.Bytes `json:"root"`
239+
}
240+
241+
// If a Merkle tree has been published to Lanyard,
242+
// GetRootFromLeaf will return the root of the tree
243+
// based on a proof of a leaf. This endpoint will return
244+
// ErrNotFound if the tree associated with the
245+
// leaf has not been published.
246+
func (c *Client) GetRootFromProof(
247+
ctx context.Context,
248+
proof []hexutil.Bytes,
249+
) (*RootResponse, error) {
250+
resp := &RootResponse{}
251+
252+
if len(proof) == 0 {
253+
return nil, xerrors.New("proof must not be empty")
254+
}
255+
256+
var pq []string
257+
for _, p := range proof {
258+
pq = append(pq, p.String())
259+
}
260+
261+
err := c.sendRequest(
262+
ctx, http.MethodGet,
263+
fmt.Sprintf("/root?proof=%s",
264+
strings.Join(pq, ","),
265+
),
266+
nil, resp,
267+
)
268+
269+
if err != nil {
270+
return nil, err
271+
}
272+
273+
return resp, nil
274+
}

0 commit comments

Comments
 (0)