added some posts and fixed the mobile button
59fc0b59
3 file(s) · +710 −47
| 1 | 1 | --- |
|
| 2 | - | import ThemeToggle from "../ThemeToggle.astro"; |
|
| 3 | 2 | import { MENU_LINKS } from "@/data/constants"; |
|
| 4 | 3 | ||
| 5 | 4 | const url = new URL(Astro.request.url); |
|
| 8 | 7 | <script> |
|
| 9 | 8 | import { toggleClass } from "@/utils"; |
|
| 10 | 9 | ||
| 11 | - | document.addEventListener("DOMContentLoaded", () => { |
|
| 12 | - | const header = document.getElementById("main-header") as HTMLElement; |
|
| 13 | - | const toggleMenuButton = document.getElementById("toggle-navigation-menu") as HTMLButtonElement; |
|
| 14 | - | let menuOpen = false; |
|
| 10 | + | class MobileNavBtn extends HTMLElement { |
|
| 11 | + | constructor() { |
|
| 12 | + | super(); |
|
| 13 | + | const headerEl = document.getElementById("main-header")!; |
|
| 14 | + | const mobileButtonEl = document.getElementById("toggle-navigation-menu") as HTMLButtonElement; |
|
| 15 | + | let menuOpen = false; |
|
| 16 | + | ||
| 17 | + | function toggleMobileMenu() { |
|
| 18 | + | toggleClass(headerEl, "menu-open"); |
|
| 19 | + | menuOpen = !menuOpen; |
|
| 20 | + | mobileButtonEl.setAttribute("aria-expanded", menuOpen.toString()); |
|
| 21 | + | } |
|
| 22 | + | ||
| 23 | + | mobileButtonEl.addEventListener("click", toggleMobileMenu); |
|
| 24 | + | ||
| 25 | + | document.addEventListener("astro:after-swap", () => { |
|
| 26 | + | if (menuOpen) toggleMobileMenu(); |
|
| 27 | + | }); |
|
| 28 | + | } |
|
| 29 | + | } |
|
| 15 | 30 | ||
| 16 | - | toggleMenuButton.addEventListener("click", () => { |
|
| 17 | - | toggleClass(header, "menu-open"); |
|
| 18 | - | menuOpen = !menuOpen; |
|
| 19 | - | toggleMenuButton.setAttribute("aria-expanded", menuOpen.toString()); |
|
| 20 | - | }); |
|
| 21 | - | }); |
|
| 31 | + | customElements.define("mobile-button", MobileNavBtn); |
|
| 32 | + | ||
| 22 | 33 | </script> |
|
| 23 | 34 | ||
| 24 | 35 | <header id="main-header" class="group relative mb-28 flex items-center justify-between sm:pl-[4.5rem]"> |
|
| 65 | 76 | } |
|
| 66 | 77 | </nav> |
|
| 67 | 78 | </div> |
|
| 68 | - | <button |
|
| 69 | - | id="toggle-navigation-menu" |
|
| 70 | - | class="group relative ml-8 h-7 w-7 sm:invisible sm:hidden" |
|
| 71 | - | type="button" |
|
| 72 | - | aria-label="Open main menu" |
|
| 73 | - | aria-expanded="false" |
|
| 74 | - | aria-haspopup="menu" |
|
| 75 | - | > |
|
| 76 | - | <svg |
|
| 77 | - | id="line-svg" |
|
| 78 | - | class="absolute top-1/2 left-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 transition-all group-aria-expanded:scale-0 group-aria-expanded:opacity-0" |
|
| 79 | - | aria-hidden="true" |
|
| 80 | - | focusable="false" |
|
| 81 | - | xmlns="http://www.w3.org/2000/svg" |
|
| 82 | - | fill="none" |
|
| 83 | - | viewBox="0 0 24 24" |
|
| 84 | - | stroke-width="1.5" |
|
| 85 | - | stroke="currentColor" |
|
| 86 | - | > |
|
| 87 | - | <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 9h16.5m-16.5 6.75h16.5"></path> |
|
| 88 | - | </svg> |
|
| 89 | - | <svg |
|
| 90 | - | id="cross-svg" |
|
| 91 | - | class="absolute top-1/2 left-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 scale-0 text-accent opacity-0 transition-all group-aria-expanded:scale-100 group-aria-expanded:opacity-100" |
|
| 92 | - | class="text-accent" |
|
| 93 | - | aria-hidden="true" |
|
| 94 | - | focusable="false" |
|
| 95 | - | xmlns="http://www.w3.org/2000/svg" |
|
| 96 | - | fill="none" |
|
| 97 | - | viewBox="0 0 24 24" |
|
| 98 | - | stroke-width="1.5" |
|
| 99 | - | stroke="currentColor" |
|
| 100 | - | > |
|
| 101 | - | <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path> |
|
| 102 | - | </svg> |
|
| 103 | - | </button> |
|
| 79 | + | <mobile-button> |
|
| 80 | + | <button |
|
| 81 | + | id="toggle-navigation-menu" |
|
| 82 | + | class="group relative ml-8 h-7 w-7 sm:invisible sm:hidden" |
|
| 83 | + | type="button" |
|
| 84 | + | aria-label="Open main menu" |
|
| 85 | + | aria-expanded="false" |
|
| 86 | + | aria-haspopup="menu" |
|
| 87 | + | > |
|
| 88 | + | <svg |
|
| 89 | + | id="line-svg" |
|
| 90 | + | class="absolute top-1/2 left-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 transition-all group-aria-expanded:scale-0 group-aria-expanded:opacity-0" |
|
| 91 | + | aria-hidden="true" |
|
| 92 | + | focusable="false" |
|
| 93 | + | xmlns="http://www.w3.org/2000/svg" |
|
| 94 | + | fill="none" |
|
| 95 | + | viewBox="0 0 24 24" |
|
| 96 | + | stroke-width="1.5" |
|
| 97 | + | stroke="currentColor" |
|
| 98 | + | > |
|
| 99 | + | <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 9h16.5m-16.5 6.75h16.5"></path> |
|
| 100 | + | </svg> |
|
| 101 | + | <svg |
|
| 102 | + | id="cross-svg" |
|
| 103 | + | class="absolute top-1/2 left-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 scale-0 text-accent opacity-0 transition-all group-aria-expanded:scale-100 group-aria-expanded:opacity-100" |
|
| 104 | + | class="text-accent" |
|
| 105 | + | aria-hidden="true" |
|
| 106 | + | focusable="false" |
|
| 107 | + | xmlns="http://www.w3.org/2000/svg" |
|
| 108 | + | fill="none" |
|
| 109 | + | viewBox="0 0 24 24" |
|
| 110 | + | stroke-width="1.5" |
|
| 111 | + | stroke="currentColor" |
|
| 112 | + | > |
|
| 113 | + | <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path> |
|
| 114 | + | </svg> |
|
| 115 | + | </button> |
|
| 116 | + | </mobile-button> |
|
| 117 | + | ||
| 104 | 118 | </header> |
|
| 1 | + | --- |
|
| 2 | + | title: "How to Run Your Own Public IPFS Gateway" |
|
| 3 | + | publishDate: "10 Oct 2023" |
|
| 4 | + | description: "Learn how to run a public IPFS gateway with a custom domain using Digital Ocean" |
|
| 5 | + | tags: ["IPFS", "Gateway", "Dedicated Gateway", "Hosting", "Digital Ocean"] |
|
| 6 | + | ogImage: "https://assets-global.website-files.com/629e4fe96456f8219203e7f1/6525558858895876456798a8_20231010_How%20To%20Run%20Your%20Own%20IPFS%20Gateway.jpeg" |
|
| 7 | + | --- |
|
| 8 | + | ||
| 9 | + | import { Image } from "@astrojs/image/components"; |
|
| 10 | + | ||
| 11 | + | ||
| 12 | + | IPFS has proven to be the decentralized storage protocol of choice by many blockchain developers, and one of the crucial tools used to access content on IPFS are [Gateways](https://www.pinata.cloud/blog/what-is-an-ipfs-gateway). IPFS Gateways are like bridges between the IPFS protocol and the HTTP protocol that we use everyday to browse websites. There are lots of different options to choose from when it comes to IPFS Gateways, and in this post we’ll show you how to host and build your own! |
|
| 13 | + | ||
| 14 | + | <aside> |
|
| 15 | + | ⚠️ Warning! This guide will show you how to make a public IPFS Gateway that can access any CID on IPFS, which means it has the potential to be abused. Please be cautious and look into ways you can secure your gateway. |
|
| 16 | + | ||
| 17 | + | </aside> |
|
| 18 | + | ||
| 19 | + | ## Requirements |
|
| 20 | + | ||
| 21 | + | In order to follow this guide you’ll need a few things. First and foremost you’ll need a decent amount of experience using Linux servers and navigating around in the terminal, things like creating daemons or editing text files with vi or nano. You’ll also need a cloud server provider, and there’s plenty to choose from. In this guide we’ll use Digital Ocean and get a simple droplet. Also if you want to have a custom domain instead of using an IP address you can get something through a domain provider like Namecheap. |
|
| 22 | + | ||
| 23 | + | ## Setting Up the Server |
|
| 24 | + | ||
| 25 | + | Before we rent a server for our IPFS node, you’ll want to create an SSH key to login with. This is the preferred secure way to SSH into your server versus a user name and password. You can check out [this guide](https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/) on how to create them. |
|
| 26 | + | ||
| 27 | + | It will prompt you to put in a passphrase to access the key so choose something secure, and once created it will make a file located in `~/.ssh/rsa.pub`. We’ll use the contents in just a moment when we setup the server. |
|
| 28 | + | ||
| 29 | + | Since we’ll be using DigitalOcean you can head over there to create an account and buy a Droplet. You definitely don’t need anything crazy, I just got the following: |
|
| 30 | + | ||
| 31 | + | <Image |
|
| 32 | + | src="https://res.cloudinary.com/df9dofjus/image/upload/v1701308413/digital-ocean-1_d3skf1.png" |
|
| 33 | + | alt="digital ocean droplet creation" |
|
| 34 | + | width={1920} |
|
| 35 | + | aspectRatio={ 2/1 } |
|
| 36 | + | /> |
|
| 37 | + | ||
| 38 | + | For the authorization select SSH Keys, then copy and paste the contents of `~/.ssh/rsa.pub` and paste it in as a new key. |
|
| 39 | + | ||
| 40 | + | <Image |
|
| 41 | + | src="https://res.cloudinary.com/df9dofjus/image/upload/v1701308420/digital-ocean-2_xhdpos.png" |
|
| 42 | + | alt="digital ocean ssh key creation" |
|
| 43 | + | width={1920} |
|
| 44 | + | aspectRatio={ 3/1 } |
|
| 45 | + | /> |
|
| 46 | + | ||
| 47 | + | After the droplet has been created, you will actually want to turn it off, go to the Network settings, and enable IPV6. Once enabled turn it back on and try to SSH into it with the following command with the IPV4 address of the server: |
|
| 48 | + | ||
| 49 | + | `ssh root@ipv4address` |
|
| 50 | + | ||
| 51 | + | It should prompt you to enter in the passphrase for you SSH key, and after entering it you should be in! |
|
| 52 | + | ||
| 53 | + | While we are signed in, we are currently logged in as root, which is not the most secure practice. This next step is optional, but highly recommend. First we’ll create a new user with the command `adduser steve`, and of course you can use whatever username you want to. It will prompt you to make a new password and for some other information you can leave blank. Next we need to give the user the permissions necessary to run the IPFS node with `sudo usermod -aG sudo steve` (and of course from this point on replace `steve` with the username you chose). |
|
| 54 | + | ||
| 55 | + | Next we’ll need to run the following commands to create an `.ssh` directory for our new user and give proper permissions so we can edit it. |
|
| 56 | + | ||
| 57 | + | ``` |
|
| 58 | + | mkdir /home/steve/.ssh |
|
| 59 | + | touch /home/steve/.ssh/authorized_keys |
|
| 60 | + | sudo chown -R steve:steve /home/steve/.ssh |
|
| 61 | + | sudo chmod 700 /home/steve/.ssh |
|
| 62 | + | sudo chmod 600 /home/steve/.ssh/authorized_keys |
|
| 63 | + | ``` |
|
| 64 | + | ||
| 65 | + | With those commands completed you can now login as your user with `su steve` and then edit the SSH keys files to paste in your own that we used earlier with either `vim` or `nano` then `/.ssh/authorized_keys`. After pasting in your key you can run `exit` to log out of the user, then again to leave the SSH session. Now you should be able to SSH in with the new user `ssh steve@ipv4address`. |
|
| 66 | + | ||
| 67 | + | ## Install IPFS |
|
| 68 | + | ||
| 69 | + | Once you’re in your server you can run `sudo apt update` just to make sure all your packages are up to date. Then you will want to visit the [release page for IPFS Kubo](https://github.com/ipfs/kubo/releases), the Go implementation of an IPFS node used pretty much everywhere. On that page you can choose the latest stable release, then locate the correct distribution for your OS. In my particular case it ended up being `kubo_v0.22.0_linux-amd64.tar.gz`. Copy the link to that file, then back in your terminal for the droplet run |
|
| 70 | + | ||
| 71 | + | ```bash |
|
| 72 | + | wget "https://github.com/ipfs/kubo/releases/download/v0.22.0/kubo_v0.22.0_linux-amd64.tar.gz" |
|
| 73 | + | ``` |
|
| 74 | + | ||
| 75 | + | This will download the Kubo zip file to your home directory. You can unzip it with `tar -xf kubo_v0.22.0_linux-amd64.tar.gz` and then you should see a folder just called “kubo.” `cd` into that folder then run `sudo ./install.sh` and that will move the binary from the folder into your `/usr/local/bin` folder. To make sure it worked, try running `ipfs --version` , it should show the version number if successful. |
|
| 76 | + | ||
| 77 | + | With IPFS installed on our server the next thing we need to do is create a `systemd` service aka a daemon. This will make sure that IPFS is always running and will start up automatically if we ever reboot the server. To do this you will want to either use `sudo` with either `vim` or `nano` and create a file called `ipfs.service` under `/etc/systemd/user/` , so altogether would look something like `sudo vim /etc/systemd/user/ipfs.service`. Once the editor is open you can paste in the following: |
|
| 78 | + | ||
| 79 | + | ```makefile |
|
| 80 | + | [Unit] |
|
| 81 | + | Description=InterPlanetary File System (IPFS) daemon |
|
| 82 | + | Documentation=https://docs.ipfs.io/ |
|
| 83 | + | After=network.target |
|
| 84 | + | ||
| 85 | + | [Service] |
|
| 86 | + | Type=notify |
|
| 87 | + | ExecStart=/usr/local/bin/ipfs daemon --enable-gc=true --migrate=true |
|
| 88 | + | ExecStop=/usr/local/bin/ipfs shutdown |
|
| 89 | + | Restart=on-failure |
|
| 90 | + | KillSignal=SIGINT |
|
| 91 | + | ||
| 92 | + | [Install] |
|
| 93 | + | WantedBy=default.target |
|
| 94 | + | ``` |
|
| 95 | + | ||
| 96 | + | Save the file and exit the editor, then run the following commands to start up the daemon and make it persist between logins: |
|
| 97 | + | ||
| 98 | + | ```bash |
|
| 99 | + | ipfs init --profile=server --empty-repo |
|
| 100 | + | systemctl --user enable ipfs |
|
| 101 | + | systemctl --user start ipfs |
|
| 102 | + | systemctl --user status ipfs |
|
| 103 | + | loginctl enable-linger $USER |
|
| 104 | + | ``` |
|
| 105 | + | ||
| 106 | + | All of this together should have the IPFS node running, and you can test it out by running the following command |
|
| 107 | + | ||
| 108 | + | ```bash |
|
| 109 | + | curl -L http://localhost:8080/ipfs/QmPyCYfL5oF79cfXjbt5cyr5hAZcyNrPNV9ytvUPdk8KT9 |
|
| 110 | + | ``` |
|
| 111 | + | ||
| 112 | + | <aside> |
|
| 113 | + | 💡 Keep in mind that with a fresh node like this with zero configuration might be slow and take a while to pull content and be connected with other major IPFS networks |
|
| 114 | + | ||
| 115 | + | </aside> |
|
| 116 | + | ||
| 117 | + | ## Setting Up Custom Domain |
|
| 118 | + | ||
| 119 | + | Now that your IPFS node is setup and we can use the gateway, these next steps will help you assign a domain to the gateway and make it public. First you will need to acquire a domain name which you can get from multiple providers like Namecheap. For this tutorial we’ll use the example `[domain.com](http://domain.com)` (very original). After purchasing the domain you will want to go into the advance DNS settings through your domain provider, and there we’ll add some records so we can use `[ipfs.domain.com](http://ipfs.domain.com)` as our gateway domain. You can get the IPV4 and IPV6 addresses from your Digital Ocean console. |
|
| 120 | + | ||
| 121 | + | | Type | Host | Value | TTL | |
|
| 122 | + | | --- | --- | --- | --- | |
|
| 123 | + | | A | ipfs | IPV4 Address | Automatic | |
|
| 124 | + | | A | *.ipfs | IPV4 Address | Automatic | |
|
| 125 | + | | AAAA | ipfs | IPV6 Address | Automatic | |
|
| 126 | + | | AAAA | *.ipfs | IPV6 Address | Automatic | |
|
| 127 | + | ||
| 128 | + | You can use a DNS checker for `[ipfs.domain.com](http://ipfs.domain.com)` to make sure everything is propagating but it can take some time depending on your provider. |
|
| 129 | + | ||
| 130 | + | After assigning the domain to the IP addresses of our droplet, we need to go in and edit our IPFS config. |
|
| 131 | + | ||
| 132 | + | <aside> |
|
| 133 | + | ⚠️ WARNING: The following command will open your gateway and IPFS node for anyone to use, do so with caution and do further research before releasing it to the while. |
|
| 134 | + | ||
| 135 | + | </aside> |
|
| 136 | + | ||
| 137 | + | You can paste `ipfs config Addresses.Gateway /ip4/0.0.0.0/tcp/8080` into the terminal and it will change the IP from your local network to the outside network, allowing external traffic to use it. We stress caution here because IPFS gateways are known to be abused which we’ll get into later. After changing that setting on the IPFS config we need to make one additional edit with `vim ~/.ipfs.config`. Once the file is open navigate under the following and add your specific domain and configs for `UseSubdomains` and `Paths`. |
|
| 138 | + | ||
| 139 | + | ```json |
|
| 140 | + | "Gateway": { |
|
| 141 | + | "PublicGateways": { |
|
| 142 | + | "domain.com": { |
|
| 143 | + | "UseSubdomains": true, |
|
| 144 | + | "Paths": [ |
|
| 145 | + | "/ipfs" |
|
| 146 | + | ] |
|
| 147 | + | } |
|
| 148 | + | } |
|
| 149 | + | } |
|
| 150 | + | ``` |
|
| 151 | + | ||
| 152 | + | After we have written and saved those changes we’ll need to restart the IPFS daemon with `systemctl --user restart ipfs`. Then we can test if our custom domain works in our own browser with a link like this: `http://ipfs.domain.com:8080/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng`. Now keep in mind that we did not use `https` as that will come later, and you’ll also notice we had to include that nasty port number which is not very smooth. So let’s fix that! |
|
| 153 | + | ||
| 154 | + | We’ll use nginx to help with some re-routing on our server so we can just leave out the port number in our urls. Run `sudo apt install nginx` to get started. Once installed we will want to rename the default configuration as a backup with `sudo mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default-back` then create a new one by running `sudo vim /etc/nginx/sites-available/default`. In that file you can paste the following: |
|
| 155 | + | ||
| 156 | + | ``` |
|
| 157 | + | server { |
|
| 158 | + | listen 80; |
|
| 159 | + | server_name ipfs.domain.com; |
|
| 160 | + | ||
| 161 | + | location / { |
|
| 162 | + | proxy_pass http://127.0.0.1:8080; |
|
| 163 | + | proxy_set_header Host $host; |
|
| 164 | + | proxy_set_header X-Real-IP $remote_addr; |
|
| 165 | + | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|
| 166 | + | proxy_set_header X-Forwarded-Proto $scheme; |
|
| 167 | + | } |
|
| 168 | + | } |
|
| 169 | + | ``` |
|
| 170 | + | ||
| 171 | + | Write and save that file, then run `systemctl restart nginx`. If successful we can now use a url like this: `[http://ipfs.domain.com/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng](http://ipfs.domain.com/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng)`. |
|
| 172 | + | ||
| 173 | + | ## Setting up SSL (HTTPS) |
|
| 174 | + | ||
| 175 | + | As you saw in the last url we used, we’re still using http which is a no go in today’s standards. In order to fix that we need to get an SSL certificate for our domain. Thankfully its pretty straight forward with a package called certbot. You can install it with `sudo nnap install certbot --classic` then run the command `sudo certbot --nginx -d [ipfs.domain.com](http://ipfs.domain.com)`. It should walk you though some questions you can answer, then it should issue a certificate for your domain. Last step is to go back to your domain provider and add this DNS record: |
|
| 176 | + | ||
| 177 | + | | Type | Host | Value | |
|
| 178 | + | | --- | --- | --- | |
|
| 179 | + | | CAA | ipfs | http://letsencrypt.org/ | |
|
| 180 | + | ||
| 181 | + | Now you can test it out with `[https://ipfs.domain.com/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng](https://ipfs.domain.com/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng)`. Congrats!! You just setup your own public IPFS gateway. But, something isn’t quite right: its super slow isn’t it? Let’s talk about that. |
|
| 182 | + | ||
| 183 | + | ## Further Steps |
|
| 184 | + | ||
| 185 | + | You have your public gateway setup and it sorta works, but its also super slow. There are some things you can do to help relieve this. One of those things is setting up a cache layer or CDN to help make fetching files a second time much faster. You can also look into peering your gateway with IPFS pinning services to tap into their network and get faster speeds, or configure your IPFS node to work with the Distributed Hash Table (DHT) to assist with finding files. Even with all those things, it can be tough to maintain good speeds. |
|
| 186 | + | ||
| 187 | + | Another thing you have to consider when hosting an IPFS gateway yourself is abuse. The unfortunately piece of a decentralized network is that there is plenty of people out there who want to abuse public gateways by hammering them with requests for files or use your gateway for phishing content. When that happens you have to keep up with a list of CIDs to block from your gateway or risk it have it taken down by domain registrars. You can check out a list of CIDs to block by IPFS [here](https://github.com/ipfs/infra/blob/master/ipfs/gateway/denylist.conf), however it is no longer being maintained making it even more difficult to keep up yourself. |
|
| 188 | + | ||
| 189 | + | ## Another Option: Pinata Dedicated IPFS Gateways |
|
| 190 | + | ||
| 191 | + | With any open source software endeavor, you have to ask yourself an important question: “Is this worth my time?” There are plenty of things you can do yourself when it comes to networking, like setting up your own custom email server, and doing those things will help you learn a lot. However if you’re trying to heavily use and depend on IPFS as a service for your decentralized applications, then it becomes a different question. Even if you got the speeds of your gateway up to a decent level, would it be worth the expense and upkeep to keep running it yourself and hope it does not get abused? |
|
| 192 | + | ||
| 193 | + | This is why IPFS pinning services like Pinata exist: to make IPFS easy and simple for developers. With Pinata you can not only upload files easily, but with Pinata Dedicated Gateways you get unmatched speeds thanks to a built in 200 location edge cache CDN. You get the benefits of being hooked up to a large network of nodes instead of just your solitary node. Plus, you get Gateway Access Controls so you can access content on IPFS with protection from spam and abuse. Setting up this public gateway was fun, but in a production environment, I’m thankful to have Pinata :) Happy Pinning! |
| 1 | + | --- |
|
| 2 | + | title: "How to Encrypt and Decrypt Files on IPFS Using Lit Protocol" |
|
| 3 | + | publishDate: "04 Nov 2023" |
|
| 4 | + | description: "Experience the power of decentralized storage, encryption, and token gating with this tutorial" |
|
| 5 | + | tags: ["ipfs", "lit-protocol", "encryption", "token-gating"] |
|
| 6 | + | ogImage: "https://assets-global.website-files.com/629e4fe96456f8219203e7f1/6545bfa112815d6340466066_20231103_How%20to%20Encrypt%20and%20Decrypt%20Files%20on%20IPFS%20Using%20Lit%20Protocol%20and%20Pinata.jpeg" |
|
| 7 | + | --- |
|
| 8 | + | ||
| 9 | + | ||
| 10 | + | The most popular method used for sharing files off-chain in Web3 is IPFS, and there are some [good reasons for that](https://www.pinata.cloud/blog/why-ipfs-is-the-storage-solution-for-web3-developers). However it does not come without its own share of problems, and one of those is the ability to share private files. IPFS is a public network so anyone with a CID can access and download that content, and this hinders projects that may want to token gate content or create subscriptions to content. With that said, encryption has proven to be one solution to this problem. Remarkably, the solution of [asymmetric encryption](https://www.okta.com/identity-101/asymmetric-encryption) is used in blockchain all the time and can be reused for the purpose of token gating. [Lit Protocol](https://litprotocol.com/) is a decentralized middleware client that enables access controls to help extend asymmetric encryption to token gating based on crypto ownership, such as owning an NFT, ERC-20 token balance, or simply designating a recipient address. In this post, we’ll show you how you can combine the best of both worlds and create an app that will encrypt content, upload it to IPFS, and then given an encrypted CID, decrypt it. |
|
| 11 | + | ||
| 12 | + | ## Why IPFS? |
|
| 13 | + | ||
| 14 | + | IPFS is public and openly available, but it’s also not permanent by default. This means that unlike blockchain storage protocols that make every piece of content permanent as soon as it’s uploaded, you can potentially remove content from IPFS. You’ll see why this is important as we explore encryption more deeply. |
|
| 15 | + | ||
| 16 | + | The biggest problem with encryption is that its always evolving. One encryption method we use today will be outdated one day in the future. An example is [MD5 which was cracked almost perhaps 10 years ago](https://www.okta.com/identity-101/md5/#:~:text=The%20MD5%20hash%20function's%20security,be%20used%20for%20malicious%20purposes.) but people still use it without knowing the risk. When we consider putting files on a decentralized network that are specifically designed to not be taken down, things get messy. Arweave is a common consideration for encryption and decentralized storage, however, their model puts content on the network permanently. There is the possibility those encrypted files could be cracked in another 10 years. |
|
| 17 | + | ||
| 18 | + | IPFS is different in that content is not “permanent,” but rather it is “persistent.” It's a subtle difference but has massive ramifications. With IPFS, the content will only stay on the network if at least one IPFS node keeps the content “[pinned](https://www.pinata.cloud/blog/what-is-pinning),” which tells other nodes that might have a cached copy of the content to keep it available. As soon as there are no nodes pinning a particular CID, then the nodes holding that cache will dump it when they use garbage collection. It's a unique mechanism that helps prevent digital waste and ensures only the content we value will persist. The concepts of permanence and persistence are truly philosophical differences of approaching the same problem. |
|
| 19 | + | ||
| 20 | + | When you combine IPFS with encryption, you get a unique situation where content that is no longer used can be unpinned. Granted it does not guarantee the content will be completely wiped from the network, but it does give users a level of control over their content they would normally not have with other decentralized storage networks. It is also unlikely that bad actors would go through the trouble and costs to keep encrypted content pinned for the purpose of decrypting it years down the road. The [cost of storage](https://www.pinata.cloud/blog/is-ipfs-free) helps balance situations like these. With that said let's actually build this thing! |
|
| 21 | + | ||
| 22 | + | <aside> |
|
| 23 | + | ℹ️ <b>Disclosure:</b> There will always be limitations to encryption and eventually current methods may be cracked. Please be aware of the risk involved and be sure to read <a href="https://developer.litprotocol.com/v3/sdk/authentication/security">Lit Protocol’s best practices for security.</a> |
|
| 24 | + | </aside> |
|
| 25 | + | ||
| 26 | + | ## Building the App |
|
| 27 | + | ||
| 28 | + | What’s great about this project is that we already have most of what we need to build it! Pinata created a [Next.js template](https://www.pinata.cloud/blog/announcing-pinata-ipfs-developer-starter-templates) a while back which we can use again and just add in our Lit Protocol SDK. |
|
| 29 | + | ||
| 30 | + | To follow this tutorial you will want to make sure you have the following: |
|
| 31 | + | ||
| 32 | + | - Node.js 18 or higher |
|
| 33 | + | - [A Free Pinata Account](https://app.pinata.cloud/register) |
|
| 34 | + | - A text editor like VSCode |
|
| 35 | + | ||
| 36 | + | Before going any further make sure you get a [free Pinata account](https://app.pinata.cloud/register) so you can make an [API key](https://docs.pinata.cloud/docs/api-keys) and get a [free Dedicated Gateway](https://docs.pinata.cloud/docs/dedicated-ipfs-gateways) for this project! Once you make an API key, save the `JWT` that we’ll use in a little bit, as well as the gateway domain for your Dedicated Gateways. |
|
| 37 | + | ||
| 38 | + | Thats it! To kick it off, simply run the command to use the Pinata Next.js Template |
|
| 39 | + | ||
| 40 | + | ```bash |
|
| 41 | + | npx create-pinata-app |
|
| 42 | + | ``` |
|
| 43 | + | ||
| 44 | + | You will be prompted to choose your flavors of the app, such as Typescript vs Javascript, or Tailwindcss vs Vanilla CSS. For this template, I chose Typescript and Tailwindcss but feel free to choose your own. After giving it a name and making your selections go ahead and open the project in VSCode. |
|
| 45 | + | ||
| 46 | + | Back in the terminal the next thing we’re going to do is install the [Lit Protocol SDK](https://developer.litprotocol.com/v3/sdk/installation). We’ll be using the V3 of the SDK which is in beta, and you can install based on their docs [here](https://developer.litprotocol.com/v3/sdk/installation) or use this command: |
|
| 47 | + | ||
| 48 | + | ```bash |
|
| 49 | + | npm install @lit-protocol/lit-node-client@cayenne |
|
| 50 | + | ``` |
|
| 51 | + | ||
| 52 | + | The last thing you need to do to set up the project is open the `.env.sample` file which should look like this: |
|
| 53 | + | ||
| 54 | + | ``` |
|
| 55 | + | PINATA_JWT= |
|
| 56 | + | NEXT_PUBLIC_GATEWAY_URL= |
|
| 57 | + | NEXT_PUBLIC_GATEWAY_TOKEN= |
|
| 58 | + | ``` |
|
| 59 | + | ||
| 60 | + | Paste in the `PINATA_JWT` that you made earlier when you set up your Pinata account and also paste in the `NEXT_PUBLIC_GATEWAY_URL` with the format `[https://mygateway.mypinata.cloud](https://mygateway.mypinata.cloud)` with of course your own domain URL. Then change the name of the file from `.env.sample` to `.env.local`, a very important step for our app to work! |
|
| 61 | + | ||
| 62 | + | Now lets go ahead and spin up the dev server with `npm run dev` and start building. All of our work will be done in just one file, `pages/index.tsx`; easy! We’ll start by importing the Lit Protocol SDK at the top of the file. |
|
| 63 | + | ||
| 64 | + | ```jsx |
|
| 65 | + | import { useState, useRef } from "react"; |
|
| 66 | + | import Head from "next/head"; |
|
| 67 | + | import Image from "next/image"; |
|
| 68 | + | import Files from "@/components/Files"; |
|
| 69 | + | // import lit protocol sdk |
|
| 70 | + | import * as LitJsSdk from "@lit-protocol/lit-node-client"; |
|
| 71 | + | ``` |
|
| 72 | + | ||
| 73 | + | Inside the `Home` component, we’ll add another state variable that we’ll come back to later. |
|
| 74 | + | ||
| 75 | + | ```jsx |
|
| 76 | + | const [file, setFile] = useState(""); |
|
| 77 | + | const [cid, setCid] = useState(""); |
|
| 78 | + | const [uploading, setUploading] = useState(false); |
|
| 79 | + | // add a new state for the cid to decrypt |
|
| 80 | + | const [decryptionCid, setDecryptionCid] = useState(""); |
|
| 81 | + | ``` |
|
| 82 | + | ||
| 83 | + | The great thing about this template is that it's already got uploads to IPFS with Pinata baked in with an `/api/files` route on the backend, so all we have to do is encrypt the file before we upload it. We’ll do this in the `uploadFile` function inside of `Home`, and it should look like this to start. |
|
| 84 | + | ||
| 85 | + | ```jsx |
|
| 86 | + | const uploadFile = async (fileToUpload) => { |
|
| 87 | + | try { |
|
| 88 | + | setUploading(true); |
|
| 89 | + | const formData = new FormData(); |
|
| 90 | + | formData.append("file", fileToUpload, fileToUpload.name) |
|
| 91 | + | const res = await fetch("/api/files", { |
|
| 92 | + | method: "POST", |
|
| 93 | + | body: formData, |
|
| 94 | + | }); |
|
| 95 | + | const ipfsHash = await res.text(); |
|
| 96 | + | setCid(ipfsHash); |
|
| 97 | + | setUploading(false); |
|
| 98 | + | } catch (e) { |
|
| 99 | + | console.log(e); |
|
| 100 | + | setUploading(false); |
|
| 101 | + | alert("Trouble uploading file"); |
|
| 102 | + | } |
|
| 103 | + | }; |
|
| 104 | + | ``` |
|
| 105 | + | ||
| 106 | + | At the top of our `try` statement but underneath our `setUploading` state, we’ll initialize the `LitNodeClient` using the `ceyenne` network, connect our app to that network, then get the `authSig`. Lit Protocol is a decentralized network middleware that helps us do some cool token gating and lets us do encryption. In these few statements, we create a client that connects to that middleware network and then gets a signature from the user. This signature will be used for signing the encrypted files. |
|
| 107 | + | ||
| 108 | + | ```jsx |
|
| 109 | + | const uploadFile = async (fileToUpload) => { |
|
| 110 | + | try { |
|
| 111 | + | setUploading(true); |
|
| 112 | + | const litNodeClient = new LitJsSdk.LitNodeClient({ |
|
| 113 | + | litNetwork: 'cayenne', |
|
| 114 | + | }); |
|
| 115 | + | // then get the authSig |
|
| 116 | + | await litNodeClient.connect(); |
|
| 117 | + | const authSig = await LitJsSdk.checkAndSignAuthMessage({ |
|
| 118 | + | chain: 'ethereum' |
|
| 119 | + | }); |
|
| 120 | + | // rest of the code |
|
| 121 | + | ``` |
|
| 122 | + | ||
| 123 | + | Next up we’ll set up our access controls for this encrypted content. We’ll dive deeper into this later in the tutorial, but essentially this is the most important part of our app as it determines who can decrypt the content we encrypt. It could be something like token gating by NFT collection or a direct address. For now, we’ll keep it simple and allow anyone with a balance of 0 ETH or higher to decrypt it (that should be everyone with a wallet). |
|
| 124 | + | ||
| 125 | + | ```jsx |
|
| 126 | + | const uploadFile = async (fileToUpload) => { |
|
| 127 | + | try { |
|
| 128 | + | setUploading(true); |
|
| 129 | + | // Create our litNodeClient |
|
| 130 | + | const litNodeClient = new LitJsSdk.LitNodeClient({ |
|
| 131 | + | litNetwork: 'cayenne', |
|
| 132 | + | }); |
|
| 133 | + | // Then get the authSig |
|
| 134 | + | await litNodeClient.connect(); |
|
| 135 | + | const authSig = await LitJsSdk.checkAndSignAuthMessage({ |
|
| 136 | + | chain: 'ethereum' |
|
| 137 | + | }); |
|
| 138 | + | // Define our access controls, this is set to be anyone |
|
| 139 | + | const accs = [ |
|
| 140 | + | { |
|
| 141 | + | contractAddress: '', |
|
| 142 | + | standardContractType: '', |
|
| 143 | + | chain: 'ethereum', |
|
| 144 | + | method: 'eth_getBalance', |
|
| 145 | + | parameters: [':userAddress', 'latest'], |
|
| 146 | + | returnValueTest: { |
|
| 147 | + | comparator: '>=', |
|
| 148 | + | value: '0', |
|
| 149 | + | }, |
|
| 150 | + | }, |
|
| 151 | + | ]; |
|
| 152 | + | // rest of the code |
|
| 153 | + | ``` |
|
| 154 | + | ||
| 155 | + | The fun part; encrypting! There are several methods of encryption that Lit Protocol offers through their SDK, such as just a string, or a file, and in our case, we’ll use the `encryptFileAndZipWithMetadata` method. This is handy because in order to decrypt a file, our recipient will need the `accs` parameters we set and a secure hash. We want a simple way for all of this to be packaged and included in our IPFS CID, and that's exactly what this method will do. All we have to do is pass in our access control conditions array, our `authSig`, the chain, our `fileToUpload` that we passed into the function argument, the `litNodeClient`, and finally a simple `readme` that will explain to whoever happens to download it from IPFS what they need to do with it. |
|
| 156 | + | ||
| 157 | + | ```jsx |
|
| 158 | + | const uploadFile = async (fileToUpload) => { |
|
| 159 | + | try { |
|
| 160 | + | setUploading(true); |
|
| 161 | + | // Create our litNodeClient |
|
| 162 | + | const litNodeClient = new LitJsSdk.LitNodeClient({ |
|
| 163 | + | litNetwork: 'cayenne', |
|
| 164 | + | }); |
|
| 165 | + | // Then get the authSig |
|
| 166 | + | await litNodeClient.connect(); |
|
| 167 | + | const authSig = await LitJsSdk.checkAndSignAuthMessage({ |
|
| 168 | + | chain: 'ethereum' |
|
| 169 | + | }); |
|
| 170 | + | // Define our access controls, this is set to be anyone |
|
| 171 | + | const accs = [ |
|
| 172 | + | { |
|
| 173 | + | contractAddress: '', |
|
| 174 | + | standardContractType: '', |
|
| 175 | + | chain: 'ethereum', |
|
| 176 | + | method: 'eth_getBalance', |
|
| 177 | + | parameters: [':userAddress', 'latest'], |
|
| 178 | + | returnValueTest: { |
|
| 179 | + | comparator: '>=', |
|
| 180 | + | value: '0', |
|
| 181 | + | }, |
|
| 182 | + | }, |
|
| 183 | + | ]; |
|
| 184 | + | // Then we use our access controls and authSig to encrypt the file and zip it up with the metadata |
|
| 185 | + | const encryptedZip = await LitJsSdk.encryptFileAndZipWithMetadata({ |
|
| 186 | + | accessControlConditions: accs, |
|
| 187 | + | authSig, |
|
| 188 | + | chain: 'ethereum', |
|
| 189 | + | file: fileToUpload, |
|
| 190 | + | litNodeClient: litNodeClient, |
|
| 191 | + | readme: "Use IPFS CID of this file to decrypt it" |
|
| 192 | + | }); |
|
| 193 | + | // rest of the code |
|
| 194 | + | ``` |
|
| 195 | + | ||
| 196 | + | One last little touch we need to do is adapt this encrypted zip file so it will be accepted by our `/api/files` endpoint, and we’ll do so with just two lines of code. |
|
| 197 | + | ||
| 198 | + | ```jsx |
|
| 199 | + | // Then we turn it into a file that will be accepted by the API endpoint |
|
| 200 | + | const encryptedBlob = new Blob([encryptedZip], { type: 'text/plain' }) |
|
| 201 | + | const encryptedFile = new File([encryptedBlob], fileToUpload.name) |
|
| 202 | + | ``` |
|
| 203 | + | ||
| 204 | + | All together we should have an upload function that looks like this: |
|
| 205 | + | ||
| 206 | + | ```jsx |
|
| 207 | + | const uploadFile = async (fileToUpload) => { |
|
| 208 | + | try { |
|
| 209 | + | setUploading(true); |
|
| 210 | + | // Create our litNodeClient |
|
| 211 | + | const litNodeClient = new LitJsSdk.LitNodeClient({ |
|
| 212 | + | litNetwork: 'cayenne', |
|
| 213 | + | }); |
|
| 214 | + | // Then get the authSig |
|
| 215 | + | await litNodeClient.connect(); |
|
| 216 | + | const authSig = await LitJsSdk.checkAndSignAuthMessage({ |
|
| 217 | + | chain: 'ethereum' |
|
| 218 | + | }); |
|
| 219 | + | // Define our access controls, this is set to be anyone |
|
| 220 | + | const accs = [ |
|
| 221 | + | { |
|
| 222 | + | contractAddress: '', |
|
| 223 | + | standardContractType: '', |
|
| 224 | + | chain: 'ethereum', |
|
| 225 | + | method: 'eth_getBalance', |
|
| 226 | + | parameters: [':userAddress', 'latest'], |
|
| 227 | + | returnValueTest: { |
|
| 228 | + | comparator: '>=', |
|
| 229 | + | value: '0', |
|
| 230 | + | }, |
|
| 231 | + | }, |
|
| 232 | + | ]; |
|
| 233 | + | // Then we use our access controls and authSig to encrypt the file and zip it up with the metadata |
|
| 234 | + | const encryptedZip = await LitJsSdk.encryptFileAndZipWithMetadata({ |
|
| 235 | + | accessControlConditions: accs, |
|
| 236 | + | authSig, |
|
| 237 | + | chain: 'ethereum', |
|
| 238 | + | file: fileToUpload, |
|
| 239 | + | litNodeClient: litNodeClient, |
|
| 240 | + | readme: "Use IPFS CID of this file to decrypt it" |
|
| 241 | + | }); |
|
| 242 | + | ||
| 243 | + | // Then we turn it into a file that will be accepted by the Pinata API |
|
| 244 | + | const encryptedBlob = new Blob([encryptedZip], { type: 'text/plain' }) |
|
| 245 | + | const encryptedFile = new File([encryptedBlob], fileToUpload.name) |
|
| 246 | + | ||
| 247 | + | // Finally we upload the file by passing it to our /api/files endpoint |
|
| 248 | + | // Keep in mind this works for smaller files and you may need to do a presigned JWT and upload from the client if you're dealing with larger files |
|
| 249 | + | // Read more about that here: https://www.pinata.cloud/blog/how-to-upload-to-ipfs-from-the-frontend-with-signed-jwts |
|
| 250 | + | const formData = new FormData(); |
|
| 251 | + | formData.append("file", encryptedFile, encryptedFile.name) |
|
| 252 | + | const res = await fetch("/api/files", { |
|
| 253 | + | method: "POST", |
|
| 254 | + | body: formData, |
|
| 255 | + | }); |
|
| 256 | + | const ipfsHash = await res.text(); |
|
| 257 | + | setCid(ipfsHash); |
|
| 258 | + | setUploading(false); |
|
| 259 | + | } catch (e) { |
|
| 260 | + | console.log(e); |
|
| 261 | + | setUploading(false); |
|
| 262 | + | alert("Trouble uploading file"); |
|
| 263 | + | } |
|
| 264 | + | }; |
|
| 265 | + | ``` |
|
| 266 | + | ||
| 267 | + | One thing to note is that there is a file size restriction when using the Next API routes, so if you have larger files you may want to move uploading to the client side and utilize pre-signed JWTs which we talk about in [this post.](https://www.pinata.cloud/blog/how-to-upload-to-ipfs-from-the-frontend-with-signed-jwts) |
|
| 268 | + | ||
| 269 | + | We now have encrypted uploads! If you upload a file through the app you should get a CID, and if you download the file it will result in a zip folder with all the stuff we just made. This is cool, but how do we decrypt it? How do we let other people decrypt it? Its actually pretty easy! We’re gonna make a new function right below our upload function called `decryptFile()`, which will take a `fileToDecrypt` CID. First thing we’ll do is fetch that file using our Dedicated Gateway and turn it into a blob. |
|
| 270 | + | ||
| 271 | + | ```jsx |
|
| 272 | + | const decryptFile = async (fileToDecrypt) => { |
|
| 273 | + | try { |
|
| 274 | + | // First we fetch the file from IPFS using the CID and our Gateway URL, then turn it into a blob |
|
| 275 | + | const fileRes = await fetch(`${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/${fileToDecrypt}?filename=encrypted.zip`) |
|
| 276 | + | const file = await fileRes.blob() |
|
| 277 | + | } catch (error) { |
|
| 278 | + | alert("Trouble decrypting file") |
|
| 279 | + | console.log(error) |
|
| 280 | + | } |
|
| 281 | + | ``` |
|
| 282 | + | ||
| 283 | + | Now we can re-create the `litNodeClient` and get the auth signature. The beauty of this SDK is that once some signs they will not need to sign again unless they disconnect from the app, making the interactions fairly smooth. |
|
| 284 | + | ||
| 285 | + | ```jsx |
|
| 286 | + | const decryptFile = async (fileToDecrypt) => { |
|
| 287 | + | try { |
|
| 288 | + | // First we fetch the file from IPFS using the CID and our Gateway URL, then turn it into a blob |
|
| 289 | + | const fileRes = await fetch(`${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/${fileToDecrypt}?filename=encrypted.zip`) |
|
| 290 | + | const file = await fileRes.blob() |
|
| 291 | + | // We recreated the litNodeClient and the authSig |
|
| 292 | + | const litNodeClient = new LitJsSdk.LitNodeClient({ |
|
| 293 | + | litNetwork: 'cayenne', |
|
| 294 | + | }); |
|
| 295 | + | await litNodeClient.connect(); |
|
| 296 | + | const authSig = await LitJsSdk.checkAndSignAuthMessage({ |
|
| 297 | + | chain: 'ethereum' |
|
| 298 | + | }); |
|
| 299 | + | } catch (error) { |
|
| 300 | + | alert("Trouble decrypting file") |
|
| 301 | + | console.log(error) |
|
| 302 | + | } |
|
| 303 | + | ``` |
|
| 304 | + | ||
| 305 | + | Just like we used `encryptFileAndZipWithMetadata` method, we have a matching method for decryption called `decryptZipFileWithMetadata` which we’ll use very similarly to the encryption. The zip folder has everything we need so all we have to pass in is the `file` blob, our `litNodeClient`, and the recipient `authSig`. Piece of cake! From this, we’ll extract the `decryptedFile` and `metadata` from our request. |
|
| 306 | + | ||
| 307 | + | ```jsx |
|
| 308 | + | const decryptFile = async (fileToDecrypt) => { |
|
| 309 | + | try { |
|
| 310 | + | // First we fetch the file from IPFS using the CID and our Gateway URL, then turn it into a blob |
|
| 311 | + | const fileRes = await fetch(`${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/${fileToDecrypt}?filename=encrypted.zip`) |
|
| 312 | + | const file = await fileRes.blob() |
|
| 313 | + | // We recreated the litNodeClient and the authSig |
|
| 314 | + | const litNodeClient = new LitJsSdk.LitNodeClient({ |
|
| 315 | + | litNetwork: 'cayenne', |
|
| 316 | + | }); |
|
| 317 | + | await litNodeClient.connect(); |
|
| 318 | + | const authSig = await LitJsSdk.checkAndSignAuthMessage({ |
|
| 319 | + | chain: 'ethereum' |
|
| 320 | + | }); |
|
| 321 | + | // Then we simpyl extract the file and metadata from the zip |
|
| 322 | + | // We could do more with this, like try to display it in the app UI if we wanted to |
|
| 323 | + | const { decryptedFile, metadata } = await LitJsSdk.decryptZipFileWithMetadata({ |
|
| 324 | + | file: file, |
|
| 325 | + | litNodeClient: litNodeClient, |
|
| 326 | + | authSig: authSig, |
|
| 327 | + | }) |
|
| 328 | + | } catch (error) { |
|
| 329 | + | alert("Trouble decrypting file") |
|
| 330 | + | console.log(error) |
|
| 331 | + | } |
|
| 332 | + | ``` |
|
| 333 | + | ||
| 334 | + | All that’s left to do is deliver the file to the user! There are several ways you could go about it, for example, if you want to display the content to the user such as an image or video you could do so with a bit of formatting. In this example, we’ll just trigger a download to the recipient’s computer. All together we should have the following. |
|
| 335 | + | ||
| 336 | + | ```jsx |
|
| 337 | + | const decryptFile = async (fileToDecrypt) => { |
|
| 338 | + | try { |
|
| 339 | + | // First we fetch the file from IPFS using the CID and our Gateway URL, then turn it into a blob |
|
| 340 | + | const fileRes = await fetch(`${process.env.NEXT_PUBLIC_GATEWAY_URL}/ipfs/${fileToDecrypt}?filename=encrypted.zip`) |
|
| 341 | + | const file = await fileRes.blob() |
|
| 342 | + | // We recreated the litNodeClient and the authSig |
|
| 343 | + | const litNodeClient = new LitJsSdk.LitNodeClient({ |
|
| 344 | + | litNetwork: 'cayenne', |
|
| 345 | + | }); |
|
| 346 | + | await litNodeClient.connect(); |
|
| 347 | + | const authSig = await LitJsSdk.checkAndSignAuthMessage({ |
|
| 348 | + | chain: 'ethereum' |
|
| 349 | + | }); |
|
| 350 | + | // Then we simpyl extract the file and metadata from the zip |
|
| 351 | + | // We could do more with this, like try to display it in the app UI if we wanted to |
|
| 352 | + | const { decryptedFile, metadata } = await LitJsSdk.decryptZipFileWithMetadata({ |
|
| 353 | + | file: file, |
|
| 354 | + | litNodeClient: litNodeClient, |
|
| 355 | + | authSig: authSig, |
|
| 356 | + | }) |
|
| 357 | + | // After we have our dcypted file we can download it |
|
| 358 | + | const blob = new Blob([decryptedFile], { type: 'application/octet-stream' }); |
|
| 359 | + | const downloadLink = document.createElement('a'); |
|
| 360 | + | downloadLink.href = URL.createObjectURL(blob); |
|
| 361 | + | downloadLink.download = metadata.name; // Use the metadata to get the file name and type |
|
| 362 | + | ||
| 363 | + | } catch (error) { |
|
| 364 | + | alert("Trouble decrypting file") |
|
| 365 | + | console.log(error) |
|
| 366 | + | } |
|
| 367 | + | ``` |
|
| 368 | + | ||
| 369 | + | One small little change we’ll make to the JSX is a text input where someone can paste in a CID and a “Decrypt” button someone can press after pasting in their CID. |
|
| 370 | + | ||
| 371 | + | ```jsx |
|
| 372 | + | <input |
|
| 373 | + | type="text" |
|
| 374 | + | onChange={(e) => setDecryptionCid(e.target.value)} |
|
| 375 | + | className="px-4 py-2 border-2 border-secondary rounded-3xl text-lg" |
|
| 376 | + | placeholder="Enter CID to decrypt" |
|
| 377 | + | /> |
|
| 378 | + | <button |
|
| 379 | + | onClick={() => decryptFile(decryptionCid)} |
|
| 380 | + | className="mr-10 w-[150px] bg-light text-secondary border-2 border-secondary rounded-3xl py-2 px-4 hover:bg-secondary hover:text-light transition-all duration-300 ease-in-out" |
|
| 381 | + | >Decrypt</button> |
|
| 382 | + | ``` |
|
| 383 | + | ||
| 384 | + | With all of this together, you should have an app with the following flow! |
|
| 385 | + | ||
| 386 | + | <video muted autoplay style="width: 100%; height: auto; position: relative;"> |
|
| 387 | + | <source src="https://mktg.mypinata.cloud/ipfs/QmcoP5gj1gJyZNCDUHE2e1g1cbwvEHo6E56xMhPV8KaHaf/file-encryption.mp4" type="video/mp4"> |
|
| 388 | + | </video> |
|
| 389 | + | ||
| 390 | + | You can also download and use this exact template [here](https://github.com/PinataCloud/pinata-lit-protocol-template)! |
|
| 391 | + | ||
| 392 | + | ## Going Further |
|
| 393 | + | ||
| 394 | + | This little template is really designed just to get you started and help you understand how Pinata and Lit Protocol work, and there is so much you can do with it. I would highly recommend checking out Lit Protocol’s documentation, in particular their section on all the [different access controls](https://developer.litprotocol.com/v3/sdk/access-control/evm/basic-examples) you can do. For example, if you only wanted holders of a particular ERC721 NFT you could use the following. |
|
| 395 | + | ||
| 396 | + | ```jsx |
|
| 397 | + | const accessControlConditions = [ |
|
| 398 | + | { |
|
| 399 | + | contractAddress: '0xA80617371A5f511Bf4c1dDf822E6040acaa63e71', |
|
| 400 | + | standardContractType: 'ERC721', |
|
| 401 | + | chain, |
|
| 402 | + | method: 'balanceOf', |
|
| 403 | + | parameters: [ |
|
| 404 | + | ':userAddress' |
|
| 405 | + | ], |
|
| 406 | + | returnValueTest: { |
|
| 407 | + | comparator: '>', |
|
| 408 | + | value: '0' |
|
| 409 | + | } |
|
| 410 | + | } |
|
| 411 | + | ] |
|
| 412 | + | ``` |
|
| 413 | + | ||
| 414 | + | Or you could do DAO membership (MolochDAOv2.1, also supports DAOHaus)****[](https://developer.litprotocol.com/v3/sdk/access-control/evm/basic-examples#must-be-a-member-of-a-dao-molochdaov21-also-supports-daohaus)**** |
|
| 415 | + | ||
| 416 | + | ```jsx |
|
| 417 | + | const accessControlConditions = [ |
|
| 418 | + | { |
|
| 419 | + | contractAddress: '0x50D8EB685a9F262B13F28958aBc9670F06F819d9', |
|
| 420 | + | standardContractType: 'MolochDAOv2.1', |
|
| 421 | + | chain, |
|
| 422 | + | method: 'members', |
|
| 423 | + | parameters: [ |
|
| 424 | + | ':userAddress', |
|
| 425 | + | ], |
|
| 426 | + | returnValueTest: { |
|
| 427 | + | comparator: '=', |
|
| 428 | + | value: 'true' |
|
| 429 | + | } |
|
| 430 | + | } |
|
| 431 | + | ] |
|
| 432 | + | ``` |
|
| 433 | + | ||
| 434 | + | You can even do a simple check if the recipient is a particular wallet address. |
|
| 435 | + | ||
| 436 | + | ```jsx |
|
| 437 | + | const accessControlConditions = [ |
|
| 438 | + | { |
|
| 439 | + | contractAddress: '', |
|
| 440 | + | standardContractType: '', |
|
| 441 | + | chain, |
|
| 442 | + | method: '', |
|
| 443 | + | parameters: [ |
|
| 444 | + | ':userAddress', |
|
| 445 | + | ], |
|
| 446 | + | returnValueTest: { |
|
| 447 | + | comparator: '=', |
|
| 448 | + | value: '0x50e2dac5e78B5905CB09495547452cEE64426db2' |
|
| 449 | + | } |
|
| 450 | + | } |
|
| 451 | + | ] |
|
| 452 | + | ``` |
|
| 453 | + | ||
| 454 | + | With these building blocks, you could easily build a standalone token gating app, or build a custom solution for your holders. The possibilities are endless! |
|
| 455 | + | ||
| 456 | + | Happy Pinning! |