├── .air.toml ├── .github └── workflows │ ├── contracts-test-ignore.yml │ ├── contracts-test.yml │ ├── integration-test.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── README.md ├── api.go ├── migrations │ ├── migrations.go │ └── scripts │ │ └── 000-rebuild-proofs-hashes │ │ └── main.go ├── proof.go ├── root.go ├── schema.sql ├── tracing │ └── main.go ├── tree.go └── tree_test.go ├── clients ├── go │ └── lanyard │ │ ├── client.go │ │ └── client_integration_test.go └── js │ ├── .gitignore │ ├── .npmignore │ ├── .prettierrc │ ├── README.md │ ├── lib │ ├── index.ts │ └── types.ts │ ├── package.json │ ├── tsconfig.json │ └── yarn.lock ├── cmd ├── api │ ├── deploy.sh │ └── main.go └── dumpschema │ └── main.go ├── config └── deploy.yml ├── contracts ├── .gas-snapshot ├── .gitignore ├── .vscode │ └── settings.json ├── foundry.toml ├── remappings.txt ├── src │ └── MerkleAllowListedNFT.sol └── test │ └── MerkleAllowListedNFT.t.sol ├── example ├── .gitignore ├── .prettierrc ├── README.md ├── bun.lockb ├── package.json ├── src │ ├── api.ts │ └── merkle.ts └── tsconfig.json ├── go.mod ├── go.sum ├── merkle ├── tree.go └── tree_test.go └── www ├── .env ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .prettierrc ├── components ├── About.tsx ├── Button.tsx ├── CodeBlock │ ├── CopyCodeButton.tsx │ ├── index.tsx │ └── loadSolidityLanguageHighlighting.ts ├── Container.tsx ├── CreateRoot.tsx ├── Docs │ ├── Section.tsx │ ├── codeSnippets.ts │ └── index.tsx ├── FAQ.tsx ├── Layout │ ├── Head.tsx │ └── index.tsx ├── Logo.tsx ├── MetaTags.tsx ├── PageTitle.tsx ├── Providers.tsx ├── SiteNav │ ├── NavTab.tsx │ └── index.tsx └── Tutorial │ ├── Section.tsx │ ├── codeSnippets.ts │ └── index.tsx ├── hooks ├── useQuery.ts └── useWindowSize.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── docs.tsx ├── index.tsx ├── membership │ └── [merkleRoot].tsx ├── search.tsx └── tree │ ├── [merkleRoot].tsx │ └── sample.tsx ├── postcss.config.js ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── collablogos.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts │ ├── chivo400.woff2 │ ├── chivo400ext.woff2 │ ├── chivo700.woff2 │ └── chivo700ext.woff2 ├── meta-image.png ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── mstile-70x70.png ├── safari-pinned-tab.svg └── site.webmanifest ├── styles ├── fonts.css └── global.css ├── tailwind.config.js ├── tsconfig.json ├── utils ├── address.ts ├── addressParsing.ts ├── api │ ├── client.ts │ ├── helpers.ts │ ├── index.ts │ └── types.ts ├── clipboard.ts ├── constants.ts ├── ens.ts └── theme.ts └── yarn.lock /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | tmp_dir = "tmp" 3 | 4 | [build] 5 | bin = "./tmp/main" 6 | cmd = "go build -o ./tmp/main ./cmd/api/main.go" 7 | delay = 1000 8 | exclude_dir = ["tmp", "contracts", "example", "www"] 9 | exclude_file = [] 10 | exclude_regex = ["node_modules"] 11 | exclude_unchanged = false 12 | follow_symlink = false 13 | full_bin = "" 14 | include_dir = [] 15 | include_ext = ["go", "tpl", "tmpl", "html"] 16 | kill_delay = "0s" 17 | log = "build-errors.log" 18 | send_interrupt = false 19 | stop_on_error = true 20 | 21 | [color] 22 | app = "" 23 | build = "yellow" 24 | main = "magenta" 25 | runner = "green" 26 | watcher = "cyan" 27 | 28 | [log] 29 | time = false 30 | 31 | [misc] 32 | clean_on_exit = false 33 | -------------------------------------------------------------------------------- /.github/workflows/contracts-test-ignore.yml: -------------------------------------------------------------------------------- 1 | name: contracts-test 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "contracts/**" 7 | - ".github/**" 8 | 9 | jobs: 10 | tests: 11 | name: Test contracts 12 | runs-on: ubuntu-latest 13 | steps: 14 | - run: 'echo "No build required"' 15 | -------------------------------------------------------------------------------- /.github/workflows/contracts-test.yml: -------------------------------------------------------------------------------- 1 | name: contracts-test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "contracts/**" 8 | - ".github/**" 9 | pull_request: 10 | branches: [main] 11 | paths: 12 | - "contracts/**" 13 | - ".github/**" 14 | 15 | env: 16 | FOUNDRY_PROFILE: ci 17 | 18 | jobs: 19 | tests: 20 | name: Test contracts 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | submodules: recursive 26 | 27 | - name: Install Foundry 28 | uses: foundry-rs/foundry-toolchain@v1 29 | with: 30 | version: nightly 31 | 32 | - name: Run Forge build 33 | run: | 34 | forge --version 35 | forge build --sizes 36 | id: build 37 | working-directory: ./contracts 38 | 39 | - name: Run tests 40 | run: forge test 41 | id: test 42 | working-directory: ./contracts 43 | env: 44 | # Only fuzz intensely if we're running this action on a push to main or for a PR going into main: 45 | FOUNDRY_PROFILE: ${{ (github.ref == 'refs/heads/main' || github.base_ref == 'main') && 'intense' }} 46 | -------------------------------------------------------------------------------- /.github/workflows/integration-test.yml: -------------------------------------------------------------------------------- 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 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | name: packages 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/setup-go@v3 11 | with: 12 | go-version: "1.19" 13 | - uses: actions/cache@v3 14 | with: 15 | path: | 16 | ~/.cache/go-build 17 | ~/go/pkg/mod 18 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 19 | restore-keys: | 20 | ${{ runner.os }}-go- 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | - name: test 24 | run: go test ./... 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | .env 3 | *.pprof 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "contracts/lib/forge-std"] 2 | path = contracts/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "contracts/lib/openzeppelin-contracts"] 5 | path = contracts/lib/openzeppelin-contracts 6 | url = https://github.com/openzeppelin/openzeppelin-contracts 7 | [submodule "contracts/lib/solmate"] 8 | path = contracts/lib/solmate 9 | url = https://github.com/transmissions11/solmate 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine AS build 2 | WORKDIR /go/src/app 3 | ENV CGO_ENABLED=0 4 | 5 | RUN apk --no-cache add ca-certificates 6 | 7 | COPY go.mod go.sum ./ 8 | RUN go mod download 9 | 10 | COPY . . 11 | ARG GIT_SHA 12 | RUN cd cmd/api && go build -ldflags="-X 'main.GitSHA=$GIT_SHA'" main.go && mv main /go/bin/app 13 | 14 | FROM scratch 15 | COPY --from=build /go/bin/app /app 16 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 17 | ENTRYPOINT ["/app"] 18 | EXPOSE 8080 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Context Systems, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lanyard 2 | 3 | Create an allow list in seconds that works across web3 4 | 5 | [lanyard.org](https://lanyard.org) 6 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # api 2 | 3 | ## Endpoints 4 | 5 | ``` 6 | POST /api/v1/tree 7 | 8 | Request Body: 9 | { 10 | "unhashedLeaves": [ 11 | "0x0000000000000000000000000000000000000001", 12 | "0x0000000000000000000000000000000000000002" 13 | ], 14 | "leafTypeDescriptor": "address", 15 | "packedEncoding": true 16 | } 17 | 18 | Response Body: 19 | { 20 | "merkleRoot": "0x0000000000000000000000000000000000000000000000000000000000000001", 21 | } 22 | ``` 23 | 24 | ``` 25 | GET /api/v1/tree?root={root} 26 | 27 | Response Body: 28 | { 29 | "unhashedLeaves": [ 30 | "0x0000000000000000000000000000000000000001", 31 | "0x0000000000000000000000000000000000000002" 32 | ], 33 | "leafCount": 2 34 | } 35 | ``` 36 | 37 | ``` 38 | GET /api/v1/proof?root={root}&unhashedLeaf={unhashedLeaf} 39 | 40 | Response Body: 41 | { 42 | "proof": [ // or empty if the address is not in the merkle tree 43 | "0x0000000000000000000000000000000000000001", 44 | "0x0000000000000000000000000000000000000002" 45 | ], 46 | "unhashedLeaf": "0x0000000000000000000000000000000000000003" // or null if not in the tree 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/contextwtf/lanyard/api/tracing" 13 | "github.com/ethereum/go-ethereum/common" 14 | 15 | lru "github.com/hashicorp/golang-lru/v2" 16 | "github.com/jackc/pgx/v4/pgxpool" 17 | "github.com/rs/cors" 18 | "github.com/rs/zerolog" 19 | "github.com/rs/zerolog/hlog" 20 | "github.com/rs/zerolog/log" 21 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" 22 | ) 23 | 24 | type Server struct { 25 | db *pgxpool.Pool 26 | tlru *lru.Cache[common.Hash, cachedTree] 27 | } 28 | 29 | func New(db *pgxpool.Pool) *Server { 30 | l, err := lru.New[common.Hash, cachedTree](1000) 31 | if err != nil { 32 | log.Fatal().Err(err).Msg("failed to create lru cache") 33 | } 34 | return &Server{ 35 | db: db, 36 | tlru: l, 37 | } 38 | } 39 | 40 | func (s *Server) Handler(env, gitSha string) http.Handler { 41 | mux := http.NewServeMux() 42 | mux.HandleFunc("/api/v1/tree", s.TreeHandler) 43 | mux.HandleFunc("/api/v1/proof", s.GetProof) 44 | mux.HandleFunc("/api/v1/root", s.GetRoot) 45 | mux.HandleFunc("/api/v1/roots", s.GetRoot) 46 | mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { 47 | fmt.Fprint(w, gitSha) 48 | }) 49 | 50 | h := http.Handler(mux) 51 | h = versionHandler(h, gitSha) 52 | h = hlog.UserAgentHandler("user_agent")(h) 53 | h = hlog.RefererHandler("referer")(h) 54 | h = hlog.RequestIDHandler("req_id", "Request-Id")(h) 55 | h = hlog.URLHandler("path")(h) 56 | h = hlog.MethodHandler("method")(h) 57 | h = tracingHandler(os.Getenv("DD_ENV"), os.Getenv("DD_SERVICE"), gitSha, h) 58 | h = RemoteAddrHandler("ip")(h) 59 | h = hlog.NewHandler(log.Logger)(h) // needs to be last for log values to correctly be passed to context 60 | 61 | if env == "production" { 62 | return h 63 | } 64 | 65 | c := cors.New(cors.Options{ 66 | AllowedOrigins: []string{"*"}, 67 | AllowCredentials: false, 68 | OptionsPassthrough: false, 69 | }) 70 | 71 | h = c.Handler(h) 72 | 73 | return h 74 | } 75 | 76 | func ipFromRequest(r *http.Request) string { 77 | if r.Header.Get("fastly-client-ip") != "" { 78 | return r.Header.Get("fastly-client-ip") 79 | } 80 | 81 | if r.Header.Get("x-forwarded-for") != "" { 82 | group := strings.Split(r.Header.Get("x-forwarded-for"), ", ") 83 | if len(group) > 0 { 84 | return group[len(group)-1] 85 | } 86 | } 87 | 88 | host, _, err := net.SplitHostPort(r.RemoteAddr) 89 | if err == nil { 90 | return host 91 | } 92 | 93 | return "" 94 | } 95 | 96 | func RemoteAddrHandler(fieldKey string) func(next http.Handler) http.Handler { 97 | return func(next http.Handler) http.Handler { 98 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 99 | ip := ipFromRequest(r) 100 | if ip != "" { 101 | log := zerolog.Ctx(r.Context()) 102 | log.UpdateContext(func(c zerolog.Context) zerolog.Context { 103 | return c.Str("ip", ip) 104 | }) 105 | } 106 | 107 | next.ServeHTTP(w, r.WithContext(r.Context())) 108 | }) 109 | } 110 | } 111 | 112 | func versionHandler(h http.Handler, sha string) http.Handler { 113 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 114 | w.Header().Add("server-version", sha) 115 | h.ServeHTTP(w, r) 116 | }) 117 | } 118 | 119 | func tracingHandler(env, service, sha string, h http.Handler) http.Handler { 120 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 121 | if r.URL.Path == "/health" { 122 | h.ServeHTTP(w, r) 123 | return 124 | } 125 | 126 | span, ctx := tracing.SpanFromContext(r.Context(), "http.request") 127 | 128 | defer span.Finish() 129 | log := zerolog.Ctx(ctx) 130 | 131 | if env != "" { 132 | log.UpdateContext(func(c zerolog.Context) zerolog.Context { 133 | return c.Uint64("dd.trace_id", span.Context().TraceID()). 134 | Str("dd.service", service). 135 | Str("dd.env", env). 136 | Str("dd.version", sha) 137 | }) 138 | } 139 | 140 | span.SetTag(ext.ResourceName, r.URL.Path) 141 | span.SetTag(ext.SpanType, ext.SpanTypeWeb) 142 | span.SetTag(ext.HTTPMethod, r.Method) 143 | 144 | sc := &statusCapture{ResponseWriter: w} 145 | 146 | requestStart := time.Now() 147 | h.ServeHTTP(sc, r.WithContext(ctx)) 148 | 149 | // log every request 150 | log.Info(). 151 | Int("status", sc.status). 152 | Dur("duration", time.Since(requestStart)). 153 | Msg("") 154 | 155 | span.SetTag(ext.HTTPCode, sc.status) 156 | }) 157 | } 158 | 159 | type statusCapture struct { 160 | http.ResponseWriter 161 | wroteHeader bool 162 | status int 163 | } 164 | 165 | func (s *statusCapture) WriteHeader(c int) { 166 | s.status = c 167 | s.wroteHeader = true 168 | s.ResponseWriter.WriteHeader(c) 169 | } 170 | 171 | func (s *statusCapture) Write(b []byte) (int, error) { 172 | if !s.wroteHeader { 173 | s.WriteHeader(http.StatusOK) 174 | } 175 | return s.ResponseWriter.Write(b) 176 | } 177 | 178 | func (s *Server) sendJSON(r *http.Request, w http.ResponseWriter, response any) { 179 | w.Header().Set("Content-Type", "application/json") 180 | w.WriteHeader(http.StatusOK) 181 | json.NewEncoder(w).Encode(response) 182 | } 183 | 184 | func (s *Server) sendJSONError( 185 | r *http.Request, 186 | w http.ResponseWriter, 187 | err error, 188 | code int, 189 | customMessage string, 190 | ) { 191 | w.Header().Set("Content-Type", "application/json") 192 | if code == http.StatusNotFound && w.Header().Get("Cache-Control") == "" { 193 | w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") 194 | } 195 | 196 | // all headers need to be set before this line 197 | w.WriteHeader(code) 198 | 199 | if err != nil { 200 | log.Ctx(r.Context()).Err(err).Send() 201 | } 202 | 203 | message := http.StatusText(code) 204 | if customMessage != "" { 205 | message = customMessage 206 | } 207 | 208 | json.NewEncoder(w).Encode(map[string]any{ 209 | "error": true, 210 | "message": message, 211 | }) 212 | } 213 | -------------------------------------------------------------------------------- /api/migrations/migrations.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import "github.com/contextwtf/migrate" 4 | 5 | var Migrations = []migrate.Migration{ 6 | { 7 | Name: "2022-08-04.0.init.sql", 8 | SQL: ` 9 | CREATE TABLE merkle_trees ( 10 | root bytea, 11 | addresses bytea[] NOT NULL, 12 | PRIMARY KEY (root) 13 | ); 14 | CREATE TABLE merkle_proofs ( 15 | root bytea NOT NULL, 16 | address bytea NOT NULL, 17 | proof bytea[] NOT NULL 18 | ); 19 | CREATE UNIQUE INDEX ON merkle_proofs(root, address); 20 | `, 21 | }, 22 | { 23 | Name: "2022-08-05.0.rename.sql", 24 | SQL: ` 25 | ALTER TABLE merkle_trees 26 | RENAME COLUMN addresses TO unhashed_leaves; 27 | 28 | ALTER TABLE merkle_trees 29 | ADD COLUMN ltd text[]; 30 | 31 | ALTER TABLE merkle_trees 32 | ADD COLUMN packed boolean; 33 | 34 | ALTER TABLE merkle_proofs 35 | ADD COLUMN unhashed_leaf bytea NOT NULL; 36 | 37 | ALTER TABLE merkle_proofs 38 | ALTER COLUMN address 39 | DROP NOT NULL; 40 | 41 | DROP INDEX merkle_proofs_root_address_idx; 42 | CREATE UNIQUE INDEX on merkle_proofs(root, unhashed_leaf); 43 | `, 44 | }, 45 | { 46 | Name: "2022-08-17.0.proofs.sql", 47 | SQL: ` 48 | ALTER TABLE merkle_trees 49 | ADD COLUMN proofs jsonb; 50 | `, 51 | }, 52 | { 53 | Name: "2022-08-18.0.drop-proofs.sql", 54 | SQL: ` 55 | DROP TABLE merkle_proofs; 56 | `, 57 | }, 58 | { 59 | Name: "2022-08-18.1.rename-trees.sql", 60 | SQL: ` 61 | ALTER TABLE merkle_trees RENAME TO trees; 62 | `, 63 | }, 64 | { 65 | Name: "2022-08-22.0.add-proof-idx.sql", 66 | SQL: ` 67 | CREATE INDEX on trees USING gin(proofs jsonb_path_ops); 68 | `, 69 | }, 70 | { 71 | Name: "2022-08-22.1.add-inserted-at.sql", 72 | SQL: ` 73 | ALTER TABLE trees 74 | ADD COLUMN "inserted_at" timestamptz NOT NULL DEFAULT now(); 75 | `, 76 | }, 77 | { 78 | Name: "2022-08-30.0.packed-bool.sql", 79 | SQL: ` 80 | UPDATE trees SET packed = true 81 | WHERE packed IS NULL; 82 | 83 | ALTER TABLE trees 84 | ALTER COLUMN packed 85 | SET NOT NULL; 86 | `, 87 | }, 88 | { 89 | Name: "2022-08-31.0.root-func-index.sql", 90 | SQL: ` 91 | CREATE OR REPLACE FUNCTION proofs_array (data jsonb) 92 | RETURNS text[] 93 | AS $CODE$ 94 | BEGIN 95 | RETURN ARRAY ( 96 | SELECT 97 | jsonb_array_elements(data) ->> 'proof'); 98 | END 99 | $CODE$ 100 | LANGUAGE plpgsql 101 | IMMUTABLE; 102 | 103 | CREATE INDEX proofs_arr_idx ON trees USING GIN ((proofs_array(proofs))); 104 | DROP INDEX "trees_proofs_idx"; 105 | `, 106 | }, 107 | { 108 | Name: "2022-10-07.0.trees-proofs.sql", 109 | SQL: ` 110 | DROP INDEX proofs_arr_idx; 111 | CREATE TABLE trees_proofs ( 112 | root bytea PRIMARY KEY, 113 | proofs jsonb, 114 | inserted_at timestamp with time zone NOT NULL DEFAULT now() 115 | ); 116 | CREATE INDEX proofs_arr_idx ON trees_proofs USING GIN ((proofs_array(proofs))); 117 | `, 118 | }, 119 | { 120 | Name: "2023-06-26.0.proofs_hashes.sql", 121 | SQL: ` 122 | CREATE TABLE IF NOT EXISTS proofs_hashes ( 123 | hash bytea, 124 | root bytea 125 | ); 126 | CREATE INDEX IF NOT EXISTS proofs_hashes_hash_idx ON proofs_hashes (hash); 127 | `, 128 | }, 129 | { 130 | Name: "2023-06-26.1.drop_data.sql", 131 | SQL: ` 132 | ALTER TABLE "trees" DROP COLUMN "proofs"; 133 | DROP TABLE "trees_proofs"; 134 | `, 135 | }, 136 | } 137 | -------------------------------------------------------------------------------- /api/migrations/scripts/000-rebuild-proofs-hashes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "runtime" 9 | "runtime/debug" 10 | "sync" 11 | 12 | "github.com/contextwtf/lanyard/merkle" 13 | "github.com/ethereum/go-ethereum/crypto" 14 | "github.com/jackc/pgx/v4" 15 | "github.com/jackc/pgx/v4/pgxpool" 16 | "golang.org/x/sync/errgroup" 17 | ) 18 | 19 | func check(err error) { 20 | if err != nil { 21 | fmt.Fprintf(os.Stderr, "processor error: %s", err) 22 | debug.PrintStack() 23 | os.Exit(1) 24 | } 25 | } 26 | 27 | func hashProof(p [][]byte) []byte { 28 | return crypto.Keccak256(p...) 29 | } 30 | 31 | func migrateTree( 32 | ctx context.Context, 33 | tx pgx.Tx, 34 | leaves [][]byte, 35 | ) error { 36 | tree := merkle.New(leaves) 37 | 38 | var ( 39 | proofHashes = [][]any{} 40 | eg errgroup.Group 41 | pm sync.Mutex 42 | ) 43 | eg.SetLimit(runtime.NumCPU()) 44 | 45 | for i := range leaves { 46 | i := i 47 | eg.Go(func() error { 48 | pf := tree.Proof(i) 49 | proofHash := hashProof(pf) 50 | pm.Lock() 51 | proofHashes = append(proofHashes, []any{tree.Root(), proofHash}) 52 | pm.Unlock() 53 | return nil 54 | }) 55 | } 56 | err := eg.Wait() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | _, err = tx.CopyFrom(ctx, pgx.Identifier{"proofs_hashes"}, 62 | []string{"root", "hash"}, 63 | pgx.CopyFromRows(proofHashes), 64 | ) 65 | 66 | return err 67 | } 68 | 69 | func main() { 70 | ctx := context.Background() 71 | const defaultPGURL = "postgres:///al" 72 | dburl := os.Getenv("DATABASE_URL") 73 | if dburl == "" { 74 | dburl = defaultPGURL 75 | } 76 | dbc, err := pgxpool.ParseConfig(dburl) 77 | check(err) 78 | 79 | db, err := pgxpool.ConnectConfig(ctx, dbc) 80 | check(err) 81 | 82 | log.Println("fetching roots from db") 83 | const q = ` 84 | SELECT unhashed_leaves 85 | FROM trees 86 | WHERE root not in (select root from proofs_hashes group by 1) 87 | ` 88 | rows, err := db.Query(ctx, q) 89 | check(err) 90 | defer rows.Close() 91 | 92 | trees := [][][]byte{} 93 | 94 | for rows.Next() { 95 | var t [][]byte 96 | err := rows.Scan(&t) 97 | trees = append(trees, t) 98 | check(err) 99 | } 100 | 101 | if len(trees) == 0 { 102 | log.Println("no trees to process") 103 | return 104 | } 105 | 106 | log.Printf("migrating %d trees", len(trees)) 107 | 108 | tx, err := db.Begin(ctx) 109 | check(err) 110 | defer tx.Rollback(ctx) 111 | 112 | var count int 113 | 114 | for _, tree := range trees { 115 | err = migrateTree(ctx, tx, tree) 116 | check(err) 117 | count++ 118 | if count%1000 == 0 { 119 | log.Printf("migrated %d/%d trees", count, len(trees)) 120 | } 121 | } 122 | 123 | log.Printf("committing %d trees", len(trees)) 124 | err = tx.Commit(ctx) 125 | check(err) 126 | log.Printf("done") 127 | } 128 | -------------------------------------------------------------------------------- /api/proof.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "net/http" 8 | 9 | "github.com/contextwtf/lanyard/merkle" 10 | "github.com/ethereum/go-ethereum/common" 11 | "github.com/ethereum/go-ethereum/common/hexutil" 12 | "github.com/jackc/pgx/v4" 13 | ) 14 | 15 | type getProofResp struct { 16 | UnhashedLeaf hexutil.Bytes `json:"unhashedLeaf"` 17 | Proof []hexutil.Bytes `json:"proof"` 18 | } 19 | 20 | type cachedTree struct { 21 | r getTreeResp 22 | t merkle.Tree 23 | } 24 | 25 | func (s *Server) getCachedTree(ctx context.Context, root common.Hash) (cachedTree, error) { 26 | r, ok := s.tlru.Get(root) 27 | if ok { 28 | return r, nil 29 | } 30 | 31 | td, err := getTree(ctx, s.db, root.Bytes()) 32 | if err != nil { 33 | return cachedTree{}, err 34 | } 35 | 36 | leaves := [][]byte{} 37 | for _, l := range td.UnhashedLeaves { 38 | leaves = append(leaves, l[:]) 39 | } 40 | 41 | t := merkle.New(leaves) 42 | ct := cachedTree{ 43 | r: td, 44 | t: t, 45 | } 46 | 47 | s.tlru.Add(root, ct) 48 | return ct, nil 49 | } 50 | 51 | func (s *Server) GetProof(w http.ResponseWriter, r *http.Request) { 52 | var ( 53 | ctx = r.Context() 54 | root = common.HexToHash(r.URL.Query().Get("root")) 55 | leaf = common.FromHex(r.URL.Query().Get("unhashedLeaf")) 56 | addr = common.FromHex(r.URL.Query().Get("address")) 57 | ) 58 | 59 | if len(root) == 0 { 60 | s.sendJSONError(r, w, nil, http.StatusBadRequest, "missing root") 61 | return 62 | } 63 | if len(leaf) == 0 && len(addr) == 0 { 64 | s.sendJSONError(r, w, nil, http.StatusBadRequest, "missing leaf") 65 | return 66 | } 67 | 68 | ct, err := s.getCachedTree(ctx, root) 69 | if errors.Is(err, pgx.ErrNoRows) { 70 | s.sendJSONError(r, w, nil, http.StatusNotFound, "tree not found") 71 | w.Header().Set("Cache-Control", "public, max-age=60") 72 | return 73 | } else if err != nil { 74 | s.sendJSONError(r, w, err, http.StatusInternalServerError, "selecting proof") 75 | return 76 | } 77 | 78 | var ( 79 | target []byte 80 | ) 81 | // check if leaf is in tree and error if not 82 | for _, l := range ct.r.UnhashedLeaves { 83 | if len(target) == 0 { 84 | if len(leaf) > 0 { 85 | if bytes.Equal(l, leaf) { 86 | target = l 87 | } 88 | } else if bytes.Equal(leaf2Addr(l, ct.r.Ltd, ct.r.Packed), addr) { 89 | target = l 90 | } 91 | } 92 | } 93 | 94 | if len(target) == 0 { 95 | s.sendJSONError(r, w, nil, http.StatusNotFound, "leaf not found in tree") 96 | return 97 | } 98 | 99 | var ( 100 | p = ct.t.Proof(ct.t.Index(target)) 101 | phex = []hexutil.Bytes{} 102 | ) 103 | 104 | // convert [][]byte to []hexutil.Bytes 105 | for _, p := range p { 106 | phex = append(phex, p) 107 | } 108 | 109 | // cache for 1 year if we're returning an unhashed leaf proof 110 | // or 60 seconds for an address proof 111 | if len(leaf) > 0 { 112 | w.Header().Set("Cache-Control", "public, max-age=31536000") 113 | } else { 114 | w.Header().Set("Cache-Control", "public, max-age=60") 115 | } 116 | s.sendJSON(r, w, getProofResp{ 117 | UnhashedLeaf: target, 118 | Proof: phex, 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /api/root.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/ethereum/go-ethereum/common/hexutil" 8 | "github.com/jackc/pgx/v4" 9 | ) 10 | 11 | func (s *Server) GetRoot(w http.ResponseWriter, r *http.Request) { 12 | type rootResp struct { 13 | Root hexutil.Bytes `json:"root"` 14 | Note string `json:"note"` 15 | } 16 | 17 | type rootsResp struct { 18 | Roots []hexutil.Bytes `json:"roots"` 19 | } 20 | 21 | var ( 22 | ctx = r.Context() 23 | err error 24 | proof = r.URL.Query().Get("proof") 25 | ps = strings.Split(proof, ",") 26 | pb = [][]byte{} 27 | ) 28 | 29 | for _, s := range ps { 30 | var b []byte 31 | b, err = hexutil.Decode(s) 32 | if err != nil { 33 | break 34 | } 35 | pb = append(pb, b) 36 | } 37 | 38 | if len(pb) == 0 || err != nil { 39 | s.sendJSONError(r, w, nil, http.StatusBadRequest, "missing or malformed list of proofs") 40 | return 41 | } 42 | 43 | const q = ` 44 | SELECT root 45 | FROM proofs_hashes 46 | WHERE hash = $1 47 | group by 1; 48 | ` 49 | var ( 50 | roots []hexutil.Bytes 51 | rb hexutil.Bytes 52 | ph = hashProof(pb) 53 | ) 54 | 55 | _, err = s.db.QueryFunc(ctx, q, []interface{}{&ph}, []interface{}{&rb}, func(qfr pgx.QueryFuncRow) error { 56 | roots = append(roots, rb) 57 | return nil 58 | }) 59 | 60 | if err != nil { 61 | s.sendJSONError(r, w, err, http.StatusInternalServerError, "selecting root") 62 | return 63 | } else if len(roots) == 0 { // db.QueryFunc doesn't return pgx.ErrNoRows 64 | w.Header().Set("Cache-Control", "public, max-age=60") 65 | s.sendJSONError(r, w, nil, http.StatusNotFound, "root not found for proofs") 66 | return 67 | } 68 | 69 | w.Header().Set("Cache-Control", "public, max-age=3600") 70 | 71 | if strings.HasPrefix(r.URL.Path, "/api/v1/roots") { 72 | s.sendJSON(r, w, rootsResp{Roots: roots}) 73 | } else { 74 | // The original functionality of this endpoint, getting one root for 75 | // a given proof, is deprecated. This is because for smaller trees, 76 | // there are often collisions with the same root for different proofs. 77 | // This bit of code is for backwards compatibility. 78 | const note = `This endpoint is deprecated. For smaller trees, there are often collisions with the same root for different proofs. Please use the /v1/api/roots endpoint instead.` 79 | s.sendJSON(r, w, rootResp{Root: roots[0], Note: note}) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /api/schema.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | SET statement_timeout = 0; 4 | SET lock_timeout = 0; 5 | SET idle_in_transaction_session_timeout = 0; 6 | SET client_encoding = 'UTF8'; 7 | SET standard_conforming_strings = on; 8 | SELECT pg_catalog.set_config('search_path', '', false); 9 | SET check_function_bodies = false; 10 | SET xmloption = content; 11 | SET client_min_messages = warning; 12 | SET row_security = off; 13 | 14 | 15 | CREATE SEQUENCE public.migration_seq 16 | START WITH 1 17 | INCREMENT BY 1 18 | NO MINVALUE 19 | NO MAXVALUE 20 | CACHE 1; 21 | 22 | 23 | SET default_tablespace = ''; 24 | 25 | SET default_table_access_method = heap; 26 | 27 | 28 | CREATE TABLE public.migrations ( 29 | filename text NOT NULL, 30 | hash text NOT NULL, 31 | applied_at timestamp with time zone DEFAULT now() NOT NULL, 32 | index integer DEFAULT nextval('public.migration_seq'::regclass) NOT NULL 33 | ); 34 | 35 | 36 | 37 | CREATE TABLE public.trees ( 38 | root bytea NOT NULL, 39 | unhashed_leaves bytea[] NOT NULL, 40 | ltd text[], 41 | packed boolean, 42 | proofs jsonb 43 | ); 44 | 45 | 46 | 47 | ALTER TABLE ONLY public.trees 48 | ADD CONSTRAINT merkle_trees_pkey PRIMARY KEY (root); 49 | 50 | 51 | 52 | ALTER TABLE ONLY public.migrations 53 | ADD CONSTRAINT migrations_pkey PRIMARY KEY (filename); 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /api/tracing/main.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/jackc/pgx/v4" 9 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" 10 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" 11 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 12 | ) 13 | 14 | func getQueryName(query string) (name string) { 15 | lines := strings.SplitN(query, "\n", 1) 16 | name = "pgx" 17 | if len(lines) > 0 { 18 | first := lines[0] 19 | params := strings.SplitN(first, ":", 3) 20 | if len(params) > 1 { 21 | name += "." + strings.TrimSpace(params[1]) 22 | } 23 | } 24 | 25 | return name 26 | } 27 | 28 | func SpanFromContext(ctx context.Context, name string, opts ...tracer.StartSpanOption) (ddtrace.Span, context.Context) { 29 | parent, ok := tracer.SpanFromContext(ctx) 30 | if !ok { 31 | return tracer.StartSpanFromContext(ctx, name, opts...) 32 | } 33 | 34 | optsWithChild := []tracer.StartSpanOption{tracer.ChildOf(parent.Context())} 35 | optsWithChild = append(optsWithChild, opts...) 36 | span := tracer.StartSpan( 37 | name, 38 | optsWithChild..., 39 | ) 40 | 41 | return span, tracer.ContextWithSpan(ctx, span) 42 | } 43 | 44 | type DBTracer struct { 45 | serviceName string 46 | } 47 | 48 | func NewDBTracer(serviceName string) *DBTracer { 49 | return &DBTracer{ 50 | serviceName: serviceName, 51 | } 52 | } 53 | 54 | func (l *DBTracer) Log(ctx context.Context, level pgx.LogLevel, msg string, data map[string]any) { 55 | t, timeOk := data["time"].(time.Duration) 56 | q, queryOk := data["sql"].(string) 57 | if timeOk && queryOk { 58 | end := time.Now() 59 | start := end.Add(-t) 60 | startTime := tracer.StartTime(start) 61 | 62 | opts := []ddtrace.StartSpanOption{ 63 | tracer.SpanType(ext.SpanTypeSQL), 64 | tracer.ResourceName(string(q)), 65 | tracer.ServiceName(l.serviceName), 66 | startTime, 67 | } 68 | 69 | span, _ := SpanFromContext(ctx, getQueryName(q), opts...) 70 | span.Finish(tracer.FinishTime(end)) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /api/tree.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | 9 | "github.com/contextwtf/lanyard/merkle" 10 | "github.com/ethereum/go-ethereum/accounts/abi" 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/ethereum/go-ethereum/common/hexutil" 13 | "github.com/ethereum/go-ethereum/crypto" 14 | "github.com/jackc/pgx/v4" 15 | "github.com/jackc/pgx/v4/pgxpool" 16 | ) 17 | 18 | func (s *Server) TreeHandler(w http.ResponseWriter, r *http.Request) { 19 | switch r.Method { 20 | case http.MethodPost: 21 | s.CreateTree(w, r) 22 | return 23 | case http.MethodGet: 24 | s.GetTree(w, r) 25 | return 26 | default: 27 | http.Error(w, "unsupported method", http.StatusMethodNotAllowed) 28 | return 29 | } 30 | } 31 | 32 | func leaf2Addr(leaf []byte, ltd []string, packed bool) []byte { 33 | if len(ltd) == 0 || (len(ltd) == 1 && ltd[0] == "address" && len(leaf) == 20) { 34 | return leaf 35 | } 36 | if ltd[len(ltd)-1] == "address" && len(leaf) > 20 { 37 | return leaf[len(leaf)-20:] 38 | } 39 | 40 | if packed { 41 | return addrPacked(leaf, ltd) 42 | } 43 | return addrUnpacked(leaf, ltd) 44 | } 45 | 46 | func addrUnpacked(leaf []byte, ltd []string) []byte { 47 | var addrStart, pos int 48 | for _, desc := range ltd { 49 | if desc == "address" { 50 | addrStart = pos 51 | break 52 | } 53 | pos += 32 54 | } 55 | 56 | if len(leaf) >= addrStart+32 { 57 | l := leaf[addrStart:(addrStart + 32)] 58 | return l[len(l)-20:] // take last 20 bytes 59 | } 60 | return []byte{} 61 | } 62 | 63 | func addrPacked(leaf []byte, ltd []string) []byte { 64 | var addrStart, pos int 65 | for _, desc := range ltd { 66 | t, err := abi.NewType(desc, "", nil) 67 | if err != nil { 68 | return []byte{} 69 | } else if desc == "address" { 70 | addrStart = pos 71 | break 72 | } 73 | pos += int(t.GetType().Size()) 74 | } 75 | if addrStart == 0 && pos != 0 { 76 | return []byte{} 77 | } 78 | if len(leaf) >= addrStart+20 { 79 | return leaf[addrStart:(addrStart + 20)] 80 | } 81 | return []byte{} 82 | } 83 | 84 | func hashProof(p [][]byte) []byte { 85 | return crypto.Keccak256(p...) 86 | } 87 | 88 | type createTreeReq struct { 89 | Leaves []string `json:"unhashedLeaves"` 90 | Ltd []string `json:"leafTypeDescriptor"` 91 | Packed bool `json:"packedEncoding"` 92 | } 93 | 94 | type createTreeResp struct { 95 | MerkleRoot string `json:"merkleRoot"` 96 | } 97 | 98 | func (s *Server) CreateTree(w http.ResponseWriter, r *http.Request) { 99 | var ( 100 | req createTreeReq 101 | ctx = r.Context() 102 | ) 103 | defer r.Body.Close() 104 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 105 | s.sendJSONError(r, w, err, http.StatusBadRequest, "invalid request body") 106 | return 107 | } 108 | switch len(req.Leaves) { 109 | case 0: 110 | s.sendJSONError(r, w, nil, http.StatusBadRequest, "No leaves provided") 111 | return 112 | case 1: 113 | s.sendJSONError(r, w, nil, http.StatusBadRequest, "You must provide at least two values") 114 | return 115 | } 116 | 117 | var leaves [][]byte 118 | for _, l := range req.Leaves { 119 | // use the go-ethereum FromHex method because it is more 120 | // lenient and will allow for odd-length hex strings (by padding them) 121 | leaves = append(leaves, common.FromHex(l)) 122 | } 123 | 124 | var ( 125 | tree = merkle.New(leaves) 126 | root = tree.Root() 127 | exists bool 128 | ) 129 | 130 | const existsQ = ` 131 | select exists( 132 | select 1 from trees where root = $1 133 | ) 134 | ` 135 | 136 | err := s.db.QueryRow(ctx, existsQ, root).Scan(&exists) 137 | 138 | if err != nil { 139 | s.sendJSONError(r, w, err, http.StatusInternalServerError, "failed to check if tree already exists") 140 | return 141 | } 142 | 143 | if exists { 144 | s.sendJSON(r, w, createTreeResp{hexutil.Encode(root)}) 145 | return 146 | } 147 | 148 | var ( 149 | proofHashes = make([][]any, 0, len(leaves)) 150 | allProofs = tree.LeafProofs() 151 | ) 152 | 153 | for _, p := range allProofs { 154 | proofHash := hashProof(p) 155 | proofHashes = append(proofHashes, []any{root, proofHash}) 156 | } 157 | 158 | if err != nil { 159 | s.sendJSONError(r, w, err, http.StatusBadRequest, "generating proofs for tree") 160 | return 161 | } 162 | 163 | const q = ` 164 | INSERT INTO trees( 165 | root, 166 | unhashed_leaves, 167 | ltd, 168 | packed 169 | ) VALUES ($1, $2, $3, $4) 170 | ON CONFLICT (root) 171 | DO NOTHING 172 | ` 173 | 174 | tx, err := s.db.Begin(ctx) 175 | if err != nil { 176 | s.sendJSONError(r, w, err, http.StatusInternalServerError, "creating transaction") 177 | return 178 | } 179 | defer tx.Rollback(ctx) 180 | 181 | _, err = tx.Exec(ctx, q, 182 | tree.Root(), 183 | leaves, 184 | req.Ltd, 185 | req.Packed, 186 | ) 187 | if err != nil { 188 | s.sendJSONError(r, w, err, http.StatusInternalServerError, "inserting tree") 189 | return 190 | } 191 | 192 | _, err = tx.CopyFrom(ctx, pgx.Identifier{"proofs_hashes"}, 193 | []string{"root", "hash"}, 194 | pgx.CopyFromRows(proofHashes), 195 | ) 196 | 197 | if err != nil { 198 | s.sendJSONError(r, w, err, http.StatusInternalServerError, "inserting proof hashes") 199 | return 200 | } 201 | 202 | err = tx.Commit(ctx) 203 | if err != nil { 204 | s.sendJSONError(r, w, err, http.StatusInternalServerError, "committing transaction") 205 | return 206 | } 207 | 208 | s.sendJSON(r, w, createTreeResp{hexutil.Encode(root)}) 209 | } 210 | 211 | type getTreeResp struct { 212 | UnhashedLeaves []hexutil.Bytes `json:"unhashedLeaves"` 213 | LeafCount int `json:"leafCount"` 214 | Ltd []string `json:"leafTypeDescriptor"` 215 | Packed bool `json:"packedEncoding"` 216 | } 217 | 218 | func getTree(ctx context.Context, db *pgxpool.Pool, root []byte) (getTreeResp, error) { 219 | const q = ` 220 | SELECT unhashed_leaves, ltd, packed 221 | FROM trees 222 | WHERE root = $1 223 | ` 224 | tr := getTreeResp{} 225 | err := db.QueryRow(ctx, q, root).Scan( 226 | &tr.UnhashedLeaves, 227 | &tr.Ltd, 228 | &tr.Packed, 229 | ) 230 | if err != nil { 231 | return tr, err 232 | } 233 | return tr, nil 234 | } 235 | 236 | func (s *Server) GetTree(w http.ResponseWriter, r *http.Request) { 237 | var ( 238 | ctx = r.Context() 239 | root = r.URL.Query().Get("root") 240 | ) 241 | if root == "" { 242 | s.sendJSONError(r, w, nil, http.StatusBadRequest, "missing root") 243 | return 244 | } 245 | 246 | tr, err := getTree(ctx, s.db, common.FromHex(root)) 247 | 248 | if errors.Is(err, pgx.ErrNoRows) { 249 | s.sendJSONError(r, w, nil, http.StatusNotFound, "tree not found for root") 250 | w.Header().Set("Cache-Control", "public, max-age=60") 251 | return 252 | } else if err != nil { 253 | s.sendJSONError(r, w, err, http.StatusInternalServerError, "selecting tree") 254 | return 255 | } 256 | 257 | tr.LeafCount = len(tr.UnhashedLeaves) 258 | 259 | w.Header().Set("Cache-Control", "public, max-age=86400") 260 | s.sendJSON(r, w, tr) 261 | } 262 | -------------------------------------------------------------------------------- /api/tree_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | ) 9 | 10 | func TestAddrUnpacked(t *testing.T) { 11 | cases := []struct { 12 | leaf []byte 13 | ltd []string 14 | want []byte 15 | }{ 16 | { 17 | common.FromHex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), 18 | []string{"uint32", "address"}, 19 | common.FromHex("0x0000000000000000000000000000000000000001"), 20 | }, 21 | { 22 | common.FromHex("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000"), 23 | []string{"address", "uint32"}, 24 | common.FromHex("0x0000000000000000000000000000000000000001"), 25 | }, 26 | } 27 | 28 | for _, c := range cases { 29 | addr := addrUnpacked(c.leaf, c.ltd) 30 | if !bytes.Equal(addr, c.want) { 31 | t.Errorf("expected: %v got: %v", c.want, addr) 32 | } 33 | } 34 | } 35 | 36 | func TestAddrPacked(t *testing.T) { 37 | cases := []struct { 38 | leaf []byte 39 | ltd []string 40 | want []byte 41 | }{ 42 | { 43 | common.FromHex("000000000000000000000000000000000000000000000001"), 44 | []string{"uint32", "address"}, 45 | common.FromHex("0x0000000000000000000000000000000000000001"), 46 | }, 47 | { 48 | common.FromHex("000000000000000000000000000000000000000100000000"), 49 | []string{"address", "uint32"}, 50 | common.FromHex("0x0000000000000000000000000000000000000001"), 51 | }, 52 | } 53 | 54 | for _, c := range cases { 55 | addr := addrPacked(c.leaf, c.ltd) 56 | if !bytes.Equal(addr, c.want) { 57 | t.Errorf("expected: %v got: %v", c.want, addr) 58 | } 59 | } 60 | } 61 | 62 | func TestLeaf2Addr(t *testing.T) { 63 | cases := []struct { 64 | leaf []byte 65 | ltd []string 66 | packed bool 67 | want []byte 68 | }{ 69 | { 70 | common.FromHex("000000000000000000000000000000000000000000000001"), 71 | []string{"uint32", "address"}, 72 | true, 73 | common.FromHex("0x0000000000000000000000000000000000000001"), 74 | }, 75 | { 76 | common.FromHex("000000000000000000000000000000000000000100000000"), 77 | []string{"address", "uint32"}, 78 | true, 79 | common.FromHex("0x0000000000000000000000000000000000000001"), 80 | }, 81 | { 82 | common.FromHex("0x0000000000000000000000000000000000000001"), 83 | []string{"address"}, 84 | false, 85 | common.FromHex("0x0000000000000000000000000000000000000001"), 86 | }, 87 | { 88 | common.FromHex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), 89 | []string{"uint32", "address"}, 90 | false, 91 | common.FromHex("0x0000000000000000000000000000000000000001"), 92 | }, 93 | { 94 | common.FromHex("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000"), 95 | []string{"address", "uint32"}, 96 | false, 97 | common.FromHex("0x0000000000000000000000000000000000000001"), 98 | }, 99 | { 100 | common.FromHex("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001"), 101 | []string{"uint256", "address"}, 102 | false, 103 | common.FromHex("0x0000000000000000000000000000000000000001"), 104 | }, 105 | { 106 | common.FromHex("0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002d"), 107 | []string{"address", "uint256"}, 108 | false, 109 | common.FromHex("0x0000000000000000000000000000000000000001"), 110 | }, 111 | { 112 | common.FromHex("0x0000000000000000000000000000000000000000000000000000000000000001"), 113 | []string{"address"}, 114 | true, 115 | common.FromHex("0x0000000000000000000000000000000000000001"), 116 | }, 117 | } 118 | 119 | for _, c := range cases { 120 | addr := leaf2Addr(c.leaf, c.ltd, c.packed) 121 | if !bytes.Equal(addr, c.want) { 122 | t.Errorf("expected: %v got: %v", c.want, addr) 123 | } 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /clients/go/lanyard/client.go: -------------------------------------------------------------------------------- 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/accounts/abi" 16 | "github.com/ethereum/go-ethereum/common" 17 | "github.com/ethereum/go-ethereum/common/hexutil" 18 | "golang.org/x/xerrors" 19 | ) 20 | 21 | var ErrNotFound error = xerrors.New("resource not found") 22 | 23 | type Client struct { 24 | httpClient *http.Client 25 | url string 26 | } 27 | 28 | type ClientOpt func(*Client) 29 | 30 | func WithURL(url string) ClientOpt { 31 | return func(c *Client) { 32 | c.url = url 33 | } 34 | } 35 | 36 | func WithClient(hc *http.Client) ClientOpt { 37 | return func(c *Client) { 38 | c.httpClient = hc 39 | } 40 | } 41 | 42 | // Uses https://lanyard.org/api/v1 for a default url 43 | // and http.Client with a 30s timeout unless specified 44 | // using [WithURL] or [WithClient] 45 | func New(opts ...ClientOpt) *Client { 46 | const url = "https://lanyard.org/api/v1" 47 | c := &Client{ 48 | url: url, 49 | httpClient: &http.Client{ 50 | Timeout: 30 * time.Second, 51 | }, 52 | } 53 | for _, opt := range opts { 54 | opt(c) 55 | } 56 | return c 57 | } 58 | 59 | func (c *Client) sendRequest( 60 | ctx context.Context, 61 | method, path string, 62 | body, destination any, 63 | ) error { 64 | var ( 65 | url string = c.url + path 66 | jsonb []byte 67 | err error 68 | ) 69 | 70 | if body != nil { 71 | jsonb, err = json.Marshal(body) 72 | if err != nil { 73 | return err 74 | } 75 | } 76 | 77 | req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(jsonb)) 78 | if err != nil { 79 | return xerrors.Errorf("error creating request: %w", err) 80 | } 81 | 82 | req.Header.Set("Content-Type", "application/json") 83 | req.Header.Set("User-Agent", "lanyard-go+v1.0.3") 84 | 85 | resp, err := c.httpClient.Do(req) 86 | if err != nil { 87 | return xerrors.Errorf("failed to send request: %w", err) 88 | } 89 | 90 | if resp.StatusCode >= 400 { 91 | // special case 404s to make consuming client API easier 92 | if resp.StatusCode == http.StatusNotFound { 93 | return ErrNotFound 94 | } 95 | 96 | return xerrors.Errorf("error making http request: %s", resp.Status) 97 | } 98 | 99 | defer resp.Body.Close() 100 | 101 | if err := json.NewDecoder(resp.Body).Decode(&destination); err != nil { 102 | return xerrors.Errorf("failed to decode response: %w", err) 103 | } 104 | 105 | return nil 106 | } 107 | 108 | type createTreeRequest struct { 109 | UnhashedLeaves []hexutil.Bytes `json:"unhashedLeaves"` 110 | LeafTypeDescriptor []string `json:"leafTypeDescriptor,omitempty"` 111 | PackedEncoding bool `json:"packedEncoding"` 112 | } 113 | 114 | type CreateResponse struct { 115 | // MerkleRoot is the root of the created merkle tree 116 | MerkleRoot hexutil.Bytes `json:"merkleRoot"` 117 | } 118 | 119 | // If you have a list of addresses for an allowlist, you can 120 | // create a Merkle tree using CreateTree. Any Merkle tree 121 | // published on Lanyard will be publicly available to any 122 | // user of Lanyard’s API, including minting interfaces such 123 | // as Zora or mint.fun. 124 | func (c *Client) CreateTree( 125 | ctx context.Context, 126 | addresses []hexutil.Bytes, 127 | ) (*CreateResponse, error) { 128 | req := &createTreeRequest{ 129 | UnhashedLeaves: addresses, 130 | PackedEncoding: true, 131 | } 132 | 133 | resp := &CreateResponse{} 134 | err := c.sendRequest(ctx, http.MethodPost, "/tree", req, resp) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | return resp, nil 140 | } 141 | 142 | // CreateTypedTree is a more advanced way of creating a tree. 143 | // Useful if your tree has ABI encoded data, such as quantity 144 | // or other values. 145 | // unhashedLeaves is a slice of addresses or ABI encoded types. 146 | // leafTypeDescriptor describes the abi-encoded types of the leaves, and 147 | // is required if leaves are not address types. 148 | // Set packedEncoding to true if your arguments are packed/encoded 149 | func (c *Client) CreateTypedTree( 150 | ctx context.Context, 151 | unhashedLeaves []hexutil.Bytes, 152 | leafTypeDescriptor []string, 153 | packedEncoding bool, 154 | ) (*CreateResponse, error) { 155 | req := &createTreeRequest{ 156 | UnhashedLeaves: unhashedLeaves, 157 | LeafTypeDescriptor: leafTypeDescriptor, 158 | PackedEncoding: packedEncoding, 159 | } 160 | 161 | resp := &CreateResponse{} 162 | 163 | err := c.sendRequest(ctx, http.MethodPost, "/tree", req, resp) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | return resp, nil 169 | } 170 | 171 | type TreeResponse struct { 172 | // UnhashedLeaves is a slice of addresses or ABI encoded types 173 | UnhashedLeaves []hexutil.Bytes `json:"unhashedLeaves"` 174 | 175 | // LeafTypeDescriptor describes the abi-encoded types of the leaves, and 176 | // is required if leaves are not address types 177 | LeafTypeDescriptor []string `json:"leafTypeDescriptor"` 178 | 179 | // PackedEncoding is true by default 180 | PackedEncoding bool `json:"packedEncoding"` 181 | 182 | LeafCount int `json:"leafCount"` 183 | } 184 | 185 | // If a Merkle tree has been published to Lanyard, GetTreeFromRoot 186 | // will return the entire tree based on the root. 187 | // This endpoint will return ErrNotFound if the tree 188 | // associated with the root has not been published. 189 | func (c *Client) GetTreeFromRoot( 190 | ctx context.Context, 191 | root hexutil.Bytes, 192 | ) (*TreeResponse, error) { 193 | resp := &TreeResponse{} 194 | 195 | err := c.sendRequest( 196 | ctx, http.MethodGet, 197 | fmt.Sprintf("/tree?root=%s", root.String()), 198 | nil, resp, 199 | ) 200 | 201 | if err != nil { 202 | return nil, err 203 | } 204 | 205 | return resp, nil 206 | } 207 | 208 | type ProofResponse struct { 209 | UnhashedLeaf hexutil.Bytes `json:"unhashedLeaf"` 210 | Proof []hexutil.Bytes `json:"proof"` 211 | } 212 | 213 | // If the tree has been published to Lanyard, 214 | // GetProofFromLeaf will return the proof associated 215 | // with an unhashedLeaf. This endpoint will return 216 | // ErrNotFound if the tree associated with the root 217 | // has not been published. 218 | func (c *Client) GetProofFromLeaf( 219 | ctx context.Context, 220 | root, unhashedLeaf hexutil.Bytes, 221 | ) (*ProofResponse, error) { 222 | resp := &ProofResponse{} 223 | 224 | err := c.sendRequest( 225 | ctx, http.MethodGet, 226 | fmt.Sprintf("/proof?root=%s&unhashedLeaf=%s", 227 | root.String(), unhashedLeaf.String(), 228 | ), 229 | nil, resp, 230 | ) 231 | 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | return resp, nil 237 | } 238 | 239 | // If the tree has been published to Lanyard, 240 | // GetProofFromAddr will return the proof associated 241 | // with an address. This endpoint will return 242 | // ErrNotFound if the tree associated with the root 243 | // has not been published. 244 | func (c *Client) GetProofFromAddr( 245 | ctx context.Context, 246 | root, addr hexutil.Bytes, 247 | ) (*ProofResponse, error) { 248 | resp := &ProofResponse{} 249 | 250 | err := c.sendRequest( 251 | ctx, http.MethodGet, 252 | fmt.Sprintf("/proof?root=%s&address=%s", 253 | root.String(), addr.String(), 254 | ), 255 | nil, resp, 256 | ) 257 | 258 | if err != nil { 259 | return nil, err 260 | } 261 | 262 | return resp, nil 263 | } 264 | 265 | type RootsResponse struct { 266 | Roots []hexutil.Bytes `json:"roots"` 267 | } 268 | 269 | // If a Merkle tree has been published to Lanyard, 270 | // GetRootsFromProof will return the root of the tree 271 | // based on a proof of a leaf. This endpoint will return 272 | // ErrNotFound if the tree associated with the 273 | // leaf has not been published. This API response is deprecated 274 | // as there may be more than one root per proof. Use GetRootsFromProof 275 | // instead. 276 | func (c *Client) GetRootsFromProof( 277 | ctx context.Context, 278 | proof []hexutil.Bytes, 279 | ) (*RootsResponse, error) { 280 | resp := &RootsResponse{} 281 | 282 | if len(proof) == 0 { 283 | return nil, xerrors.New("proof must not be empty") 284 | } 285 | 286 | var pq []string 287 | for _, p := range proof { 288 | pq = append(pq, p.String()) 289 | } 290 | 291 | err := c.sendRequest( 292 | ctx, http.MethodGet, 293 | fmt.Sprintf("/roots?proof=%s", 294 | strings.Join(pq, ","), 295 | ), 296 | nil, resp, 297 | ) 298 | 299 | if err != nil { 300 | return nil, err 301 | } 302 | 303 | return resp, nil 304 | } 305 | 306 | // With a given leaf and type descriptor, decode an address 307 | func Leaf2Addr(leaf []byte, ltd []string, packed bool) common.Address { 308 | if len(ltd) == 0 || (len(ltd) == 1 && ltd[0] == "address") { 309 | return common.BytesToAddress(leaf) 310 | } 311 | if packed { 312 | return addrPacked(leaf, ltd) 313 | } 314 | return addrUnpacked(leaf, ltd) 315 | } 316 | 317 | func addrUnpacked(leaf []byte, ltd []string) common.Address { 318 | var addrStart, pos int 319 | for _, desc := range ltd { 320 | if desc == "address" { 321 | addrStart = pos 322 | break 323 | } 324 | pos += 32 325 | } 326 | if len(leaf) >= addrStart+32 { 327 | return common.BytesToAddress(leaf[addrStart:(addrStart + 32)]) 328 | } 329 | return common.Address{} 330 | } 331 | 332 | func addrPacked(leaf []byte, ltd []string) common.Address { 333 | var addrStart, pos int 334 | for _, desc := range ltd { 335 | t, err := abi.NewType(desc, "", nil) 336 | if err != nil { 337 | return common.Address{} 338 | } 339 | if desc == "address" { 340 | addrStart = pos 341 | break 342 | } 343 | pos += int(t.GetType().Size()) 344 | } 345 | if addrStart == 0 && pos != 0 { 346 | return common.Address{} 347 | } 348 | if len(leaf) >= addrStart+20 { 349 | return common.BytesToAddress(leaf[addrStart:(addrStart + 20)]) 350 | } 351 | return common.Address{} 352 | } 353 | -------------------------------------------------------------------------------- /clients/go/lanyard/client_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package lanyard 4 | 5 | import ( 6 | "context" 7 | "os" 8 | "testing" 9 | 10 | "github.com/ethereum/go-ethereum/common/hexutil" 11 | ) 12 | 13 | var ( 14 | client *Client 15 | basicMerkle = []hexutil.Bytes{ 16 | hexutil.MustDecode("0x0000000000000000000000000000000000000001"), 17 | hexutil.MustDecode("0x0000000000000000000000000000000000000002"), 18 | hexutil.MustDecode("0x0000000000000000000000000000000000000003"), 19 | hexutil.MustDecode("0x0000000000000000000000000000000000000004"), 20 | hexutil.MustDecode("0x0000000000000000000000000000000000000005"), 21 | } 22 | ) 23 | 24 | func init() { 25 | if os.Getenv("LANYARD_API_BASE_URL") == "" { 26 | client = New() 27 | } else { 28 | client = New(WithURL(os.Getenv("LANYARD_API_BASE_URL"))) 29 | } 30 | } 31 | 32 | const ( 33 | basicRoot = "0xa7a6b1cb6d12308ec4818baac3413fafa9e8b52cdcd79252fa9e29c9a2f8aff1" 34 | typedRoot = "0x6306f03ad6ae2ffeca080333a0a6828669192f5f8b61f70738bfe8ceb7e0a434" 35 | ) 36 | 37 | func TestBasicMerkleTree(t *testing.T) { 38 | tree, err := client.CreateTree(context.Background(), basicMerkle) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | if tree.MerkleRoot.String() != basicRoot { 44 | t.Fatalf("expected %s, got %s", basicRoot, tree.MerkleRoot.String()) 45 | } 46 | } 47 | 48 | func TestCreateTypedTree(t *testing.T) { 49 | tree, err := client.CreateTypedTree( 50 | context.Background(), 51 | []hexutil.Bytes{ 52 | hexutil.MustDecode("0x00000000000000000000000000000000000000010000000000000000000000000000000000000000000000008ac7230489e80000"), 53 | hexutil.MustDecode("0x0000000000000000000000000000000000000002000000000000000000000000000000000000000000000001e5b8fa8fe2ac0000"), 54 | }, 55 | []string{"address", "uint256"}, 56 | true, 57 | ) 58 | 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | if tree.MerkleRoot.String() != typedRoot { 64 | t.Fatalf("expected %s, got %s", typedRoot, tree.MerkleRoot.String()) 65 | } 66 | } 67 | 68 | func TestBasicMerkleProof(t *testing.T) { 69 | _, err := client.GetProofFromLeaf(context.Background(), hexutil.MustDecode(basicRoot), basicMerkle[0]) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | } 74 | 75 | func TestBasicMerkleProof404(t *testing.T) { 76 | _, err := client.GetProofFromLeaf(context.Background(), []byte{0x01}, hexutil.MustDecode("0x0000000000000000000000000000000000000001")) 77 | if err != ErrNotFound { 78 | t.Fatal("expected custom 404 err type for invalid request, got %w", err) 79 | } 80 | } 81 | 82 | func TestGetProofFromAddr(t *testing.T) { 83 | _, err := client.GetProofFromAddr(context.Background(), hexutil.MustDecode(typedRoot), hexutil.MustDecode("0x0000000000000000000000000000000000000001")) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | } 88 | 89 | func TestGetRootsFromProof(t *testing.T) { 90 | p, err := client.GetProofFromLeaf(context.Background(), hexutil.MustDecode(basicRoot), basicMerkle[0]) 91 | 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | resp, err := client.GetRootsFromProof(context.Background(), p.Proof) 97 | 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | if resp.Roots[0].String() != basicRoot { 103 | t.Fatalf("expected %s, got %s", basicRoot, resp.Roots[0].String()) 104 | } 105 | 106 | } 107 | 108 | func TestGetTree(t *testing.T) { 109 | tree, err := client.GetTreeFromRoot(context.Background(), hexutil.MustDecode(basicRoot)) 110 | 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | 115 | if tree.UnhashedLeaves[0].String() != basicMerkle[0].String() { 116 | t.Fatalf("expected %s, got %s", basicMerkle[0].String(), tree.UnhashedLeaves[0].String()) 117 | } 118 | 119 | if tree.LeafCount != len(basicMerkle) { 120 | t.Fatalf("expected %d, got %d", len(basicMerkle), tree.LeafCount) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /clients/js/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | -------------------------------------------------------------------------------- /clients/js/.npmignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | .prettierrc 3 | tsconfig.json -------------------------------------------------------------------------------- /clients/js/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /clients/js/README.md: -------------------------------------------------------------------------------- 1 | # lanyard 2 | 3 | lanyard is a javascript client for [lanyard.org](https://lanyard.org) – a decentralized way to create and consume allowlists. 4 | 5 | http is handled by [isomorphic-fetch](https://www.npmjs.com/package/isomorphic-fetch); this package targets browsers and backend servers. 6 | 7 | ## functionality 8 | 9 | ### creating an allowlist 10 | 11 | ```js 12 | import lanyard from 'lanyard' 13 | 14 | const resp = await lanyard.createTree({ 15 | unhashedLeaves: [ 16 | '0xfb843f8c4992efdb6b42349c35f025ca55742d33', 17 | '0x7e5507281f62c0f8d666beaea212751cd88994b8', 18 | '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', 19 | ], 20 | // leafTypeDescriptor: ["address"] // optional, used for abi encoded types 21 | // packedEncoding: boolean // optional, default false 22 | }) 23 | 24 | console.log(resp.merkleRoot) 25 | // 0x8aeeaf632a31342dfccb7dd4f1654ec602c263b33769062bd6ed59d1644d2af6 26 | ``` 27 | 28 | ### getting a tree by root 29 | 30 | ```js 31 | const tree = await lanyard.getTree( 32 | '0x8aeeaf632a31342dfccb7dd4f1654ec602c263b33769062bd6ed59d1644d2af6', 33 | ) 34 | 35 | console.log(tree) 36 | // { 37 | // "unhashedLeaves": [ 38 | // "0xfb843f8c4992efdb6b42349c35f025ca55742d33", 39 | // "0x7e5507281f62c0f8d666beaea212751cd88994b8", 40 | // "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" 41 | // ], 42 | // "leafCount": 3, 43 | // "leafTypeDescriptor": null, 44 | // "packedEncoding": false 45 | // } 46 | ``` 47 | 48 | ### getting a proof for item 49 | 50 | ```js 51 | const proof = await lanyard.getProof({ 52 | merkleRoot: 53 | '0x8aeeaf632a31342dfccb7dd4f1654ec602c263b33769062bd6ed59d1644d2af6', 54 | unhashedLeaf: '0xfb843f8c4992efdb6b42349c35f025ca55742d33', 55 | }) 56 | 57 | console.log(proof) 58 | // { 59 | // "unhashedLeaf": "0xfb843f8c4992efdb6b42349c35f025ca55742d33", 60 | // "proof": [ 61 | // "0xdb740d4f5f900a98f8513824cbcb164917f4e0b948914b750613b76063b70565", 62 | // "0x06e120c2c3547c60ee47f712d32e5acf38b35d1cc62e23b055a69bb88284c281" 63 | // ] 64 | // } 65 | ``` 66 | 67 | ### getting roots for a proof 68 | 69 | ```js 70 | const roots = await lanyard.getRoots([ 71 | '0xdb740d4f5f900a98f8513824cbcb164917f4e0b948914b750613b76063b70565', 72 | '0x06e120c2c3547c60ee47f712d32e5acf38b35d1cc62e23b055a69bb88284c281', 73 | ]) 74 | 75 | console.log(roots) 76 | 77 | // { 78 | // "roots": [ 79 | // "0x8aeeaf632a31342dfccb7dd4f1654ec602c263b33769062bd6ed59d1644d2af6" 80 | // ] 81 | // } 82 | ``` 83 | -------------------------------------------------------------------------------- /clients/js/lib/index.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | import { 3 | CreateTreeRequest, 4 | CreateTreeResponse, 5 | GetProofByAddress, 6 | GetProofByLeaf, 7 | GetProofRequest, 8 | GetProofResponse, 9 | GetRootsResponse, 10 | GetTreeResponse, 11 | } from './types' 12 | const baseUrl = 'https://lanyard.org/api/v1/' 13 | 14 | const client = async (method: 'GET' | 'POST', path: string, data?: any) => { 15 | const opts: { 16 | method: string 17 | body?: string 18 | headers?: any 19 | } = { 20 | method: method, 21 | } 22 | 23 | if (method !== 'GET') { 24 | opts.body = JSON.stringify(data) 25 | opts.headers = { 26 | 'Content-Type': 'application/json', 27 | } 28 | } 29 | 30 | const resp = await fetch(baseUrl + path, opts) 31 | if (resp.status > 299) { 32 | if (resp.status === 404) { 33 | return null 34 | } 35 | throw new Error( 36 | `Request failed with status ${resp.status}: ${await resp.json()}`, 37 | ) 38 | } 39 | 40 | return resp.json() 41 | } 42 | 43 | export const createTree = ( 44 | req: CreateTreeRequest, 45 | ): Promise => client('POST', 'tree', req) 46 | 47 | export const getTree = (merkleRoot: string): Promise => 48 | client('GET', `tree?root=${encodeURIComponent(merkleRoot)}`) 49 | 50 | export const getProof = (req: GetProofRequest): Promise => { 51 | let url = `proof?root=${encodeURIComponent(req.merkleRoot)}` 52 | 53 | if ((req as GetProofByAddress).address) { 54 | url += `&address=${encodeURIComponent((req as GetProofByAddress).address)}` 55 | } else { 56 | url += `&unhashedLeaf=${encodeURIComponent( 57 | (req as GetProofByLeaf).unhashedLeaf, 58 | )}` 59 | } 60 | 61 | return client('GET', url) 62 | } 63 | 64 | export const getRoots = (proof: string[]): Promise => 65 | client('GET', `roots?proof=${encodeURIComponent(proof.join(','))}`) 66 | 67 | export * from './types' 68 | -------------------------------------------------------------------------------- /clients/js/lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface CreateTreeRequest { 2 | unhashedLeaves: string[] 3 | leafTypeDescriptor?: string[] 4 | packedEncoding?: boolean 5 | } 6 | 7 | export interface CreateTreeResponse { 8 | merkleRoot: string 9 | } 10 | 11 | export type GetTreeResponse = { 12 | unhashedLeaves: string[] 13 | leafCount: number 14 | leafTypeDescriptor: string[] | null 15 | packedEncoding: boolean | null 16 | } | null 17 | 18 | export type GetProofByLeaf = { 19 | merkleRoot: string 20 | unhashedLeaf: string 21 | } | null 22 | 23 | export type GetProofByAddress = { 24 | merkleRoot: string 25 | address: string 26 | } | null 27 | 28 | export type GetProofRequest = GetProofByLeaf | GetProofByAddress 29 | 30 | export type GetProofResponse = { 31 | proof: string[] 32 | unhashedLeaf: string 33 | } | null 34 | 35 | export interface GetRootsResponse { 36 | roots: string[] 37 | } 38 | -------------------------------------------------------------------------------- /clients/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lanyard", 3 | "version": "1.1.2", 4 | "description": "lanyard api client", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/contextwtf/lanyard", 7 | "author": "Context Systems, Inc", 8 | "license": "MIT", 9 | "scripts": { 10 | "prepare": "rm -rf dist && tsc && esbuild lib/*.ts --platform=node --outdir=dist --format=cjs" 11 | }, 12 | "dependencies": { 13 | "isomorphic-fetch": "^3.0.0" 14 | }, 15 | "devDependencies": { 16 | "esbuild": "^0.15.10", 17 | "typescript": "^4.8.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /clients/js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["lib/**/*"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "dist", 7 | "target": "ES6", 8 | "moduleResolution": "node" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /clients/js/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@esbuild/android-arm@0.15.10": 6 | version "0.15.10" 7 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.10.tgz#a5f9432eb221afc243c321058ef25fe899886892" 8 | integrity sha512-FNONeQPy/ox+5NBkcSbYJxoXj9GWu8gVGJTVmUyoOCKQFDTrHVKgNSzChdNt0I8Aj/iKcsDf2r9BFwv+FSNUXg== 9 | 10 | "@esbuild/linux-loong64@0.15.10": 11 | version "0.15.10" 12 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.10.tgz#78a42897c2cf8db9fd5f1811f7590393b77774c7" 13 | integrity sha512-w0Ou3Z83LOYEkwaui2M8VwIp+nLi/NA60lBLMvaJ+vXVMcsARYdEzLNE7RSm4+lSg4zq4d7fAVuzk7PNQ5JFgg== 14 | 15 | esbuild-android-64@0.15.10: 16 | version "0.15.10" 17 | resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.10.tgz#8a59a84acbf2eca96996cadc35642cf055c494f0" 18 | integrity sha512-UI7krF8OYO1N7JYTgLT9ML5j4+45ra3amLZKx7LO3lmLt1Ibn8t3aZbX5Pu4BjWiqDuJ3m/hsvhPhK/5Y/YpnA== 19 | 20 | esbuild-android-arm64@0.15.10: 21 | version "0.15.10" 22 | resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.10.tgz#f453851dc1d8c5409a38cf7613a33852faf4915d" 23 | integrity sha512-EOt55D6xBk5O05AK8brXUbZmoFj4chM8u3riGflLa6ziEoVvNjRdD7Cnp82NHQGfSHgYR06XsPI8/sMuA/cUwg== 24 | 25 | esbuild-darwin-64@0.15.10: 26 | version "0.15.10" 27 | resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.10.tgz#778bd29c8186ff47b176c8af58c08cf0fb8e6b86" 28 | integrity sha512-hbDJugTicqIm+WKZgp208d7FcXcaK8j2c0l+fqSJ3d2AzQAfjEYDRM3Z2oMeqSJ9uFxyj/muSACLdix7oTstRA== 29 | 30 | esbuild-darwin-arm64@0.15.10: 31 | version "0.15.10" 32 | resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.10.tgz#b30bbefb46dc3c5d4708b0435e52f6456578d6df" 33 | integrity sha512-M1t5+Kj4IgSbYmunf2BB6EKLkWUq+XlqaFRiGOk8bmBapu9bCDrxjf4kUnWn59Dka3I27EiuHBKd1rSO4osLFQ== 34 | 35 | esbuild-freebsd-64@0.15.10: 36 | version "0.15.10" 37 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.10.tgz#ab301c5f6ded5110dbdd611140bef1a7c2e99236" 38 | integrity sha512-KMBFMa7C8oc97nqDdoZwtDBX7gfpolkk6Bcmj6YFMrtCMVgoU/x2DI1p74DmYl7CSS6Ppa3xgemrLrr5IjIn0w== 39 | 40 | esbuild-freebsd-arm64@0.15.10: 41 | version "0.15.10" 42 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.10.tgz#a5b09b867a6ff49110f52343b6f12265db63d43f" 43 | integrity sha512-m2KNbuCX13yQqLlbSojFMHpewbn8wW5uDS6DxRpmaZKzyq8Dbsku6hHvh2U+BcLwWY4mpgXzFUoENEf7IcioGg== 44 | 45 | esbuild-linux-32@0.15.10: 46 | version "0.15.10" 47 | resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.10.tgz#5282fe9915641caf9c8070e4ba2c3e16d358f837" 48 | integrity sha512-guXrwSYFAvNkuQ39FNeV4sNkNms1bLlA5vF1H0cazZBOLdLFIny6BhT+TUbK/hdByMQhtWQ5jI9VAmPKbVPu1w== 49 | 50 | esbuild-linux-64@0.15.10: 51 | version "0.15.10" 52 | resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.10.tgz#f3726e85a00149580cb19f8abfabcbb96f5d52bb" 53 | integrity sha512-jd8XfaSJeucMpD63YNMO1JCrdJhckHWcMv6O233bL4l6ogQKQOxBYSRP/XLWP+6kVTu0obXovuckJDcA0DKtQA== 54 | 55 | esbuild-linux-arm64@0.15.10: 56 | version "0.15.10" 57 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.10.tgz#2f0056e9d5286edb0185b56655caa8c574d8dbe7" 58 | integrity sha512-GByBi4fgkvZFTHFDYNftu1DQ1GzR23jws0oWyCfhnI7eMOe+wgwWrc78dbNk709Ivdr/evefm2PJiUBMiusS1A== 59 | 60 | esbuild-linux-arm@0.15.10: 61 | version "0.15.10" 62 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.10.tgz#40a9270da3c8ffa32cf72e24a79883e323dff08d" 63 | integrity sha512-6N8vThLL/Lysy9y4Ex8XoLQAlbZKUyExCWyayGi2KgTBelKpPgj6RZnUaKri0dHNPGgReJriKVU6+KDGQwn10A== 64 | 65 | esbuild-linux-mips64le@0.15.10: 66 | version "0.15.10" 67 | resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.10.tgz#90ce1c4ee0202edb4ac69807dea77f7e5804abc4" 68 | integrity sha512-BxP+LbaGVGIdQNJUNF7qpYjEGWb0YyHVSKqYKrn+pTwH/SiHUxFyJYSP3pqkku61olQiSBnSmWZ+YUpj78Tw7Q== 69 | 70 | esbuild-linux-ppc64le@0.15.10: 71 | version "0.15.10" 72 | resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.10.tgz#782837ae7bd5b279178106c9dd801755a21fabdf" 73 | integrity sha512-LoSQCd6498PmninNgqd/BR7z3Bsk/mabImBWuQ4wQgmQEeanzWd5BQU2aNi9mBURCLgyheuZS6Xhrw5luw3OkQ== 74 | 75 | esbuild-linux-riscv64@0.15.10: 76 | version "0.15.10" 77 | resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.10.tgz#d7420d806ece5174f24f4634303146f915ab4207" 78 | integrity sha512-Lrl9Cr2YROvPV4wmZ1/g48httE8z/5SCiXIyebiB5N8VT7pX3t6meI7TQVHw/wQpqP/AF4SksDuFImPTM7Z32Q== 79 | 80 | esbuild-linux-s390x@0.15.10: 81 | version "0.15.10" 82 | resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.10.tgz#21fdf0cb3494a7fb520a71934e4dffce67fe47be" 83 | integrity sha512-ReP+6q3eLVVP2lpRrvl5EodKX7EZ1bS1/z5j6hsluAlZP5aHhk6ghT6Cq3IANvvDdscMMCB4QEbI+AjtvoOFpA== 84 | 85 | esbuild-netbsd-64@0.15.10: 86 | version "0.15.10" 87 | resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.10.tgz#6c06b3107e3df53de381e6299184d4597db0440f" 88 | integrity sha512-iGDYtJCMCqldMskQ4eIV+QSS/CuT7xyy9i2/FjpKvxAuCzrESZXiA1L64YNj6/afuzfBe9i8m/uDkFHy257hTw== 89 | 90 | esbuild-openbsd-64@0.15.10: 91 | version "0.15.10" 92 | resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.10.tgz#4daef5f5d8e74bbda53b65160029445d582570cf" 93 | integrity sha512-ftMMIwHWrnrYnvuJQRJs/Smlcb28F9ICGde/P3FUTCgDDM0N7WA0o9uOR38f5Xe2/OhNCgkjNeb7QeaE3cyWkQ== 94 | 95 | esbuild-sunos-64@0.15.10: 96 | version "0.15.10" 97 | resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.10.tgz#5fe7bef267a02f322fd249a8214d0274937388a7" 98 | integrity sha512-mf7hBL9Uo2gcy2r3rUFMjVpTaGpFJJE5QTDDqUFf1632FxteYANffDZmKbqX0PfeQ2XjUDE604IcE7OJeoHiyg== 99 | 100 | esbuild-windows-32@0.15.10: 101 | version "0.15.10" 102 | resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.10.tgz#48e3dde25ab0135579a288b30ab6ddef6d1f0b28" 103 | integrity sha512-ttFVo+Cg8b5+qHmZHbEc8Vl17kCleHhLzgT8X04y8zudEApo0PxPg9Mz8Z2cKH1bCYlve1XL8LkyXGFjtUYeGg== 104 | 105 | esbuild-windows-64@0.15.10: 106 | version "0.15.10" 107 | resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.10.tgz#387a9515bef3fee502d277a5d0a2db49a4ecda05" 108 | integrity sha512-2H0gdsyHi5x+8lbng3hLbxDWR7mKHWh5BXZGKVG830KUmXOOWFE2YKJ4tHRkejRduOGDrBvHBriYsGtmTv3ntA== 109 | 110 | esbuild-windows-arm64@0.15.10: 111 | version "0.15.10" 112 | resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.10.tgz#5a6fcf2fa49e895949bf5495cf088ab1b43ae879" 113 | integrity sha512-S+th4F+F8VLsHLR0zrUcG+Et4hx0RKgK1eyHc08kztmLOES8BWwMiaGdoW9hiXuzznXQ0I/Fg904MNbr11Nktw== 114 | 115 | esbuild@^0.15.10: 116 | version "0.15.10" 117 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.10.tgz#85c2f8446e9b1fe04fae68daceacba033eedbd42" 118 | integrity sha512-N7wBhfJ/E5fzn/SpNgX+oW2RLRjwaL8Y0ezqNqhjD6w0H2p0rDuEz2FKZqpqLnO8DCaWumKe8dsC/ljvVSSxng== 119 | optionalDependencies: 120 | "@esbuild/android-arm" "0.15.10" 121 | "@esbuild/linux-loong64" "0.15.10" 122 | esbuild-android-64 "0.15.10" 123 | esbuild-android-arm64 "0.15.10" 124 | esbuild-darwin-64 "0.15.10" 125 | esbuild-darwin-arm64 "0.15.10" 126 | esbuild-freebsd-64 "0.15.10" 127 | esbuild-freebsd-arm64 "0.15.10" 128 | esbuild-linux-32 "0.15.10" 129 | esbuild-linux-64 "0.15.10" 130 | esbuild-linux-arm "0.15.10" 131 | esbuild-linux-arm64 "0.15.10" 132 | esbuild-linux-mips64le "0.15.10" 133 | esbuild-linux-ppc64le "0.15.10" 134 | esbuild-linux-riscv64 "0.15.10" 135 | esbuild-linux-s390x "0.15.10" 136 | esbuild-netbsd-64 "0.15.10" 137 | esbuild-openbsd-64 "0.15.10" 138 | esbuild-sunos-64 "0.15.10" 139 | esbuild-windows-32 "0.15.10" 140 | esbuild-windows-64 "0.15.10" 141 | esbuild-windows-arm64 "0.15.10" 142 | 143 | isomorphic-fetch@^3.0.0: 144 | version "3.0.0" 145 | resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" 146 | integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== 147 | dependencies: 148 | node-fetch "^2.6.1" 149 | whatwg-fetch "^3.4.1" 150 | 151 | node-fetch@^2.6.1: 152 | version "2.6.7" 153 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" 154 | integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== 155 | dependencies: 156 | whatwg-url "^5.0.0" 157 | 158 | tr46@~0.0.3: 159 | version "0.0.3" 160 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 161 | integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== 162 | 163 | typescript@^4.8.4: 164 | version "4.8.4" 165 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" 166 | integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== 167 | 168 | webidl-conversions@^3.0.0: 169 | version "3.0.1" 170 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 171 | integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== 172 | 173 | whatwg-fetch@^3.4.1: 174 | version "3.6.2" 175 | resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" 176 | integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== 177 | 178 | whatwg-url@^5.0.0: 179 | version "5.0.0" 180 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 181 | integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== 182 | dependencies: 183 | tr46 "~0.0.3" 184 | webidl-conversions "^3.0.0" 185 | -------------------------------------------------------------------------------- /cmd/api/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eux 4 | 5 | mrsk deploy 6 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "flag" 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "os" 11 | "runtime/debug" 12 | 13 | "github.com/contextwtf/lanyard/api" 14 | "github.com/contextwtf/lanyard/api/migrations" 15 | "github.com/contextwtf/lanyard/api/tracing" 16 | "github.com/contextwtf/migrate" 17 | "github.com/jackc/pgx/v4" 18 | "github.com/jackc/pgx/v4/pgxpool" 19 | _ "github.com/jackc/pgx/v4/stdlib" 20 | "github.com/opentracing/opentracing-go" 21 | "github.com/pkg/profile" 22 | "github.com/rs/zerolog" 23 | "github.com/rs/zerolog/log" 24 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer" 25 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 26 | ) 27 | 28 | var GitSha string 29 | 30 | func check(err error) { 31 | if err != nil { 32 | fmt.Fprintf(os.Stderr, "processor error: %s", err) 33 | debug.PrintStack() 34 | os.Exit(1) 35 | } 36 | } 37 | 38 | func main() { 39 | env := os.Getenv("ENV") 40 | if env == "" { 41 | env = "dev" 42 | } 43 | 44 | shouldProfile := flag.Bool("profile", false, "enable profiling") 45 | flag.Parse() 46 | 47 | if *shouldProfile { 48 | prof := profile.Start(profile.CPUProfile, profile.ProfilePath(".")) 49 | defer prof.Stop() 50 | } 51 | 52 | ddAgent := os.Getenv("DD_AGENT_HOST") 53 | if ddAgent != "" { 54 | t := opentracer.New( 55 | tracer.WithEnv(os.Getenv("DD_ENV")), 56 | tracer.WithService(os.Getenv("DD_SERVICE")), 57 | tracer.WithServiceVersion(GitSha), 58 | tracer.WithAgentAddr(net.JoinHostPort(ddAgent, "8126")), 59 | ) 60 | opentracing.SetGlobalTracer(t) 61 | } 62 | 63 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 64 | if env == "dev" { 65 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 66 | } 67 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix 68 | var ( 69 | logger = log.Logger.With().Caller().Logger() 70 | ctx = logger.WithContext(context.Background()) 71 | ) 72 | const defaultPGURL = "postgres:///al" 73 | dburl := os.Getenv("DATABASE_URL") 74 | if dburl == "" { 75 | dburl = defaultPGURL 76 | } 77 | dbc, err := pgxpool.ParseConfig(dburl) 78 | check(err) 79 | 80 | if ddAgent != "" { 81 | // trace db queries if tracing is enabled 82 | dbc.ConnConfig.Logger = tracing.NewDBTracer( 83 | dbc.ConnConfig.Host, 84 | ) 85 | dbc.ConnConfig.LogLevel = pgx.LogLevelTrace 86 | } 87 | 88 | dbc.MaxConns = 30 89 | db, err := pgxpool.ConnectConfig(ctx, dbc) 90 | check(err) 91 | 92 | //The migrate package wants a database/sql type 93 | //In the future, migrate may support pgx but for 94 | //now we open and close a connection for running 95 | //migrations. 96 | mdb, err := sql.Open("pgx", dburl) 97 | check(err) 98 | check(migrate.Run(ctx, mdb, migrations.Migrations)) 99 | check(mdb.Close()) 100 | 101 | s := api.New(db) 102 | 103 | const defaultListen = ":8080" 104 | listen := os.Getenv("LISTEN") 105 | if listen == "" { 106 | listen = defaultListen 107 | } 108 | hs := &http.Server{ 109 | Addr: listen, 110 | Handler: s.Handler(env, GitSha), 111 | } 112 | log.Ctx(ctx).Info().Str("listen", listen).Str("git-sha", GitSha).Msg("http server") 113 | check(hs.ListenAndServe()) 114 | } 115 | -------------------------------------------------------------------------------- /cmd/dumpschema/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "database/sql" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/contextwtf/lanyard/api/migrations" 14 | "github.com/contextwtf/migrate" 15 | _ "github.com/lib/pq" 16 | ) 17 | 18 | func check(err error) { 19 | if err != nil { 20 | panic(err) 21 | } 22 | } 23 | 24 | const temporaryDatabase = `tmp_db_for_dump_schema` 25 | 26 | func main() { 27 | check(run("dropdb", "--if-exists", temporaryDatabase)) 28 | check(run("createdb", temporaryDatabase)) 29 | 30 | db, err := sql.Open("postgres", fmt.Sprintf("postgres:///%s?sslmode=disable", temporaryDatabase)) 31 | check(err) 32 | _, err = db.Exec(fmt.Sprintf(`CREATE SCHEMA IF NOT EXISTS %s`, "public")) 33 | check(err) 34 | _, err = db.Exec(fmt.Sprintf(`ALTER DATABASE %s SET TIMEZONE TO 'UTC'`, temporaryDatabase)) 35 | check(err) 36 | db.Close() 37 | 38 | db, err = sql.Open("postgres", fmt.Sprintf("postgres:///%s?sslmode=disable&search_path=%s", temporaryDatabase, "public")) 39 | check(err) 40 | check(migrate.Run(context.Background(), db, migrations.Migrations)) 41 | 42 | var buf bytes.Buffer 43 | pgdump := exec.Command("pg_dump", "-sOx", temporaryDatabase) 44 | pgdump.Stdout = &buf 45 | pgdump.Stderr = os.Stderr 46 | check(pgdump.Run()) 47 | 48 | f, err := os.Create(filepath.Join("api", "schema.sql")) 49 | check(err) 50 | defer f.Close() 51 | 52 | for _, line := range strings.Split(buf.String(), "\n") { 53 | if strings.HasPrefix(line, "--") || strings.Contains(line, "COMMENT") { 54 | continue 55 | } 56 | _, err = f.WriteString(line + "\n") 57 | check(err) 58 | } 59 | check(db.Close()) 60 | check(run("dropdb", temporaryDatabase)) 61 | } 62 | 63 | func run(name string, args ...string) error { 64 | cmd := exec.Command(name, args...) 65 | cmd.Stderr = os.Stderr 66 | return cmd.Run() 67 | } 68 | -------------------------------------------------------------------------------- /config/deploy.yml: -------------------------------------------------------------------------------- 1 | service: lanyard 2 | 3 | # Name of the container image. 4 | image: contextwtf/lanyard 5 | 6 | # Deploy to these servers. 7 | servers: 8 | - lanyard-prod.vaquita-turtle.ts.net 9 | - lanyard-prod-2.vaquita-turtle.ts.net 10 | 11 | # Credentials for your image host. 12 | registry: 13 | server: ghcr.io 14 | username: lanyard 15 | 16 | # Always use an access token rather than real password when possible. 17 | password: 18 | - GITHUB_TOKEN 19 | 20 | traefik: 21 | host_port: 80 22 | 23 | # Inject ENV variables into containers (secrets come from .env). 24 | env: 25 | clear: 26 | DD_ENV: production 27 | DD_SERVICE: al-prod 28 | DD_AGENT_HOST: 172.17.0.1 29 | secret: 30 | - DATABASE_URL 31 | 32 | # Configure a custom healthcheck (default is /up on port 3000) 33 | healthcheck: 34 | path: /health 35 | port: 8080 36 | -------------------------------------------------------------------------------- /contracts/.gas-snapshot: -------------------------------------------------------------------------------- 1 | MerkleAllowListedNFTTest:testDeploy() (gas: 935450) 2 | MerkleAllowListedNFTTest:testEmptyProofFails() (gas: 939445) 3 | MerkleAllowListedNFTTest:testFiveAddresses() (gas: 1299445) 4 | MerkleAllowListedNFTTest:testFourAddresses() (gas: 1225742) 5 | MerkleAllowListedNFTTest:testMintNotAllowed() (gas: 940472) 6 | MerkleAllowListedNFTTest:testOneAddressEmptyProof() (gas: 1010962) 7 | MerkleAllowListedNFTTest:testOnlyMintOnce() (gas: 1011020) 8 | MerkleAllowListedNFTTest:testThreeAddresses() (gas: 1153181) 9 | MerkleAllowListedNFTTest:testTwoAddresses() (gas: 1080380) 10 | -------------------------------------------------------------------------------- /contracts/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/* 8 | /broadcast/*/31337/ 9 | -------------------------------------------------------------------------------- /contracts/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.packageDefaultDependenciesContractsDirectory": "src", 3 | "solidity.packageDefaultDependenciesDirectory": "lib" 4 | } -------------------------------------------------------------------------------- /contracts/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /contracts/remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/solmate/lib/ds-test/src/ 2 | forge-std/=lib/forge-std/src/ 3 | openzeppelin/=lib/openzeppelin-contracts/contracts/ 4 | solmate/=lib/solmate/src/ 5 | -------------------------------------------------------------------------------- /contracts/src/MerkleAllowListedNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.15; 3 | 4 | import "solmate/tokens/ERC721.sol"; 5 | import "openzeppelin/utils/cryptography/MerkleProof.sol"; 6 | 7 | contract MerkleAllowListedNFT is ERC721 { 8 | bytes32 public merkleRoot; 9 | 10 | constructor(bytes32 _merkleRoot) ERC721("MerkleAllowListedNFT", "MALNFT") { 11 | merkleRoot = _merkleRoot; 12 | } 13 | 14 | mapping(address => bool) allowListAddressMinted; 15 | 16 | function mint(uint256 tokenId, bytes32[] calldata proof) external { 17 | require(allowListed(msg.sender, proof), "not on the allow list"); 18 | require(allowListAddressMinted[msg.sender] == false, "already minted"); 19 | 20 | allowListAddressMinted[msg.sender] = true; 21 | _mint(msg.sender, tokenId); 22 | } 23 | 24 | function allowListed(address _address, bytes32[] calldata _proof) 25 | public 26 | view 27 | returns (bool) 28 | { 29 | return 30 | MerkleProof.verify( 31 | _proof, 32 | merkleRoot, 33 | keccak256(abi.encodePacked(_address)) 34 | ); 35 | } 36 | 37 | function tokenURI(uint256) public pure override returns (string memory) { 38 | revert(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/test/MerkleAllowListedNFT.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.15; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | import "../src/MerkleAllowListedNFT.sol"; 7 | 8 | contract MerkleAllowListedNFTTest is Test { 9 | function testDeploy() public { 10 | bytes32 root = 0x0000000000000000000000000000000000000000000000000000000000000001; 11 | MerkleAllowListedNFT malnft = new MerkleAllowListedNFT(root); 12 | assertEq(malnft.merkleRoot(), root); 13 | } 14 | 15 | function testMintNotAllowed() public { 16 | bytes32 root = 0x0000000000000000000000000000000000000000000000000000000000000001; 17 | MerkleAllowListedNFT malnft = new MerkleAllowListedNFT(root); 18 | 19 | bytes32[] memory proof = new bytes32[](2); 20 | proof[ 21 | 0 22 | ] = 0x1000000000000000000000000000000000000000000000000000000000000000; 23 | proof[ 24 | 1 25 | ] = 0x2000000000000000000000000000000000000000000000000000000000000000; 26 | 27 | vm.expectRevert("not on the allow list"); 28 | malnft.mint(0, proof); 29 | } 30 | 31 | function testEmptyProofFails() public { 32 | bytes32 root = 0x0000000000000000000000000000000000000000000000000000000000000001; 33 | MerkleAllowListedNFT malnft = new MerkleAllowListedNFT(root); 34 | 35 | bytes32[] memory proof = new bytes32[](0); 36 | 37 | vm.expectRevert("not on the allow list"); 38 | malnft.mint(0, proof); 39 | } 40 | 41 | function testOneAddressEmptyProof() public { 42 | bytes32 root = 0x1468288056310c82aa4c01a7e12a10f8111a0560e72b700555479031b86c357d; 43 | MerkleAllowListedNFT malnft = new MerkleAllowListedNFT(root); 44 | 45 | bytes32[] memory emptyProof = new bytes32[](0); 46 | 47 | address allowed = address(1); 48 | vm.prank(allowed); 49 | malnft.mint(0, emptyProof); 50 | 51 | address notAllowed = address(2); 52 | vm.prank(notAllowed); 53 | vm.expectRevert("not on the allow list"); 54 | malnft.mint(1, emptyProof); 55 | } 56 | 57 | function testOnlyMintOnce() public { 58 | bytes32 root = 0x1468288056310c82aa4c01a7e12a10f8111a0560e72b700555479031b86c357d; 59 | MerkleAllowListedNFT malnft = new MerkleAllowListedNFT(root); 60 | 61 | bytes32[] memory emptyProof = new bytes32[](0); 62 | 63 | address allowed = address(1); 64 | vm.prank(allowed); 65 | malnft.mint(0, emptyProof); 66 | 67 | vm.expectRevert("already minted"); 68 | vm.prank(allowed); 69 | malnft.mint(1, emptyProof); 70 | } 71 | 72 | function testTwoAddresses() public { 73 | bytes32 root = 0xf95c14e6953c95195639e8266ab1a6850864d59a829da9f9b13602ee522f672b; 74 | MerkleAllowListedNFT malnft = new MerkleAllowListedNFT(root); 75 | 76 | bytes32[] memory proof = new bytes32[](1); 77 | 78 | proof[ 79 | 0 80 | ] = 0xd52688a8f926c816ca1e079067caba944f158e764817b83fc43594370ca9cf62; 81 | vm.prank(address(1)); 82 | malnft.mint(0, proof); 83 | 84 | proof[ 85 | 0 86 | ] = 0x1468288056310c82aa4c01a7e12a10f8111a0560e72b700555479031b86c357d; 87 | vm.prank(address(2)); 88 | malnft.mint(1, proof); 89 | } 90 | 91 | function testThreeAddresses() public { 92 | bytes32 root = 0x344510bd0c324c3912b13373e89df42d1b50450e9764a454b2aa6e2968a4578a; 93 | MerkleAllowListedNFT malnft = new MerkleAllowListedNFT(root); 94 | 95 | bytes32[] memory firstProof = new bytes32[](2); 96 | firstProof[ 97 | 0 98 | ] = 0xd52688a8f926c816ca1e079067caba944f158e764817b83fc43594370ca9cf62; 99 | firstProof[ 100 | 1 101 | ] = 0x5b70e80538acdabd6137353b0f9d8d149f4dba91e8be2e7946e409bfdbe685b9; 102 | vm.prank(address(1)); 103 | malnft.mint(0, firstProof); 104 | 105 | bytes32[] memory secondProof = new bytes32[](2); 106 | secondProof[ 107 | 0 108 | ] = 0x1468288056310c82aa4c01a7e12a10f8111a0560e72b700555479031b86c357d; 109 | secondProof[ 110 | 1 111 | ] = 0x5b70e80538acdabd6137353b0f9d8d149f4dba91e8be2e7946e409bfdbe685b9; 112 | vm.prank(address(2)); 113 | malnft.mint(1, secondProof); 114 | 115 | bytes32[] memory thirdProof = new bytes32[](1); 116 | thirdProof[ 117 | 0 118 | ] = 0xf95c14e6953c95195639e8266ab1a6850864d59a829da9f9b13602ee522f672b; 119 | vm.prank(address(3)); 120 | malnft.mint(2, thirdProof); 121 | } 122 | 123 | function testFourAddresses() public { 124 | bytes32 root = 0x5071e19149cc9b870c816e671bc5db717d1d99185c17b082af957a0a93888dd9; 125 | MerkleAllowListedNFT malnft = new MerkleAllowListedNFT(root); 126 | 127 | bytes32[] memory firstProof = new bytes32[](2); 128 | firstProof[ 129 | 0 130 | ] = 0xd52688a8f926c816ca1e079067caba944f158e764817b83fc43594370ca9cf62; 131 | firstProof[ 132 | 1 133 | ] = 0x735c77c52a2b69afcd4e13c0a6ece7e4ccdf2b379d39417e21efe8cd10b5ff1b; 134 | vm.prank(address(1)); 135 | malnft.mint(0, firstProof); 136 | 137 | bytes32[] memory secondProof = new bytes32[](2); 138 | secondProof[ 139 | 0 140 | ] = 0x1468288056310c82aa4c01a7e12a10f8111a0560e72b700555479031b86c357d; 141 | secondProof[ 142 | 1 143 | ] = 0x735c77c52a2b69afcd4e13c0a6ece7e4ccdf2b379d39417e21efe8cd10b5ff1b; 144 | vm.prank(address(2)); 145 | malnft.mint(1, secondProof); 146 | 147 | bytes32[] memory thirdProof = new bytes32[](2); 148 | thirdProof[ 149 | 0 150 | ] = 0xa876da518a393dbd067dc72abfa08d475ed6447fca96d92ec3f9e7eba503ca61; 151 | thirdProof[ 152 | 1 153 | ] = 0xf95c14e6953c95195639e8266ab1a6850864d59a829da9f9b13602ee522f672b; 154 | vm.prank(address(3)); 155 | malnft.mint(2, thirdProof); 156 | 157 | bytes32[] memory fourthProof = new bytes32[](2); 158 | fourthProof[ 159 | 0 160 | ] = 0x5b70e80538acdabd6137353b0f9d8d149f4dba91e8be2e7946e409bfdbe685b9; 161 | fourthProof[ 162 | 1 163 | ] = 0xf95c14e6953c95195639e8266ab1a6850864d59a829da9f9b13602ee522f672b; 164 | vm.prank(address(4)); 165 | malnft.mint(3, fourthProof); 166 | } 167 | 168 | function testFiveAddresses() public { 169 | bytes32 root = 0xa7a6b1cb6d12308ec4818baac3413fafa9e8b52cdcd79252fa9e29c9a2f8aff1; 170 | MerkleAllowListedNFT malnft = new MerkleAllowListedNFT(root); 171 | 172 | bytes32[] memory firstProof = new bytes32[](3); 173 | firstProof[ 174 | 0 175 | ] = 0xd52688a8f926c816ca1e079067caba944f158e764817b83fc43594370ca9cf62; 176 | firstProof[ 177 | 1 178 | ] = 0x735c77c52a2b69afcd4e13c0a6ece7e4ccdf2b379d39417e21efe8cd10b5ff1b; 179 | firstProof[ 180 | 2 181 | ] = 0x421df1fa259221d02aa4956eb0d35ace318ca24c0a33a64c1af96cf67cf245b6; 182 | vm.prank(address(1)); 183 | malnft.mint(0, firstProof); 184 | 185 | bytes32[] memory secondProof = new bytes32[](3); 186 | secondProof[ 187 | 0 188 | ] = 0x1468288056310c82aa4c01a7e12a10f8111a0560e72b700555479031b86c357d; 189 | secondProof[ 190 | 1 191 | ] = 0x735c77c52a2b69afcd4e13c0a6ece7e4ccdf2b379d39417e21efe8cd10b5ff1b; 192 | secondProof[ 193 | 2 194 | ] = 0x421df1fa259221d02aa4956eb0d35ace318ca24c0a33a64c1af96cf67cf245b6; 195 | vm.prank(address(2)); 196 | malnft.mint(1, secondProof); 197 | 198 | bytes32[] memory thirdProof = new bytes32[](3); 199 | thirdProof[ 200 | 0 201 | ] = 0xa876da518a393dbd067dc72abfa08d475ed6447fca96d92ec3f9e7eba503ca61; 202 | thirdProof[ 203 | 1 204 | ] = 0xf95c14e6953c95195639e8266ab1a6850864d59a829da9f9b13602ee522f672b; 205 | thirdProof[ 206 | 2 207 | ] = 0x421df1fa259221d02aa4956eb0d35ace318ca24c0a33a64c1af96cf67cf245b6; 208 | vm.prank(address(3)); 209 | malnft.mint(2, thirdProof); 210 | 211 | bytes32[] memory fourthProof = new bytes32[](3); 212 | fourthProof[ 213 | 0 214 | ] = 0x5b70e80538acdabd6137353b0f9d8d149f4dba91e8be2e7946e409bfdbe685b9; 215 | fourthProof[ 216 | 1 217 | ] = 0xf95c14e6953c95195639e8266ab1a6850864d59a829da9f9b13602ee522f672b; 218 | fourthProof[ 219 | 2 220 | ] = 0x421df1fa259221d02aa4956eb0d35ace318ca24c0a33a64c1af96cf67cf245b6; 221 | vm.prank(address(4)); 222 | malnft.mint(3, fourthProof); 223 | 224 | bytes32[] memory fifthProof = new bytes32[](1); 225 | fifthProof[ 226 | 0 227 | ] = 0x5071e19149cc9b870c816e671bc5db717d1d99185c17b082af957a0a93888dd9; 228 | vm.prank(address(5)); 229 | malnft.mint(4, fifthProof); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /example/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | To run these examples, you need to have [bun](https://bun.sh/) installed. 4 | 5 | ```sh 6 | curl https://bun.sh/install | bash 7 | ``` 8 | 9 | ## merkle 10 | 11 | ```sh 12 | bun run src/merkle.ts 13 | ``` 14 | 15 | ## api 16 | 17 | ```sh 18 | bun run src/api.ts 19 | ``` 20 | -------------------------------------------------------------------------------- /example/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ourzora/lanyard/f080b6510b13f6f2f67cf323ba0114598230af84/example/bun.lockb -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "devDependencies": { 5 | "bun-types": "latest", 6 | "prettier": "^2.7.1" 7 | }, 8 | "dependencies": { 9 | "ethers": "^5.6.9", 10 | "lanyard": "1.1.1", 11 | "merkletreejs": "^0.2.32" 12 | } 13 | } -------------------------------------------------------------------------------- /example/src/api.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers' 2 | import { MerkleTree } from 'merkletreejs' 3 | import { createTree, getTree, getRoots, getProof } from 'lanyard' 4 | 5 | const encode = utils.defaultAbiCoder.encode.bind(utils.defaultAbiCoder) 6 | const encodePacked = utils.solidityPack 7 | 8 | const makeMerkleTree = (leafData: string[]) => 9 | new MerkleTree(leafData.map(utils.keccak256), utils.keccak256, { 10 | sortPairs: true, 11 | }) 12 | 13 | const checkRootEquality = (remote: string, local: string) => { 14 | if (remote !== local) { 15 | throw new Error(`Remote root ${remote} does not match local root ${local}`) 16 | } 17 | } 18 | 19 | const strArrEqual = (a: string[], b: string[]) => 20 | a.length === b.length && a.every((v, i) => v === b[i]) 21 | 22 | const checkProofEquality = (remote: string[], local: string[]) => { 23 | if (!strArrEqual(remote, local)) { 24 | throw new Error( 25 | `Remote proof ${remote} does not match local proof ${local}`, 26 | ) 27 | } 28 | } 29 | 30 | // basic merkle tree 31 | 32 | const unhashedLeaves = [ 33 | '0x0000000000000000000000000000000000000001', 34 | '0x0000000000000000000000000000000000000002', 35 | '0x0000000000000000000000000000000000000003', 36 | '0x0000000000000000000000000000000000000004', 37 | '0x0000000000000000000000000000000000000005', 38 | ] 39 | 40 | const { merkleRoot: basicMerkleRoot } = await createTree({ unhashedLeaves }) 41 | 42 | console.log('basic merkle root', basicMerkleRoot) 43 | checkRootEquality(basicMerkleRoot, makeMerkleTree(unhashedLeaves).getHexRoot()) 44 | 45 | const basicTree = await getTree(basicMerkleRoot) 46 | console.log('basic leaf count', basicTree.leafCount) 47 | 48 | console.log(basicMerkleRoot, unhashedLeaves[0]) 49 | 50 | const { proof: basicProof } = await getProof({ 51 | merkleRoot: basicMerkleRoot, 52 | unhashedLeaf: unhashedLeaves[0], 53 | }) 54 | 55 | console.log('proof', basicProof) 56 | checkProofEquality( 57 | basicProof, 58 | makeMerkleTree(unhashedLeaves).getHexProof( 59 | utils.keccak256(unhashedLeaves[0]), 60 | ), 61 | ) 62 | 63 | // non-address leaf data 64 | 65 | const num2Addr = (num: number) => 66 | utils.hexlify(utils.zeroPad(utils.hexlify(num), 20)) 67 | 68 | const leafData = [] 69 | 70 | for (let i = 1; i <= 5; i++) { 71 | leafData.push([num2Addr(i), 2, utils.parseEther('0.01').toString()]) 72 | } 73 | 74 | // encoded data 75 | 76 | const encodedLeafData = leafData.map((leafData) => 77 | encode(['address', 'uint256', 'uint256'], leafData), 78 | ) 79 | 80 | const { merkleRoot: encodedMerkleRoot } = await createTree({ 81 | unhashedLeaves: encodedLeafData, 82 | leafTypeDescriptor: ['address', 'uint256', 'uint256'], 83 | packedEncoding: false, 84 | }) 85 | 86 | console.log('encoded tree', encodedMerkleRoot) 87 | checkRootEquality( 88 | encodedMerkleRoot, 89 | makeMerkleTree(encodedLeafData).getHexRoot(), 90 | ) 91 | 92 | const { proof: encodedProof } = await getProof({ 93 | merkleRoot: encodedMerkleRoot, 94 | unhashedLeaf: encodedLeafData[0], 95 | }) 96 | 97 | console.log('encoded proof', encodedProof) 98 | checkProofEquality( 99 | encodedProof, 100 | makeMerkleTree(encodedLeafData).getHexProof( 101 | utils.keccak256(encodedLeafData[0]), 102 | ), 103 | ) 104 | 105 | // packed data 106 | 107 | const encodedPackedLeafData = leafData.map((leafData) => 108 | encodePacked(['address', 'uint256', 'uint256'], leafData), 109 | ) 110 | 111 | const { merkleRoot: encodedPackedMerkleRoot } = await createTree({ 112 | unhashedLeaves: encodedPackedLeafData, 113 | leafTypeDescriptor: ['address', 'uint256', 'uint256'], 114 | packedEncoding: true, 115 | }) 116 | 117 | console.log('encoded packed tree', encodedPackedMerkleRoot) 118 | checkRootEquality( 119 | encodedPackedMerkleRoot, 120 | makeMerkleTree(encodedPackedLeafData).getHexRoot(), 121 | ) 122 | 123 | const { proof: encodedPackedProof } = await getProof({ 124 | merkleRoot: encodedPackedMerkleRoot, 125 | unhashedLeaf: encodedPackedLeafData[0], 126 | }) 127 | 128 | console.log('encoded packed proof', encodedPackedProof) 129 | checkProofEquality( 130 | encodedPackedProof, 131 | makeMerkleTree(encodedPackedLeafData).getHexProof( 132 | utils.keccak256(encodedPackedLeafData[0]), 133 | ), 134 | ) 135 | 136 | const { proof: encodedPackedProofByAddress } = await getProof({ 137 | merkleRoot: encodedPackedMerkleRoot, 138 | address: num2Addr(1), 139 | }) 140 | 141 | console.log( 142 | 'encoded packed proof by indexed address', 143 | encodedPackedProofByAddress, 144 | ) 145 | checkProofEquality( 146 | encodedPackedProofByAddress, 147 | makeMerkleTree(encodedPackedLeafData).getHexProof( 148 | utils.keccak256(encodedPackedLeafData[0]), 149 | ), 150 | ) 151 | 152 | const root = await getRoots(encodedPackedProof) 153 | console.log('roots from proof', root.roots[0]) 154 | checkRootEquality(root.roots[0], encodedPackedMerkleRoot) 155 | -------------------------------------------------------------------------------- /example/src/merkle.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers' 2 | import { MerkleTree } from 'merkletreejs' 3 | 4 | const addresses: string[] = [ 5 | '0x0000000000000000000000000000000000000001', 6 | '0x0000000000000000000000000000000000000002', 7 | '0x0000000000000000000000000000000000000003', 8 | '0x0000000000000000000000000000000000000004', 9 | '0x0000000000000000000000000000000000000005', 10 | ] 11 | 12 | const tree = new MerkleTree(addresses.map(utils.keccak256), utils.keccak256, { 13 | sortPairs: true, 14 | }) 15 | 16 | console.log('the Merkle root is:', tree.getRoot().toString('hex')) 17 | 18 | export function getMerkleRoot() { 19 | return tree.getRoot().toString('hex') 20 | } 21 | 22 | export function getMerkleProof(address: string) { 23 | const hashedAddress = utils.keccak256(address) 24 | return tree.getHexProof(hashedAddress) 25 | } 26 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext"], 4 | "types": ["bun-types"], 5 | "module": "esnext", 6 | "target": "esnext", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true 11 | }, 12 | "include": ["**/*.ts", "**/*.tsx"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/contextwtf/lanyard 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/contextwtf/migrate v0.0.1 7 | github.com/ethereum/go-ethereum v1.10.21 8 | github.com/hashicorp/golang-lru/v2 v2.0.4 9 | github.com/jackc/pgx/v4 v4.16.1 10 | github.com/lib/pq v1.10.9 11 | github.com/opentracing/opentracing-go v1.2.0 12 | github.com/pkg/profile v1.2.1 13 | github.com/rs/cors v1.8.2 14 | github.com/rs/zerolog v1.29.1 15 | golang.org/x/sync v0.3.0 16 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 17 | gopkg.in/DataDog/dd-trace-go.v1 v1.40.1 18 | ) 19 | 20 | require ( 21 | github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 // indirect 22 | github.com/DataDog/datadog-go v4.8.2+incompatible // indirect 23 | github.com/DataDog/datadog-go/v5 v5.0.2 // indirect 24 | github.com/DataDog/sketches-go v1.2.1 // indirect 25 | github.com/Microsoft/go-winio v0.5.2 // indirect 26 | github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect 27 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 28 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 29 | github.com/dgraph-io/ristretto v0.1.0 // indirect 30 | github.com/dustin/go-humanize v1.0.0 // indirect 31 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 32 | github.com/google/uuid v1.3.0 // indirect 33 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 34 | github.com/jackc/pgconn v1.12.1 // indirect 35 | github.com/jackc/pgio v1.0.0 // indirect 36 | github.com/jackc/pgpassfile v1.0.0 // indirect 37 | github.com/jackc/pgproto3/v2 v2.3.0 // indirect 38 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 39 | github.com/jackc/pgtype v1.11.0 // indirect 40 | github.com/jackc/puddle v1.2.1 // indirect 41 | github.com/josharian/intern v1.0.0 // indirect 42 | github.com/mailru/easyjson v0.7.7 // indirect 43 | github.com/mattn/go-colorable v0.1.12 // indirect 44 | github.com/mattn/go-isatty v0.0.14 // indirect 45 | github.com/philhofer/fwd v1.1.1 // indirect 46 | github.com/pkg/errors v0.9.1 // indirect 47 | github.com/rs/xid v1.4.0 // indirect 48 | github.com/stretchr/testify v1.8.0 // indirect 49 | github.com/tinylib/msgp v1.1.2 // indirect 50 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 51 | golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect 52 | golang.org/x/text v0.3.8 // indirect 53 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect 54 | google.golang.org/protobuf v1.27.1 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /merkle/tree.go: -------------------------------------------------------------------------------- 1 | // A merkle tree for [lanyard.org]. 2 | // merkle uses keccak256 hashing for leaves 3 | // and intermediary nodes and therefore is vulnerable to a 4 | // second preimage attack. This package does not duplicate or pad leaves in 5 | // the case of odd cardinality and therefore may be unsafe for certain 6 | // use cases. If you are curious about this type of bug, 7 | // see the following [bitcoin issue]. 8 | // 9 | // [bitcoin issue]: https://bitcointalk.org/index.php?topic=102395.0 10 | // [lanyard.org]: https://lanyard.org 11 | package merkle 12 | 13 | import ( 14 | "bytes" 15 | 16 | "github.com/ethereum/go-ethereum/crypto" 17 | ) 18 | 19 | // A the outer list represents levels in the tree. Each level is a list 20 | // of nodes in the tree. Each node is a hash of its children. 21 | type Tree [][][]byte 22 | 23 | // Returns a complete Tree using items for the leaves. 24 | // Intermediary nodes and items will be hashed using Keccak256. 25 | func New(items [][]byte) Tree { 26 | var leaves [][]byte 27 | for i := range items { 28 | leaves = append(leaves, crypto.Keccak256(items[i])) 29 | } 30 | var t Tree 31 | t = append(t, leaves) 32 | 33 | for { 34 | level := t[len(t)-1] 35 | if len(level) == 1 { //root node 36 | break 37 | } 38 | t = append(t, hashMerge(level)) 39 | } 40 | return t 41 | } 42 | 43 | func hashPair(a, b []byte) []byte { 44 | if bytes.Compare(a, b) == -1 { // a < b 45 | return crypto.Keccak256(a, b) 46 | } 47 | return crypto.Keccak256(b, a) 48 | } 49 | 50 | // Iterates through the level pairwise merging each 51 | // pair with a hash function creating a new level that 52 | // is half the size of the level. 53 | func hashMerge(level [][]byte) [][]byte { 54 | newLevel := make([][]byte, 0, (len(level)+1)/2) 55 | for i := 0; i < len(level); i += 2 { 56 | switch { 57 | case i+1 == len(level): 58 | // In the case of a level with an odd number of nodes 59 | // we leave the parent with a single child. 60 | // Some merkle tree designs allow for the parent 61 | // to duplicate the child so that it has both children 62 | // thus leaving the level with an even number of nodes. 63 | // We don't have that requirement yet and if one day we do 64 | // this is the spot to change: 65 | newLevel = append(newLevel, level[i]) 66 | default: 67 | newLevel = append(newLevel, hashPair(level[i], level[i+1])) 68 | } 69 | } 70 | return newLevel 71 | } 72 | 73 | func (t Tree) Root() []byte { 74 | return t[len(t)-1][0] 75 | } 76 | 77 | // Returns the index of the target leaf in the tree. 78 | // If the target is not a leaf in the tree, returns -1. 79 | func (t Tree) Index(target []byte) int { 80 | ht := crypto.Keccak256(target) 81 | for i, h := range t[0] { 82 | if bytes.Equal(ht, h) { 83 | return i 84 | } 85 | } 86 | return -1 87 | } 88 | 89 | // Returns a list of hashes such that 90 | // cumulatively hashing the list pairwise 91 | // will yield the root hash of the tree. Example: 92 | // 93 | // [abcde] 94 | // [abcd, e] 95 | // [ab, cd, e] 96 | // [a, b, c, d, e] 97 | // 98 | // If the target is 'c' Proof returns: 99 | // [d, ab, e] 100 | // 101 | // If the target is 'e' Proof returns: 102 | // [cd] 103 | // 104 | // The result of this func will be used in [Valid] 105 | func (t Tree) Proof(index int) [][]byte { 106 | var proof [][]byte 107 | for _, level := range t { 108 | var i int 109 | switch { 110 | case index%2 == 0: 111 | i = index + 1 112 | case index%2 == 1: 113 | i = index - 1 114 | } 115 | if i < len(level) { 116 | proof = append(proof, level[i]) 117 | } 118 | index = index / 2 119 | } 120 | return proof 121 | } 122 | 123 | // Returns proofs for all leafs in the tree. 124 | // For details on how an individual proof is calculated, see [Tree.Proof]. 125 | func (t Tree) LeafProofs() [][][]byte { 126 | proofs := make([][][]byte, len(t[0])) 127 | 128 | for i := range t[0] { 129 | proofs[i] = t.Proof(i) 130 | } 131 | 132 | return proofs 133 | } 134 | 135 | // Cumulatively hashes the list pairwise starting with 136 | // (target, proof[0]). Finally, the cumulative hash is compared with the root. 137 | func Valid(root []byte, proof [][]byte, target []byte) bool { 138 | target = crypto.Keccak256(target) 139 | for i := range proof { 140 | target = hashPair(target, proof[i]) 141 | } 142 | return bytes.Equal(target, root) 143 | } 144 | -------------------------------------------------------------------------------- /merkle/tree_test.go: -------------------------------------------------------------------------------- 1 | package merkle 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | "golang.org/x/sync/errgroup" 10 | ) 11 | 12 | func ExampleTree() { 13 | var addrStrs = []string{ 14 | "0xE124F06277b5AC791bA45B92853BA9A0ea93327D", 15 | "0x07d048f78B7C093B3Ef27D478B78026a70D9734e", 16 | "0x38976611f5f7bEAd7e79E752f5B80AE72dD3eFa7", 17 | "0x1Ab00ffedD724B930080aD30269083F1453cF34E", 18 | "0x860a6bC426C3bb1186b2E11Ac486ABa000C209B4", 19 | "0x0B3eC21fc53AD8b17AF4A80723c1496541fCb35f", 20 | "0x2D13F6CEe6dA8b30a84ee7954594925bd5E47Ab7", 21 | "0x3C64Cd43331beb5B6fAb76dbAb85226955c5CC3A", 22 | "0x238dA873f984188b4F4c7efF03B5580C65a49dcB", 23 | "0xbAfC038aDfd8BcF6E632C797175A057714416d04", 24 | } 25 | var addrs [][]byte 26 | for i := range addrStrs { 27 | addrs = append(addrs, common.HexToAddress(addrStrs[i]).Bytes()) 28 | } 29 | var tr Tree 30 | tr = New(addrs) 31 | fmt.Println(common.Bytes2Hex(tr.Root())) 32 | 33 | // Output: 34 | // ed40d49077a2cd13601cf79a512e6b92c7fd0f952e7dc9f4758d7134f9712bc4 35 | } 36 | 37 | func TestRoot(t *testing.T) { 38 | cases := []struct { 39 | desc string 40 | leaves [][]byte 41 | wantRoot []byte 42 | }{ 43 | { 44 | leaves: [][]byte{ 45 | []byte("a"), 46 | []byte("b"), 47 | []byte("c"), 48 | []byte("d"), 49 | []byte("e"), 50 | []byte("f"), 51 | }, 52 | wantRoot: common.Hex2Bytes("9012f1e18a87790d2e01faace75aaaca38e53df437cdce2c0552464dda4af49c"), 53 | }, 54 | } 55 | 56 | for _, tc := range cases { 57 | mt := New(tc.leaves) 58 | if !bytes.Equal(mt.Root(), tc.wantRoot) { 59 | t.Errorf("got: %s want: %s", 60 | common.Bytes2Hex(mt.Root()), 61 | common.Bytes2Hex(tc.wantRoot), 62 | ) 63 | } 64 | } 65 | } 66 | 67 | func TestProof(t *testing.T) { 68 | cases := []struct { 69 | leaves [][]byte 70 | }{ 71 | { 72 | leaves: [][]byte{ 73 | []byte("a"), 74 | []byte("b"), 75 | []byte("c"), 76 | []byte("d"), 77 | []byte("e"), 78 | }, 79 | }, 80 | } 81 | 82 | for _, tc := range cases { 83 | mt := New(tc.leaves) 84 | for i, l := range tc.leaves { 85 | pf := mt.Proof(i) 86 | if !Valid(mt.Root(), pf, l) { 87 | t.Error("invalid proof") 88 | } 89 | } 90 | } 91 | } 92 | 93 | func TestIndex(t *testing.T) { 94 | leaves := [][]byte{ 95 | []byte("a"), 96 | []byte("b"), 97 | []byte("c"), 98 | []byte("d"), 99 | []byte("e"), 100 | } 101 | mt := New(leaves) 102 | for i, l := range leaves { 103 | r := mt.Index(l) 104 | if r != i { 105 | t.Errorf("incorrect index, expected %d, got %d", i, r) 106 | } 107 | } 108 | } 109 | 110 | func TestMissingIndex(t *testing.T) { 111 | mt := New([][]byte{ 112 | []byte("a"), 113 | []byte("b"), 114 | []byte("c"), 115 | []byte("d"), 116 | []byte("e"), 117 | }) 118 | 119 | pf := mt.Index([]byte("f")) 120 | if pf != -1 { 121 | t.Errorf("incorrect index, expected %d, got %d", -1, pf) 122 | } 123 | } 124 | 125 | func BenchmarkNew(b *testing.B) { 126 | var leaves [][]byte 127 | for i := 0; i < 50000; i++ { 128 | leaves = append(leaves, []byte{byte(i)}) 129 | } 130 | 131 | for i := 0; i < b.N; i++ { 132 | New(leaves) 133 | } 134 | } 135 | 136 | func BenchmarkProof(b *testing.B) { 137 | var leaves [][]byte 138 | for i := 0; i < 50000; i++ { 139 | leaves = append(leaves, []byte{byte(i)}) 140 | } 141 | mt := New(leaves) 142 | for i := 0; i < b.N; i++ { 143 | var eg errgroup.Group 144 | for i := range leaves { 145 | i := i 146 | eg.Go(func() error { 147 | mt.Proof(i) 148 | return nil 149 | }) 150 | } 151 | eg.Wait() 152 | } 153 | } 154 | 155 | func BenchmarkProofs(b *testing.B) { 156 | var leaves [][]byte 157 | for i := 0; i < 50000; i++ { 158 | leaves = append(leaves, []byte{byte(i)}) 159 | } 160 | mt := New(leaves) 161 | for i := 0; i < b.N; i++ { 162 | mt.LeafProofs() 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /www/.env: -------------------------------------------------------------------------------- 1 | API_URL=https://lanyard.org 2 | -------------------------------------------------------------------------------- /www/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.typegen.ts 2 | .vscode/**/* 3 | next-env.d.ts 4 | postcss.config.js 5 | -------------------------------------------------------------------------------- /www/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaVersion: 2020, 5 | sourceType: 'module', 6 | project: './tsconfig.json', 7 | ecmaFeatures: { 8 | jsx: true, 9 | }, 10 | }, 11 | settings: { 12 | react: { 13 | version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use 14 | }, 15 | }, 16 | plugins: ['lodash', 'react-hooks'], 17 | extends: [ 18 | 'plugin:react-hooks/recommended', 19 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 20 | 'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 21 | 'next', 22 | 'plugin:compat/recommended', 23 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 24 | ], 25 | env: { 26 | browser: true, 27 | }, 28 | rules: { 29 | '@typescript-eslint/explicit-module-boundary-types': 'off', 30 | 'react/react-in-jsx-scope': 'off', 31 | '@typescript-eslint/restrict-template-expressions': 'error', 32 | '@typescript-eslint/switch-exhaustiveness-check': 'error', 33 | 'lodash/import-scope': 'error', 34 | 'react/prop-types': 'off', 35 | '@next/next/no-img-element': 'off', 36 | 'jsx-a11y/alt-text': 'off', 37 | 'no-debugger': 'error', 38 | 'no-warning-comments': ['error', { terms: ['fixme'] }], 39 | 'no-console': ['error', { allow: ['warn', 'error'] }], 40 | 'no-warning-comments': ['error', { terms: ['fixme'] }], 41 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 42 | 'no-extra-boolean-cast': 'error', 43 | '@typescript-eslint/strict-boolean-expressions': 'error', 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | 15 | # production 16 | build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # we don't use NPM 37 | package-lock.json 38 | 39 | # build folder 40 | .build/ 41 | 42 | .eslintcache 43 | 44 | tsconfig.tsbuildinfo 45 | 46 | test-output 47 | test-results 48 | -------------------------------------------------------------------------------- /www/.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /www/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /www/components/About.tsx: -------------------------------------------------------------------------------- 1 | import FAQ from 'components/FAQ' 2 | 3 | export default function About() { 4 | return ( 5 | <> 6 |
7 | partner logos 12 |
13 | An open source project from{' '} 14 | mint.fun,{' '} 15 | Zora, and{' '} 16 | Context 17 |
18 |
19 | 20 |
21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | const PartnerLink = ({ 28 | href, 29 | children, 30 | }: { 31 | href: string 32 | children: React.ReactNode 33 | }) => ( 34 | 40 | {children} 41 | 42 | ) 43 | -------------------------------------------------------------------------------- /www/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import React from 'react' 3 | 4 | type Props = { 5 | onClick?: () => void 6 | label: string 7 | disabled?: boolean 8 | pending?: boolean 9 | className?: string 10 | } 11 | 12 | function Button({ 13 | onClick, 14 | label, 15 | disabled = false, 16 | pending = false, 17 | className, 18 | }: Props) { 19 | return ( 20 | 50 | ) 51 | } 52 | 53 | export default React.memo(Button) 54 | 55 | const Spinner = () => ( 56 | 62 | 70 | 75 | 76 | ) 77 | -------------------------------------------------------------------------------- /www/components/CodeBlock/CopyCodeButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react' 2 | import classNames from 'classnames' 3 | import { copyToClipboard } from 'utils/clipboard' 4 | 5 | type Props = { 6 | codeForCopy(): string 7 | className?: string 8 | } 9 | 10 | function CopyCodeButton({ codeForCopy, className }: Props) { 11 | const [justCopied, justCopiedSet] = useState(false) 12 | 13 | const copyCode = useCallback(() => { 14 | copyToClipboard(codeForCopy()) 15 | justCopiedSet(true) 16 | const id = setTimeout(() => justCopiedSet(false), 1000) 17 | return () => clearTimeout(id) 18 | }, [codeForCopy]) 19 | 20 | return ( 21 | 33 | ) 34 | } 35 | 36 | export default React.memo(CopyCodeButton) 37 | -------------------------------------------------------------------------------- /www/components/CodeBlock/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import Highlight, { defaultProps, Language } from 'prism-react-renderer' 3 | import builtinTheme from 'prism-react-renderer/themes/ultramin' 4 | import classNames from 'classnames' 5 | import loadSolidityLanguageHighlighting from './loadSolidityLanguageHighlighting' 6 | import CopyCodeButton from './CopyCodeButton' 7 | 8 | loadSolidityLanguageHighlighting() 9 | 10 | const theme = { 11 | ...builtinTheme, 12 | plain: { 13 | backgroundColor: 'transparent', 14 | }, 15 | } 16 | 17 | type Props = { 18 | code: string 19 | codeForCopy?(): string 20 | language: Language | 'sol' | 'txt' 21 | oneLiner?: boolean 22 | title?: string 23 | } 24 | 25 | function CodeBlock({ 26 | title, 27 | code, 28 | codeForCopy, 29 | oneLiner = false, 30 | language, 31 | }: Props) { 32 | const codeForCopyButton = useCallback( 33 | () => codeForCopy?.() ?? code, 34 | [codeForCopy, code], 35 | ) 36 | 37 | return ( 38 |
39 | {title !== undefined && ( 40 |
46 | {title} 47 | 48 |
49 | )} 50 | 51 |
52 | {oneLiner ? ( 53 |
{code}
54 | ) : ( 55 | 61 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 62 |
66 |                 {tokens.map((line, i) => (
67 |                   
68 | {line.map((token, key) => ( 69 | 70 | ))} 71 |
72 | ))} 73 |
74 | )} 75 |
76 | )} 77 | {oneLiner && title === undefined && ( 78 | 82 | )} 83 |
84 |
85 | ) 86 | } 87 | 88 | export default React.memo(CodeBlock) 89 | -------------------------------------------------------------------------------- /www/components/CodeBlock/loadSolidityLanguageHighlighting.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-nocheck 3 | import Prism from 'prism-react-renderer/prism' 4 | 5 | // Ref: https://github.com/FormidableLabs/prism-react-renderer#faq 6 | // (open the "How do I add more language highlighting support?" question) 7 | 8 | export default function loadSolidityLanguageHighlighting() { 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | const _global: { Prism?: any } | undefined = 11 | typeof global !== 'undefined' 12 | ? global 13 | : typeof window !== 'undefined' 14 | ? window 15 | : undefined 16 | if (_global !== undefined) { 17 | _global.Prism = Prism 18 | } 19 | 20 | require('prismjs/components/prism-solidity') 21 | } 22 | -------------------------------------------------------------------------------- /www/components/Container.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import { ReactNode } from 'react' 3 | 4 | const Container = ({ 5 | children, 6 | variant, 7 | }: { 8 | children: ReactNode 9 | variant: 'success' | 'failure' 10 | }) => ( 11 |
18 | {children} 19 |
20 | ) 21 | 22 | export const FailureContainer = ({ children }: { children: ReactNode }) => ( 23 | {children} 24 | ) 25 | 26 | export const SuccessContainer = ({ children }: { children: ReactNode }) => ( 27 | {children} 28 | ) 29 | -------------------------------------------------------------------------------- /www/components/CreateRoot.tsx: -------------------------------------------------------------------------------- 1 | import { useAsync } from '@react-hook/async' 2 | import classNames from 'classnames' 3 | import { useCallback, useEffect, useMemo, useState } from 'react' 4 | import { isErrorResponse, createMerkleRoot } from 'utils/api' 5 | import Button from 'components/Button' 6 | import { parseAddressesFromText, prepareAddresses } from 'utils/addressParsing' 7 | import { useRouter } from 'next/router' 8 | import { randomBytes } from 'crypto' 9 | import { resolveEnsDomains } from 'utils/ens' 10 | 11 | const useCreateMerkleRoot = () => { 12 | const [ensMap, setEnsMap] = useState>({}) 13 | 14 | const [{ value, status, error: reqError }, create] = useAsync( 15 | async (addressesOrENSNames: string[]) => { 16 | let prepared = prepareAddresses(addressesOrENSNames, ensMap) 17 | 18 | if (prepared.unresolvedEnsNames.length > 0) { 19 | const ensAddresses = await resolveEnsDomains( 20 | prepared.unresolvedEnsNames, 21 | ) 22 | 23 | setEnsMap((prev) => ({ 24 | ...prev, 25 | ...ensAddresses, 26 | })) 27 | 28 | prepared = prepareAddresses(addressesOrENSNames, { 29 | ...ensMap, 30 | ...ensAddresses, 31 | }) 32 | 33 | if (prepared.unresolvedEnsNames.length > 0) { 34 | throw new Error(`Could not resolve all ENS names`) 35 | } 36 | } 37 | 38 | if (prepared.addresses.length !== prepared.dedupedAddresses.length) { 39 | return ( 40 | await Promise.all([ 41 | createMerkleRoot(prepared.dedupedAddresses), 42 | createMerkleRoot(prepared.addresses), 43 | ]) 44 | )[0] 45 | } 46 | 47 | return await createMerkleRoot(prepared.dedupedAddresses) 48 | }, 49 | ) 50 | 51 | const merkleRoot = useMemo(() => { 52 | if (value === undefined) return undefined 53 | if (isErrorResponse(value)) return undefined 54 | return value.merkleRoot 55 | }, [value]) 56 | 57 | const error = useMemo(() => { 58 | if (isErrorResponse(value)) return value 59 | if (reqError !== undefined) 60 | return { error: true, message: reqError.message } 61 | return undefined 62 | }, [value, reqError]) 63 | 64 | return { merkleRoot, error, status, create, parsedEnsNames: ensMap } 65 | } 66 | 67 | const randomAddress = () => `0x${randomBytes(20).toString('hex')}` 68 | 69 | export default function CreateRoot() { 70 | const { 71 | merkleRoot, 72 | error: errorResponse, 73 | status, 74 | create, 75 | parsedEnsNames, 76 | } = useCreateMerkleRoot() 77 | const [addressInput, addressInputSet] = useState('') 78 | 79 | const handleSubmit = useCallback(() => { 80 | if (addressInput.trim().length === 0) { 81 | return 82 | } 83 | 84 | const addresses = parseAddressesFromText(addressInput) 85 | create(addresses) 86 | }, [addressInput, create]) 87 | 88 | const parsedAddresses = useMemo( 89 | () => parseAddressesFromText(addressInput), 90 | [addressInput], 91 | ) 92 | 93 | const parsedAddressesCount = useMemo( 94 | () => parsedAddresses.length, 95 | [parsedAddresses], 96 | ) 97 | 98 | const router = useRouter() 99 | 100 | useEffect(() => { 101 | if (merkleRoot !== undefined) { 102 | router.push(`/tree/${merkleRoot}`) 103 | } 104 | }, [merkleRoot, router]) 105 | 106 | const handleLoadExample = useCallback(() => { 107 | const addresses = Array(Math.floor(Math.random() * 16) + 3) 108 | .fill(0) 109 | .map(() => randomAddress()) 110 | 111 | addressInputSet(addresses.join('\n')) 112 | }, []) 113 | 114 | const handleRemoveInvalidENSNames = useCallback(() => { 115 | const addresses = parsedAddresses 116 | .filter((address) => { 117 | return ( 118 | !address.includes('.') || 119 | parsedEnsNames[address.toLowerCase()] !== undefined 120 | ) 121 | }) 122 | .join('\n') 123 | addressInputSet(addresses) 124 | }, [parsedAddresses, parsedEnsNames]) 125 | 126 | const showRemoveInvalidENSNames = useMemo( 127 | // show the button if the error message contains `Could not resolve all ENS names` 128 | () => 129 | errorResponse?.message?.includes('Could not resolve all ENS names') ?? 130 | false, 131 | [errorResponse?.message], 132 | ) 133 | 134 | const buttonPending = status === 'loading' || merkleRoot !== undefined 135 | 136 | return ( 137 |
138 |
139 |