Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
Binary file — no preview.
| 1 | - | --- |
|
| 2 | - | title: "How to Create a Dynamic App NFT Resume" |
|
| 3 | - | publishDate: "10 Jan 2023" |
|
| 4 | - | description: "Level up your job search with a dynamic app NFT resume that will wow any employer." |
|
| 5 | - | ogImage: "/blog-images/other/6410b6848afd85df8fe0a193_2023-01-10_How-to-Create_blog-img-tiny.png" |
|
| 6 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvussqrf2v" |
|
| 7 | - | --- |
|
| 8 | - | ||
| 9 | - | import pinnie from "../../assets/pinnie.png"; |
|
| 10 | - | import OutLinkButton from "@/components/common/OutLinkButton"; |
|
| 11 | - | ||
| 12 | - | <OutLinkButton |
|
| 13 | - | link="https://www.pinata.cloud/blog/resume-app-nft" |
|
| 14 | - | site="Pinata" |
|
| 15 | - | image={pinnie} |
|
| 16 | - | />{" "} |
|
| 17 | - | ||
| 18 | - | Despite the current market conditions, web3 jobs are still on the rise. But competition is at an all-time high. Sure you can build a portfolio of projects to help display your skills, but that’s what everyone else is doing too. What can you do to stand out among the crowds of developers out there like yourself? How are you thinking outside the box? |
|
| 19 | - | ||
| 20 | - | Rather than another LinkedIn resume or eye-glazing PDF, what if a potential employer opened up their crypto wallet and saw an interactive resume NFT with your name on it? It would demonstrate not only your ability to make NFTs, but [App NFTs](https://medium.com/pinata/how-to-build-an-app-nft-7c57b51698e7)—this concept that a full blown and functional application can be an NFT. You think that might crank things up a bit? |
|
| 21 | - | ||
| 22 | - | This dynamic resume app NFT would show an ability to make anything an NFT, like an interactive comic book or even a small game. And you’d better believe it would get that employer intrigued enough to set up some time to chat with the power of simply dropping your resume into their wallet. |
|
| 23 | - | ||
| 24 | - | Sound interesting? Let’s talk about how you can create your own dynamic resume app NFT. |
|
| 25 | - | ||
| 26 | - | ## Building your Resume App NFT |
|
| 27 | - | ||
| 28 | - | Here we’ll give a general walkthrough of how to create this Resume App NFT, but the ultimate tutorial (and what is primarily referenced while building this) is [Justin’s guide on how to create App NFTs using Pinata](https://medium.com/pinata/how-to-build-an-app-nft-7c57b51698e7), Polygon and Opensea. |
|
| 29 | - | ||
| 30 | - | Let’s get into it. |
|
| 31 | - | ||
| 32 | - | First things first, you need a vision of what your resume could look like. As the primary builder, our Head of Community Steve Simkins envisioned something more dynamic and interactive, taking advantage of all the fun web dev tools out there. It was important to keep in mind what this would look like from an NFT marketplace like Opensea or a crypto wallet, making sure it’s size would not cause problems. |
|
| 33 | - | ||
| 34 | - | Steve spent sometime tinkering in Figma and ended up with a card like design that could have multiple screens with different pieces of the resume. The reader could hover over the card and get a cool 3D effect, and click on the forward and back buttons to read through his skills. |
|
| 35 | - | ||
| 36 | - | After the design was finished, it was time to build the app. Steve followed Justin’s tutorial by making a simple React application and made sure to use **“homepage”: “.”** in the **package.json** file; very important to make sure it runs correctly on Pinata and IPFS. From there Steve used some standard issue web design methods and created a static “website” or app that was his resume. Of course for this step you can use just about any kind of javascript framework or tools, not just React. Once the development was complete, Steve ran the build command and uploaded the build folder to Pinata. |
|
| 37 | - | ||
| 38 | - | As said in Justin’s tutorial, you can use a free Pinata account to do all of this, but to truly harness the full user experience of this app, using a paid account with a Dedicated Gateway will make this app much faster than if you used a public gateway or depended on external sources. Here is an example of the application through Steve’s Dedicated Gateway: |
|
| 39 | - | ||
| 40 | - | <iframe |
|
| 41 | - | src="/blog-images/files-stevedylan-dev/QmdKYQpczE7giv15Yx2tkk1pkbRe862eaLhTR5e7FjhJ8F.html" |
|
| 42 | - | frameborder="0" |
|
| 43 | - | height="500px" |
|
| 44 | - | width="100%" |
|
| 45 | - | class="hidden sm:block" |
|
| 46 | - | /> |
|
| 47 | - | <img |
|
| 48 | - | src="/blog-images/other/63bd95503c654e14fc1b3b00_Slide-16-9-3.png" |
|
| 49 | - | alt="Screenshot of web app" |
|
| 50 | - | height={1080} |
|
| 51 | - | width={1920} |
|
| 52 | - | aspectRatio={1 / 1} |
|
| 53 | - | class="sm:hidden" |
|
| 54 | - | /> |
|
| 55 | - | ||
| 56 | - | Awesome! Resume app is done, time to turn it into an NFT. Following the guide, Steve created a simple smart contract to deploy the App NFT in a way that he could keep updating it with new versions, very handy if you ever have an update in experience or education. Also keep in mind you can do your own smart contract customization here to do cool stuff, like mass airdrop your NFT to a list of wallets or ENS addresses! |
|
| 57 | - | ||
| 58 | - | With the smart contract tested and ready to go, it was time to prep the metadata for the NFT. Steve kept it pretty simple with a name, description, image link for thumbnails, and used his Dedicated Gateway link in the **animation_url** for performance. |
|
| 59 | - | ||
| 60 | - | ```json |
|
| 61 | - | { |
|
| 62 | - | "name": "Steve's App NFT Resume", |
|
| 63 | - | "description": "A dynamic NFT resume by Steve", |
|
| 64 | - | "image": "ipfs://QmTa46bKHxcQCBoNt887X2zNJwAHpAZ93hTXDi9KeJeM4W", |
|
| 65 | - | "animation_url": "https://stevedsimkins.mypinata.cloud/ipfs/QmdKYQpczE7giv15Yx2tkk1pkbRe862eaLhTR5e7FjhJ8F/index.html" |
|
| 66 | - | } |
|
| 67 | - | ``` |
|
| 68 | - | ||
| 69 | - | With the metadata.json file complete, Steve uploaded that file to Pinata as well and used the CID as the token URI like so: |
|
| 70 | - | ||
| 71 | - | ```javascript |
|
| 72 | - | const URI = "ipfs://QmU85vmit8ShrUpnJFg3wEAMA61GcQB2X5KcgabchDV1kt"; |
|
| 73 | - | ``` |
|
| 74 | - | ||
| 75 | - | That’s it! After Steve ran the deployment command with Hardhat, the NFT had been minted in his wallet where we could see it on OpenSea! [Check it out](https://testnets.opensea.io/assets/goerli/0x45602432657d8100119e8633b677043b9022c22b/1) 😎 |
|
| 76 | - | ||
| 77 | - | Of course this app is a fun proof of concept, but with some more refining and perhaps a better framework, you can make efficient applications that can be owned as NFTs. This is just one of the many possibilities, all of which could either make your portfolio impressive, or even launch your own company 👀 What will you build? |
| 1 | - | --- |
|
| 2 | - | title: "How to Create 3D NFTs on Solana" |
|
| 3 | - | publishDate: "18 May 2022" |
|
| 4 | - | description: "Learn how to scan and mint real life objects on Solana" |
|
| 5 | - | ogImage: "/blog-images/medium/v2/resize:fit:4800/format:webp/1*5vzpNGiQSZqe1EX6ghDINg.png" |
|
| 6 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvut5khv2v" |
|
| 7 | - | --- |
|
| 8 | - | ||
| 9 | - | import medium from "../../assets/medium.png"; |
|
| 10 | - | import OutLinkButton from "@/components/common/OutLinkButton"; |
|
| 11 | - | ||
| 12 | - | <OutLinkButton |
|
| 13 | - | link="https://medium.com/pinata/how-to-scan-and-create-1-1-3d-nfts-on-solana-using-polycam-and-pinata-df513dd87937" |
|
| 14 | - | site="Medium" |
|
| 15 | - | image={medium} |
|
| 16 | - | /> |
|
| 17 | - | ||
| 18 | - | The growth and evolution of NFTs has come a long way from the early days. Some of the early NFT projects were simple .png files or a link to a YouTube video, but now they are an entire industry that consumes brand, utility, even augmented reality. Metaverses, 3D objects, and other virtual reality experiences are all the rage, and I firmly believe we will see more of this in the near future. |
|
| 19 | - | ||
| 20 | - | One unique tool that I stumbled upon was Polycam. Polycam is a mobile iOS app that utilizes the iPhone’s LiDAR scanner to map 3D objects and environments. 3D NFTs are certainly popular, but what about 3D NFTs of real life objects? Or even better: real life environments? I think this is an angle that not many people have thought about creating, and opens up a whole new realm of artistic expression in ways people have not seen before. |
|
| 21 | - | ||
| 22 | - | In this tutorial we’ll cover how to use Polycam to scan and create a 3D models and environments, and then we’ll cover how to make them into NFTs using Pinata and Solana. |
|
| 23 | - | ||
| 24 | - | ## Scanning with Polycam |
|
| 25 | - | ||
| 26 | - | To get started you’ll want to download the Polycam app on the App Store. Be aware that this is only for iPhones, and scanning environments will be more effective if you have a LiDAR scanner on your iPhone. Having a LiDAR scanner is not necessary though! Once you download it you will need to make an account with them and start their 14 day free trial. |
|
| 27 | - | ||
| 28 | - | Once you do that Polycam will take you to the “captures” page where you can see all your scans. To learn how the scan process works, I would highly recommend their well done tutorials on their website and in the app. |
|
| 29 | - | ||
| 30 | - | [Using LiDAR Mode to Capture Large Spaces](https://learn.poly.cam/capturing-large-spaces) |
|
| 31 | - | ||
| 32 | - | [Using Photo Mode to Capture 3D Objects](https://learn.poly.cam/building-a-studio-for-photo-mode-captures) |
|
| 33 | - | ||
| 34 | - | In short, you will want to use the LiDAR mode when you are trying to scan environments or large spaces, and for 3D objects you will want to use the photo mode! |
|
| 35 | - | ||
| 36 | - | For this tutorial I’m going to scan my desk space using the LiDAR method, and for the 3D objects we’ll use an old book, and of course Pinnie! |
|
| 37 | - | ||
| 38 | - | With the video below you can get glimpse of how the LiDAR scanner maps over surfaces. Taking it real slow and covering every angle really helps with scanning environments. |
|
| 39 | - | ||
| 40 | - | <div style="padding:216.22% 0 0 0;position:relative;"> |
|
| 41 | - | <iframe |
|
| 42 | - | src="https://player.vimeo.com/video/703877466?h=9659ba4952" |
|
| 43 | - | style="position:absolute;top:0;left:0;width:100%;height:100%;" |
|
| 44 | - | frameborder="0" |
|
| 45 | - | allow="autoplay; fullscreen; picture-in-picture" |
|
| 46 | - | allowfullscreen |
|
| 47 | - | ></iframe> |
|
| 48 | - | </div> |
|
| 49 | - | <script src="https://player.vimeo.com/api/player.js"></script> |
|
| 50 | - | ||
| 51 | - | After you’re done scanning there will be a processing step that will take all the data and map it into a 3D environment! |
|
| 52 | - | ||
| 53 | - | <div style="padding:216.22% 0 0 0;position:relative;"> |
|
| 54 | - | <iframe |
|
| 55 | - | src="https://player.vimeo.com/video/703880632?h=52b5f574b7" |
|
| 56 | - | style="position:absolute;top:0;left:0;width:100%;height:100%;" |
|
| 57 | - | frameborder="0" |
|
| 58 | - | allow="autoplay; fullscreen; picture-in-picture" |
|
| 59 | - | allowfullscreen |
|
| 60 | - | ></iframe> |
|
| 61 | - | </div> |
|
| 62 | - | <script src="https://player.vimeo.com/api/player.js"></script> |
|
| 63 | - | ||
| 64 | - | Once it’s processed you can go ahead and view the 3D model/environment! Polycam really is first class; you can create videos, you can view the model in Augmented Reality, it just has so many awesome features! My model isn’t the cleanest since I have so many small detailed objects on my desk, but for something a bit simpler this feature is amazing. |
|
| 65 | - | ||
| 66 | - | <div style="padding:216.22% 0 0 0;position:relative;"> |
|
| 67 | - | <iframe |
|
| 68 | - | src="https://player.vimeo.com/video/703884780?h=de95c5de7a" |
|
| 69 | - | style="position:absolute;top:0;left:0;width:100%;height:100%;" |
|
| 70 | - | frameborder="0" |
|
| 71 | - | allow="autoplay; fullscreen; picture-in-picture" |
|
| 72 | - | allowfullscreen |
|
| 73 | - | ></iframe> |
|
| 74 | - | </div> |
|
| 75 | - | <script src="https://player.vimeo.com/api/player.js"></script> |
|
| 76 | - | ||
| 77 | - | Now that we got our model, we can upload it to the cloud and look at it through our web browser! Just click on the little cloud icon on the top of the screen when viewing your model and it will be uploaded to your cloud storage with Polycam. |
|
| 78 | - | ||
| 79 | - | Once viewing it in the web browser, we can download the model by clicking “export.” |
|
| 80 | - | ||
| 81 | - | > |
|
| 82 | - | ||
| 83 | - | From there we want to select the GLTF format and start the download! |
|
| 84 | - | ||
| 85 | - | > |
|
| 86 | - | ||
| 87 | - | Ok so we got our 3D file for our environment, now lets do some 3D objects as well! The process is pretty similar, except for 3D objects we’ll switch to the “photo” mode instead of the LiDAR mode. The photo mode will take a bunch of different pictures and put them together to make a 3D model! In the video below you can see what the process looks like as I scan one of my prized possessions, a 1930's edition of Moby Dick illustrated by Rockwell Kent. |
|
| 88 | - | ||
| 89 | - | <div style="padding:216.22% 0 0 0;position:relative;"> |
|
| 90 | - | <iframe |
|
| 91 | - | src="https://player.vimeo.com/video/704181596?h=92cada3864" |
|
| 92 | - | style="position:absolute;top:0;left:0;width:100%;height:100%;" |
|
| 93 | - | frameborder="0" |
|
| 94 | - | allow="autoplay; fullscreen; picture-in-picture" |
|
| 95 | - | allowfullscreen |
|
| 96 | - | ></iframe> |
|
| 97 | - | </div> |
|
| 98 | - | <script src="https://player.vimeo.com/api/player.js"></script> |
|
| 99 | - | ||
| 100 | - | Once we finish scanning the book, I like to select the higher end of the quality allowance, and I like to use the object masking to get all the fine details. These photo mode objects are a bit more work, so when you take one Polycam sends it off to a server for higher powered processing. Due to how intense the work is, you can only take 150 of these a month, but I don’t think that will be an issue for most people. Once they finish processing it, the model looks like this! |
|
| 101 | - | ||
| 102 | - | <div style="padding:216.22% 0 0 0;position:relative;"> |
|
| 103 | - | <iframe |
|
| 104 | - | src="https://player.vimeo.com/video/704185926?h=2a929c3d73" |
|
| 105 | - | style="position:absolute;top:0;left:0;width:100%;height:100%;" |
|
| 106 | - | frameborder="0" |
|
| 107 | - | allow="autoplay; fullscreen; picture-in-picture" |
|
| 108 | - | allowfullscreen |
|
| 109 | - | ></iframe> |
|
| 110 | - | </div> |
|
| 111 | - | <script src="https://player.vimeo.com/api/player.js"></script> |
|
| 112 | - | ||
| 113 | - | The download process looks exactly the same as we did the desk, very easy! Also while you’re still in Polycam, you can edit, adjust, crop, etc. your model all inside their app or website, so that way when the model is exported it’s ready to be turned into an NFT! |
|
| 114 | - | ||
| 115 | - | Last but not least, we got a model of Pinnie too 😉 |
|
| 116 | - | ||
| 117 | - | [Pinata Polycam Capture](https://poly.cam/capture/FAE73483-C416-49D6-B92C-33260BF924E1) |
|
| 118 | - | ||
| 119 | - | ## Creating 1/1 NFTs on Solana |
|
| 120 | - | ||
| 121 | - | Alright alright alright, we got our models from Polycam, now lets turn them into 1/1 NFTs on Solana! |
|
| 122 | - | ||
| 123 | - | First thing you’ll want to do is get a [Pinata account](https://pinat.cloud). I would recommend getting the professional account so you can use a Dedicated Gateway, you’ll see why as we go further! |
|
| 124 | - | ||
| 125 | - | There’s a lot of great benefits of using IPFS for NFTs, such as [content addressability](https://docs.ipfs.io/concepts/content-addressing/) and [portability](https://medium.com/pinata/web3-data-portability-through-ipfs-saved-hicetnunc-724e3df2948d), but especially speed using a dedicated gateway. Since Solana is still adopting IPFS, not all the wallets and marketplaces have their own gateway solutions to resolve links like “ipfs://CID.” However with our own dedicated gateway we can use a standard “https://” link and deliver it with speed. You’ll see how we’ll do this in a bit! |
|
| 126 | - | ||
| 127 | - | Let’s log into Pinata and upload our file. Really simple, just click on the upload button in the top left, give it a name, and upload! |
|
| 128 | - | ||
| 129 | - | > |
|
| 130 | - | ||
| 131 | - | Then we just need to choose a subdomain and make sure its available. If it is, click next! |
|
| 132 | - | ||
| 133 | - | If we just clicked on this file to preview it, it’s gonna take us straight to download since the browser doesn’t know what to do. That’s ok! We’re gonna format the link for our NFT metadata. It will look something like this. |
|
| 134 | - | ||
| 135 | - | ``` |
|
| 136 | - | https://{your-subdomain}.mypinata.cloud/ipfs/{your-3d-file-cid}?filename={file-name-and-extension} |
|
| 137 | - | ``` |
|
| 138 | - | ||
| 139 | - | For our particular CID the link will look like this! |
|
| 140 | - | ||
| 141 | - | ``` |
|
| 142 | - | https://pinnieblog.mypinata.cloud/ipfs/QmWmcX9ikvNTJtCmuX9oWiUvTABQZ6tC6UARqa7ozBh2Ry?filename=Pinnie.glb |
|
| 143 | - | ``` |
|
| 144 | - | ||
| 145 | - | Ok our 3D file is on IPFS, the next step is to create a metadata file that will point to that 3D file. Open up your terminal and run: |
|
| 146 | - | ||
| 147 | - | ```bash |
|
| 148 | - | mkdir 3d-nfts && cd 3d-nfts && touch pinnie.json |
|
| 149 | - | ``` |
|
| 150 | - | ||
| 151 | - | Then open up that pinnie.json file in your text editor of choice. Since we’re using Solana we will want to use [Metaplex Token Metadata Standard](https://docs.metaplex.com/token-metadata/Versions/v1.0.0/nft-standard#json-structure) which you will definitely want to save as a reference, but we’ll be simplifying ours just a little bit. |
|
| 152 | - | ||
| 153 | - | ```json |
|
| 154 | - | { |
|
| 155 | - | "name": "3D Pinnie", |
|
| 156 | - | "symbol": "PIN", |
|
| 157 | - | "description": "A 3D scan of Pinnie taken with Polycam", |
|
| 158 | - | "seller_fee_basis_points": 0, |
|
| 159 | - | "image": "null", |
|
| 160 | - | "animation_url": "https://pinnieblog.mypinata.cloud/ipfs/QmWmcX9ikvNTJtCmuX9oWiUvTABQZ6tC6UARqa7ozBh2Ry?filename=Pinnie.glb", |
|
| 161 | - | "external_url": "https://pinata.cloud", |
|
| 162 | - | "collection": { |
|
| 163 | - | "name": "Pinnie's 3D NFTs", |
|
| 164 | - | "family": "3D NFTs" |
|
| 165 | - | }, |
|
| 166 | - | "properties": { |
|
| 167 | - | "files": [ |
|
| 168 | - | { |
|
| 169 | - | "uri": "null", |
|
| 170 | - | "type": "image/png" |
|
| 171 | - | }, |
|
| 172 | - | { |
|
| 173 | - | "uri": "https://pinnieblog.mypinata.cloud/ipfs/QmWmcX9ikvNTJtCmuX9oWiUvTABQZ6tC6UARqa7ozBh2Ry?filename=Pinnie.glb", |
|
| 174 | - | "type": "vr/glb" |
|
| 175 | - | } |
|
| 176 | - | ], |
|
| 177 | - | "category": "3D", |
|
| 178 | - | "creators": [ |
|
| 179 | - | { |
|
| 180 | - | "address": "D8KLFUfnRwGsMt6n56FzkyRYmVUQXiRnJWFV7rZYCYdd", |
|
| 181 | - | "share": 100 |
|
| 182 | - | } |
|
| 183 | - | ] |
|
| 184 | - | } |
|
| 185 | - | } |
|
| 186 | - | ``` |
|
| 187 | - | ||
| 188 | - | Couple of things you’ll want to take note here. We could use an image URL if you wanted a backup, just make sure you upload a separate file to Pinata. For our example I’ve left it as null. The key thing to notice is the “animation_url” which I have set to our link we created earlier. Under properties and files, we left the image null again, and created another one that includes: |
|
| 189 | - | ||
| 190 | - | ```json |
|
| 191 | - | { |
|
| 192 | - | "uri": "https://pinnieblog.mypinata.cloud/ipfs/QmWmcX9ikvNTJtCmuX9oWiUvTABQZ6tC6UARqa7ozBh2Ry?filename=Pinnie.glb", |
|
| 193 | - | "type": "vr/glb" |
|
| 194 | - | } |
|
| 195 | - | ``` |
|
| 196 | - | ||
| 197 | - | The uri is the link to our content, and the type defines the kind of file we want to use, which in this case is virtual reality (vr) and the file format (glb). Once you get familiar with the metadata standard it’s pretty straight forward. Now that we created this metadata file, save it and upload it to Pinata as well. Once it’s uploaded take note of the CID and save the link to it, should look something like this: |
|
| 198 | - | ||
| 199 | - | ``` |
|
| 200 | - | https://pinnieblog.mypinata.cloud/ipfs/QmYTQDE2ur5Lo4z76cNjwMAq6H3oNyDb1xuCQfdxMBnmon |
|
| 201 | - | ``` |
|
| 202 | - | ||
| 203 | - | We’re really close to minting this NFT! [Solana](https://solana.com/) is an up and coming blockchain that is capable of incredible speeds and low transactions fees. Before we go any further you’ll want to make sure you have a couple of things installed on your computer: |
|
| 204 | - | ||
| 205 | - | - [Solana CLI](https://docs.solana.com/cli/install-solana-cli-tools) |
|
| 206 | - | - [Metaboss](https://github.com/samuelvanderwaal/metaboss) |
|
| 207 | - | ||
| 208 | - | Be sure to follow the instructions for installing each very carefully! The Solana CLI is the tool that will let us interact with the Solana blockchain, and Metaboss is nicknamed “The Solana Metaplex NFT ‘Swiss Army Knife’” and rightly so. It is packed with features, including the minting NFTs! |
|
| 209 | - | ||
| 210 | - | Once you’ve installed both, let’s make sure the Solana CLI is setup properly by running the following command. |
|
| 211 | - | ||
| 212 | - | ```bash |
|
| 213 | - | solana --version |
|
| 214 | - | ``` |
|
| 215 | - | ||
| 216 | - | If that returns the version then we are good to go! Next thing we’ll do is create a wallet, set our network to devnet, and get some fake sol. To create a new wallet run the command below. |
|
| 217 | - | ||
| 218 | - | ```bash |
|
| 219 | - | solana-keygen new --outfile ~/.config/solana/devnet.json |
|
| 220 | - | ``` |
|
| 221 | - | ||
| 222 | - | This will create a file on your computer and is the code version of a new Solana wallet! Next we want to set our Solana config to use this wallet by default. |
|
| 223 | - | ||
| 224 | - | ```bash |
|
| 225 | - | solana config set --keypair ~/.config/solana/devnet.json |
|
| 226 | - | ``` |
|
| 227 | - | ||
| 228 | - | Last but not least, we’ll set our Solana config to use the devnet environment so we can use fake money and have room to mess around. |
|
| 229 | - | ||
| 230 | - | ```bash |
|
| 231 | - | solana config set --url https://api.devnet.solana.com |
|
| 232 | - | ``` |
|
| 233 | - | ||
| 234 | - | Now we can make sure all is good by running: |
|
| 235 | - | ||
| 236 | - | ```bash |
|
| 237 | - | solana config get |
|
| 238 | - | ``` |
|
| 239 | - | ||
| 240 | - | This will return what wallet we’re using and what network we’re on! Last step is to get some test SOL, which is so easy! Just run the command below. Once it’s done it should return a balance! |
|
| 241 | - | ||
| 242 | - | ```bash |
|
| 243 | - | solana airdrop 2 |
|
| 244 | - | ``` |
|
| 245 | - | ||
| 246 | - | As far as Metaboss goes, it’s pretty simple to install after installing Solana. Once you have installed it, run the following command to make sure it’s ready to go. |
|
| 247 | - | ||
| 248 | - | ```bash |
|
| 249 | - | metaboss --version |
|
| 250 | - | ``` |
|
| 251 | - | ||
| 252 | - | To mint our NFT, Metaboss makes it so easy! We just gotta run this command: |
|
| 253 | - | ||
| 254 | - | ```bash |
|
| 255 | - | metaboss mint one --external-metadata-uri https://pinnieblog.mypinata.cloud/ipfs/QmYTQDE2ur5Lo4z76cNjwMAq6H3oNyDb1xuCQfdxMBnmon --keypair ~/.config/solana/devnet.json |
|
| 256 | - | ``` |
|
| 257 | - | ||
| 258 | - | Let’s break down what’s happening here. Metaboss is going to mint one token for us, and we set the “external-metadata-uri” to the url we created a little while ago that points to our uploaded metadata. Then we just set the keypair or wallet to authorize the transaction, and that’s it! If it worked you should get a result with the transaction ID and the mint account! |
|
| 259 | - | ||
| 260 | - | Ok but we want to see it right? All we gotta do is open our ~/.config/solana/devnet.json file, and copy the numbers in it (this is your private key, keep it safe and do not share it!) |
|
| 261 | - | ||
| 262 | - | Then using a wallet like Phantom, click on “Add / Connect wallet, select “import private key,” then paste in our keypair from earlier. Once it’s in your wallet you will want to click on the settings icon in the bottom right, scroll down to “change network” and set it to “Devnet.” Once you do that, go back to your NFTs page and you should see it!! |
|
| 263 | - | ||
| 264 | - |  |
|
| 265 | - | ||
| 266 | - | ## Conclusion |
|
| 267 | - | ||
| 268 | - | Polycam really introduces a whole new way of how content can be created using modern tech. You can imagine how this might be used for journalism, where instead of just looking at a photo of a story, you can view it as a 3D object with augmented reality. Someone who explores caves can share their experience as a 3D environment! Paired with Solana as a much faster and affordable layer one solution, and [Pinata](https://pinata.cloud) to deliver IPFS content quickly, the possibilities are endless! |
| 3 | 3 | publishDate: "02 May 2023" |
|
| 4 | 4 | description: "I spent two days with zero access to my cell phone or computer, and this is what I discovered" |
|
| 5 | 5 | tags: ["philosophy", "personal"] |
|
| 6 | - | ogImage: "/blog-images/cloudinary/v1683045399/misc/D59779D7-75D0-4EE8-8C39-6C3EDA5D5CFC_1_105_c_hbikqe.jpg" |
|
| 6 | + | ogImage: "/blog-images/files-stevedylan-dev/disconnected.jpg" |
|
| 7 | 7 | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvusirgn2v" |
|
| 8 | 8 | --- |
|
| 9 | 9 | ||
| 10 | - | import substack from "../../assets/substack.png"; |
|
| 11 | - | import OutLinkButton from "@/components/common/OutLinkButton"; |
|
| 12 | - | ||
| 13 | - | <OutLinkButton |
|
| 14 | - | link="https://open.substack.com/pub/stevedsimkins/p/48-hours-disconnected?r=1iyrw0&utm_campaign=post&utm_medium=web" |
|
| 15 | - | site="Substack" |
|
| 16 | - | image={substack} |
|
| 17 | - | />{" "} |
|
| 18 | - | ||
| 19 | - |  |
|
| 10 | + |  |
|
| 20 | 11 | ||
| 21 | 12 | It was a Thursday afternoon at the chiropractor. I was still on my paternity leave, helping my wife take our kids so our youngest could get some adjustments. Like most dads I sat around, waited, and scrolled on my phone. Halfway through I could tell my wife was miffed, and I thought it was due to our appointment being fifteen minutes late. In reality, my son had been trying to get my attention for several minutes, I ignored him, and he gave up. When she told me later that day I could vividly see it happen, like I was there, but I wasn’t. |
|
| 22 | 13 |
| 1 | - | --- |
|
| 2 | - | title: "How to Run Your Own Public IPFS Gateway" |
|
| 3 | - | publishDate: "2023-10-10T00:00:00.000Z" |
|
| 4 | - | description: "Learn how to run a public IPFS gateway with a custom domain using Digital Ocean" |
|
| 5 | - | tags: ["ipfs", "tutorials"] |
|
| 6 | - | ogImage: "/blog-images/other/6525558858895876456798a8_20231010_How-To-Run-Your-Own-IPFS-Gateway.jpeg" |
|
| 7 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvus7pff2v" |
|
| 8 | - | --- |
|
| 9 | - | ||
| 10 | - | 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! |
|
| 11 | - | ||
| 12 | - | <aside> |
|
| 13 | - | ⚠️ 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. |
|
| 14 | - | ||
| 15 | - | </aside> |
|
| 16 | - | ||
| 17 | - | ## Requirements |
|
| 18 | - | ||
| 19 | - | 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. |
|
| 20 | - | ||
| 21 | - | ## Setting Up the Server |
|
| 22 | - | ||
| 23 | - | 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. |
|
| 24 | - | ||
| 25 | - | 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. |
|
| 26 | - | ||
| 27 | - | 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: |
|
| 28 | - | ||
| 29 | - | <img |
|
| 30 | - | src="/blog-images/placeholder.png" |
|
| 31 | - | alt="digital ocean droplet creation" |
|
| 32 | - | width={1920} |
|
| 33 | - | height={1080} |
|
| 34 | - | aspectRatio={2 / 1} |
|
| 35 | - | /> |
|
| 36 | - | ||
| 37 | - | For the authorization select SSH Keys, then copy and paste the contents of `~/.ssh/rsa.pub` and paste it in as a new key. |
|
| 38 | - | ||
| 39 | - | <img |
|
| 40 | - | src="/blog-images/placeholder.png" |
|
| 41 | - | alt="digital ocean ssh key creation" |
|
| 42 | - | width={1920} |
|
| 43 | - | height={1080} |
|
| 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", "blockchain", "tutorials"] |
|
| 6 | - | ogImage: "/blog-images/other/6545bfa112815d6340466066_20231103_How-to-Encrypt-and-Decrypt-Files-on-IPFS-Using-Lit-Protocol-and-Pinata.jpeg" |
|
| 7 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvus3hnn2v" |
|
| 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: [":userAddress"], |
|
| 404 | - | returnValueTest: { |
|
| 405 | - | comparator: ">", |
|
| 406 | - | value: "0", |
|
| 407 | - | }, |
|
| 408 | - | }, |
|
| 409 | - | ]; |
|
| 410 | - | ``` |
|
| 411 | - | ||
| 412 | - | 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)\*\*** |
|
| 413 | - | ||
| 414 | - | ```jsx |
|
| 415 | - | const accessControlConditions = [ |
|
| 416 | - | { |
|
| 417 | - | contractAddress: "0x50D8EB685a9F262B13F28958aBc9670F06F819d9", |
|
| 418 | - | standardContractType: "MolochDAOv2.1", |
|
| 419 | - | chain, |
|
| 420 | - | method: "members", |
|
| 421 | - | parameters: [":userAddress"], |
|
| 422 | - | returnValueTest: { |
|
| 423 | - | comparator: "=", |
|
| 424 | - | value: "true", |
|
| 425 | - | }, |
|
| 426 | - | }, |
|
| 427 | - | ]; |
|
| 428 | - | ``` |
|
| 429 | - | ||
| 430 | - | You can even do a simple check if the recipient is a particular wallet address. |
|
| 431 | - | ||
| 432 | - | ```jsx |
|
| 433 | - | const accessControlConditions = [ |
|
| 434 | - | { |
|
| 435 | - | contractAddress: "", |
|
| 436 | - | standardContractType: "", |
|
| 437 | - | chain, |
|
| 438 | - | method: "", |
|
| 439 | - | parameters: [":userAddress"], |
|
| 440 | - | returnValueTest: { |
|
| 441 | - | comparator: "=", |
|
| 442 | - | value: "0x50e2dac5e78B5905CB09495547452cEE64426db2", |
|
| 443 | - | }, |
|
| 444 | - | }, |
|
| 445 | - | ]; |
|
| 446 | - | ``` |
|
| 447 | - | ||
| 448 | - | 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! |
|
| 449 | - | ||
| 450 | - | Happy Pinning! |
| 24 | 24 | loop |
|
| 25 | 25 | playsinline |
|
| 26 | 26 | className="aspect-video w-full" |
|
| 27 | - | src="/blog-images/files-stevedylan-dev/Qmeb4797YyF2FhwTdoPuuQi4LXgdXaGoGcTRqU85JAEv9M.mp4" |
|
| 27 | + | src="/blog-images/files-stevedylan-dev/tmux-1.mp4" |
|
| 28 | 28 | ></video> |
|
| 29 | 29 | ||
| 30 | 30 | Additionally I can create multiple panes and windows inside a session. I generally create a session per project, and each session might have two windows (e.g. one for a client side repo, the other for a server side repo). This both helps keep projects unified yet organized. |
|
| 35 | 35 | loop |
|
| 36 | 36 | playsinline |
|
| 37 | 37 | className="aspect-video w-full" |
|
| 38 | - | src="/blog-images/files-stevedylan-dev/QmWHHCV9YVecdDVcbKXLdfcudW5XBiTt4EfLagP9cowJjC.mp4" |
|
| 38 | + | src="/blog-images/files-stevedylan-dev/tmux-2.mp4" |
|
| 39 | 39 | ></video> |
|
| 40 | 40 | ||
| 41 | 41 | Where it gets really good is having a solid session manager, and that’s where [Josh Medeski’s Sesh](https://github.com/joshmedeski/sesh) comes into play. With this I can easily change between different sessions, and thus easily switch between different projects. This has become essential to my workflow as I often might be working on 2-3 different projects at a time, all with multiple windows and panes each. Each one can be unique to what I’m working on as well. |
|
| 46 | 46 | loop |
|
| 47 | 47 | playsinline |
|
| 48 | 48 | className="aspect-video w-full" |
|
| 49 | - | src="/blog-images/files-stevedylan-dev/Qmd3SNkNTms4JQCtv4wmFUKS679A2zsrG7e58szLWS3pmM.mp4" |
|
| 49 | + | src="/blog-images/files-stevedylan-dev/sesh.mp4" |
|
| 50 | 50 | ></video> |
|
| 51 | 51 | ||
| 52 | 52 | ## Neovim Plugins |
|
| 61 | 61 | loop |
|
| 62 | 62 | playsinline |
|
| 63 | 63 | className="aspect-video w-full" |
|
| 64 | - | src="/blog-images/files-stevedylan-dev/QmTRit4eCktJ87NjxdErZ3csXimNq43G8SueQP77mvtvkE.mp4" |
|
| 64 | + | src="/blog-images/files-stevedylan-dev/telescope.mp4" |
|
| 65 | 65 | ></video> |
|
| 66 | 66 | ||
| 67 | 67 | Another one I use fairly often is [Neo-tree](https://github.com/nvim-neo-tree/neo-tree.nvim). I know some people are anti-file-tree but I personally really enjoy it. It is setup to appear in the middle of my screen, and I use it to help navigate where a file might be, edit a file name, add new files, etc. Since the majority of my work is in JavaScript / Next.js, it’s helpful to distinguish which `page.tsx` or `route.ts` file I’m currently working on. |
|
| 72 | 72 | loop |
|
| 73 | 73 | playsinline |
|
| 74 | 74 | className="aspect-video w-full" |
|
| 75 | - | src="/blog-images/files-stevedylan-dev/QmVSc4amHjygkTcd4Rrx62DaYWMXSYpJD8fBhSinGyChyp.mp4" |
|
| 75 | + | src="/blog-images/files-stevedylan-dev/neotree.mp4" |
|
| 76 | 76 | ></video> |
|
| 77 | 77 | ||
| 78 | 78 | Having an LSP (Language Server Protocol) and completions setup is also essential for a good workflow in Neovim. This is what provides hints, completions, diagnostics, or even docs for the language you’re working in. Cannot state how helpful these are when working in a typed language like Typescript or Go. I would also say it’s beneficial to learn how to set it up manually, and [Typecraft's video](https://youtu.be/S-xzYgTLVJE?si=xG7c-Yx0fkxRHwx0) does a great job showing how it’s done. |
|
| 83 | 83 | loop |
|
| 84 | 84 | playsinline |
|
| 85 | 85 | className="aspect-video w-full" |
|
| 86 | - | src="/blog-images/files-stevedylan-dev/QmdQSzMuPiDEcFhWLMLsabLLsHD4qG74Qh1WtvQ8SnSa1B.mp4" |
|
| 86 | + | src="/blog-images/files-stevedylan-dev/lsp-completions.mp4" |
|
| 87 | 87 | ></video> |
|
| 88 | 88 | ||
| 89 | 89 | These two are on the smaller side but are still really great to use. The first is [Tmux Navigator.](https://github.com/christoomey/vim-tmux-navigator) This allows you to navigate between an open Neovim pane and a Tmux pane without using a Tmux prefix. For example, instead of navigating with `Ctrl + b - l` I can just use `Ctrl - l`. It’s the same mapping for switching between Neovim panes and Tmux panes, which is a huge quality of life improvement. The other small mention is [blame.nvim.](https://github.com/FabijanZulj/blame.nvim) With this tool I can hit `Space-b` to see line by line who changed what when. This is great when working with other people on a project and you’re trying to find out what changed when. There’s also the LazyGit plugin for Neovim, but it deserves its own section. |
|
| 98 | 98 | loop |
|
| 99 | 99 | playsinline |
|
| 100 | 100 | className="aspect-video w-full" |
|
| 101 | - | src="/blog-images/files-stevedylan-dev/QmZou8CVipfiFxYkYYm3H6BAzKk1ks6mkosTULxk7GgFUb.mp4" |
|
| 101 | + | src="/blog-images/files-stevedylan-dev/lazygit.mp4" |
|
| 102 | 102 | ></video> |
|
| 103 | 103 | ||
| 104 | 104 | ## Bringing it All Together |
|
| 111 | 111 | loop |
|
| 112 | 112 | playsinline |
|
| 113 | 113 | className="aspect-video w-full" |
|
| 114 | - | src="/blog-images/files-stevedylan-dev/QmUW8QWY1ug3uHzLWtc6F9pFHAurgbjG3qmnc9o6whZDoZ.mp4" |
|
| 114 | + | src="/blog-images/files-stevedylan-dev/term-walkthrough-1.mp4" |
|
| 115 | 115 | ></video> |
|
| 116 | 116 | ||
| 117 | 117 | If the project has multiple repos like a server/client combo or I’m referencing another repo, I’ll create a new window with the same pane setup. As I work and make changes, I’ll open LazyGit in one of my Tmux panes and run `ctrl-b + z` to make it full screen. From there I’ll add my commits and push them up, or make a branch that I can merge main into if I’m already working on a shared project. |
|
| 122 | 122 | loop |
|
| 123 | 123 | playsinline |
|
| 124 | 124 | className="aspect-video w-full" |
|
| 125 | - | src="/blog-images/files-stevedylan-dev/QmdrPRHBkMNCvcbCEaQp9PeUKQsuCHKHgEecBMG6tRnLyB.mp4" |
|
| 125 | + | src="/blog-images/files-stevedylan-dev/term-walkthrough-2.mp4" |
|
| 126 | 126 | ></video> |
|
| 127 | 127 | ||
| 128 | 128 | Back in my main Next.js window I might open another split pane below the right one so while the dev server is running I can make test API calls from the terminal with httpie, maybe pipe the results into jq then into a file. |
|
| 133 | 133 | loop |
|
| 134 | 134 | playsinline |
|
| 135 | 135 | className="aspect-video w-full" |
|
| 136 | - | src="/blog-images/files-stevedylan-dev/Qmc1Lfd6KmrWSU1kTtrnT59nyPx1QpTvhEzPY9taagWCuv.mp4" |
|
| 136 | + | src="/blog-images/files-stevedylan-dev/term-walkthrough-3.mp4" |
|
| 137 | 137 | ></video> |
|
| 138 | 138 | ||
| 139 | 139 | This is the flexibility of a terminal based workflow that is hard to replicate on something like VSCode or Zed. It’s not even an editor issue in my opinion: it’s a development environment issue. Do code editors like VSCode take out out of that environment? Sorta, not totally, but it’s definitely not the same. |
|
| 1 | - | --- |
|
| 2 | - | title: "Arc: The Internet Computer" |
|
| 3 | - | publishDate: "08 Mar 2023" |
|
| 4 | - | description: "How the Arc web browser is paving the way for the future of consumer computers" |
|
| 5 | - | tags: ["open web", "philosophy"] |
|
| 6 | - | ogImage: "/blog-images/cloudinary/v1678385122/arc-browser-blog-post/opluqtxq1ceoigepyjwf.png" |
|
| 7 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvuskpwn2v" |
|
| 8 | - | --- |
|
| 9 | - | ||
| 10 | - |  |
|
| 11 | - | height={1080} |
|
| 12 | - | aspectRatio={16 / 9} |
|
| 13 | - | /> |
|
| 14 | - | ||
| 15 | - | ## 20 Years of the Same Thing |
|
| 16 | - | ||
| 17 | - | The internet has grown significantly in the last twenty years. What was once just static web pages with facts is now a bustling cyber metropolis where we write essays, share photos of food, and buy airline tickets. It's something we use every day and take for granted. What's interesting is that in the last twenty years of the internet's evolution, the way we experience it has stayed mostly the same. Web browsers have certainly changed in appearance and performance, but the tab model and disconnect from the rest of the computer have stayed. The Browser Company making the Arc Browser has plans to change that and has the ambitious goal of creating an "Internet Computer." Before we get ahead of ourselves, let's have a brief overview of Arc and its features. |
|
| 18 | - | ||
| 19 | - | ## Tabs, Folders, and Spaces |
|
| 20 | - | ||
| 21 | - | <img |
|
| 22 | - | src="/blog-images/placeholder.png" |
|
| 23 | - | alt="Tabs folders and spaces in arc" |
|
| 24 | - | width={1920} |
|
| 25 | - | height={1080} |
|
| 26 | - | aspectRatio={16 / 9} |
|
| 27 | - | /> |
|
| 28 | - | ||
| 29 | - | One of the fundamental differences between Arc and other browsers is how it handles tabs. Instead of a row of tabs at the top of a window, Arc keeps them all in a sidebar. I genuinely believe there is a strong UX decision being made here: organization is simply easier to accomplish with a side menu than it is with a top menu. Beyond that, Arc uses a “Pinned Tabs” and “Today’s Tabs” approach to organizing your website. You may often feel hesitant to close a website because you might need it later. If you do, you can move it to the pinned tabs. When you feel like you don’t need it, then you can remove it from pinned tabs. Anything that stays in today’s tabs can be automatically cleared after a certain amount of time, or you can click a button to wipe them all out. This is powerful because it makes the decision of what is important and what isn’t easy for people to make. |
|
| 30 | - | ||
| 31 | - | <img |
|
| 32 | - | src="/blog-images/placeholder.png" |
|
| 33 | - | alt="View of spaces in Arc" |
|
| 34 | - | width={1920} |
|
| 35 | - | height={1080} |
|
| 36 | - | aspectRatio={16 / 9} |
|
| 37 | - | /> |
|
| 38 | - | ||
| 39 | - | Of course, you may have lots of websites that you need to keep pinned, and for that, Arc has Folders and Spaces. Folders work like any other bookmark folder, where you can store as many as you want with sub-folders as well. This can be useful if you have a lot of sites that need to be referenced for a project. Folders work to a degree, but even still, you can have too many folders. That’s where spaces come in. Spaces are designed to be different workspaces for whatever you do on the internet. You could have a shopping space, an entertainment space, a developer space, anything really. You can customize the appearance, name, icon, and of course, the pinned tabs and folders for each one. Arc makes it simple to swipe between spaces and re-organize them to fit your needs. |
|
| 40 | - | ||
| 41 | - | <img |
|
| 42 | - | src="/blog-images/placeholder.png" |
|
| 43 | - | alt="View of favorites in Arc" |
|
| 44 | - | width={912} |
|
| 45 | - | height={528} |
|
| 46 | - | /> |
|
| 47 | - | ||
| 48 | - | Favorites are special in that they are in nice little squares are the top of the sidebar no matter what space you are in, so for instance if you use Spotify a lot then that would be an ideal favorite app or website. They also have a preview feature when you hover over them for selective sites, e.g. Google Calendar will show you a brief schedule window. |
|
| 49 | - | ||
| 50 | - | These simple changes can drastically clear up and organize your internet browser if you typically have thirty Chrome tabs open, and it's seriously good. There are even more reasons why this feature set is so important, but we’ll get back to that later. |
|
| 51 | - | ||
| 52 | - | ## Split View |
|
| 53 | - | ||
| 54 | - | <img |
|
| 55 | - | src="/blog-images/placeholder.png" |
|
| 56 | - | alt="Split view in Arc" |
|
| 57 | - | height={1080} |
|
| 58 | - | width={1920} |
|
| 59 | - | aspectRatio={16 / 9} |
|
| 60 | - | /> |
|
| 61 | - | ||
| 62 | - | Another killer feature on Arc is split views. With a few clicks, you can easily have side-by-side panels of two different websites. You can even pin these dual tabs if you have to reference them quickly. You can actually do more than just two; depending on how big your screen is, you can go crazy! Personally, I use this all the time when reading an article while taking notes or moving information from one app to another. The Arc team recently released vertical splits as well, perhaps enough persuasion to get a vertical monitor. |
|
| 63 | - | ||
| 64 | - | Managing split views is effortless, with the ability to expand tabs to full screen, delete or replace a pane, resize panes, and rearrange them. It's such a simple feature but executed so well. |
|
| 65 | - | ||
| 66 | - | ## Peek View |
|
| 67 | - | ||
| 68 | - |  |
|
| 69 | - | ||
| 70 | - | Peek is a new feature for Arc but perhaps one of my favorites. When you click on a link, instead of taking you to a new tab or taking you off the current site, a smaller window pops up and allows you to see the content, where you can either close it or expand it into a full tab. You could be browsing Twitter, see someone reference an article, take a quick peek, and be done! It's a great way to reduce tab clutter, with a world-class UX. |
|
| 71 | - | ||
| 72 | - | ## Easels |
|
| 73 | - | ||
| 74 | - | <img |
|
| 75 | - | src="/blog-images/placeholder.png" |
|
| 76 | - | alt="Easels in Arc" |
|
| 77 | - | height={1080} |
|
| 78 | - | width={1920} |
|
| 79 | - | aspectRatio={16 / 9} |
|
| 80 | - | /> |
|
| 81 | - | ||
| 82 | - | Easels are something completely unique to Arc and have some pretty impressive abilities. The concept is like internet chalkboards. You can snip pieces of websites, add them to an easel, then add other things like your images, text, shapes, etc. Arc provides a great shortcut (that can be customized), where all you have to do is hold down Command and Shift, then click & drag across the screen to select what you want to snip. Then it will prompt what easel you want to add the snippet to, or if you want to make a new one. Easels can also be made public and shared with other people! |
|
| 83 | - | ||
| 84 | - |  |
|
| 85 | - | ||
| 86 | - | The craziest part? They feature a LIVE mode. Let’s say you take a snippet of the weather of your hometown from Google and save it to your easel. At any time, you can visit the easel and press the “play” button, and Arc will update the easel in real-time! I’ve used this feature to track packages and share research with my team. Others have used it to make personalized dashboards to feature live info. You really need to try it for yourself! |
|
| 87 | - | ||
| 88 | - | ## Boosts |
|
| 89 | - | ||
| 90 | - | <img |
|
| 91 | - | src="/blog-images/placeholder.png" |
|
| 92 | - | alt="A view of the Boosts menu in Arc" |
|
| 93 | - | height={1080} |
|
| 94 | - | width={1920} |
|
| 95 | - | aspectRatio={16 / 9} |
|
| 96 | - | /> |
|
| 97 | - | ||
| 98 | - | Arc has an affinity to bring back the excitement of the internet from the 90s, both in design and in customization. Not only does Arc feature themes for your browser to personalize your experience, they give you the ability to do “Boosts” to websites. Boosts are simply CSS or JS injections to websites so you can customize the color of a website or give it additional functionality. |
|
| 99 | - | ||
| 100 | - | <img |
|
| 101 | - | src="/blog-images/placeholder.png" |
|
| 102 | - | alt="Boost being made in Arc for Tiwtter" |
|
| 103 | - | height={1080} |
|
| 104 | - | width={1920} |
|
| 105 | - | aspectRatio={16 / 9} |
|
| 106 | - | /> |
|
| 107 | - | ||
| 108 | - | As someone who is obsessed with customizing the personal computer environment, I absolutely love Boosts. [Nord](https://nordtheme.com/) is one of my favorite color palettes, and I use it for as many ports as I can. With Boosts, I’ve been able to customize all my regular sites to give it some flare. This comes a bit naturally for me since I have some web development experience and know how to snag CSS selectors easily, however, there are possibilities for Boosts to become a marketplace feature where non-technical users could save and use Boosts made by others. |
|
| 109 | - | ||
| 110 | - | ## One App to Rule Them All |
|
| 111 | - | ||
| 112 | - | A fun fact about most applications running on your computer: they're likely Electron apps. Electron is a framework for building desktop applications, and it uses web languages like Javascript. In a lot of ways, Electron apps are just web browsers. Most apps in this format work in your web browser too, like Slack, Spotify, Discord, etc. My personal computer operated like this: I had a dozen apps downloaded for individual web applications. Why? 1. It was cumbersome to use them in my web browser, and 2. I thought it would be more performant than using the web browser (it was not). Arc solved both of those problems for me. |
|
| 113 | - | ||
| 114 | - | This goes back to how Arc manages pinned tabs. Since it has the ability to treat websites like applications, it makes it much easier to access them and use them. Instead of having a bunch of tabs on the top of my browser where Slack could get lost, I can always find it in my pinned tabs on my sidebar. Within two days of adjusting to Arc, I was able to delete all of my excess Electron apps. Now instead of running ten apps at once, I run maybe two or three, with Arc being the heavy lifter. When I’m not using apps, I can simply close them out but keep it pinned in my tabs for easy access later on. |
|
| 115 | - | ||
| 116 | - | ## Internet Computers |
|
| 117 | - | ||
| 118 | - | That last point brings us to the concept of Internet Computers that Josh, the CEO of The Browser Company, talks about in [videofile\_ : the internet computer](https://youtu.be/v0160IirdL4). He essentially breaks down how most of our lives run on applications in the cloud rather than the computers we use, whether it's our work, our photos and videos, or our entertainment; it doesn’t live locally. An Internet Computer is a fluid concept without definition, but perhaps could look like |
|
| 119 | - | ||
| 120 | - | > I can tap my finger on the device, or I can toss at a glance and whoosh: my computer comes down from the internet, comes down from the cloud, and is right there on that machine. Because all of the stuff I need, all of my tools, all of my files, all of my people, my teams, all of those things are out there on the internet too so it doesn't matter where I access it from. |
|
| 121 | - | ||
| 122 | - | Josh talks about this as a possible reality in five to ten years, but to be honest, it feels like we could be so much closer. Arc is already an app that can do just about anything you need it to do, thanks to the power of developers building web applications. Of course, there are limitations with heavier software, and there is still a lot of work to be done, but the concept of a computer that lives in the cloud and could seamlessly travel between your phone, your computer, or your partner’s tablet, really excites me! |
|
| 123 | - | ||
| 124 | - | I can also see some possibilities of Web3 being part of this future, where your crypto wallet could be used as your internet identity. Your assets and funds could be used with an Internet computer to send tokens, vote on proposals, or integrate digital collectibles. The future of crypto and blockchain is still muddy and has a lot of work before it becomes a reality, yet it's easy to visualize something like this. |
|
| 125 | - | ||
| 126 | - | ## Join the Arc Side |
|
| 127 | - | ||
| 128 | - | Whatever the future might hold, Arc and its incredible features are available now! At the time of this blogpost it is only available on MacOS, but the team is currently building out a mobile app as well as a Windows version. It is also only accessed by invite only. however there are many being passed around. My best suggestion is to check out their [Discord](https://discord.gg/arcinternet) where they have a channel for invite codes, or if you want, you can [email me](mailto:hello@stevedsimkins.dev), and I’ll give out what I can! 😁 |
|
| 129 | - | ||
| 130 | - | I appreciate everything The Browser Company has accomplished so far, and I wish them the best as they attempt to revolutionize the way we experience the Internet! |
| 1 | - | --- |
|
| 2 | - | title: "Scaling Your NFT Project: A Beginner’s Guide to IPFS" |
|
| 3 | - | publishDate: "07 Oct 2022" |
|
| 4 | - | description: "Storage ain’t sexy, but if web3 is gonna take a leap, it's one of the biggest problems we need to solve for." |
|
| 5 | - | ogImage: "/blog-images/other/62fd1b0820188c4271c6f5ac_Thumbnail1.png" |
|
| 6 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvusztdf2v" |
|
| 7 | - | --- |
|
| 8 | - | ||
| 9 | - | import bueno from "../../assets/bueno.png"; |
|
| 10 | - | import OutLinkButton from "@/components/common/OutLinkButton"; |
|
| 11 | - | ||
| 12 | - | <OutLinkButton link="https://bueno.art/blog/pinata-ipfs-guide" site="Bueno" image={bueno} /> |
|
| 13 | - | ||
| 14 | - | As an NFT creator, you want to make sure your files are safe, verifiable, and that you’re not paying an arm and a leg to keep them that way. IPFS (accessed by a pinning service like Pinata) solves all these issues, but before we talk about how, let’s paint the picture and explain how all of this applies to you as an NFT creator and collector. |
|
| 15 | - | ||
| 16 | - | ## The NFT Storage Problem |
|
| 17 | - | ||
| 18 | - | If you’re familiar with Ethereum, you know that it costs gas fees to mint, transfer, or buy NFTs. You’re paying the network to do all of that computing for you, and in the case of storing data on the blockchain, every character counts (and costs). Storing large amounts of data on-chain can be incredibly expensive. In the case of Ethereum, we’re talking ~$40M to store 1GB of data. Yikes. |
|
| 19 | - | ||
| 20 | - | To avoid having to store all of that data on-chain, most NFTs are stored as metadata that points to an image off the blockchain where the actual file lives. It’s like when you share a Dropbox link. The link you share isn’t the actual thing, it just points you to where the actual thing is stored. That's why most early NFT projects were actually stored on Dropbox or a similar cloud service like AWS or Google Drive. While a full PNG of an NFT might be 4 mb, its metadata is only about 800 bytes. Definitely a step in the right direction, but this solution caused another problem: people started getting rugged. |
|
| 21 | - | ||
| 22 | - | Let’s say an influencer decides to launch a new NFT project called “Ceramic Cars." Sounds cool. She uploads an image of the first drop of cars to a storage provider and each car is named “crazycar1.png.” Luckily you snag one. She put a link to that image in your NFT metadata that looks something like this: |
|
| 23 | - | ||
| 24 | - | ``` |
|
| 25 | - | https://storageservice.com/friend/crazycar1.png |
|
| 26 | - | ``` |
|
| 27 | - | ||
| 28 | - | Looks fine at first. But here’s the kicker: at any time, that influencer or the storage provider can simply remove that image. Suddenly the NFT is useless. And if she really wanted to be a jerk, she could replace it with a poop emoji with the same name. And since there is no regulation or laws around this, you would basically be screwed with no way of getting what you paid for. And that’s exactly what was happening in those early days: people were getting rugged left and right. These problems begged for a solution. That solution was [IPFS](https://ipfs.io) - the InterPlanetary File System. |
|
| 29 | - | ||
| 30 | - | ## How does IPFS solve the storage problem? |
|
| 31 | - | ||
| 32 | - | To understand how IPFS works, you need to understand how file sharing has always worked, before the days of web3. |
|
| 33 | - | ||
| 34 | - | The traditional way of sharing files over the internet is through centralized servers. For example, if you write up and send a Tweet, the data is sent to Twitter’s servers. Then when other users want to see that Tweet, the servers pass that information to them. It’s a direct up and down motion. |
|
| 35 | - | ||
| 36 | - | > |
|
| 37 | - | ||
| 38 | - | Rather than straight up and down, IPFS is up, down, and side to side. IPFS is a decentralized network of nodes that share content with each other. If we imagine a Twitter run on IPFS, every user would have their own IPFS node that would upload data to the network, and as it is requested by other users, the data is passed from node to node. Rather than one company holding and owning that data, everyone owns that data. IPFS gives users the ability to own their data. |
|
| 39 | - | ||
| 40 | - | > |
|
| 41 | - | ||
| 42 | - | ## How does IPFS protect your NFTs? |
|
| 43 | - | ||
| 44 | - | When you share a file through IPFS, the file is run through a cryptographic algorithm that gives you something called the Content Identifier, or “CID” for short. This CID plays a huge part in how IPFS works and operates, and it looks something like this: |
|
| 45 | - | ||
| 46 | - | ``` |
|
| 47 | - | QmRAuxeMnsjPsbwW8LkKtk6Nh6MoqTvyKwP3zwuwJnB2yP |
|
| 48 | - | ``` |
|
| 49 | - | ||
| 50 | - | Every CID is determined by the content of the file, making it completely unique. If you change a picture by even one pixel, it would give you a different CID. This unique identifier makes content verifiable. In our example of an image being swapped out for a poop emoji, it wouldn’t be possible since the two images would have completely different CIDs. Combine this power with blockchain, and you get a reference to an image that is verified and cannot be changed. |
|
| 51 | - | ||
| 52 | - | As an NFT creator, this is a huge benefit that you can use to reassure your audience. Any NFT that they collect from your collection will be 100% verifiable. Beyond that, the CID makes content portable and addressable. |
|
| 53 | - | ||
| 54 | - | When I buy an NFT, I can get the CID for the metadata and image, then pin it to my Pinata account. That means, if the project owner for some reason stops pinning the data on their end, it can persist through my responsibility. As long as I pin those CIDs, my NFT will live on. This means you can truly OWN your data. It gives communities the ability to preserve content that they want available, much like how art is preserved today in museums by people who value it. |
|
| 55 | - | ||
| 56 | - | ## How Pinata Connects Creators to IPFS |
|
| 57 | - | ||
| 58 | - | As you might already know, Pinata is an IPFS pinning service that makes IPFS easy for creators. We do this in several ways. First and foremost, we help creators get their original content on IPFS through our easy-to-use web app. Once it's uploaded through Pinata, it's automatically pinned to IPFS and gets a unique CID. Just like that, no extra work. |
|
| 59 | - | ||
| 60 | - | After uploading, the next step of course is sharing it! One way we do this is through Dedicated Gateways, which serve as hyperspeed bridges between IPFS and regular websites. With a Dedicated Gateway you can quickly share content, use a custom domain to match your brand, stream videos, and more. |
|
| 61 | - | ||
| 62 | - | Additionally, Pinata has also pioneered a new feature called "Submarining," our take on unlockable content. Content creators can give their audience access to exclusive gated content via NFT ownership, Retweets and geo-location. The possibilities are endless with this tool; exclusive content for your NFT holders, NFTs being used as movie passes for film makers, or music albums by artists! |
|
| 63 | - | ||
| 64 | - | And there you have it. Hopefully this gives you an idea of the power of IPFS, and how it can help creators like you protect your content and distribute it in a way that no other platform can. We invite you to sign up for an account and discover Pinata for yourself. Our community is here to help creators become their own platform and share content their way. Happy pinning! |
| 3 | 3 | publishDate: "24 Sep 2024" |
|
| 4 | 4 | description: "A quick walkthough of how I built a guestbook for my website" |
|
| 5 | 5 | tags: ["programming", "web development"] |
|
| 6 | - | ogImage: "/blog-images/files-stevedylan-dev/QmU4XNzvRej9soBFdShhSb3KiTpN45hziDCbzdc5hBW1Nk.webp" |
|
| 6 | + | ogImage: "/blog-images/files-stevedylan-dev/guestbook-cover.png" |
|
| 7 | 7 | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvurhwnn2v" |
|
| 8 | 8 | --- |
|
| 9 | 9 | ||
| 10 | - | > |
|
| 10 | + |  |
|
| 11 | 11 | ||
| 12 | 12 | When I was first getting started in web development I remember seeing someone's website and was immediately impressed by one thing: a guestbook. You could sign in with Github and leave a message, similar to someone's Facebook wall back in the day. I thought that was the coolest thing but had no idea how to build it. Fast forward to this weekend, I was reminded how cool that was and I decided to build it for my own website. |
|
| 13 | 13 |
| 1 | - | --- |
|
| 2 | - | title: "Building Snippets.so" |
|
| 3 | - | publishDate: "02 Aug 2024" |
|
| 4 | - | description: "Insights into why snippets.so was built and the tech stack behind it" |
|
| 5 | - | tags: ["programming", "ipfs"] |
|
| 6 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvurteuf2v" |
|
| 7 | - | --- |
|
| 8 | - | ||
| 9 | - | > |
|
| 10 | - | ||
| 11 | - | "I don't know why this isn't working" is a question I get often, and I usually respond with "could you share your code with me?" Next thing you know I get a cell phone image of someone's fingerprint covered laptop screen with a blur of code on it. I wish this wasn't the first time, and I certainly hope it's the last, but to be honest there was a problem. |
|
| 12 | - | ||
| 13 | - | I continue to find myself needing to share a code snippet with a user, or needing them to share one with me. Of course there is Pastebin, perhaps the most popular, but it's littered with ads and bloat that I hated to use or point people to. There's also Gists, which generally are great, but only if you want them to stick to your Github profile (which I usually don't). On the other side of the spectrum there's [Ray.so](https://ray.so), a beautiful app built by the team at Raycast that generates the best looking images of code you can get. Unfortunately that doesn't always help me if I need to copy and paste the code somewhere. |
|
| 14 | - | ||
| 15 | - | This is what led me to build [Snippets.so](https://snippets.so), my take on a cleaner and more efficient way to share code. I wanted the style and ease of Ray.so with the simplicity of Pastebin, and I'm happy with the ground I found in the middle. I decided to take a few moments to share how it was built and some of the unique stack choices I made in hopes that you may find it beneficial, or perhaps use Snippets.so for your own code sharing needs. |
|
| 16 | - | ||
| 17 | - | ## The Stack |
|
| 18 | - | ||
| 19 | - | For this app I took a few obvious choices for the stack, but others that might puzzle the majority of developers out there. Let's start right off the bat with the weird one ;) |
|
| 20 | - | ||
| 21 | - | ### IPFS |
|
| 22 | - | ||
| 23 | - | If you're not familiar the InterPlanetary File System (IPFS), it’s a unique distribute file sharing protocol commonly used alongside blockchains. In the app I use it to store JSON files that contain everything I need for each snippet, and it's convenient that the CID (Content Identifier) or address to the file works as a unique identifier as well which we'll cover soon. However there are some other benefits I wanted to outline as well. |
|
| 24 | - | ||
| 25 | - | One of the main reasons blockchain developers use IPFS for their offchain storage is due to immutability. Once something is on the network it cannot change, and in our case that's very useful. People generally don't need to update a snippet once they share it, and it makes the content ideal for a CDN for high cache hit rates. Once a snippet is loaded at least once, the other requests will be very speedy. |
|
| 26 | - | ||
| 27 | - | Since CIDs are cryptographically determined by the content of the file, uploading the same content will give you the same CID. This prevents any possibility of duplicate storage and taking up unwanted space. Additionally CIDs work both as a content hash and the address to the content. It's a publicly accessible network where anyone can take a CID and access the content through a gateway, which adds a nice layer of interoperability. |
|
| 28 | - | ||
| 29 | - | ### Next.js |
|
| 30 | - | ||
| 31 | - | I've certainly had bad history with Next, particularly with heavy caching in API routes. However it does provide a pretty nice experience when doing server side rendering, which is the majority of Snippets. I've also built so many projects on Next that its almost second nature to me, and I appreciate the speed that brings. If I had to pick another framework I might try Astro instead. For this app Next worked just fine. |
|
| 32 | - | ||
| 33 | - | ### shadcn/ui |
|
| 34 | - | ||
| 35 | - | Undoubtedly the best component library out there, I love every chance I get to use it. Anything I really need is right there in the docs, and its super easy to customize if I need to. Would highly recommend giving it a shot if you haven't already! |
|
| 36 | - | ||
| 37 | - | ## Building the Editor |
|
| 38 | - | ||
| 39 | - | When it comes to enabling writing in an app beyond just a text area there are many library choices out there. For Snippets I went with `@uiw/codemirror` for several reasons. For starters it was pretty easy to use and setup, had it running in no time. |
|
| 40 | - | ||
| 41 | - | ```typescript |
|
| 42 | - | import React from "react"; |
|
| 43 | - | import CodeMirror from "@uiw/react-codemirror"; |
|
| 44 | - | import { javascript } from "@codemirror/lang-javascript"; |
|
| 45 | - | ||
| 46 | - | function App() { |
|
| 47 | - | const [value, setValue] = React.useState("console.log('hello world!');"); |
|
| 48 | - | const onChange = React.useCallback((val, viewUpdate) => { |
|
| 49 | - | console.log("val:", val); |
|
| 50 | - | setValue(val); |
|
| 51 | - | }, []); |
|
| 52 | - | return ( |
|
| 53 | - | <CodeMirror |
|
| 54 | - | value={value} |
|
| 55 | - | height="200px" |
|
| 56 | - | extensions={[javascript({ jsx: true })]} |
|
| 57 | - | onChange={onChange} |
|
| 58 | - | /> |
|
| 59 | - | ); |
|
| 60 | - | } |
|
| 61 | - | export default App; |
|
| 62 | - | ``` |
|
| 63 | - | ||
| 64 | - | It also has a huge number of supported languages for syntax highlighting. For this app I chose some popular ones and used an extension to load based on a state change. |
|
| 65 | - | ||
| 66 | - | ```typescript |
|
| 67 | - | // other imports |
|
| 68 | - | import { loadLanguage } from "@uiw/codemirror-extensions-langs"; |
|
| 69 | - | import { languages } from "@/lib/languages"; |
|
| 70 | - | ||
| 71 | - | export function CodeForm({ readOnly, content }: any) { |
|
| 72 | - | //... |
|
| 73 | - | const [value, setValue] = useState(defaultCode); |
|
| 74 | - | const [lang, setLang]: any = useState("tsx"); |
|
| 75 | - | ||
| 76 | - | const languageExtension = useMemo(() => { |
|
| 77 | - | const extension = loadLanguage(lang); |
|
| 78 | - | return extension ? [extension] : []; |
|
| 79 | - | }, [lang]); |
|
| 80 | - | ||
| 81 | - | //... |
|
| 82 | - | ||
| 83 | - | return ( |
|
| 84 | - | {/*rest of UI*/} |
|
| 85 | - | <CodeMirror |
|
| 86 | - | className="text-md opacity-75 p-2 sm:w-[600px] sm:h-[700px] w-[350px] h-[450px]" |
|
| 87 | - | height="100%" |
|
| 88 | - | width="100%" |
|
| 89 | - | value={value} |
|
| 90 | - | basicSetup={{ |
|
| 91 | - | lineNumbers: false, |
|
| 92 | - | foldGutter: false, |
|
| 93 | - | }} |
|
| 94 | - | extensions={languageExtension} |
|
| 95 | - | onChange={onChange} |
|
| 96 | - | theme={githubLight} |
|
| 97 | - | readOnly={readOnly} |
|
| 98 | - | /> |
|
| 99 | - | {/*...*/} |
|
| 100 | - | ); |
|
| 101 | - | } |
|
| 102 | - | ``` |
|
| 103 | - | ||
| 104 | - | While this library does have several themes to choose from I decided to stick with a light theme (blasphemy I know haha). A simple GitHub light theme with reduced opacity actually does a decent job. Definitely looked into trying to customize it a bit more but the way it handles syntax highlighting isn't as good as something like shiki. This might be something I look into down the road. |
|
| 105 | - | ||
| 106 | - | > |
|
| 107 | - | ||
| 108 | - | When it comes to actually uploading the content after the user has put their code in I used the built in API routes in Next.js, simply passing in the `content` of the file, `name` if one given, and `lang` for the language used from the dropdown menu. As mentioned earlier I'm using Pinata/IPFS for uploading the content and there is a convenient API route for JSON objects, so its a simple plug and post operation. |
|
| 109 | - | ||
| 110 | - | ```typescript |
|
| 111 | - | import { NextResponse } from "next/server"; |
|
| 112 | - | import type { NextRequest } from "next/server"; |
|
| 113 | - | ||
| 114 | - | export const revalidate = 0; |
|
| 115 | - | ||
| 116 | - | type PinResponse = { |
|
| 117 | - | IpfsHash: string; |
|
| 118 | - | PinSize: number; |
|
| 119 | - | Timestamp: string; |
|
| 120 | - | isDuplicate?: boolean; |
|
| 121 | - | }; |
|
| 122 | - | ||
| 123 | - | export async function POST(request: NextRequest) { |
|
| 124 | - | try { |
|
| 125 | - | const body = await request.json(); |
|
| 126 | - | console.log(body); |
|
| 127 | - | const data = JSON.stringify({ |
|
| 128 | - | pinataContent: { |
|
| 129 | - | content: body.content, |
|
| 130 | - | name: body.name, |
|
| 131 | - | lang: body.lang, |
|
| 132 | - | }, |
|
| 133 | - | pinataMetadata: { |
|
| 134 | - | name: body.name, |
|
| 135 | - | }, |
|
| 136 | - | pinataOptions: { |
|
| 137 | - | cidVersion: 1, |
|
| 138 | - | }, |
|
| 139 | - | }); |
|
| 140 | - | const req = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { |
|
| 141 | - | method: "POST", |
|
| 142 | - | headers: { |
|
| 143 | - | "Content-Type": "application/json", |
|
| 144 | - | Authorization: `Bearer ${process.env.PINATA_JWT}`, |
|
| 145 | - | }, |
|
| 146 | - | body: data, |
|
| 147 | - | }); |
|
| 148 | - | const res: PinResponse = await req.json(); |
|
| 149 | - | return NextResponse.json(res); |
|
| 150 | - | } catch (error) { |
|
| 151 | - | console.log(error); |
|
| 152 | - | return NextResponse.json(error); |
|
| 153 | - | } |
|
| 154 | - | } |
|
| 155 | - | ``` |
|
| 156 | - | ||
| 157 | - | The API request returns an `IpfsHash` or CID that acts as both a unique identifier and the address to the content on the network. Inside the `submissionHandler` function used for uploading I'm able to use `router.push` to move the user from the editing page to the dynamic route where you view the snippet. |
|
| 158 | - | ||
| 159 | - | ```typescript |
|
| 160 | - | async function submitHandler() { |
|
| 161 | - | try { |
|
| 162 | - | setLoading(true); |
|
| 163 | - | const body = JSON.stringify({ |
|
| 164 | - | content: value, |
|
| 165 | - | name: name, |
|
| 166 | - | lang: lang, |
|
| 167 | - | }); |
|
| 168 | - | const req = await fetch(`/api/upload`, { |
|
| 169 | - | method: "POST", |
|
| 170 | - | headers: { |
|
| 171 | - | "Content-Type": "application/json", |
|
| 172 | - | }, |
|
| 173 | - | body: body, |
|
| 174 | - | }); |
|
| 175 | - | const res = await req.json(); |
|
| 176 | - | setComplete(true); |
|
| 177 | - | router.push(`/snip/${res.IpfsHash}`); |
|
| 178 | - | } catch (error) { |
|
| 179 | - | console.log(error); |
|
| 180 | - | setLoading(false); |
|
| 181 | - | return error; |
|
| 182 | - | } |
|
| 183 | - | } |
|
| 184 | - | ``` |
|
| 185 | - | ||
| 186 | - | ## Building the Renderer |
|
| 187 | - | ||
| 188 | - | Now that the user has uploaded their snippet to IPFS and we have a CID representing the JSON data, we can use it as a path variable and extract the data from it. In our app we use the following file structure: |
|
| 189 | - | ||
| 190 | - | ``` |
|
| 191 | - | app |
|
| 192 | - | ├── api |
|
| 193 | - | │ ├── languages |
|
| 194 | - | │ │ └── route.ts |
|
| 195 | - | │ └── upload |
|
| 196 | - | │ └── route.ts |
|
| 197 | - | ├── favicon.ico |
|
| 198 | - | ├── globals.css |
|
| 199 | - | ├── layout.tsx |
|
| 200 | - | ├── page.tsx |
|
| 201 | - | └── snip |
|
| 202 | - | └── [cid] |
|
| 203 | - | └── page.tsx |
|
| 204 | - | ``` |
|
| 205 | - | ||
| 206 | - | `[cid]` is our dynamic path variable, and with server side pages we can pull the data from IPFS and feed it into our renderer component in one fell swoop. |
|
| 207 | - | ||
| 208 | - | ```typescript |
|
| 209 | - | import { Footer } from "@/components/footer"; |
|
| 210 | - | import { Header } from "@/components/header"; |
|
| 211 | - | import { ReadOnlyEditor } from "@/components/read-only-editor"; |
|
| 212 | - | ||
| 213 | - | async function fetchData(cid: string) { |
|
| 214 | - | try { |
|
| 215 | - | const req = await fetch(`https://${process.env.GATEWAY_DOMAIN}/ipfs/${cid}`); |
|
| 216 | - | const res = await req.json(); |
|
| 217 | - | return res; |
|
| 218 | - | } catch (error) { |
|
| 219 | - | console.log(error); |
|
| 220 | - | return error; |
|
| 221 | - | } |
|
| 222 | - | } |
|
| 223 | - | ||
| 224 | - | export default async function Page({ params }: { params: { cid: string } }) { |
|
| 225 | - | const cid = params.cid; |
|
| 226 | - | const data = await fetchData(cid); |
|
| 227 | - | return ( |
|
| 228 | - | <main className="flex min-h-screen flex-col items-center justify-start sm:justify-center"> |
|
| 229 | - | <Header /> |
|
| 230 | - | <ReadOnlyEditor content={data.content} name={data.name} cid={cid} lang={data.lang} /> |
|
| 231 | - | <Footer /> |
|
| 232 | - | </main> |
|
| 233 | - | ); |
|
| 234 | - | } |
|
| 235 | - | ``` |
|
| 236 | - | ||
| 237 | - | This really is one of my favorite parts with Next.js App router; if you can structure the project correctly where you feed server data into client components you get the best of both worlds. |
|
| 238 | - | ||
| 239 | - | With our `cid`, `name`, `content`, and `lang` we can rebuild what the editor saw with a "read-only" version of the same editor. |
|
| 240 | - | ||
| 241 | - | ```typescript |
|
| 242 | - | <CodeMirror |
|
| 243 | - | className="text-md font-commitMono h-[450px] w-[350px] p-2 opacity-75 sm:h-[700px] sm:w-[600px]" |
|
| 244 | - | height="100%" |
|
| 245 | - | width="100%" |
|
| 246 | - | value={content} |
|
| 247 | - | basicSetup={{ |
|
| 248 | - | lineNumbers: false, |
|
| 249 | - | foldGutter: false, |
|
| 250 | - | rectangularSelection: false, |
|
| 251 | - | }} |
|
| 252 | - | extensions={languageExtension} |
|
| 253 | - | theme={githubLight} |
|
| 254 | - | readOnly |
|
| 255 | - | editable={false} |
|
| 256 | - | /> |
|
| 257 | - | ``` |
|
| 258 | - | ||
| 259 | - | Along with viewing the content we can also enable some fun stuff like copying it to clipboard, downloading it as a file, or sharing the snippet with a link! |
|
| 260 | - | ||
| 261 | - |  |
|
| 262 | - | ||
| 263 | - | ## API + CLI |
|
| 264 | - | ||
| 265 | - | Since there isn't really any authentication in this app and anyone can upload snippets as much as they want, I figured "why not make the API accessible?" Anyone can make an API request to the app to make a snippet and use the data returned to make the link! |
|
| 266 | - | ||
| 267 | - | Request: |
|
| 268 | - | ||
| 269 | - | ```bash |
|
| 270 | - | curl --location '/blog-images/other/api/upload' \ |
|
| 271 | - | --header 'Content-Type: application/json' \ |
|
| 272 | - | --data '{ |
|
| 273 | - | "content": "console.log(\"hello world!\")", |
|
| 274 | - | "name": "hello.ts", |
|
| 275 | - | "lang": "typescript" |
|
| 276 | - | }' |
|
| 277 | - | ``` |
|
| 278 | - | ||
| 279 | - | Returns: |
|
| 280 | - | ||
| 281 | - | ```json |
|
| 282 | - | { |
|
| 283 | - | "IpfsHash": "bafkreiccdt64k6d4wjgz5ebqee4rvmkauoiygc5egwtssl2zqq3o74zlti", |
|
| 284 | - | "PinSize": 81, |
|
| 285 | - | "Timestamp": "2024-07-10T02:25:51.052Z", |
|
| 286 | - | "isDuplicate": true |
|
| 287 | - | } |
|
| 288 | - | ``` |
|
| 289 | - | ||
| 290 | - | Link: |
|
| 291 | - | ||
| 292 | - | ``` |
|
| 293 | - | https://snippets.so/snip/bafkreiccdt64k6d4wjgz5ebqee4rvmkauoiygc5egwtssl2zqq3o74zlti |
|
| 294 | - | ``` |
|
| 295 | - | ||
| 296 | - | Of course this led me to make a CLI in Go for the app as well, which you can download with `brew install stevedylandev/snippets-cli/snippets-cli` or by building it yourself from [this repo](https://github.com/stevedylandev/snippets-cli). To use it you can just run the command `snip` followed by the file you want to upload. |
|
| 297 | - | ||
| 298 | - | ```bash |
|
| 299 | - | snip helloWorld.ts |
|
| 300 | - | ``` |
|
| 301 | - | ||
| 302 | - | ## Wrapping Up |
|
| 303 | - | ||
| 304 | - | Even though this was a relatively simply project I love how it turned out. I started with the goal of making a better tool that I would use and I did just that. A smile creeps onto my face every time I have the chance to use it when helping another developer, and its from the satisfaction of programming away a problem. I truly believe that the most meaningful pieces of code we write are the ones that make our lives just a little bit better, and I can't wait to keep doing just that. |
| 1 | - | --- |
|
| 2 | - | title: "A Case for IPFS on Layer 1 Blockchains" |
|
| 3 | - | publishDate: "06 Dec 2022" |
|
| 4 | - | description: "There are so many new blockahins appearing and they all need a solution to off-chain storage" |
|
| 5 | - | tags: ["blockchain", "ipfs"] |
|
| 6 | - | ogImage: "/blog-images/medium/v2/resize:fit:4800/format:webp/1*4xA96GrA9iLYMp5vorcjyQ.jpeg" |
|
| 7 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvusuqan2v" |
|
| 8 | - | --- |
|
| 9 | - | ||
| 10 | - | import medium from "../../assets/medium.png"; |
|
| 11 | - | import OutLinkButton from "@/components/common/OutLinkButton"; |
|
| 12 | - | ||
| 13 | - | <OutLinkButton |
|
| 14 | - | link="https://medium.com/pinata/a-case-for-ipfs-on-layer-1-blockchains-like-solana-aptos-and-sui-165a9732c214" |
|
| 15 | - | site="Medium" |
|
| 16 | - | image={medium} |
|
| 17 | - | /> |
|
| 18 | - | ||
| 19 | - | There has been a Cambrian explosion of new layer 1 blockchains — all with brand new technologies that make them faster and more efficient. However there is one problem that no blockchain has managed to overcome: asset storage. |
|
| 20 | - | ||
| 21 | - | Blockchains are great for keeping a ledger of transactions, but storing large amounts of data on-chain is expensive and doesn’t scale well. Worse, most of the centralized off chain storage methods have a bad reputation of enabling rug pulls and bad actors in the NFT space. Even though Pinata solves these problems, giving creators and developers easy access to the heaven-sent distributed file system known as IPFS, many of these new layer 1 chains still don’t understand why IPFS is the solution to their problems. Let’s break it down. |
|
| 22 | - | ||
| 23 | - | ## A Brief history of NFTs (How we got here) |
|
| 24 | - | ||
| 25 | - | NFTs and web3 are still pretty bright and new, but they have already created quite the history over the last 5 to 6 years. Ethereum has been the original playground for NFTs since 2016, and with that early usage comes experience. There were a lot of problems to overcome when creating NFTs, mainly storage. |
|
| 26 | - | ||
| 27 | - | When it comes to storing data on chain, every byte counts and costs gas. If you have too many bytes, it will get expensive quickly. Some NFTs took the approach of on chain SVGs which work in a pinch, but if you want to host something like a Bored Ape, it would cost a lot of money to deploy 10,000 of them on chain. Because of this, Ethereum turned to off chain storage and used a link or “token URI” to point to metadata and images not on Ethereum. This saved gas, but the solutions had their own problems. Most of the early storage solutions were simply centralized storage services or private servers, and using these came with the risk of rugs. With a server or storage provider, the link “https://steve.com/api/images/1.png" could host pretty much anything. “1.png” could be an image of a cool Pinata one week, and then whoever is in control of the server could simply [replace the image with a picture of PePe the Frog with the same name “1.png.”](https://medium.com/pinata/who-is-responsible-for-nft-data-99fb4e8147e4) |
|
| 28 | - | ||
| 29 | - | Kinda goes against the point of NFTs and having actual ownership of the content; if the content can change, what’s the point of having the reference on chain? |
|
| 30 | - | ||
| 31 | - | ## The Birth of IPFS |
|
| 32 | - | ||
| 33 | - | [IPFS](https://ipfs.io) (the Interplanetary File System) solved a lot of the problems that centralized storage had with NFTs, and still does to this day. Pinata started not too long after in 2018 at EthBerlin, creating an IPFS service that was easy to use and accessible to the average person. With that said, we’ve certainly seen it all over the years. Pinata has also witnessed the birth of many new blockchains such as Solana, Cardano, or Aptos. One of our favorite parts of IPFS is that it’s blockchain agnostic, meaning it will work with any blockchain! It’s been great to see creators build NFTs and amazing projects on these new blockchains using Pinata. With the great, we’ve also seen plenty of examples of creators make the same mistakes made by Ethereum back in the early days, using centralized storage methods and witnessing rugs right before our eyes. |
|
| 34 | - | ||
| 35 | - | In this post we’ll dive into IPFS, why it’s the most ideal solution, and best practices when implementing it across NFT projects! |
|
| 36 | - | ||
| 37 | - | ## Why should Layer 1 Blockchains use IPFS? |
|
| 38 | - | ||
| 39 | - | Sometimes covering IPFS can be daunting as there are lots of moving pieces, but after you break down some basic concepts, it can be much easier to grasp. If you are looking for a non-technical introduction we would highly recommend our blog post on Bueno.art which you can find [here!](https://www.bueno.art/blog/pinata-ipfs-guide) With that said, let’s look into some reasons why IPFS should be considered for off chain storage for all blockchains. |
|
| 40 | - | ||
| 41 | - | ## Immutable and Verifiable |
|
| 42 | - | ||
| 43 | - | Right off the bat IPFS solves one of the biggest problems for off chain storage by introducing immutability. This means when you share a file with IPFS, the content cannot change. If you share an image, that image will stay the same; it cannot be altered in any way. The way IPFS achieves this is with the “CID” or “Content Identifier.” When you share a file on IPFS, it goes through a cryptographic process that spits out the CID, which can look something like this: |
|
| 44 | - | ||
| 45 | - | ``` |
|
| 46 | - | QmeUmuQaTpoqk51uEYgrSjZvCqvtPw2ARdWnZEMb2ky25N |
|
| 47 | - | ``` |
|
| 48 | - | ||
| 49 | - | Each piece of that CID, every letter and number, is determined by the contents of the file. So if we shared the same file but perhaps we changed just one pixel, it would spit out a different file. Recall our example from earlier about someone swapping out a picture of Pinnie for a Pepe the frog, its just not possible in IPFS. They are two different files and will result in two different CIDs based on their content. |
|
| 50 | - | ||
| 51 | - | This is incredibly powerful if you stop and think about all the ramifications beyond just images. Think of all the things we want to accomplish with smart contracts and NFTs: real estate, health documents, it just goes on. These are documents that depend on data being immutable and verifiable, and IPFS provides this. With the CID, the owner of an NFT can rest assured that they can verify content as the original when tied to a smart contract. Even in the case of smart contracts that have functions to update the token URI, that change is on chain and the user has visibility and know what they’re getting into. |
|
| 52 | - | ||
| 53 | - | ## Portability and Ownership |
|
| 54 | - | ||
| 55 | - | One of my favorite pieces of IPFS is the ability to truly take ownership of your content. Due to content on IPFS being immutable, and the CID serves as the unchanging reference to a file, it can be used to address the content too. With the CID, files can be shared from one owner to another. Not only can it be shared or viewed, the content can move from one owner to another. |
|
| 56 | - | ||
| 57 | - | For example, if I own an NFT that has metadata (the name of the nft, description, image link, etc.) and an image hosted on IPFS, I can get the CIDs for both and pin it to my Pinata account. When a file is pinned on IPFS, it keeps it from falling off the network during garbage collection run on individual IPFS nodes. So in our scenario, if the original creator of the NFT ever stopped pinning the content on his end, the NFT would live on because I’m taking responsibility for the content and keeping it alive. |
|
| 58 | - | ||
| 59 | - | We call this portability and ownership, as the content can freely be persisted by any individual or community. It makes data ownership and sharing actually possible, where in the past the data was truly owned by a larger cloud provider. This IPFS ability also helps save projects from rugs. I can’t count the number of communities I’ve helped over the past year recover their project due to someone abandoning it. Thanks to IPFS all they have to do is get the CIDs, which is usually just two, and pin by CID through their Pinata account. Just like that, the project continues to live! |
|
| 60 | - | ||
| 61 | - | Our CEO Kyle Tut wrote a great piece on how IPFS portability changes the responsibility of hosting content from the creator to the owner, and how communities can help preserve data very similar to how art is preserved. You can read that [here](https://medium.com/pinata/who-is-responsible-for-nft-data-99fb4e8147e4) if you’re interested! |
|
| 62 | - | ||
| 63 | - | ## Speed and Efficiency |
|
| 64 | - | ||
| 65 | - | Believe it or not, IPFS does have the ability to be fast. If you are accessing IPFS through a local IPFS node, your experience will likely not be the best. However with the right tools, IPFS can be ideal for delivering content at blazing speeds. Since the content does not change and neither does the CID, the reference to the file, this makes an excellent use case for caching content. |
|
| 66 | - | ||
| 67 | - | Pinata released [Dedicated Gateways](https://www.pinata.cloud/dedicated-gateways) back in 2021 that use a global CDN with 200 worldwide locations, so when content is loaded through the gateway, that content is cached. This makes every other request after that incredibly fast. |
|
| 68 | - | ||
| 69 | - | In addition to speed, IPFS is also efficient with data that is no longer wanted or used. In order for content to stay on IPFS, it has to be pinned by at least one IPFS node. If it is no longer pinned on any nodes, then it will slowly fall off the network as IPFS nodes run garbage collection. Some people might see this as a downside, but in reality this gives us a much more practical way of handling data and reducing data waste. It gives users the option of data persistence, rather than being forced into a model of permanence. |
|
| 70 | - | ||
| 71 | - | ## Best Practices |
|
| 72 | - | ||
| 73 | - | Now that you have an itch to try out IPFS and see for yourself, you might want to consider some best practices for how you want to reference IPFS content in NFT projects and other use cases. |
|
| 74 | - | ||
| 75 | - | ## Protocol URIs |
|
| 76 | - | ||
| 77 | - | One of the oldest and best ways to make sure IPFS data is future proof and easy to preserve is to use the protocol URI, especially in a smart contract for NFTs. The protocol URI starts with “ipfs://”, and a full url might look something like this: |
|
| 78 | - | ||
| 79 | - | ``` |
|
| 80 | - | ipfs://QmeUmuQaTpoqk51uEYgrSjZvCqvtPw2ARdWnZEMb2ky25N |
|
| 81 | - | ``` |
|
| 82 | - | ||
| 83 | - | At the time of this article, you may not be able to simply copy and paste this URL into your browser (unless you are using a browser like Brave or have a browser extension). Why? Because these normally require you to run an IPFS node to access them. That might change overtime, but right now this is the easiest way for marketplaces and wallets to locate the CID and run it through their gateway of choice. |
|
| 84 | - | ||
| 85 | - | This is a common practice on Ethereum where IPFS makes sense (BAYC is an example), however it’s not as popular on Layer 1 blockchains that expect a working HTTP url. We hope that as IPFS grows on those Layer 1 chains that fact will change, as people see the value in a data persistence model. |
|
| 86 | - | ||
| 87 | - | ## Public Gateways |
|
| 88 | - | ||
| 89 | - | Another way to access IPFS content is through a public gateway like this one: |
|
| 90 | - | ||
| 91 | - | ``` |
|
| 92 | - | https://gateway.pinata.cloud/ipfs/QmeUmuQaTpoqk51uEYgrSjZvCqvtPw2ARdWnZEMb2ky25N |
|
| 93 | - | ``` |
|
| 94 | - | ||
| 95 | - | Gateways are simply bridges between the IPFS protocol and the HTTP protocol that works on everyone’s computers. With them we can fetch content from IPFS without a local node running, making it really easy for everyone! |
|
| 96 | - | ||
| 97 | - | These are best used in testing environments or low traffic scenarios as public gateways are usually rate limited and under heavy use due to their open nature. This can also make them a bit slower. |
|
| 98 | - | ||
| 99 | - | ## Dedicated Gateways |
|
| 100 | - | ||
| 101 | - | If you want to take full advantage of IPFS, a Dedicated Gateway or Private IPFS Gateway is the ideal way to go! These have extra features built in to make streaming content faster and more reliable. An example of one might look like this: |
|
| 102 | - | ||
| 103 | - | ``` |
|
| 104 | - | https://stevedsimkins.mypinata.cloud/ipfs/QmeUmuQaTpoqk51uEYgrSjZvCqvtPw2ARdWnZEMb2ky25N |
|
| 105 | - | ``` |
|
| 106 | - | ||
| 107 | - | Not only do [Pinata’s Dedicated Gateways](https://pinaa.cloud/dedicated-gateways) include that global CDN we talked about earlier, but they can also do video streaming, and image optimizations. They’re fast enough that you can host entire applications on them to create App / Executable NFTs on them. For personal use they can be a great way to share content or display your NFT collection on your own personal website, and in the hands of a platform like OpenSea, they can be used to display tens of thousands of NFT collections with speed and reliability. |
|
| 108 | - | ||
| 109 | - | ## The Future |
|
| 110 | - | ||
| 111 | - | One of the best parts of my job is interacting with all the different blockchains out there and helping them use IPFS through Pinata. I also love helping communities across these chains understand the power of IPFS and how it can really save a rugged project in this space whether the founder knew it or not. The worst part is easily when there’s nothing I can do because the data sits on a centralized server that the community will never have access to. |
|
| 112 | - | ||
| 113 | - | The future of the web that I want to help build is one where the responsibility of data lies with the user and the communities that find value in it. Whether it’s a moving essay, a photo of a volcano, or some picture of a goblin, IPFS is here to make it possible for data to persist beyond its original source and into the future of web3. Let’s build something great together. |
| 1 | - | --- |
|
| 2 | - | title: "Genesis" |
|
| 3 | - | publishDate: "09 Sep 2025" |
|
| 4 | - | description: "Imagining an all-in-one tool to start developing on Ethereum" |
|
| 5 | - | tags: [] |
|
| 6 | - | ogImage: "/blog-images/other/genesis.png" |
|
| 7 | - | hidden: true |
|
| 8 | - | --- |
|
| 9 | - | ||
| 10 | - |  |
|
| 11 | - | ||
| 12 | - | Many of us remember our first steps into Ethereum as we wrote some Solidity, compiled it, and deployed it to a long forgotten testnet. It's an exhilarating feeling, something we can't quite shake. Time goes on, you build some more, and soon you start to experience some of the pain points many devs experience daily when using Ethereum. This ranges from conflicting libraries, debugging web wallet interactions, scaffolding projects, interacting and testing contracts, the list goes on. After a few years you get comfortable with the bugs and mess, perhaps **too comfortable**. We become blinded to how new developers might experience Ethereum for the first time, as they have to navigate multiple different tools, old tutorials referencing frameworks that no longer exist, and beyond. Developers need a foundation, and in this post we will explore a possible solution: Genesis. |
|
| 13 | - | ||
| 14 | - | ## Genesis |
|
| 15 | - | ||
| 16 | - | In short, Genesis would be an all-in-one CLI tool for building on Ethereum. While this concept is nothing new, it's surprisingly lacking in the Ethereum ecosystem. Alternative L1 blockchains usually have something like this where it can manage all aspects of the developer workflow for that given chain; Stellar is a great example of this. Even while the chain is new, the CLI is impressive and gives developers everything they need to go from zero to deployed dApp. They have the advantage of using techniques and fundamentals created by diverse tools in the EVM ecosystem. The key is a central binary that helps aggregate different aspects of the developer workflow, so a new dev (or even an experienced one) doesn't need to reach for five different tools to accomplish one task. It helps improve the flow of tutorials and provides a high bar of standards set for EVM development. |
|
| 17 | - | ||
| 18 | - | How far this goes is pretty flexible; even if Genesis was just a startup script that installs some of the fundamental tools developers need it would be a step in the right direction. If it makes sense there could be multiple layers of interfacing and abstraction on top of those tools. Because of this Genesis would likely be written in Typescript, Go, or Rust depending on the tooling interface that would take place. While Rust is one of the more intimidating languages to choose from, it is ideal for CLIs and system programs. Many of the new and best tools are being written in Rust as well, so depending on the interfacing needs it might make more sense to take that path. With that said there will still probably be parts of the tool written in Typescript as there are necessary web UIs that will just be simpler to work with in those environments. |
|
| 19 | - | ||
| 20 | - | Genesis would have the goal of being anything from a simple getting started tool to an all-in-one tool for developing on Ethereum, and it could include the following features. |
|
| 21 | - | ||
| 22 | - | ### Wallet Keystore + Web UI |
|
| 23 | - | ||
| 24 | - | ```bash |
|
| 25 | - | genesis wallet new |
|
| 26 | - | ``` |
|
| 27 | - | ||
| 28 | - | One of the most fundamental things a developer needs is an Externally Owned Account, and generally accessed through a "wallet." The wallet ecosystem is quite vast for EVM chains these days, but I would argue there are few focused on developers or even support testnet networks. There is also the subject of security, as many developers have to copy and paste their private keys in `.env` files or inside a web wallet. Genesis would take the approach of using the Foundry approach of using keystores, where the private key is an encrypted local text file that can only be accessed with a passphrase. Further, it would be extended to a web UI wallet that simply accesses the private key with said passphrase on a case by case basis. Since Genesis would also handle project scaffolding, we could encourage better key management in Solidity projects too. |
|
| 29 | - | ||
| 30 | - | ```bash |
|
| 31 | - | # Private key only accessed at runtime |
|
| 32 | - | PRIVATE_KEY=$(genesis wallet private-key --account testnet) |
|
| 33 | - | ``` |
|
| 34 | - | ||
| 35 | - | ### Project Scaffolding |
|
| 36 | - | ||
| 37 | - | ```bash |
|
| 38 | - | genesis contracts new |
|
| 39 | - | ``` |
|
| 40 | - | ||
| 41 | - | Genesis would come with scaffolding options from Foundry, Hardhat and Scaffold Eth. The power would be the integrations from other parts of the Genesis toolkit, such as settings for the local testnet node, wallet keystore + web UI, and more. |
|
| 42 | - | ||
| 43 | - | ### Solidity Compiler, Scanner, and LSP |
|
| 44 | - | ||
| 45 | - | ```bash |
|
| 46 | - | genesis compile |
|
| 47 | - | ``` |
|
| 48 | - | ||
| 49 | - | Many of the foundational tools for building on Solidity would be bundled into Genesis, such as a compiler, security scanners / fuzzers, and LSP integrations. These are all crucial pieces to a good developer experience and can often help save smart contracts from vulnerabilities or prevent malicious code editor extensions. |
|
| 50 | - | ||
| 51 | - | ### Default RPCs |
|
| 52 | - | ||
| 53 | - | If possible, Genesis would default to a local lite client such as Helios to use as an RPC. There could be others set as backups, but ideally we can set developers up with some of the more secure and private connections and set a standard for other tools in the ecosystem. |
|
| 54 | - | ||
| 55 | - | ### Faucet |
|
| 56 | - | ||
| 57 | - | ```bash |
|
| 58 | - | genesis fund --account test-wallet |
|
| 59 | - | ``` |
|
| 60 | - | ||
| 61 | - | This one might be harder to attain, but it would be nice to have an easy way to get testnet eth to a developer's wallet. Many of the options out there require a mainnet balance or some other kind of hoop that can limit privacy. One option might be a POW faucet similar to [this one](https://sepolia-faucet.pk910.de), and while it's not ideal, I think it's better than the other existing options. There are many alt L1's that have easy to access faucets, and while Ethereum faces many more challenges comparatively, we should strive for a better DX. |
|
| 62 | - | ||
| 63 | - | ### Integrated Local Node |
|
| 64 | - | ||
| 65 | - | ```bash |
|
| 66 | - | genesis node --local |
|
| 67 | - | ``` |
|
| 68 | - | ||
| 69 | - | The `node` command would spin up a local network like Anvil or Hardhat Network, but further down the road as execution clients improve it would be great to add testnet or mainnet options. The local node is still a crucial piece of building on Ethereum. |
|
| 70 | - | ||
| 71 | - | ### Contract Deployment and Interaction |
|
| 72 | - | ||
| 73 | - | ```bash |
|
| 74 | - | genesis deploy Counter.sol --network testnet --account test-wallet |
|
| 75 | - | ||
| 76 | - | genesis write 0x8C9EC9c13812C7F9F26AB934d4bF36206240dDA8 "increment()" --account test-wallet |
|
| 77 | - | ``` |
|
| 78 | - | ||
| 79 | - | Tools like Foundry make contract deployments and interactions pretty decent, however there is still some room for UX improvements. For example we could pull the ABI for a given contract and provide autocompletions for the shell, making it much easier to interact and test functions. Turning it into a TUI and providing an experience like Etherscan would also be great to see. Currently there's a lot you have to type out in the terminal to interact with a contract, and some people may not want to write a full Solidity script just to try something. |
|
| 80 | - | ||
| 81 | - | ## Development Plan |
|
| 82 | - | ||
| 83 | - | We've discussed _a lot_ of features, and it's hard to imagine where we would even start. Yet like any project, the key is to start small and simple. Genesis would begin and start as a tool installer; a simple CLI that helps developers acquire the fundamental tools they need to start building. Perhaps a next step would be initial setups, like prompting to create a keystore wallet, showing how to access it, how to start a new project, etc. From there we could start looking into what it might look like to expand. There would need to be some decisions around whether it makes sense to wrap other tools or write our own native packages, and it would be a case by case basis. For example, writing a simple dev focused web wallet that links to keystores might make more sense than writing light abstraction over foundry tools. Those decisions would be crucial as it both dictates the developer experience of Ethereum and has the potential to drown out other tools and providers (which we obviously don't want to do). |
|
| 84 | - | ||
| 85 | - | Depending how Genesis starts out and evolves, funding and support can start small. A simple install script is something that could be done very quickly and probably handled by a single person. Building the wallet would take longer and likely more people and talent. As we slowly add these features and make decisions of how far to go we can look into how many people we might need, what approach we take for funding such as grants, and how much we have budgeted for such a tool. |
|
| 86 | - | ||
| 87 | - | A key factor in making those financial decisions would be the success metrics in its early development. Measuring the success would rely on a few key factors including: |
|
| 88 | - | - Installs from package managers or install scripts |
|
| 89 | - | - GitHub metrics (stars, issues, PRs, etc. All accessible from tools like [OSS Insight](https://ossinsight.io)) |
|
| 90 | - | - Community feedback |
|
| 91 | - | - Live sessions with new users (college hackathons and blockchain clubs are great for this) |
|
| 92 | - | Based on those metrics the team behind Genesis could make better decisions on how valuable certain pieces of the stack are compared to others. |
|
| 93 | - | ||
| 94 | - | ## Wrapping Up |
|
| 95 | - | ||
| 96 | - | While there is a delicate balance between building native tooling and supporting existing tooling, there should be a collective effort to improve the developer experience of Ethereum. We want a world where new and old builders alike can enjoy building EVM based applications and improve our ecosystem further, and it starts with us. Try out the MVP of Genesis by copying and pasting the script below into your terminal, or view the source script [here](https://stevedylan.dev/genesis.sh)! |
|
| 97 | - | ||
| 98 | - | ```bash |
|
| 99 | - | bash <(curl -fsS https://stevedylan.dev/genesis.sh) |
|
| 100 | - | ``` |
| 1 | - | --- |
|
| 2 | - | title: "How SP1 Precompiles Revolutionized zkVM Performance" |
|
| 3 | - | publishDate: "27 Oct 2024" |
|
| 4 | - | description: "Learn about Succinct, SP1, and how the innovation of Precompiles changed the zkVM space" |
|
| 5 | - | tags: [] |
|
| 6 | - | ogImage: "/blog-images/files-stevedylan-dev/bafybeicrnbi7yt5wcnaiielxhvb5fb7mocl7k2ub43e3nqwlnfev4zekkm.webp" |
|
| 7 | - | hidden: true |
|
| 8 | - | --- |
|
| 9 | - | ||
| 10 | - | > |
|
| 11 | - | ||
| 12 | - | In the last few years the terms "zk" or "zero knowledge proofs" have been buzzing and for good reason. It's a relatively new piece of tech in cryptography that allows someone to prove something without revealing the information itself, which has massive implications for not only the blockchain space but for privacy and trusted code. While zk's are powerful, they also come at a cost. The majority of real-world use zero knowledge proofs are computationally expensive and inefficient, and in order to truly scale and make a difference, the cost needs to come down. This is where [Succinct](https://succinct.xyz/) comes in, a company that specializes in zkVM technology and makes it accessible to developers. Not only has Succinct built [SP1](https://blog.succinct.xyz/sp1-is-live/), an zkVM that allows you to write zero knowledge proofs in Rust, but they have also revolutionized efficiency with Precompiles. |
|
| 13 | - | ||
| 14 | - | ## What are Precompiles? |
|
| 15 | - | ||
| 16 | - | When it comes to writing zero knowledge proofs, there are generally many cryptographic operations and methods that become repetitive, especially when being used for blockchains. These include arithmetics like elliptic curves or hashes which are accessed in Rust through crates. While building SP1, Succinct realized that improvements could be made if the execution of these programs happened through the RISC-V instruction used to make proofs. This created a balance between the ease of writing proofs in Rust but still having some of the benefits of custom circuits. The result was a series of patched crates of popular libraries that dramatically changed the speed and efficiency of zero knowledge proof calculation. |
|
| 17 | - | ||
| 18 | - | Instead of just taking my word for it, I'll show you how to spin up a quick SP1 project and we'll build proofs both with and without the precompile patches! |
|
| 19 | - | ||
| 20 | - | ## Proving the Performance |
|
| 21 | - | ||
| 22 | - | The best way to really experience these speed differences is to try SP1 out for yourself, so let's start with what you'll need to follow along. |
|
| 23 | - | ||
| 24 | - | - Install Rust - SP1 allows developers to write normal Rust to build zero knowledge proofs, so make sure to have it installed on your machine (preferably the nightly version). |
|
| 25 | - | - Install SP1 - After you have Rust installed the SP1 installation is quite straight forward. Just follow the instructions [here](https://docs.succinct.xyz/getting-started/install.html). |
|
| 26 | - | - Starter Repo - Finally you will want to clone the start repo that has everything built out. Run the following command and move into the project directory: `git clone https://github.com/stevedylandev/precompiles-demo && cd precompiles-demo` |
|
| 27 | - | ||
| 28 | - | Once you've gotten everything setup, let's do a quick walkthrough of the project structure. |
|
| 29 | - | ||
| 30 | - | ``` |
|
| 31 | - | . |
|
| 32 | - | ├── Cargo.lock |
|
| 33 | - | ├── Cargo.toml |
|
| 34 | - | ├── LICENSE-MIT |
|
| 35 | - | ├── README.md |
|
| 36 | - | ├── elf |
|
| 37 | - | │ └── riscv32im-succinct-zkvm-elf |
|
| 38 | - | ├── lib |
|
| 39 | - | │ ├── Cargo.toml |
|
| 40 | - | │ └── src |
|
| 41 | - | │ └── lib.rs |
|
| 42 | - | ├── program |
|
| 43 | - | │ ├── Cargo.toml |
|
| 44 | - | │ └── src |
|
| 45 | - | │ └── main.rs |
|
| 46 | - | ├── rust-toolchain |
|
| 47 | - | └── script |
|
| 48 | - | ├── Cargo.toml |
|
| 49 | - | ├── build.rs |
|
| 50 | - | └── src |
|
| 51 | - | └── bin |
|
| 52 | - | ``` |
|
| 53 | - | ||
| 54 | - | - `program` - In this folder we have the proof that we want to write in Rust. For our example we'll take an input and run it through a keccak256 hash. |
|
| 55 | - | - `script` - Here you will find a `bin` script that handles command arguments for either executing the program or generating the proof. It's recommended to use execute as the primary dev method since proving can be computationally expensive or take longer. |
|
| 56 | - | - `lib` - This directory has some helper structs that we'll use in the program for deserializing data. |
|
| 57 | - | - `elf` - The `elf` directory holds a special ELF (Executable and Linkable Format) file that is used to make our program and script programs talk to each other. |
|
| 58 | - | ||
| 59 | - | Let's take a quick look at the program we'll be generating a proof for: |
|
| 60 | - | ||
| 61 | - | ```rust |
|
| 62 | - | // These two lines are necessary for the program to properly compile. |
|
| 63 | - | // |
|
| 64 | - | // Under the hood, we wrap your main function with some extra code so that it behaves properly |
|
| 65 | - | // inside the zkVM. |
|
| 66 | - | #![no_main] |
|
| 67 | - | sp1_zkvm::entrypoint!(main); |
|
| 68 | - | ||
| 69 | - | use alloy_sol_types::{private::FixedBytes, SolType}; |
|
| 70 | - | use precompiles_demo::PublicValuesStruct; |
|
| 71 | - | use tiny_keccak::{Hasher, Keccak}; |
|
| 72 | - | //use patched_tiny_keccak::{Hasher, Keccak}; |
|
| 73 | - | ||
| 74 | - | pub fn main() { |
|
| 75 | - | // Read an input to the program. |
|
| 76 | - | // |
|
| 77 | - | // Behind the scenes, this compiles down to a custom system call which handles reading inputs |
|
| 78 | - | // from the prover. |
|
| 79 | - | let input = sp1_zkvm::io::read::<String>(); |
|
| 80 | - | ||
| 81 | - | // Compute a keccak hash form the input |
|
| 82 | - | let mut hasher = Keccak::v256(); |
|
| 83 | - | hasher.update(input.as_bytes()); |
|
| 84 | - | let mut hash_bytes = [0u8; 32]; |
|
| 85 | - | hasher.finalize(&mut hash_bytes); |
|
| 86 | - | ||
| 87 | - | let hash_fixed = FixedBytes::<32>(hash_bytes); |
|
| 88 | - | ||
| 89 | - | // Encode the public values of the program. |
|
| 90 | - | let bytes = PublicValuesStruct::abi_encode(&PublicValuesStruct { |
|
| 91 | - | input, |
|
| 92 | - | hash: hash_fixed, |
|
| 93 | - | }); |
|
| 94 | - | ||
| 95 | - | // Commit to the public values of the program. The final proof will have a commitment to all the |
|
| 96 | - | // bytes that were committed to. |
|
| 97 | - | sp1_zkvm::io::commit_slice(&bytes); |
|
| 98 | - | } |
|
| 99 | - | ``` |
|
| 100 | - | ||
| 101 | - | At the top of our file we have an `entrypoint` for the `sp1_zkvm` which is needed to run inside SP1. Outside of that we have some basic Rust code that takes an input, creates a hash, and returns the input and the resulting hash. SP1 provides some utilities to handle things like `io` to read inputs as well as commiting them. |
|
| 102 | - | ||
| 103 | - | Now let's start testing. If you haven't already run `cd program` to move into the program directory, and then run: |
|
| 104 | - | ||
| 105 | - | ``` |
|
| 106 | - | cargo prove build |
|
| 107 | - | ``` |
|
| 108 | - | ||
| 109 | - | This will build our program using SP1 and prepare it for the next step, execution. |
|
| 110 | - | ||
| 111 | - | Inside our `script` folder we have a `/bin/main.rs` file that will run the program through SP1: |
|
| 112 | - | ||
| 113 | - | ```rust |
|
| 114 | - | use alloy_sol_types::SolType; |
|
| 115 | - | use clap::Parser; |
|
| 116 | - | use precompiles_demo::PublicValuesStruct; |
|
| 117 | - | use sp1_sdk::{ProverClient, SP1Stdin}; |
|
| 118 | - | ||
| 119 | - | /// The ELF (executable and linkable format) file for the Succinct RISC-V zkVM. |
|
| 120 | - | pub const ELF: &[u8] = include_bytes!("../../../elf/riscv32im-succinct-zkvm-elf"); |
|
| 121 | - | ||
| 122 | - | /// The arguments for the command. |
|
| 123 | - | #[derive(Parser, Debug)] |
|
| 124 | - | #[clap(author, version, about, long_about = None)] |
|
| 125 | - | struct Args { |
|
| 126 | - | #[clap(long)] |
|
| 127 | - | execute: bool, |
|
| 128 | - | ||
| 129 | - | #[clap(long)] |
|
| 130 | - | prove: bool, |
|
| 131 | - | ||
| 132 | - | #[clap(long, default_value = "Hello World from SP1!")] |
|
| 133 | - | n: String, |
|
| 134 | - | } |
|
| 135 | - | ||
| 136 | - | fn main() { |
|
| 137 | - | // Setup the logger. |
|
| 138 | - | sp1_sdk::utils::setup_logger(); |
|
| 139 | - | ||
| 140 | - | // Parse the command line arguments. |
|
| 141 | - | let args = Args::parse(); |
|
| 142 | - | ||
| 143 | - | if args.execute == args.prove { |
|
| 144 | - | eprintln!("Error: You must specify either --execute or --prove"); |
|
| 145 | - | std::process::exit(1); |
|
| 146 | - | } |
|
| 147 | - | ||
| 148 | - | // Setup the prover client. |
|
| 149 | - | let client = ProverClient::new(); |
|
| 150 | - | ||
| 151 | - | // Setup the inputs. |
|
| 152 | - | let mut stdin = SP1Stdin::new(); |
|
| 153 | - | stdin.write(&args.n); |
|
| 154 | - | ||
| 155 | - | println!("n: {}", args.n); |
|
| 156 | - | ||
| 157 | - | if args.execute { |
|
| 158 | - | // Execute the program |
|
| 159 | - | let (output, report) = client.execute(ELF, stdin).run().unwrap(); |
|
| 160 | - | println!("Program executed successfully."); |
|
| 161 | - | ||
| 162 | - | // Read the output. |
|
| 163 | - | let decoded = PublicValuesStruct::abi_decode(output.as_slice(), true).unwrap(); |
|
| 164 | - | let PublicValuesStruct { input, hash } = decoded; |
|
| 165 | - | println!("Input: {}", input); |
|
| 166 | - | println!("Hash: 0x{}", hex::encode(hash.0)); // Convert bytes to hex string |
|
| 167 | - | ||
| 168 | - | // Record the number of cycles executed. |
|
| 169 | - | println!("Number of cycles: {}", report.total_instruction_count()); |
|
| 170 | - | } else { |
|
| 171 | - | // Setup the program for proving. |
|
| 172 | - | let (pk, vk) = client.setup(ELF); |
|
| 173 | - | ||
| 174 | - | // Generate the proof |
|
| 175 | - | let proof = client |
|
| 176 | - | .prove(&pk, stdin) |
|
| 177 | - | .run() |
|
| 178 | - | .expect("failed to generate proof"); |
|
| 179 | - | ||
| 180 | - | println!("Successfully generated proof!"); |
|
| 181 | - | ||
| 182 | - | // Verify the proof. |
|
| 183 | - | client.verify(&proof, &vk).expect("failed to verify proof"); |
|
| 184 | - | println!("Successfully verified proof!"); |
|
| 185 | - | } |
|
| 186 | - | } |
|
| 187 | - | ``` |
|
| 188 | - | ||
| 189 | - | This will look for arguments during `cargo run` to determine if it should only run an execution or if it should generate a proof. Executions are great for testing the code and will take less time and computation power than proofs. Once we have the arguments we can create an instance of the `ProverClient` to run our program and get the committed values back. Now let's try it out with the following command, making sure we have run `cd ../script` first to be in this directory instead of program: |
|
| 190 | - | ||
| 191 | - | ``` |
|
| 192 | - | cargo run --release -- --execute |
|
| 193 | - | ``` |
|
| 194 | - | ||
| 195 | - | This will start the bin command and will take a little time to run. Once complete you should see an output like this one: |
|
| 196 | - | ||
| 197 | - | ``` |
|
| 198 | - | 2024-10-27T04:45:04.296078Z INFO vk verification: true |
|
| 199 | - | n: Hello World from SP1! |
|
| 200 | - | 2024-10-27T04:45:04.388224Z INFO execute: close time.busy=6.23ms time.idle=14.2µs |
|
| 201 | - | Program executed successfully. |
|
| 202 | - | Input: Hello World from SP1! |
|
| 203 | - | Hash: 0x5de0efbc889e22250feb078959ad9aa6fb6c1301b94b3a8e6a10d49e47c074f2 |
|
| 204 | - | Number of cycles: 30635 |
|
| 205 | - | ``` |
|
| 206 | - | ||
| 207 | - | The once piece you'll want to note particularly is the cycle count. This is how many cycles were run on the circuits to produce the proof, and act as a measurement of computational efficiency. |
|
| 208 | - | ||
| 209 | - | With our base test out of the way with `30635` cycles, let's use our patched keccak library from SP1 to see the difference. We've already got the library listed in the `Cargo.toml` file inside the `program` directory: |
|
| 210 | - | ||
| 211 | - | ```toml |
|
| 212 | - | [package] |
|
| 213 | - | version = "0.1.0" |
|
| 214 | - | name = "keccak-program" |
|
| 215 | - | edition = "2021" |
|
| 216 | - | ||
| 217 | - | [dependencies] |
|
| 218 | - | tiny-keccak = { version = "2.0.2", features = ["keccak"] } |
|
| 219 | - | alloy-sol-types = { workspace = true } |
|
| 220 | - | sp1-zkvm = "3.0.0-rc4" |
|
| 221 | - | precompiles-demo = { path = "../lib" } |
|
| 222 | - | patched-tiny-keccak = { git = "https://github.com/sp1-patches/tiny-keccak", branch = "patch-v2.0.2", package = "tiny-keccak", features = [ |
|
| 223 | - | "keccak", |
|
| 224 | - | ] } |
|
| 225 | - | hex = "0.4.3" |
|
| 226 | - | ``` |
|
| 227 | - | ||
| 228 | - | To use it, we just need to update the top of the `program/src/main.rs` file to use `patched_tiny_keccak` instead of `tiny_keccak`. |
|
| 229 | - | ||
| 230 | - | ```rust |
|
| 231 | - | #![no_main] |
|
| 232 | - | sp1_zkvm::entrypoint!(main); |
|
| 233 | - | ||
| 234 | - | use alloy_sol_types::{private::FixedBytes, SolType}; |
|
| 235 | - | use precompiles_demo::PublicValuesStruct; |
|
| 236 | - | //use tiny_keccak::{Hasher, Keccak}; |
|
| 237 | - | use patched_tiny_keccak::{Hasher, Keccak}; |
|
| 238 | - | ``` |
|
| 239 | - | ||
| 240 | - | With the standard `tiny-keccak` library commented out and the patched one being put in in's place, let's navigate back to the `script` directory and run the same execute command. You should get some results like this: |
|
| 241 | - | ||
| 242 | - | ``` |
|
| 243 | - | n: Hello World from SP1! |
|
| 244 | - | Program executed successfully. |
|
| 245 | - | Input: Hello World from SP1! |
|
| 246 | - | Hash: 0x5de0efbc889e22250feb078959ad9aa6fb6c1301b94b3a8e6a10d49e47c074f2 |
|
| 247 | - | Number of cycles: 14259 |
|
| 248 | - | ``` |
|
| 249 | - | ||
| 250 | - | Just like that, our number of cycles has dropped by half! While this example is pretty elementary, in a much larger application with more computationally expensive programs the results seen are more efficient in magnitudes rather than just a 2x increase in performance. |
|
| 251 | - | ||
| 252 | - | ## Wrapping Up |
|
| 253 | - | ||
| 254 | - | While there have been many advances in zk technology, there is still much work to do for larger adoption. Writing zero knowledge proofs shouldn't take enormous amounts of time in low-level languages like assembly, and that's exactly why Succinct is making it easier with SP1. Even today there are dozens of teams using SP1 in production to help scale blockchains, rollups, bridges, and more. It's not hard to imagine a future where zkVMs are used in sectors beyond Web3, as there are many needs in our modern infrastructure for verifiable code, and Succinct will be there paving the way. |
| 1 | - | --- |
|
| 2 | - | title: "How To Create a Weekly Photo Zine" |
|
| 3 | - | publishDate: "08 June 2023" |
|
| 4 | - | description: "Learn how to use ChatGPT and Pinata to make a custom web photo zine with zero developer experience" |
|
| 5 | - | tags: ["web development", "ai"] |
|
| 6 | - | ogImage: "/blog-images/medium/v2/resize:fit:1400/format:webp/1*2Ym-4FvWAD65UiaTVZ67uA@2x.png" |
|
| 7 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvusgpyv2v" |
|
| 8 | - | --- |
|
| 9 | - | ||
| 10 | - | import medium from "../../assets/medium.png"; |
|
| 11 | - | import OutLinkButton from "@/components/common/OutLinkButton"; |
|
| 12 | - | ||
| 13 | - | <OutLinkButton |
|
| 14 | - | link="https://medium.com/pinata/how-to-create-a-weekly-photo-zine-9f592e7a5ae2" |
|
| 15 | - | site="Medium" |
|
| 16 | - | image={medium} |
|
| 17 | - | /> |
|
| 18 | - | ||
| 19 | - | It’s no secret that AI has revolutionized the tech space, and we’ve already seen some incredible things made with it. ChatGPT was one that immediately proved valuable to everyday people, being capable of answering any kind of question, have an ongoing conversation, and do things you never could before. Beyond that, AI is quickly becoming a tool to democratize the creation of web content. |
|
| 20 | - | ||
| 21 | - | Typically, if you want a custom web experience you would either need to hire a professional agency, perhaps get half way with a website builder, or learn to code yourself. But, that may not be the case soon. With AI the average person can ask the right questions and create elementary web content for their brand. In this guide, I’ll show you how to create a simple photo zine using ChatGPT and Pinata and how to update it on a weekly basis. Or, watch the video tutorial above. |
|
| 22 | - | ||
| 23 | - | ## Pinata |
|
| 24 | - | ||
| 25 | - | First, you’ll want to sign up for an account with Pinata. I would recommend choosing the Picnic plan for this project as we will utilize its speed and flexibility for hosting our content. Once you have an account, you’ll want to start uploading all your images to be used for the zine. In this demo I’ve selected some of my favorite images from a trip I took a few years ago. |
|
| 26 | - | ||
| 27 | - | > |
|
| 28 | - | ||
| 29 | - | Once you have your content uploaded, you can click on the preview button to see your photo. Copy the URL for that image and paste it somewhere for later and repeat the process for however much content you want to put in your zine. |
|
| 30 | - | ||
| 31 | - | > |
|
| 32 | - | ||
| 33 | - | ## ChatGPT |
|
| 34 | - | ||
| 35 | - | After you have done this it’s time to visit [ChatGPT](https://chat.openai.com). I would recommend starting with a prompt that tells ChatGPT who it is and what it will help you with, and ask if it has any questions. This will help set you up to make sure you have all the ground covered. Here is an example of what I said to start: |
|
| 36 | - | ||
| 37 | - | ``` |
|
| 38 | - | You are a mobile web developer. You have complete understanding of being able to prompt |
|
| 39 | - | ChatGPT. You will guide me through building a webpage. What questions do you have? |
|
| 40 | - | ``` |
|
| 41 | - | ||
| 42 | - | It will respond with several questions, so simply provide the answers like you see below. |
|
| 43 | - | ||
| 44 | - | > |
|
| 45 | - | ||
| 46 | - | Below my last answer I went ahead and gave ChatGPT the content of text and image links, and just like that it generated the html content to start with. If it doesn’t finish generating all the code at once, you can click “continue generating” and it will keep going where it left off. Also be sure it includes all your content, make sure it knows you have zero programming knowledge and it needs to write everything on its own. Once it does this there should be a copy button at the top of the code box, so go ahead and click that: |
|
| 47 | - | ||
| 48 | - | > |
|
| 49 | - | ||
| 50 | - | ## Replit |
|
| 51 | - | ||
| 52 | - | Now to actually paste our code and see what it does, we will be using an online code editor called [Replit](https://replit.com). Just sign up for a free account, and in the top right corner you will want to click “Create Repl.” |
|
| 53 | - | ||
| 54 | - | > |
|
| 55 | - | ||
| 56 | - | After that you will want to select the HTML, CSS, and Javascript template and give your project a name. |
|
| 57 | - | ||
| 58 | - | > |
|
| 59 | - | ||
| 60 | - | Once you’re in your repl project, delete out the starter HTML and paste in your own code from ChatGPT, then hit CMD/CTR + S to save it. After that you should see on the right side your project: |
|
| 61 | - | ||
| 62 | - | > |
|
| 63 | - | ||
| 64 | - | You can also change the size of your webview by clicking and dragging the pane to shrink it more towards a mobile view if that’s what you’re after. |
|
| 65 | - | ||
| 66 | - | Now your project will likely not be perfect right out of the gate, and you will want to make some changes. Since ChatGPT keeps that conversation history, you can go back and forth with it for the changes you want. |
|
| 67 | - | ||
| 68 | - | > |
|
| 69 | - | ||
| 70 | - | The key is to make sure you are precise in what changes you want, and that ChatGPT gives you the updated code afterwards. Once you’ve gotten your zine looking the way you want it, click on the little dropdown arrow next to the index.html file and click “Download.” |
|
| 71 | - | ||
| 72 | - | > |
|
| 73 | - | ||
| 74 | - | After you have download the file, go ahead and upload it to Pinata just like you did for your image files. Then just click on that file to see your zine. You can view mine with this link: |
|
| 75 | - | ||
| 76 | - | [A Venture to the North East](https://stevedylanphoto.mypinata.cloud/ipfs/QmWAUQfKhJ19kZqcJLP6nPbHFRToiaFquGJ71gJEA4cRVT) |
|
| 77 | - | ||
| 78 | - |  |
|
| 79 | - | ||
| 80 | - | ## Gateway tip |
|
| 81 | - | ||
| 82 | - | Now here’s the bonus info: if you wanted to make this a weekly update, Pinata provides a pretty neat way to do that. First you would of course need to give ChatGPT your code and the update content and it could swap it out for you (or you could even take your hand at changing it in Replit!), and once you have uploaded the new index.html to Pinata, you can click on the “more” button next to the file and select “Set gateway as a root” |
|
| 83 | - | ||
| 84 | - | > |
|
| 85 | - | ||
| 86 | - | What this will do is redirect my base url “https://stevedylanphoto.mypinata.cloud” to the new index.html file that was uploaded. I could keep updating it with each new file every week with a new photo zine, as well as improve it over time. Of course if you don’t like the “mypinata.cloud” domain for your zine, you could very easily use your own domain like “stevephotozine.com” and it would work exactly the same. My personal photography website is stevedylanphoto.com, so I just used “zine.stevedylanphoto.com” as a dedicated domain for this zine. Check it out: |
|
| 87 | - | ||
| 88 | - | [zine.stevedylanphoto.com](https://zine.stevedylanphoto.com) |
|
| 89 | - | ||
| 90 | - | We’re excited to see what you’ll make using these tools. |
|
| 91 | - | ||
| 92 | - | ## Pinata Links: |
|
| 93 | - | ||
| 94 | - | [Explore our plans and start building today!](https://www.pinata.cloud/pricing?utm_source=medium&utm_medium=referral&utm_campaign=photo_zine_blog) |
|
| 95 | - | ||
| 96 | - | [Twitter](https://twitter.com/pinatacloud) |
|
| 97 | - | ||
| 98 | - | [Instagram](https://www.linkedin.com/company/pinatacloud) |
|
| 99 | - | ||
| 100 | - | [YouTube](https://www.youtube.com/c/Pinatacloud) |
|
| 101 | - | ||
| 102 | - | [Website](https://www.pinata.cloud/) |
|
| 103 | - | ||
| 104 | - | [Discord](https://discord.gg/pinata) |
| 1 | - | --- |
|
| 2 | - | title: "How to Migrate from Neovim to VSCode" |
|
| 3 | - | publishDate: "06 July 2023" |
|
| 4 | - | description: "Some tips on how to eaily move to VSCode as a Neovim user (sorry Primeagen)" |
|
| 5 | - | tags: ["developer tools", "neovim"] |
|
| 6 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvuseriv2v" |
|
| 7 | - | --- |
|
| 8 | - | ||
| 9 | - | Okay, okay. I know. Before you come at me with virtual torches and pitchforks, hear this first: I love Neovim. I've been using it non-stop for the past two years, and honestly, I struggled to use any other kind of editor. I loved the speed, the tinkering, the ricing, etc. It's an incredible piece of open-source software and has a special place in my heart. |
|
| 10 | - | ||
| 11 | - | > The truth: use whatever tool fits you best. |
|
| 12 | - | > The harder truth: be willing to try the other tools. |
|
| 13 | - | ||
| 14 | - | Several developers have switched from Neovim to VSCode, notably [Nexxel](https://www.nexxel.dev/blog/neovim-to-vscode) and [Melkey](https://youtu.be/PLxpyUYvC_o). Both of their pieces of content resonated with me on several levels, so I thought, "Why not try it for a week?" The worst case scenario was that I would hate it and move back to Neovim. However, it turned out to be harder than I expected. The first few days were rough; nothing was smooth, everything felt clunky, it was just awful. Of course, I already had the Vim keybindings extension installed, which helped, but something was still missing. After digging into it, I realized there were two things throwing me off: |
|
| 15 | - | ||
| 16 | - | 1. The clunky UI |
|
| 17 | - | 2. Vim mode specific keybindings |
|
| 18 | - | ||
| 19 | - | Let me go into more detail about how I resolved these problems, and hopefully it will help anyone who finds themselves in the same situation. |
|
| 20 | - | ||
| 21 | - | ## UI |
|
| 22 | - | ||
| 23 | - | In Neovim, everything is stripped down to the core editor. By default, there are no extra plugins, no file tree, not even syntax highlighting. Of course, this is by design, and, like Linux, you have to make a lot of decisions as to what you want your editor to look like and how you want it to behave. In my opinion this is a good thing, and it's better to start out with less and slowly build more on top. Having just the minimal editor experience is a key UI component for the regular Neovim user. |
|
| 24 | - | ||
| 25 | - | VSCode is the exact opposite. There's loads of extra stuff all around the editor that's pure distraction. I mean, be real, who is using the minimap thing? What about that extra bar on the right with a bunch of icons that you'll never use? It's so aggressive that I wanted to get out as soon as I opened it. However, there are solutions to this! You can simply go to the View menu, then Appearance, and from there toggle all those nasty bits off. Now you can get VSCode looking more like this: |
|
| 26 | - | ||
| 27 | - |  |
|
| 28 | - | ||
| 29 | - | ## Keybindings |
|
| 30 | - | ||
| 31 | - | Altering the UI a bit is easy and takes a few minutes, but the keybindings were a whole other mess. Throughout my years of using Neovim, I had collected so many custom keymaps for different things that were unique to Neovim. I realized pretty quickly that I had no idea how I could use them again because they were bound to Vim modes. How do I use `Shift + H` to switch to the previous buffer or `Shift + L` to go to the next? How can I map that in VSCode? Thankfully, VSCodeVim has that covered, although it's not the most intuitive. |
|
| 32 | - | ||
| 33 | - | If you visit the [VSCodeVim](https://code.visualstudio.com/docs/editor/vim) docs, you can see they have some example configurations to remap keybindings for specific modes. For instance, I was able to add my lovely buffer switching remapping in normal mode with ease. |
|
| 34 | - | ||
| 35 | - | ```json |
|
| 36 | - | "vim.normalModeKeyBindings": [ |
|
| 37 | - | // switch buffers with ctrl and left and right |
|
| 38 | - | { "before": ["<S-h>"], "commands": [":bprevious"]}, |
|
| 39 | - | { "before": ["<S-l>"], "commands": [":bnext"]}, |
|
| 40 | - | ] |
|
| 41 | - | ``` |
|
| 42 | - | ||
| 43 | - | "Before" is simply the keys you're pressing and either "after" or "commands" (if you want to do `:enter commands`) will be the output of that binding. What I had a lot of trouble doing was going beyond the simpler stuff. Another one I loved using was `Ctrl + H, J, K, L` to switch between panes in the editor. Because VSCode isn't really using the same window API that Neovim has, those go out the window. How are you supposed to do those? |
|
| 44 | - | ||
| 45 | - | Here's the sauce: discovering VSCode's API. Something I had never done before is going to the built-in keybindings menu for VSCode and looking at what was in there. There's so much there and it's a bit overwhelming, but once you figure out how to find the command you're looking for, it makes just about anything possible. For my pane switching keymap, I wanted to find which command was changing the focus. I searched "focus left" in the VSCode keybindings and sure enough, there it was: `workbench.action.focusLeftGroup`. |
|
| 46 | - | ||
| 47 | - |  |
|
| 48 | - | ||
| 49 | - | Now back in our VSCodeVim config, we can add that method to the binding like so. |
|
| 50 | - | ||
| 51 | - | ```json |
|
| 52 | - | "vim.normalModeKeyBindings": [ |
|
| 53 | - | //... other bindings |
|
| 54 | - | ||
| 55 | - | // better pane navigation |
|
| 56 | - | { "before": ["<C-h>"], "commands": ["workbench.action.focusLeftGroup"]}, |
|
| 57 | - | { "before": ["<C-j>"], "commands": ["workbench.action.focusBelowGroup"]}, |
|
| 58 | - | { "before": ["<C-k>"], "commands": ["workbench.action.focusAboveGroup"]}, |
|
| 59 | - | { "before": ["<C-l>"], "commands": ["workbench.action.focusRightGroup"]}, |
|
| 60 | - | ], |
|
| 61 | - | ``` |
|
| 62 | - | ||
| 63 | - | It works like a charm. If you're doing this yourself, something I found useful is right-clicking on the command in the VSCode keybindings menu to see the method. Another example where this came in handy was using the "Find Files" command. By default, it's `Cmd + P` on Mac, but I was used to something like `<leader> + F`. When I searched "Open File..." in the keybindings, it didn't show the method like the others did. However, after right-clicking, you can click "Copy Command ID" to grab it. |
|
| 64 | - | ||
| 65 | - |  |
|
| 66 | - | ||
| 67 | - | Configuring VSCode is not the most pleasant experience, but on the plus side you can save your config file and move it wherever you want, and you don’t have to mess with configuring as many plugins with Neovim. I’m still on the fence as to whether I’ll keep trying to use VSCode or go back to Neovim, but hopefully these tips help anyone out there trying to make it work for their own personal reasons. |
|
| 68 | - | ||
| 69 | - | [Check out my VSCode config here](https://gist.github.com/stevedylandev/b6bb1eccd83ea438031158c5961fd3f8) |
| 1 | - | --- |
|
| 2 | - | title: "How to Mint an NFT on Sui using Pinata and the Sui JS SDK" |
|
| 3 | - | publishDate: "27 Feb 2023" |
|
| 4 | - | description: "Learn how to use the Sui SDK and Pinata to mint an NFT" |
|
| 5 | - | ogImage: "/blog-images/medium/v2/resize:fit:4800/format:webp/1*JbX3kWI20G3EaKJNgXfQiQ.png" |
|
| 6 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvusolxf2v" |
|
| 7 | - | --- |
|
| 8 | - | ||
| 9 | - | import medium from "../../assets/medium.png"; |
|
| 10 | - | import OutLinkButton from "@/components/common/OutLinkButton"; |
|
| 11 | - | ||
| 12 | - | <OutLinkButton |
|
| 13 | - | link="https://medium.com/pinata/how-to-mint-an-nft-on-sui-using-pinata-and-the-sui-js-sdk-4386655e403" |
|
| 14 | - | site="Medium" |
|
| 15 | - | image={medium} |
|
| 16 | - | /> |
|
| 17 | - | ||
| 18 | - | One of my favorite pastimes is playing around with new blockchains and seeing what they’re like, what they offer, and how easy they are to build on. Recently I stumbled upon Sui and some of its unique features, such as dynamic metadata and goals to help onboard everyday people. After playing with it myself I was impressed with its speed and smoothness, which is saying something as it’s still in development! |
|
| 19 | - | ||
| 20 | - | The only thing I found lacking was some basic explanations of how to mint NFTs on Sui and some of the things you might have to tackle, so I thought I would share that experience and demonstrate this! |
|
| 21 | - | ||
| 22 | - | To get started, you should only need the basics such as NodeJS and a text editor like VSCode, as well as some fundamental knowledge about Javascript. |
|
| 23 | - | ||
| 24 | - | ## Setting up your NFT with Pinata |
|
| 25 | - | ||
| 26 | - | While Sui features dynamic metadata, you will still want to use a service to store images, video, or any other kind of content you want to turn into an NFT! Pinata is ideal as it uses IPFS which prevents tampering with content which you can read more about here. |
|
| 27 | - | ||
| 28 | - | Getting started with Pinata is easy! Just visit the signup page here and start out with a free account. Now all you have to do is upload the image you want to use. Do that by visiting the main files page and clicking “Upload” and “Select File.” |
|
| 29 | - | ||
| 30 | - | > |
|
| 31 | - | ||
| 32 | - | After that just follow the steps of selecting your file, give it a name, then upload! Once done it should show up in your files page as seen below, and we will want to copy the CID for later. |
|
| 33 | - | ||
| 34 | - | > |
|
| 35 | - | ||
| 36 | - | ## Code Setup with the Sui JS SDK |
|
| 37 | - | ||
| 38 | - | First let’s make a new directory where the project will live using the following terminal command: |
|
| 39 | - | ||
| 40 | - | ```bash |
|
| 41 | - | mkdir sui-nft && cd sui-nft |
|
| 42 | - | ``` |
|
| 43 | - | ||
| 44 | - | Now let’s install the Sui Javascript SDK: |
|
| 45 | - | ||
| 46 | - | ```bash |
|
| 47 | - | npm init -y && npm install @mysten/sui.js |
|
| 48 | - | ``` |
|
| 49 | - | ||
| 50 | - | One small change we need to make to the package.json file is adding the module type, so make sure your package.json looks like this. |
|
| 51 | - | ||
| 52 | - | ```json |
|
| 53 | - | { |
|
| 54 | - | "name": "sui-nft", |
|
| 55 | - | "type": "module", |
|
| 56 | - | "version": "1.0.0", |
|
| 57 | - | "description": "", |
|
| 58 | - | "main": "index.js", |
|
| 59 | - | "scripts": { |
|
| 60 | - | "test": "echo \"Error: no test specified\" && exit 1" |
|
| 61 | - | }, |
|
| 62 | - | "keywords": [], |
|
| 63 | - | "author": "", |
|
| 64 | - | "license": "ISC", |
|
| 65 | - | "dependencies": { |
|
| 66 | - | "@mysten/sui.js": "^0.26.1" |
|
| 67 | - | } |
|
| 68 | - | } |
|
| 69 | - | ``` |
|
| 70 | - | ||
| 71 | - | Lastly, we will want to make a file to run our code which we’ll call mint-nft.js |
|
| 72 | - | ||
| 73 | - | ```bash |
|
| 74 | - | touch mint-nft.js |
|
| 75 | - | ``` |
|
| 76 | - | ||
| 77 | - | Now we can get into good stuff — writing the code to mint our NFT! |
|
| 78 | - | ||
| 79 | - | ## Minting the NFT |
|
| 80 | - | ||
| 81 | - | Go ahead and open up your mint-nft.js file in your text editor of choice, and the first thing we’re gonna do is import the following methods at the top of the page. |
|
| 82 | - | ||
| 83 | - | ```javascript |
|
| 84 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 85 | - | ``` |
|
| 86 | - | ||
| 87 | - | All we really need are four things, and you’ll see how they play a part as we start building. Next thing we need to do is create a wallet and get the public address for that wallet, which we can do like so. |
|
| 88 | - | ||
| 89 | - | ```javascript |
|
| 90 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 91 | - | ||
| 92 | - | const keypair = new Ed25519Keypair(); |
|
| 93 | - | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString(); |
|
| 94 | - | console.log(address); |
|
| 95 | - | ``` |
|
| 96 | - | ||
| 97 | - | Accessing the keypair object makes it super simple to either get the public key or private key, and then turn it into usable data. The Sui address from the public key doesn’t include the leading “0x” so I’m adding that manually here. If you go into the terminal now and run “node mint-nft.js” then you should see an address like this! |
|
| 98 | - | ||
| 99 | - | ``` |
|
| 100 | - | 0x8f7671eedff42d6dde8c365a6b641bb9769ea02e |
|
| 101 | - | ``` |
|
| 102 | - | ||
| 103 | - | Now that we have a wallet address, it’s time to connect to the Sui network and get some test Sui coin! First we’ll declare a new provider and use the JsonRpcProvider, and pass in the Network “DEVNET.” |
|
| 104 | - | ||
| 105 | - | ```javascript |
|
| 106 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 107 | - | ||
| 108 | - | //Create keypair |
|
| 109 | - | const keypair = new Ed25519Keypair(); |
|
| 110 | - | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString(); |
|
| 111 | - | console.log(address); |
|
| 112 | - | ||
| 113 | - | //Create network connection |
|
| 114 | - | const provider = new JsonRpcProvider(Network.DEVNET); |
|
| 115 | - | ``` |
|
| 116 | - | ||
| 117 | - | Then we’ll use the provider to request some test Sui from the Devnet faucet and use our new address as the receiver like so. |
|
| 118 | - | ||
| 119 | - | ```javascript |
|
| 120 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 121 | - | ||
| 122 | - | //Create keypair |
|
| 123 | - | const keypair = new Ed25519Keypair(); |
|
| 124 | - | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString(); |
|
| 125 | - | console.log(address); |
|
| 126 | - | ||
| 127 | - | //Create network connection |
|
| 128 | - | const provider = new JsonRpcProvider(Network.DEVNET); |
|
| 129 | - | ||
| 130 | - | // Get Sui from faucet |
|
| 131 | - | const fund = await provider.requestSuiFromFaucet(address); |
|
| 132 | - | console.log(fund); |
|
| 133 | - | ``` |
|
| 134 | - | ||
| 135 | - | Let’s run the node mint-nft.js command now and see what we get! |
|
| 136 | - | ||
| 137 | - | ``` |
|
| 138 | - | 0xf64640227ff94ba762252c15f2adbcedb6d3aaab |
|
| 139 | - | { |
|
| 140 | - | transferred_gas_objects: [ |
|
| 141 | - | { |
|
| 142 | - | amount: 10000000, |
|
| 143 | - | id: '0x39c25c3885c2cccea957c26219de9c7e58a33a21', |
|
| 144 | - | transfer_tx_digest: '4ETS2rGNzRYZ95SsLrUsQf8ckfZWQSSqTEpGi32RqKbk' |
|
| 145 | - | }, |
|
| 146 | - | { |
|
| 147 | - | amount: 10000000, |
|
| 148 | - | id: '0x3bdad5c729495d9d152cfd03b0e44e8549972d53', |
|
| 149 | - | transfer_tx_digest: '4ETS2rGNzRYZ95SsLrUsQf8ckfZWQSSqTEpGi32RqKbk' |
|
| 150 | - | }, |
|
| 151 | - | { |
|
| 152 | - | amount: 10000000, |
|
| 153 | - | id: '0x4a748f4e928b974dd46913e2cc069a21fecaad86', |
|
| 154 | - | transfer_tx_digest: '4ETS2rGNzRYZ95SsLrUsQf8ckfZWQSSqTEpGi32RqKbk' |
|
| 155 | - | }, |
|
| 156 | - | { |
|
| 157 | - | amount: 10000000, |
|
| 158 | - | id: '0x66d601ef1811cbdea82d2eb97a0994afdbbc888a', |
|
| 159 | - | transfer_tx_digest: '4ETS2rGNzRYZ95SsLrUsQf8ckfZWQSSqTEpGi32RqKbk' |
|
| 160 | - | }, |
|
| 161 | - | { |
|
| 162 | - | amount: 10000000, |
|
| 163 | - | id: '0xee8668e7c2fcd60047992f170da075dafd955f48', |
|
| 164 | - | transfer_tx_digest: '4ETS2rGNzRYZ95SsLrUsQf8ckfZWQSSqTEpGi32RqKbk' |
|
| 165 | - | } |
|
| 166 | - | ], |
|
| 167 | - | error: null |
|
| 168 | - | } |
|
| 169 | - | ``` |
|
| 170 | - | ||
| 171 | - | Nice! Through this we can see the Sui we received which equals out to 0.05 Sui. Something you might be wondering is why we have 5 different items here with different ids, and that’s actually a very important piece of the Sui model that isn’t natural. Every coin in Sui has its own unique object id. These are all of the same type of currency which is the default Sui type, but our five pieces are in their own camp. This can cause a problem when we try to use our Sui to mint an NFT because its gonna try to pull from just one of these objects when ideally we want to pull from all of them like a normal blockchain (huge shoutout to [Paul Fidika](https://twitter.com/PaulFidika) for helping me understand this bit!). |
|
| 172 | - | ||
| 173 | - | Think of it like this. Let’s say you have a farm with five cow fields, and each field is separated by the type of cow: short haired, long haired, etc. Each field has 10 cows so you technically have 50 cows, but in order to trade for some goats you need 20 cows. You would have to empty two of those cow fields and combine them to trade them. That’s essentially what we’re doing in Sui with coins. |
|
| 174 | - | ||
| 175 | - | With that said, in order for us to mint, we need to combine some of our recently acquired Sui drop. We’re gonna do that with the following code. |
|
| 176 | - | ||
| 177 | - | ```javascript |
|
| 178 | - | // Merge two of the Sui coin objects |
|
| 179 | - | const coin1 = fund.transferred_gas_objects[0].id; |
|
| 180 | - | const coin2 = fund.transferred_gas_objects[1].id; |
|
| 181 | - | const signer = new RawSigner(keypair, provider); |
|
| 182 | - | const mergeTxn = await signer.mergeCoin({ |
|
| 183 | - | primaryCoin: coin1, |
|
| 184 | - | coinToMerge: coin2, |
|
| 185 | - | gasBudget: 1000, |
|
| 186 | - | }); |
|
| 187 | - | console.log("MergeCoin txn", mergeTxn); |
|
| 188 | - | ``` |
|
| 189 | - | ||
| 190 | - | There’s quite a bit going on here so let’s break it down. |
|
| 191 | - | ||
| 192 | - | First we declare coin1 and coin2 from the airdrop we just received by accessing those object ids from our fund result. Then we need to declare our signer! This is what lets us use our private key from our keypair to transfer or mint on the chain, and we do that by declaring a new RawSigner and passing in our previously made keypair and provider to connect. Finally we use the mergeCoin method and pass in our two coins, along with a gas budget. |
|
| 193 | - | ||
| 194 | - | Before we run this, there is one small thing we’re missing. If you try to run this code now, you will likely get an error that it could not find the object id for the coin. That’s because its was just freshly created and we’re trying to access an object id that isn’t quite discoverable yet. To fix that we’re gonna make a small little helper function called “wait” like so. |
|
| 195 | - | ||
| 196 | - | ```javascript |
|
| 197 | - | // Pause function |
|
| 198 | - | const wait = async (time) => { |
|
| 199 | - | return new Promise((resolve, reject) => { |
|
| 200 | - | setTimeout(() => { |
|
| 201 | - | resolve(); |
|
| 202 | - | }, time); |
|
| 203 | - | }); |
|
| 204 | - | }; |
|
| 205 | - | ``` |
|
| 206 | - | ||
| 207 | - | This is really simple and just lets us pass in how many milliseconds we want to wait before continuing our function! Let’s use it after getting our airdrop and passing in 3 seconds. You code should look something like this now. |
|
| 208 | - | ||
| 209 | - | ```javascript |
|
| 210 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 211 | - | ||
| 212 | - | // Generate a new Keypair |
|
| 213 | - | const keypair = new Ed25519Keypair(); |
|
| 214 | - | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString(); |
|
| 215 | - | console.log(address); |
|
| 216 | - | ||
| 217 | - | // Create Network Connection and receive airdrop |
|
| 218 | - | const provider = new JsonRpcProvider(Network.DEVNET); |
|
| 219 | - | ||
| 220 | - | // Get Sui from faucet |
|
| 221 | - | const fund = await provider.requestSuiFromFaucet(address); |
|
| 222 | - | console.log(fund); |
|
| 223 | - | ||
| 224 | - | // Pause function |
|
| 225 | - | const wait = async (time) => { |
|
| 226 | - | return new Promise((resolve, reject) => { |
|
| 227 | - | setTimeout(() => { |
|
| 228 | - | resolve(); |
|
| 229 | - | }, time); |
|
| 230 | - | }); |
|
| 231 | - | }; |
|
| 232 | - | ||
| 233 | - | await wait(3000); |
|
| 234 | - | ||
| 235 | - | // Merge two of the Sui coin objects |
|
| 236 | - | const coin1 = fund.transferred_gas_objects[1].id; |
|
| 237 | - | const coin2 = fund.transferred_gas_objects[2].id; |
|
| 238 | - | const signer = new RawSigner(keypair, provider); |
|
| 239 | - | const mergeTxn = await signer.mergeCoin({ |
|
| 240 | - | primaryCoin: coin1, |
|
| 241 | - | coinToMerge: coin2, |
|
| 242 | - | gasBudget: 1000, |
|
| 243 | - | }); |
|
| 244 | - | console.log("MergeCoin txn", mergeTxn); |
|
| 245 | - | ``` |
|
| 246 | - | ||
| 247 | - | If you run this code you should see the result of the airdrop, a pause before merging the coins, and the successful coin merge! Now that we have all our cows in one heard, we can mint an NFT successfully. Let’s take a look! |
|
| 248 | - | ||
| 249 | - | ```javascript |
|
| 250 | - | // Call to Mint NFT |
|
| 251 | - | const mintTxn = await signer.executeMoveCall({ |
|
| 252 | - | packageObjectId: "0x2", |
|
| 253 | - | module: "devnet_nft", |
|
| 254 | - | function: "mint", |
|
| 255 | - | typeArguments: [], |
|
| 256 | - | arguments: [ |
|
| 257 | - | "gm", |
|
| 258 | - | "A nice gm brought to you by Pinata and Sui", |
|
| 259 | - | "ipfs://QmZhnkimthxvL32vin2mrQvnhN8ZbWFMvKMxRqHEq7dPz3", |
|
| 260 | - | ], |
|
| 261 | - | gasBudget: 10000, |
|
| 262 | - | }); |
|
| 263 | - | console.log("mint transaction:", mintTxn); |
|
| 264 | - | ``` |
|
| 265 | - | ||
| 266 | - | Our minting function is simply accessing a pre-built smart contract called ‘devnet_nft’ and we’re using the ‘mint’ function. All we have to pass into the arguments is the name of the NFT, the description, and then the asset link! |
|
| 267 | - | ||
| 268 | - | Before we mint, I’m gonna do one last console log that will let us take a look at the NFT in the browser. To do that , we need to access the results of the NFT mint and get the object id of the NFT, and then just pass that into a Sui explorer link like so. |
|
| 269 | - | ||
| 270 | - | ```javascript |
|
| 271 | - | // View NFT |
|
| 272 | - | const nftId = mintTxn.effects.effects.created[0].reference.objectId.toString(); |
|
| 273 | - | console.log(`View NFT: https://explorer.sui.io/object/${nftId}?network=devnet`); |
|
| 274 | - | ``` |
|
| 275 | - | ||
| 276 | - | Now let’s look at our full code to make sure everything is good, then run it! |
|
| 277 | - | ||
| 278 | - | ```javascript |
|
| 279 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 280 | - | ||
| 281 | - | // Generate a new Keypair |
|
| 282 | - | const keypair = new Ed25519Keypair(); |
|
| 283 | - | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString(); |
|
| 284 | - | console.log(address); |
|
| 285 | - | ||
| 286 | - | // Create Network Connection and receive airdrop |
|
| 287 | - | const provider = new JsonRpcProvider(Network.DEVNET); |
|
| 288 | - | ||
| 289 | - | // Get Sui from faucet |
|
| 290 | - | const fund = await provider.requestSuiFromFaucet(address); |
|
| 291 | - | console.log(fund); |
|
| 292 | - | ||
| 293 | - | // Pause function |
|
| 294 | - | const wait = async (time) => { |
|
| 295 | - | return new Promise((resolve, reject) => { |
|
| 296 | - | setTimeout(() => { |
|
| 297 | - | resolve(); |
|
| 298 | - | }, time); |
|
| 299 | - | }); |
|
| 300 | - | }; |
|
| 301 | - | ||
| 302 | - | await wait(3000); |
|
| 303 | - | ||
| 304 | - | // Merge two of the Sui coin objects |
|
| 305 | - | const coin1 = fund.transferred_gas_objects[1].id; |
|
| 306 | - | const coin2 = fund.transferred_gas_objects[2].id; |
|
| 307 | - | const signer = new RawSigner(keypair, provider); |
|
| 308 | - | const mergeTxn = await signer.mergeCoin({ |
|
| 309 | - | primaryCoin: coin1, |
|
| 310 | - | coinToMerge: coin2, |
|
| 311 | - | gasBudget: 1000, |
|
| 312 | - | }); |
|
| 313 | - | console.log("MergeCoin txn", mergeTxn); |
|
| 314 | - | ||
| 315 | - | // Call to Mint NFT |
|
| 316 | - | const mintTxn = await signer.executeMoveCall({ |
|
| 317 | - | packageObjectId: "0x2", |
|
| 318 | - | module: "devnet_nft", |
|
| 319 | - | function: "mint", |
|
| 320 | - | typeArguments: [], |
|
| 321 | - | arguments: [ |
|
| 322 | - | "gm", |
|
| 323 | - | "A nice gm brought to you by Pinata and Sui", |
|
| 324 | - | "ipfs://QmZhnkimthxvL32vin2mrQvnhN8ZbWFMvKMxRqHEq7dPz3", |
|
| 325 | - | ], |
|
| 326 | - | gasBudget: 10000, |
|
| 327 | - | }); |
|
| 328 | - | console.log("mint transaction:", mintTxn); |
|
| 329 | - | ||
| 330 | - | // View NFT |
|
| 331 | - | const nftId = mintTxn.effects.effects.created[0].reference.objectId.toString(); |
|
| 332 | - | console.log(`View NFT: https://explorer.sui.io/object/${nftId}?network=devnet`); |
|
| 333 | - | ``` |
|
| 334 | - | ||
| 335 | - | If all works as it should you’ll get a link and then you should see your final NFT! |
|
| 336 | - | ||
| 337 | - | > |
|
| 338 | - | ||
| 339 | - | ## You did it!! 🎉 |
|
| 340 | - | ||
| 341 | - | You successfully minted an NFT on Sui!! Of course this is only the beginning of what’s possible. Maybe a nice next step is transferring the NFT to another wallet, or maybe even trying to build a Sui marketplace 👀 |
|
| 342 | - | ||
| 343 | - | With whatever you’re trying to do, Pinata is here to help with tools like [our API](https://docs.pinata.cloud/pinata-api?utm_source=medium&utm_medium=referral), [Dedicated Gateways](https://www.pinata.cloud/dedicated-gateways?utm_source=medium&utm_medium=referral) to quickly stream images or video content for a marketplace, or token gated solutions using Submarine! |
|
| 344 | - | ||
| 345 | - | If you have any questions or want to learn more, feel free to join our [Discord](https://discord.gg/pianta) and say hi! :) |
| 1 | - | --- |
|
| 2 | - | title: "How to Offset Your NFT Project Carbon Emissions with Aerial" |
|
| 3 | - | publishDate: "22 Apr 2022" |
|
| 4 | - | description: "Learn how Aerial is helping make NFTs carbon neutral with their emissions API" |
|
| 5 | - | ogImage: "/blog-images/medium/v2/resize:fit:4800/format:webp/1*KxoVDEZFH3mJrlfeguMYjg.jpeg" |
|
| 6 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvut7d4f2v" |
|
| 7 | - | --- |
|
| 8 | - | ||
| 9 | - | import medium from "../../assets/medium.png"; |
|
| 10 | - | import OutLinkButton from "@/components/common/OutLinkButton"; |
|
| 11 | - | ||
| 12 | - | <OutLinkButton |
|
| 13 | - | link="https://medium.com/pinata/how-to-offset-your-nft-project-carbon-emissions-with-aerial-b5b4b95faba0" |
|
| 14 | - | site="Medium" |
|
| 15 | - | image={medium} |
|
| 16 | - | /> |
|
| 17 | - | ||
| 18 | - | As a believer in NFTs and Web3, I am always ecstatic to see what can be done with this new technology. I get bullish over new projects, experimental ideas, and cutting edge utility (especially when it’s something we make at Pinata like [Submarine.me](https://submarine.me)). However, I don’t look at this Metaverse with rose colored glasses. There are many imperfections in this space, and one of the worst ones is environmental impact. |
|
| 19 | - | ||
| 20 | - | While I love dooting around on my computer, I also love the outdoors. I love hiking, seeing mountains, hearing rivers, and smelling wildflowers. I love hearing birds in the morning and geese at night. I love to watch my son experience nature and get excited over a butterfly. So yeah, it kinda kills me to think of how this wonderful industry of crypto can harm this amazing planet. |
|
| 21 | - | ||
| 22 | - | > |
|
| 23 | - | ||
| 24 | - | The energy consumed by proof of work blockchains is staggering. It’s no secret, and if we want to enjoy this planet we need to accept it. I know I feel defensive when someone attacks something I enjoy, but it helps to take a deep breath and ask the big questions: “could I be wrong?” If crypto is putting the planet in danger, what do we do? |
|
| 25 | - | ||
| 26 | - | It might sound easy to just throw Web3 out the window and call it a day, but as with most technological advances, the cat is out of the bag. There’s no putting the lid back on Pandora’s box. In my opinion, there is no easy fix to this problem. There will be a need for multiple approaches to help offset our carbon footprint, including but not limited to better blockchain mechanisms and organizations that pursue renewable energy and conservation. |
|
| 27 | - | ||
| 28 | - | This is where [Aerial](https://aerial.is/) steps in. Aerial is a sustainability platform that helps users track their carbon footprint and make donations to help offset their carbon footprint. They are known for their beautiful [mobile application](https://aerial.is/download), but more recently they have developed an API that can track and estimate the carbon footprint of NFT projects and crypto! Per their website, “Emissions are calculated based on the energy consumption of the [Ethereum network](http://ethereumenergyconsumption.com/) and the average [gas used](https://etherscan.io/chart/gasused) per transaction.” Since [Pinata](https://pinata.cloud/) is the home of NFT media we want to always support initiatives like this to make NFTs more sustainable! |
|
| 29 | - | ||
| 30 | - | They have some awesome and simple ways you can integrate their widget to your website that looks something like this: |
|
| 31 | - | ||
| 32 | - | ```javascript |
|
| 33 | - | <iframe src="https://www.aerial.is/nft/embed?address=0x3e88721fa41d5e102d54b4a04e550222efdd234d"> |
|
| 34 | - | ``` |
|
| 35 | - | ||
| 36 | - | > |
|
| 37 | - | ||
| 38 | - | In this post we’ll take their API a step further and build our own custom widget. We can use it in our frontend website that displays more details about the project’s emissions with the goal of making visitors more aware! |
|
| 39 | - | ||
| 40 | - | If you want to follow along for this tutorial, you’ll need to have npm installed and be familiar with a frontend framework like React. First thing we’ll do is create a new React app and CD into it. |
|
| 41 | - | ||
| 42 | - | ```bash |
|
| 43 | - | npx create-react-app aerial-component |
|
| 44 | - | ||
| 45 | - | cd aerial-component && npm start |
|
| 46 | - | ``` |
|
| 47 | - | ||
| 48 | - | Then we’ll delete the CSS files and boilerplate so we are left with this in App.js |
|
| 49 | - | ||
| 50 | - | ```javascript |
|
| 51 | - | function App() { |
|
| 52 | - | return <div className="App"></div>; |
|
| 53 | - | } |
|
| 54 | - | ||
| 55 | - | export default App; |
|
| 56 | - | ``` |
|
| 57 | - | ||
| 58 | - | Now let’s make a new component. Create a new file in the src folder called Aerial.js and fill it with this starter code |
|
| 59 | - | ||
| 60 | - | ```javascript |
|
| 61 | - | const Aerial = () => { |
|
| 62 | - | return <h1>Aerial</h1>; |
|
| 63 | - | }; |
|
| 64 | - | ||
| 65 | - | export default Aerial; |
|
| 66 | - | ``` |
|
| 67 | - | ||
| 68 | - | Now let’s import the new component to our App.js |
|
| 69 | - | ||
| 70 | - | ```javascript |
|
| 71 | - | import Aerial from "./Aerial"; |
|
| 72 | - | ||
| 73 | - | function App() { |
|
| 74 | - | return ( |
|
| 75 | - | <div className="App"> |
|
| 76 | - | <Aerial /> |
|
| 77 | - | </div> |
|
| 78 | - | ); |
|
| 79 | - | } |
|
| 80 | - | ||
| 81 | - | export default App; |
|
| 82 | - | ``` |
|
| 83 | - | ||
| 84 | - | If you run npm start you should see your basic app with “Aerial” in the top left! |
|
| 85 | - | ||
| 86 | - | Now that we have our new component ready to go, we need to start making some API calls and fetching the data. If you need a reference for how to use Aerial’s API you can see their reference [here](https://www.notion.so/aerial/Aerial-API-343ebee875784ca18c244a5aae9fa7d3). |
|
| 87 | - | ||
| 88 | - | It’s really quite easy, all we have to do to fetch some basic data is use the following format |
|
| 89 | - | ||
| 90 | - | ``` |
|
| 91 | - | GET https://aerial.is/_nft/<address> |
|
| 92 | - | ``` |
|
| 93 | - | ||
| 94 | - | This will return data in the following format |
|
| 95 | - | ||
| 96 | - | ```json |
|
| 97 | - | { |
|
| 98 | - | "co2": <emissions in CO2>, |
|
| 99 | - | "gas": <gas used>, |
|
| 100 | - | "transactions": <number of transactions>, |
|
| 101 | - | "credits": <credits required to offset>, |
|
| 102 | - | "cost": <cost to offset in USD>, |
|
| 103 | - | "credits_purchased": <number of credits already purchased>, |
|
| 104 | - | "transactions_offset": <number of transactions already offset> |
|
| 105 | - | } |
|
| 106 | - | ``` |
|
| 107 | - | ||
| 108 | - | So let’s give it a shot! First thing we need to do is install Axios to help us with our API calls. Go back to the terminal and run |
|
| 109 | - | ||
| 110 | - | ```bash |
|
| 111 | - | npm install axios |
|
| 112 | - | ``` |
|
| 113 | - | ||
| 114 | - | Now make sure to import it into the top of our Aerial component like so |
|
| 115 | - | ||
| 116 | - | ```javascript |
|
| 117 | - | import axios from "axios"; |
|
| 118 | - | ``` |
|
| 119 | - | ||
| 120 | - | Now we’re going to make a quick function that will get the data. For our example we’re going to use the NFT contract address 0x2acab3dea77832c09420663b0e1cb386031ba17b. |
|
| 121 | - | ||
| 122 | - | ```javascript |
|
| 123 | - | const getEmissionsData = async () => { |
|
| 124 | - | try { |
|
| 125 | - | const response = await axios.get( |
|
| 126 | - | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 127 | - | ); |
|
| 128 | - | console.log(response.data); |
|
| 129 | - | } catch (error) { |
|
| 130 | - | console.log(error); |
|
| 131 | - | } |
|
| 132 | - | }; |
|
| 133 | - | ``` |
|
| 134 | - | ||
| 135 | - | To run this function we’ll use the useEffect hook to fetch the data as we load the app. To do that we simply need to import it at the top like so |
|
| 136 | - | ||
| 137 | - | ```javascript |
|
| 138 | - | import { useEffect } from "react"; |
|
| 139 | - | ``` |
|
| 140 | - | ||
| 141 | - | Then we need to run the function inside useEffect, |
|
| 142 | - | ||
| 143 | - | ```javascript |
|
| 144 | - | useEffect(() => { |
|
| 145 | - | getEmissionsData(); |
|
| 146 | - | }, []); |
|
| 147 | - | ``` |
|
| 148 | - | ||
| 149 | - | This is what our code will look like with everything in place: |
|
| 150 | - | ||
| 151 | - | ```javascript |
|
| 152 | - | import { useEffect } from "react"; |
|
| 153 | - | import axios from "axios"; |
|
| 154 | - | ||
| 155 | - | const Aerial = () => { |
|
| 156 | - | const getEmissionsData = async () => { |
|
| 157 | - | try { |
|
| 158 | - | const response = await axios.get( |
|
| 159 | - | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 160 | - | ); |
|
| 161 | - | console.log(response.data); |
|
| 162 | - | } catch (error) { |
|
| 163 | - | console.log(error); |
|
| 164 | - | } |
|
| 165 | - | }; |
|
| 166 | - | ||
| 167 | - | useEffect(() => { |
|
| 168 | - | getEmissionsData(); |
|
| 169 | - | }, []); |
|
| 170 | - | ||
| 171 | - | return ( |
|
| 172 | - | <div className="aerial-container"> |
|
| 173 | - | <h1>Aerial</h1> |
|
| 174 | - | </div> |
|
| 175 | - | ); |
|
| 176 | - | }; |
|
| 177 | - | ||
| 178 | - | export default Aerial; |
|
| 179 | - | ``` |
|
| 180 | - | ||
| 181 | - | If we run the app and check the dev console, we can see our data! |
|
| 182 | - | ||
| 183 | - | > |
|
| 184 | - | ||
| 185 | - | Now that we have the data, it’s as simple as displaying it so users on our website can see it! |
|
| 186 | - | ||
| 187 | - | To store the data we’ll import the useState hook at the top of our app along with useEffect |
|
| 188 | - | ||
| 189 | - | ```javascript |
|
| 190 | - | import { useEffect, useState } from "react"; |
|
| 191 | - | ``` |
|
| 192 | - | ||
| 193 | - | Then right above our function to grab the data, we’ll declare our state variable as an empty array where we can push stuff in later. |
|
| 194 | - | ||
| 195 | - | ```javascript |
|
| 196 | - | const [emissionsData, setEmissionsData] = useState([]); |
|
| 197 | - | ``` |
|
| 198 | - | ||
| 199 | - | Now all we have to do is edit our function just a little bit to push that data into our state using the “setEmissionsData”! Here is our function now |
|
| 200 | - | ||
| 201 | - | ```javascript |
|
| 202 | - | const getEmissionsData = async () => { |
|
| 203 | - | try { |
|
| 204 | - | const response = await axios.get( |
|
| 205 | - | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 206 | - | ); |
|
| 207 | - | console.log(response.data); |
|
| 208 | - | setEmissionsData(response.data); |
|
| 209 | - | } catch (error) { |
|
| 210 | - | console.log(error); |
|
| 211 | - | } |
|
| 212 | - | }; |
|
| 213 | - | ``` |
|
| 214 | - | ||
| 215 | - | The next thing we’re gonna do is create two components inside our file, one as a loading indicator, and the other as the data we want to display. To do this we’ll just make two more functions like this |
|
| 216 | - | ||
| 217 | - | ```javascript |
|
| 218 | - | const loading = () => ( |
|
| 219 | - | <div className="loading-container"> |
|
| 220 | - | <h1>Loading</h1> |
|
| 221 | - | </div> |
|
| 222 | - | ); |
|
| 223 | - | ||
| 224 | - | const emissionsComponent = () => { |
|
| 225 | - | return ( |
|
| 226 | - | <div className="data-container"> |
|
| 227 | - | <h1>Data goes here</h1> |
|
| 228 | - | </div> |
|
| 229 | - | ); |
|
| 230 | - | }; |
|
| 231 | - | ``` |
|
| 232 | - | ||
| 233 | - | To switch between the two we’ll create a new state called “isLoading” right underneath our previous state. We’ll set the default value to “false” for now |
|
| 234 | - | ||
| 235 | - | ```javascript |
|
| 236 | - | const [emissionsData, setEmissionsData] = useState([]); |
|
| 237 | - | const [isLoading, setIsLoading] = useState(false); |
|
| 238 | - | ``` |
|
| 239 | - | ||
| 240 | - | Back in our getEmissionsData function we need to turn the “loading” on when we start the request, and then off when we’re done. |
|
| 241 | - | ||
| 242 | - | ```javascript |
|
| 243 | - | const getEmissionsData = async () => { |
|
| 244 | - | try { |
|
| 245 | - | setIsLoading(true); |
|
| 246 | - | const response = await axios.get( |
|
| 247 | - | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 248 | - | ); |
|
| 249 | - | console.log(response.data); |
|
| 250 | - | setEmissionsData(response.data); |
|
| 251 | - | setIsLoading(false); |
|
| 252 | - | } catch (error) { |
|
| 253 | - | console.log(error); |
|
| 254 | - | } |
|
| 255 | - | }; |
|
| 256 | - | ``` |
|
| 257 | - | ||
| 258 | - | Finally, way down at the bottom where we render the whole app, we’ll add in some conditional rendering to say “display the loading component while loading, then display the data component when not loading.” |
|
| 259 | - | ||
| 260 | - | ```javascript |
|
| 261 | - | return <div className="aerial-container">{isLoading ? loading() : emissionsComponent()}</div>; |
|
| 262 | - | ``` |
|
| 263 | - | ||
| 264 | - | As a quick recap this is what our component looks like at the moment |
|
| 265 | - | ||
| 266 | - | ```javascript |
|
| 267 | - | import { useState, useEffect } from "react"; |
|
| 268 | - | import axios from "axios"; |
|
| 269 | - | ||
| 270 | - | const Aerial = () => { |
|
| 271 | - | const [emissionsData, setEmissionsData] = useState([]); |
|
| 272 | - | const [isLoading, setIsLoading] = useState(false); |
|
| 273 | - | ||
| 274 | - | const getEmissionsData = async () => { |
|
| 275 | - | try { |
|
| 276 | - | setIsLoading(true); |
|
| 277 | - | const response = await axios.get( |
|
| 278 | - | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 279 | - | ); |
|
| 280 | - | console.log(response.data); |
|
| 281 | - | setEmissionsData(response.data); |
|
| 282 | - | setIsLoading(false); |
|
| 283 | - | } catch (error) { |
|
| 284 | - | console.log(error); |
|
| 285 | - | } |
|
| 286 | - | }; |
|
| 287 | - | ||
| 288 | - | const loading = () => ( |
|
| 289 | - | <div className="loading-container"> |
|
| 290 | - | <h1>Loading</h1> |
|
| 291 | - | </div> |
|
| 292 | - | ); |
|
| 293 | - | ||
| 294 | - | const emissionsComponent = () => { |
|
| 295 | - | return ( |
|
| 296 | - | <div className="data-container"> |
|
| 297 | - | <h1>Data goes here</h1> |
|
| 298 | - | </div> |
|
| 299 | - | ); |
|
| 300 | - | }; |
|
| 301 | - | ||
| 302 | - | useEffect(() => { |
|
| 303 | - | getEmissionsData(); |
|
| 304 | - | }, []); |
|
| 305 | - | ||
| 306 | - | return <div className="aerial-container">{isLoading ? loading() : emissionsComponent()}</div>; |
|
| 307 | - | }; |
|
| 308 | - | ||
| 309 | - | export default Aerial; |
|
| 310 | - | ``` |
|
| 311 | - | ||
| 312 | - | Ok now the good part! Time to parse that data and display it. We’re going to go over a few of the pieces of data and how you may want to display them. |
|
| 313 | - | ||
| 314 | - | The first is the CO2 emissions, which the API returns as a raw number in the unit of Kg. The best way to make this number manageable in my opinion is to round up the number, and use some javascript to add the commas for each three digits. So back in our emissionsComponent, I have declared the following variable from our saved state. |
|
| 315 | - | ||
| 316 | - | ```javascript |
|
| 317 | - | const co2 = new Intl.NumberFormat().format(Math.round(emissionsData.co2)); |
|
| 318 | - | ``` |
|
| 319 | - | ||
| 320 | - | Next up is the gas used for this project, and for this one it returns a whole number so no need to round it up. We’ll just format it to be readable. |
|
| 321 | - | ||
| 322 | - | ```javascript |
|
| 323 | - | const gas = new Intl.NumberFormat().format(emissionsData.gas); |
|
| 324 | - | ``` |
|
| 325 | - | ||
| 326 | - | Aerial of course also provides their unit for donations to offset the carbon emissions they call “credits.” The API can return the total credits need to make the NFT project carbon neutral, how many are purchased so far, and how much it would cost in total to offset the project in USD. To make this data more readable, we want to display how many credits have been purchased, how many are needed to offset, and how much it would cost to completely offset the project. We just need a little math to make that happen! |
|
| 327 | - | ||
| 328 | - | For the credits remaining, we just need to subtract the credits already purchased from the total credits needed to offset like so. |
|
| 329 | - | ||
| 330 | - | ```javascript |
|
| 331 | - | const creditsRemaining = new Intl.NumberFormat().format( |
|
| 332 | - | emissionsData.credits - emissionsData.credits_purchased |
|
| 333 | - | ); |
|
| 334 | - | ``` |
|
| 335 | - | ||
| 336 | - | Of course we want to display how many have already purchased and that’s pretty simple. |
|
| 337 | - | ||
| 338 | - | ```javascript |
|
| 339 | - | const creditsPurchased = new Intl.NumberFormat().format(emissionsData.credits_purchased); |
|
| 340 | - | ``` |
|
| 341 | - | ||
| 342 | - | Now the more complicated part is calculating how much the remaining cost is. The API gives us the total cost, but it does not include how much has been spent. So for us to get this number we need to divide the emissions cost with the total credits needed to offset, then multiply that against the total emissions credits minus the credits already purchased. In the end it looks like this! |
|
| 343 | - | ||
| 344 | - | ```javascript |
|
| 345 | - | const cost = new Intl.NumberFormat().format( |
|
| 346 | - | (emissionsData.cost / emissionsData.credits) * |
|
| 347 | - | (emissionsData.credits - emissionsData.credits_purchased) |
|
| 348 | - | ); |
|
| 349 | - | ``` |
|
| 350 | - | ||
| 351 | - | Lastly, we want the number of total number of transactions already offset. |
|
| 352 | - | ||
| 353 | - | ``` |
|
| 354 | - | const transactions = new Intl.NumberFormat().format(emissionsData.transactions) |
|
| 355 | - | ``` |
|
| 356 | - | ||
| 357 | - | Alright that was a bit of work to get sorted, but now we can easily plug this into our emissionsComponent so people can see the data in a more easy to read format. We’re also going to display this in a nice grid with each data cell in a box, so our code inside emissions is going to look like this. |
|
| 358 | - | ||
| 359 | - | ```javascript |
|
| 360 | - | return ( |
|
| 361 | - | <div className="data-container"> |
|
| 362 | - | <div className="header"> |
|
| 363 | - | <h1>Deadfellaz Carbon Offset</h1> |
|
| 364 | - | </div> |
|
| 365 | - | <div className="data-grid"> |
|
| 366 | - | <div className="data-cell"> |
|
| 367 | - | <h2>{co2} Kg</h2> |
|
| 368 | - | <h3>CO2 Emissions</h3> |
|
| 369 | - | </div> |
|
| 370 | - | <div className="data-cell"> |
|
| 371 | - | <h2>{gas}</h2> |
|
| 372 | - | <h3>Gas Used</h3> |
|
| 373 | - | </div> |
|
| 374 | - | <div className="data-cell"> |
|
| 375 | - | <h2>{transactions}</h2> |
|
| 376 | - | <h3>Transactions</h3> |
|
| 377 | - | </div> |
|
| 378 | - | <div className="data-cell"> |
|
| 379 | - | <h2>${cost}</h2> |
|
| 380 | - | <h3>Cost to Offset</h3> |
|
| 381 | - | </div> |
|
| 382 | - | <div className="data-cell"> |
|
| 383 | - | <h2>{creditsRemaining}</h2> |
|
| 384 | - | <h3>Credits needed to offset</h3> |
|
| 385 | - | </div> |
|
| 386 | - | <div className="data-cell"> |
|
| 387 | - | <h2>{creditsPurchased}</h2> |
|
| 388 | - | <h3>Credits Purchased so Far</h3> |
|
| 389 | - | </div> |
|
| 390 | - | </div> |
|
| 391 | - | <a |
|
| 392 | - | className="cta-button" |
|
| 393 | - | target="_blank" |
|
| 394 | - | rel="noreferrer" |
|
| 395 | - | href="https://aerial.is/nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 396 | - | > |
|
| 397 | - | Offset |
|
| 398 | - | </a> |
|
| 399 | - | </div> |
|
| 400 | - | ); |
|
| 401 | - | ``` |
|
| 402 | - | ||
| 403 | - | At the bottom we’ve also added a button where a user can click on it and be directed to Aerials page for that particular NFT project, where they can make purchase credits to help offset that project. |
|
| 404 | - | ||
| 405 | - | To make this whole component feel clean as well as themed for the project, we took some colors and styled from DeadFellaz and added it to this page with some CSS; we also added a fun Lottie animation for the loading component instead of just a header that says loading. In the end our code looks like this! |
|
| 406 | - | ||
| 407 | - | ```javascript |
|
| 408 | - | import { useState, useEffect } from "react"; |
|
| 409 | - | import "./Aerial.css"; |
|
| 410 | - | import axios from "axios"; |
|
| 411 | - | import Lottie from "react-lottie"; |
|
| 412 | - | import co2 from "./co2.json"; |
|
| 413 | - | ||
| 414 | - | const Aerial = () => { |
|
| 415 | - | const [emissionsData, setEmissionsData] = useState([]); |
|
| 416 | - | const [isLoading, setIsLoading] = useState(false); |
|
| 417 | - | ||
| 418 | - | const getEmissionsData = async () => { |
|
| 419 | - | try { |
|
| 420 | - | setIsLoading(true); |
|
| 421 | - | const response = await axios.get( |
|
| 422 | - | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 423 | - | ); |
|
| 424 | - | console.log(response.data); |
|
| 425 | - | setEmissionsData(response.data); |
|
| 426 | - | setIsLoading(false); |
|
| 427 | - | } catch (error) { |
|
| 428 | - | console.log(error); |
|
| 429 | - | } |
|
| 430 | - | }; |
|
| 431 | - | ||
| 432 | - | const loading = () => ( |
|
| 433 | - | <div className="loading-container"> |
|
| 434 | - | <Lottie options={{ animationData: co2 }} height={400} width={400} /> |
|
| 435 | - | </div> |
|
| 436 | - | ); |
|
| 437 | - | ||
| 438 | - | const emissionsComponent = () => { |
|
| 439 | - | const co2 = new Intl.NumberFormat().format(Math.round(emissionsData.co2)); |
|
| 440 | - | const gas = new Intl.NumberFormat().format(emissionsData.gas); |
|
| 441 | - | const creditsRemaining = new Intl.NumberFormat().format( |
|
| 442 | - | emissionsData.credits - emissionsData.credits_purchased |
|
| 443 | - | ); |
|
| 444 | - | const creditsPurchased = new Intl.NumberFormat().format(emissionsData.credits_purchased); |
|
| 445 | - | const cost = new Intl.NumberFormat().format( |
|
| 446 | - | (emissionsData.cost / emissionsData.credits) * |
|
| 447 | - | (emissionsData.credits - emissionsData.credits_purchased) |
|
| 448 | - | ); |
|
| 449 | - | const transactions = new Intl.NumberFormat().format(emissionsData.transactions); |
|
| 450 | - | ||
| 451 | - | return ( |
|
| 452 | - | <div className="data-container"> |
|
| 453 | - | <div className="header"> |
|
| 454 | - | <h1>Deadfellaz Carbon Offset</h1> |
|
| 455 | - | </div> |
|
| 456 | - | <div className="data-grid"> |
|
| 457 | - | <div className="data-cell"> |
|
| 458 | - | <h2>{co2} Kg</h2> |
|
| 459 | - | <h3>CO2 Emissions</h3> |
|
| 460 | - | </div> |
|
| 461 | - | <div className="data-cell"> |
|
| 462 | - | <h2>{gas}</h2> |
|
| 463 | - | <h3>Gas Used</h3> |
|
| 464 | - | </div> |
|
| 465 | - | <div className="data-cell"> |
|
| 466 | - | <h2>{transactions}</h2> |
|
| 467 | - | <h3>Transactions</h3> |
|
| 468 | - | </div> |
|
| 469 | - | <div className="data-cell"> |
|
| 470 | - | <h2>${cost}</h2> |
|
| 471 | - | <h3>Cost to Offset</h3> |
|
| 472 | - | </div> |
|
| 473 | - | <div className="data-cell"> |
|
| 474 | - | <h2>{creditsRemaining}</h2> |
|
| 475 | - | <h3>Credits needed to offset</h3> |
|
| 476 | - | </div> |
|
| 477 | - | <div className="data-cell"> |
|
| 478 | - | <h2>{creditsPurchased}</h2> |
|
| 479 | - | <h3>Credits Purchased so Far</h3> |
|
| 480 | - | </div> |
|
| 481 | - | </div> |
|
| 482 | - | <a |
|
| 483 | - | className="cta-button" |
|
| 484 | - | target="_blank" |
|
| 485 | - | rel="noreferrer" |
|
| 486 | - | href="https://aerial.is/nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 487 | - | > |
|
| 488 | - | Offset |
|
| 489 | - | </a> |
|
| 490 | - | </div> |
|
| 491 | - | ); |
|
| 492 | - | }; |
|
| 493 | - | ||
| 494 | - | useEffect(() => { |
|
| 495 | - | getEmissionsData(); |
|
| 496 | - | }, []); |
|
| 497 | - | ||
| 498 | - | return <div className="aerial-container">{isLoading ? loading() : emissionsComponent()}</div>; |
|
| 499 | - | }; |
|
| 500 | - | ||
| 501 | - | export default Aerial; |
|
| 502 | - | ``` |
|
| 503 | - | ||
| 504 | - | Here is a quick preview of our final project |
|
| 505 | - | ||
| 506 | - |  |
|
| 507 | - | ||
| 508 | - | You can actually view the working component live [here](https://aerial-component.vercel.app/), and you can freely download the repo on [GitHub](https://github.com/stevedsimkins/aerial-component)! |
|
| 509 | - | ||
| 510 | - | If you wanted to take this step further you can request an API key from Aerial to use their offsetting endpoints. These let you not only fetch the data, but also provide a way to make a payment for the credits with Stripe or even Coinbase if you have merchant accounts with both of them! In the end it will always be a little easier to redirect users to Aerial.is to make those credit purchases, but I still love that Aerial has made this an option. |
|
| 511 | - | ||
| 512 | - | Aerial’s API is a great way to display emissions for Ethereum projects and help offset them, however if you are still working on your NFT project there are some simple steps you can take to help offset emissions before you launch! |
|
| 513 | - | ||
| 514 | - | One of the best things you can do is select a proof of stake blockchain like Solana, Tezos, Flow, or Polygon to create your NFTs on. You can read more on the difference between proof of work and proof of stake blockchains [here](https://www.coinbase.com/learn/crypto-basics/what-is-proof-of-work-or-proof-of-stake), but essentially the amount of energy is significantly lower on proof of stake chains because of how they verify transactions. |
|
| 515 | - | ||
| 516 | - | If you do choose Ethereum there are a lot more gas friendly smart contracts being developed such as [ERC-721A by Azuki](https://www.erc721a.org/) or [ERC-1155D](https://medium.com/donkeverse/introducing-erc1155d-the-most-efficient-non-fungible-token-contract-in-existence-c1d0a62e30f1). You can also consider changing how many NFTs you mint for a project. 5,000 to 10,000 is a really popular number, but as Solana has proven you don’t need that many to have a successful project. If you’re doing a much smaller 1/1 project you can even consider using [lazy minting](https://medium.com/rarible-dao/nft-minting-vs-lazy-minting-mining-explained-4330dd57a4c4). However, if your Ethereum contract ends up putting out emissions, you can offset them from the start with [Aerial](https://aerial.is/nft)! |
|
| 517 | - | ||
| 518 | - | In conclusion, as NFTs progress and blockchains progress, I believe we will find better and more eco-friendly solutions for energy usage. One of the best thing we can do now is to raise awareness, and it starts with each one of us! Using initiatives like Aerial to bring the issue front and center to your NFT holds is an easy way to not only help educate the public, but also provide an avenue to help offset those emissions. At [Pinata](https://pinata.cloud/) we would love to see this more and more as we assist creators with their NFT media. |
|
| 519 | - | ||
| 520 | - | It’s easy to assume that NFTs and Web3 aren’t going away anytime soon, but I’m not sure if we can say the same thing about our planet unless we take care of it. |
| 1 | - | --- |
|
| 2 | - | title: "Resizing IPFS Images with Pinata’s Image Optimization Tools" |
|
| 3 | - | publishDate: "23 June 2022" |
|
| 4 | - | description: "Learn how to use Pinata's Dedicated Gateway image optimization tools" |
|
| 5 | - | tags: ["ipfs", "tutorials"] |
|
| 6 | - | ogImage: "/blog-images/medium/v2/resize:fit:4800/format:webp/1*Tp42Ey9Uvdb6njsaXHBOTA.jpeg" |
|
| 7 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvut3ovn2v" |
|
| 8 | - | --- |
|
| 9 | - | ||
| 10 | - | import medium from "../../assets/medium.png"; |
|
| 11 | - | import OutLinkButton from "@/components/common/OutLinkButton"; |
|
| 12 | - | ||
| 13 | - | <OutLinkButton |
|
| 14 | - | link="https://medium.com/pinata/resizing-ipfs-images-with-pinatas-image-optimization-tools-fb381bee58aa" |
|
| 15 | - | site="Medium" |
|
| 16 | - | image={medium} |
|
| 17 | - | /> |
|
| 18 | - | ||
| 19 | - | If you’re a developer in the NFT space, you have probably had to fetch IPFS content before, and depending what tools you use the experience is varied. Using a local IPFS node is not very practical or fast, and using a public gateway can be risky due to congestion. Dedicated Gateways on the other hand are much faster, and are great for app development. But what if you have to fetch an entire NFT project through IPFS? That could be 10,000 images at 5Mb each, awful for web page optimization, and you have to load every. single. one. How is that gonna work? And then what happens when you have another NFT project? |
|
| 20 | - | ||
| 21 | - | Pinata’s [Dedicated Gateways](https://docs.pinata.cloud/gateways/dedicated-gateways) already have a blazing fast global CDN (content delivery network) that caches content on the first load, so already you’re off to a good start, but that’s still a lot of data to pull through a webpage. With the rollout of our new pricing plan and features, we’ve released a new tool for everyone: **Pinata Image Optimization.** |
|
| 22 | - | ||
| 23 | - | Built on top of Pinata’s Dedicated Gateways, Pinata Image Optimization allows you to resize images on the fly with URL queries, all through your own gateway. Seriously. For faster web pages and reduced bandwidth, all you have to do is add a few text characters to the end of your image link. Let’s go through how it works with an example to show how you can use it to speed up your projects. |
|
| 24 | - | ||
| 25 | - | ## How To Use Pinata Image Optimization |
|
| 26 | - | ||
| 27 | - | Image Optimization works by adding queries to the end of our Dedicated Gateway urls. Here’s an example of a basic dedicated gateway url: |
|
| 28 | - | ||
| 29 | - | ``` |
|
| 30 | - | https://stevedsimkins.mypinata.cloud/ipfs/{CID} |
|
| 31 | - | ``` |
|
| 32 | - | ||
| 33 | - | The CID is the content identifier and [how we stream IPFS content](https://docs.pinata.cloud/gateways/retrieving-content). Just for our example, I have a CID of an image I shot out west and uploaded to Pinata! |
|
| 34 | - | ||
| 35 | - | ``` |
|
| 36 | - | QmfKsRfqkWYuShSMDghMpLt8SQnWyPhDaEe8JUauM8E7Uz |
|
| 37 | - | ``` |
|
| 38 | - | ||
| 39 | - | Now if we add this file to our gateway, we get this: |
|
| 40 | - | ||
| 41 | - | ``` |
|
| 42 | - | https://stevedsimkins.mypinata.cloud/ipfs/QmfKsRfqkWYuShSMDghMpLt8SQnWyPhDaEe8JUauM8E7Uz/ |
|
| 43 | - | ``` |
|
| 44 | - | ||
| 45 | - | If you click that link you’ll see our image. If that link took a few seconds to load, there’s a reason for that: it’s huge. Shot on an old medium format film camera and scanned at high resolution, this picture of the Badlands National Park is 10,000 x 10,000 pixels, coming in at around 38Mb. For context, your standard web images are under 300K. |
|
| 46 | - | ||
| 47 | - | While the dedicated gateway uses a global CDN to cache the content on the first load, it’s still a lot to load. Most websites are gonna struggle using this image, but if you’re building something like Foundation or SuperRare, or any project that relies on a lot of images, you might not want to give up that full size image. And you certainly don’t want to take the time resizing them one by one. |
|
| 48 | - | ||
| 49 | - | And that’s where our tool comes in. Let’s see Pinata Image Optimization in action. |
|
| 50 | - | ||
| 51 | - | To add on the image resizing query to the url, we simply need to add a “?” with the query itself. Here’s an example of changing the width of an image to 1080 pixels: |
|
| 52 | - | ||
| 53 | - | ``` |
|
| 54 | - | ?img-width=1080 |
|
| 55 | - | ``` |
|
| 56 | - | ||
| 57 | - | If we want to change the height too, we just need to use an ampersand (&) to add on another query: |
|
| 58 | - | ||
| 59 | - | ``` |
|
| 60 | - | ?img-width=1080&img-height=1080 |
|
| 61 | - | ``` |
|
| 62 | - | ||
| 63 | - | Now all we need to do is add this little snippet to our previous image url: |
|
| 64 | - | ||
| 65 | - | ``` |
|
| 66 | - | https://stevedsimkins.mypinata.cloud/ipfs/QmfKsRfqkWYuShSMDghMpLt8SQnWyPhDaEe8JUauM8E7Uz?img-width=1080&img-width=1080 |
|
| 67 | - | ``` |
|
| 68 | - | ||
| 69 | - | Check that out. Instead of being a 10,000x10,000 resolution, it’s now 1080x1080 and only 266Kb. Doesn’t get any simpler than that — and this is only the beginning of what this tool can do. Here’s a small list of other things you can do: |
|
| 70 | - | ||
| 71 | - | - DPR (Device Pixel Ratio) |
|
| 72 | - | - Image Fit — for scaling down, image positions and more! |
|
| 73 | - | - Image Quality — Set a scale from 1–100 to easily reduce a high quality image |
|
| 74 | - | - Auto Image Formatting — Use Webp where supported, but then fall back to jpeg or png |
|
| 75 | - | - Animation Still — Turn a gif into a still image |
|
| 76 | - | - On Error Redirect — Redirect to a different image if there is a problem |
|
| 77 | - | - Metadata Controls — Control what EXIF data is revealed with the image |
|
| 78 | - | ||
| 79 | - | Pinata Image Resizing really gives you control as a developer in the NFT space to handle IPFS images with ease. We highly recommend checking out our [developer docs](https://docs.pinata.cloud) to see them all. |
|
| 80 | - | ||
| 81 | - | To really understand what’s possible with this tool, let’s look at a real world example. |
|
| 82 | - | ||
| 83 | - | ## Displaying a 10K PFP Project |
|
| 84 | - | ||
| 85 | - | When it comes to NFT projects, right now the classic 10,000 PFP project is the industry standard — it’s what everyone is trying to create. But if you’re an NFT marketplace trying to get up and running, you’re almost certainly going to run into some problems if you have to load an entire 10K project on a single webpage. The speed is going to suffer greatly, especially if you are pulling full sized large images from IPFS. |
|
| 86 | - | ||
| 87 | - | In our example, we’ll make a simple app that displays more images I took out West in the Badlands. First thing we need to do is spin up a React app by running the following in our terminal: |
|
| 88 | - | ||
| 89 | - | ```bash |
|
| 90 | - | npx create-react-app ipfs-image-optimization |
|
| 91 | - | ||
| 92 | - | cd ipfs-image-optimization && npm start |
|
| 93 | - | ``` |
|
| 94 | - | ||
| 95 | - | Then we’ll delete the CSS files and boilerplate in App.js. We’ll also add in some structure for our image grid. |
|
| 96 | - | ||
| 97 | - | ```javascript |
|
| 98 | - | function App() { |
|
| 99 | - | return ( |
|
| 100 | - | <div className="App"> |
|
| 101 | - | <div className="grid"></div> |
|
| 102 | - | </div> |
|
| 103 | - | ); |
|
| 104 | - | } |
|
| 105 | - | export default App; |
|
| 106 | - | ``` |
|
| 107 | - | ||
| 108 | - | Usually a PFP project will be in a single folder, and each file inside will be named in sequential order such as “1.png, 2.png, 3.png,” etc. This makes our base URL very simple. The only thing that will be changing will be the ID of the image, so it looks like this: |
|
| 109 | - | ||
| 110 | - | ``` |
|
| 111 | - | https://stevedsimkins.mypinata.cloud/ipfs/QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ/${id}.jpg?img-width=1080&img-height=1080 |
|
| 112 | - | ``` |
|
| 113 | - | ||
| 114 | - | Let’s break this down again so we know what’s going on. |
|
| 115 | - | ||
| 116 | - | We have our gateway url `https://stevedsimkins.mypinata.cloud/ipfs/`, then we have our CID `QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ`, then our dynamic image id `${id}.jpg` and finally our image optimization `?img-width=1080&img-height=1080`. Of course not all PFP projects are this simple, but with this formatting you can pass in multiple parameters with objects to adjust to your needs. |
|
| 117 | - | ||
| 118 | - | Our image folder only has 8 images, therefore we just need a simple for loop to generate an array that will hold the numbers 1 through 8. That way we can access it later to generate our image components. Just start with an empty array, then push the numbers into it with the for loop. |
|
| 119 | - | ||
| 120 | - | ```javascript |
|
| 121 | - | let imageIds = []; |
|
| 122 | - | for (let id = 1; id <= 8; id++) { |
|
| 123 | - | imageIds.push(id); |
|
| 124 | - | } |
|
| 125 | - | ``` |
|
| 126 | - | ||
| 127 | - | Now the fun part: generating the images! We’ll take our imageId array and map over it. |
|
| 128 | - | ||
| 129 | - | ```javascript |
|
| 130 | - | { |
|
| 131 | - | imageIds.map((id) => {}); |
|
| 132 | - | } |
|
| 133 | - | ``` |
|
| 134 | - | ||
| 135 | - | Then we’ll declare our base URL with our dynamic image ID, as well as a name for the alt text later. |
|
| 136 | - | ||
| 137 | - | ```javascript |
|
| 138 | - | { |
|
| 139 | - | imageIds.map((id) => { |
|
| 140 | - | let url = `https://stevedsimkins.mypinata.cloud/ipfs/QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ/${id}.jpg?img-width=1080&img-height=1080`; |
|
| 141 | - | let name = `nft ${id}`; |
|
| 142 | - | }); |
|
| 143 | - | } |
|
| 144 | - | ``` |
|
| 145 | - | ||
| 146 | - | Finally, we just need to create a component to hold the image, using the url as the image src and the name as the image alt! |
|
| 147 | - | ||
| 148 | - | ```javascript |
|
| 149 | - | { |
|
| 150 | - | imageIds.map((id) => { |
|
| 151 | - | let url = `https://stevedsimkins.mypinata.cloud/ipfs/QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ/${id}.jpg?img-width=1080&img-height=1080`; |
|
| 152 | - | let name = `nft ${id}`; |
|
| 153 | - | return ( |
|
| 154 | - | <div className="image-container"> |
|
| 155 | - | <img src={url} alt={name} /> |
|
| 156 | - | </div> |
|
| 157 | - | ); |
|
| 158 | - | }); |
|
| 159 | - | } |
|
| 160 | - | ``` |
|
| 161 | - | ||
| 162 | - | That leaves us with the final code for App.js. Add in a little CSS and we end up with a nice little image grid that loads FAST! |
|
| 163 | - | ||
| 164 | - | ```javascript |
|
| 165 | - | import "./App.css"; |
|
| 166 | - | ||
| 167 | - | function App() { |
|
| 168 | - | let imageIds = []; |
|
| 169 | - | ||
| 170 | - | for (let id = 1; id <= 8; id++) { |
|
| 171 | - | imageIds.push(id); |
|
| 172 | - | } |
|
| 173 | - | ||
| 174 | - | return ( |
|
| 175 | - | <div className="App"> |
|
| 176 | - | <div className="grid"> |
|
| 177 | - | {imageIds.map((id) => { |
|
| 178 | - | let url = `https://stevedsimkins.mypinata.cloud/ipfs/QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ/${id}.jpg?img-width=1080&img-height=1080`; |
|
| 179 | - | let name = `nft ${id}`; |
|
| 180 | - | ||
| 181 | - | return ( |
|
| 182 | - | <div className="image-container"> |
|
| 183 | - | <img src={url} alt={name} /> |
|
| 184 | - | </div> |
|
| 185 | - | ); |
|
| 186 | - | })} |
|
| 187 | - | </div> |
|
| 188 | - | </div> |
|
| 189 | - | ); |
|
| 190 | - | } |
|
| 191 | - | ||
| 192 | - | export default App; |
|
| 193 | - | ``` |
|
| 194 | - | ||
| 195 | - |  |
|
| 196 | - | ||
| 197 | - | Keep in mind, each one of these images is 10,000 x 10,000 resolution with over 35MB per file, and thanks to our dedicated gateway we loaded them like it was nothing. All of them dynamically resized to 1080 x 1080, still a decent size and high enough quality for most projects. |
|
| 198 | - | ||
| 199 | - | Now you’ve got an idea how IPFS Image Optimization with Pinata’s Dedicated Gateways can help streamline your NFT development, especially for marketplaces and other platforms that need to stream lots of IPFS content. Here are a few more articles that might be helpful as you’re building: |
|
| 200 | - | ||
| 201 | - | [How To Create an NFT Marketplace on Flow With IPFS](https://medium.com/pinata/how-to-create-an-nft-marketplace-on-flow-with-ipfs-a162a1aeb426) |
|
| 202 | - | ||
| 203 | - | [How To Prevent NFT Trait Sniping In Your PFP Project](https://medium.com/pinata/how-to-prevent-nft-trait-sniping-in-your-pfp-project-506f17ff07d6) |
|
| 204 | - | ||
| 205 | - | [How to Offset Your NFT Project Carbon Emissions with Aerial](https://medium.com/pinata/how-to-offset-your-nft-project-carbon-emissions-with-aerial-b5b4b95faba0) |
|
| 206 | - | ||
| 207 | - | And if you haven’t heard, we’ve just released a ton of new features like token gating content and 4k video streaming, putting even more power into the hands of creators. Be sure to join our [Discord community](https://discord.gg/pinata) to connect with other creators and see all the amazing projects being built with Pinata. Happy pinning! |
| 1 | - | --- |
|
| 2 | - | title: "Making ZK Accessible on Solana" |
|
| 3 | - | publishDate: "30 Oct 2024" |
|
| 4 | - | description: "Succinct is excited to announce the release of SP1-Solana, allowing any Solana developer to generate and verify proofs onchain." |
|
| 5 | - | tags: [] |
|
| 6 | - | hidden: true |
|
| 7 | - | --- |
|
| 8 | - | ||
| 9 | - | It's no surprise that that zero knowledge proofs are taking the crypto space by storm, and Solana is no exception. As laid out by [Helius](https://www.helius.dev/blog/zero-knowledge-proofs-its-applications-on-solana) there is a great need for zk technology, as zk compression on Solana could drastically improve transaction costs and speeds. Beyond the infrastructure layer, zk proofs can enable Solana developers to build the next wave of trust and privacy powered applications. Solana also has some zk native attributes, making it ideal to use for verifying proofs onchain. The only problem is that writing zk proofs isn't easy or simple due to it's ["moon math"] (link to succinct post or tweet on moon math). Even further, it can be just as difficult to verify the proof onchain once you have one. Naturally, Succinct and SP1 have solved this for the everyday Solana developer. |
|
| 10 | - | ||
| 11 | - | ## SP1-Solana |
|
| 12 | - | ||
| 13 | - | Today Succinct is thrilled to announce the release of [SP1-Solana] (link to docs), a new crate extension to the existing SP1 ecosystem. With this new library any Solana developer can quickly and easily generate Groth16 proofs in Rust, and then verify the proof onchain using SP1-Solana. You can deploy the verifying program yourself or already using Succicnt's pre-deployed programs. |
|
| 14 | - | ||
| 15 | - |  |
|
| 16 | - | ||
| 17 | - | No need to mess with circuits or writing low-level assembly to make a proof; you can just write normal Rust as you would in Solana and avoid context switching. The library takes advantage of Solana's BN254's precompiles, giving it exceptional efficiency and speed. Integrating it to existing Solana programs takes no time at all, giving developers the best experience possible. |
|
| 18 | - | ||
| 19 | - | We're excited to see what Solana developers with build with this new library, whether it's transaction privacy, DeFi protocols, or even identity solutions. Get started today! |
|
| 20 | - | ||
| 21 | - | [Start Using SP1 Solana] (link to docs) |
| 1 | - | --- |
|
| 2 | - | title: "The Power of Dedicated Gateways" |
|
| 3 | - | publishDate: "10 Feb 2022" |
|
| 4 | - | description: "Dedicated Gateways. What they are, why they're essential, and how they can revolutionize a creator's next project." |
|
| 5 | - | ogImage: "/blog-images/other/6410b46677b05b001afa5ff4_2022-02-10_The-Power-of_blog-img-tiny.png" |
|
| 6 | - | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvutcycf2v" |
|
| 7 | - | --- |
|
| 8 | - | ||
| 9 | - | import pinnie from "../../assets/pinnie.png"; |
|
| 10 | - | import OutLinkButton from "@/components/common/OutLinkButton"; |
|
| 11 | - | ||
| 12 | - | <OutLinkButton |
|
| 13 | - | link="https://www.pinata.cloud/blog/the-power-of-dedicated-gateways" |
|
| 14 | - | site="Pinata" |
|
| 15 | - | image={pinnie} |
|
| 16 | - | /> |
|
| 17 | - | ||
| 18 | - | ## What are IPFS Gateways? |
|
| 19 | - | ||
| 20 | - | f you're in the business of creating NFTs then you are probably familiar with the InterPlanetary File System also known as IPFS. It's an incredibly powerful protocol that allows creators to host content too large for blockchains on a decentralized peer-to-peer network, leveraging cryptography to ensure content is immutable. This is ideal for NFT projects that want to decentralize their NFT media and make sure it does not change over time. IPFS also allows creators to take ownership of their content and how they share it. What most people don't understand is how IPFS and HTTP communicate to each other. |
|
| 21 | - | ||
| 22 | - | For instance, I have a cool html page that uses 3D libraries and creates a spinning cube. I pinned it to IPFS using Pinata which gave me a content identifier: `QmTz8mgtvkf8fG8es5i6vr4LX7dd9vnk1XVtB6ScVuCepr`. A content identifier, or CID for short, is how we can reference content on IPFS. The direct link to that file via IPFS is |
|
| 23 | - | ||
| 24 | - | ``` |
|
| 25 | - | ipfs://QmTz8mgtvkf8fG8es5i6vr4LX7dd9vnk1XVtB6ScVuCepr |
|
| 26 | - | ``` |
|
| 27 | - | ||
| 28 | - | If you paste that link into your browser, chances are you are not going to pull anything up or you'll get a random google search. However, if you have an IPFS node running or you’re using a browser with built-in native IPFS support, then you would actually see something. Why is this? |
|
| 29 | - | ||
| 30 | - | n order to see content on the IPFS protocol, you have to participate. By running a local IPFS node you can be part of the network, receiving and sending blocks of data that are hosted on the network. In turn you can view and pin your own files, too. |
|
| 31 | - | ||
| 32 | - | This is different from HTTP, which is what most of the internet is built on. An HTTP request is when your computer asks for “https://google.com” and then a server returns the HTML file with the info. This is cool and all, but how is IPFS useful for NFTs since all of the marketplaces and wallets use HTTP? |
|
| 33 | - | ||
| 34 | - | The answer is gateways. |
|
| 35 | - | ||
| 36 | - | Gateways are exactly what they sound like; a gateway from the IPFS protocol to the HTTP protocol. They allow us to serve content from IPFS into regular https websites that we use everyday. From our example earlier, try pasting this link into your browser: |
|
| 37 | - | ||
| 38 | - | ``` |
|
| 39 | - | https://pinnieblog.mypinata.cloud/ipfs/QmTz8mgtvkf8fG8es5i6vr4LX7dd9vnk1XVtB6ScVuCepr/ |
|
| 40 | - | ``` |
|
| 41 | - | ||
| 42 | - | You got the cube right? I took the CID that was already hosted on IPFS and then fed it through a Dedicated Gateway to see the file! |
|
| 43 | - | ||
| 44 | - | Notice that I said "Dedicated Gateway." There are two types of gateways, private (dedicated) and public. Public gateways are convenient since they are available to everyone, and even built into Pinata's file manager so you can see your content without an IPFS node. However, it's important to note that IPFS is a public network, which means that it can be viewed or used by anyone. This can become a problem because gateways are still managed on traditional servers. Too much traffic without the right infrastructure could cause a failure, so most public gateways will not serve too much content at once by utilizing rate limits. Public gateways are also not very fast and can take a while to load content. These public gateways can be a good service for testing IPFS content, but not ideal for serving large amounts of data. |
|
| 45 | - | ||
| 46 | - | This is where Dedicated Gateways step in! At Pinata, we want to provide blazing fast delivery of your content, and so our engineering team has developed an infrastructure which allows us to do exactly that. |
|
| 47 | - | ||
| 48 | - | ## Dedicated Gateways |
|
| 49 | - | ||
| 50 | - | Since Pinata released this feature, it's been amazing to see what people have done with it. One of the main applications we’ve seen of dedicated gateways is NFT marketplaces. Dedicated Gateways can be set as open or restricted. This means you can control whether just your pinned content on your Pinata account are accessible through the gateway, or if any content on the entire IPFS network is accessible through the gateway. NFT marketplaces have the challenge of fetching all the metadata associated with the NFT and then fetching the NFT’s media itself. Once you get the metadata, most of the media are IPFS CIDs, and as mentioned before, you need a gateway to display those over HTTP. Dedicated Gateways make that so easy and make new marketplaces possible. |
|
| 51 | - | ||
| 52 | - | NFT marketplaces are just the beginning, just as NFTs can be the beginning of a much larger project. Bored Ape Yacht Club, a Pinata customer, is a great example of this. As many people know, they have taken an NFT project and turned it into a multi-million dollar universe. Projects can take their user base into a world of gaming, staking, earning tokens, and even virtual reality. This allows an NFT project to transform into a brand identity. Brands wanting to utilize NFTs for their metaverses will leverage IPFS, and of course Dedicated Gateways allow these brands to deliver IPFS content quickly and reliably to their platforms. |
|
| 53 | - | ||
| 54 | - | Dedicated Gateways will be important to the future of blockchain gaming, as well. There are already numerous NFT games that feature on-chain items or content, such as Axie Infinity. These are real NFTs with data that needs to be preserved through IPFS and served through a Dedicated Gateway. |
|
| 55 | - | ||
| 56 | - | At Pinata we can envision a world beyond these metaverses. We would love to see IPFS and Dedicated Gateways as a way for any type of creator to make a valuable experience. Lots of older social media platforms are still valid as a way to be discovered, but the newer web3 technologies will help creators take their content from being discovered to being truly valued. With a Dedicated Gateway, they can control their own content and display it on their own platforms. Just think about most streaming platforms and how much money they give to creators; it's little to nothing. However, with the exposure from those platforms, a creator can build a fan base. Then with Pinata, creators can build the platform where their content is viewed by their fans and directly paid for there. Now that’s true value and creative control. |
|
| 57 | - | ||
| 58 | - | The world of crypto, NFTs, and IPFS is still developing at a rapid rate. It's exciting to see how we are just scratching at the surface of what web3 will bring us, and it's important to visualize what this will look like for everyday people. |
|
| 59 | - | ||
| 60 | - | Dedicated gateways are exclusively available on Pinata for Picnic plans and up. Get your own dedicated gateway set up today! |
| 3 | 3 | publishDate: "30 Mar 2025" |
|
| 4 | 4 | description: "A perspective on the rise of AI coding and how it relates to technological shifts throughout history" |
|
| 5 | 5 | tags: ["programming", "ai"] |
|
| 6 | - | ogImage: "/blog-images/files-stevedylan-dev/bafkreibzmkrvumddklcixiwq64qru7cv24na4uy2mvruk235lyrbkqjnbi.jpg" |
|
| 6 | + | ogImage: "/blog-images/files-stevedylan-dev/vibe-coding-kodak.png" |
|
| 7 | 7 | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvur3ysv2v" |
|
| 8 | 8 | --- |
|
| 9 | 9 | ||
| 10 | - |  |
|
| 10 | + |  |
|
| 11 | 11 | ||
| 12 | 12 | I'm sure many who read this are familiar by now with the term "vibe coding," a euphoric style of programming where you prompt AI models or IDEs to write software and just "vibe." One of the more popular instances that made the practice takeoff was the indie hacker Levelsio building a flight simulator entirely in JavaScript and selling ad space within the game. Others have followed suit and even started businesses by vibe coding them into existence. It's hard to deny the reality that AI has been changing much of the software ecosystem, and with any major shift in technology there are lots of opinions. I certainly have my own, but I thought it would be more productive to look at history repeat itself. |
|
| 13 | 13 | ||
| 15 | 15 | ||
| 16 | 16 | Most people don't know it, but there was a major controversy in the world of photography in the year 1900. Before that time photography was an art form protected by its sages, who poured their money, time, and practice into it. Not anyone could just take a picture, only those who had worked in the craft and mastered it. All of that changed in February of 1900 when Kodak released the Brownie camera. It wasn't much to look at, a little cardboard box that could take a picture no bigger than 2.25 inches using 117mm film. What made it special was the service behind it. The Brownie only cost $1 (which would be $38 at the time of this post), and it included the cost to develop the film. All someone had to do was take a picture, send it off to Kodak, and they would return the picture. "You press the button—we do the rest." |
|
| 17 | 17 | ||
| 18 | - |  |
|
| 18 | + |  |
|
| 19 | 19 | ||
| 20 | 20 | Suddenly anyone could take photos: parents, grandparents, kids, truly anyone. It sold like crazy, and it upset the old photographer guild. There was great concern that there would now be a huge amount of "slop" photography and the art of photography would be washed away. Surely no one who took photos in such a way could be an artist... right? These artists were also concerned for their profession. If everyone had a camera, why would families pay for a photography session? |
|
| 21 | 21 | ||
| 33 | 33 | ||
| 34 | 34 | The evolution of photography technology enabled plenty of bad photos and photographers, but it also created a whole new series of artists we would not have otherwise. One of my favorite examples of this is Vivian Maier. If you're not familiar, Vivian Maier was an unknown nanny in the 1940's and 50's. It wasn't until after her death that her life's work as a photographer was discovered by a man who won it at an auction. To his surprise it was a stunning collection, hundreds of thousands of them, all taken by a nanny no one had ever heard of. She was passionate about photography, and her perspectives of the world at that time were unique. |
|
| 35 | 35 | ||
| 36 | - |  |
|
| 36 | + |  |
|
| 37 | 37 | ||
| 38 | 38 | They're made possible thanks to the much later successor of a Brownie style camera that she could take everywhere and shoot roll after roll of film. If photography was still stuck in the dark ages of carrying around big pieces of equipment that only certain people could afford, we wouldn't have the stunning work of Vivian Maier. |
|
| 39 | 39 | ||
| 40 | - |  |
|
| 40 | + |  |
|
| 41 | 41 | ||
| 42 | 42 | Another unrecognized photographer is one of my favorites, Joe Greer. Greer started his career on an app: Instagram. He didn't take photography lessons, he didn't have a nice camera, he just had his phone. The more and more he shot with his phone and posted his photos on Instagram, the more people liked them and the bigger it got. Eventually he did switch to professional cameras and continued his craft, but the key was his access to an art form that otherwise wouldn't be available apart from cell phone cameras. |
|
| 43 | 43 | ||
| 44 | - |  |
|
| 44 | + |  |
|
| 45 | 45 | ||
| 46 | 46 | In the realm of programming, even in the evolution of languages, we see a similar pattern where mediocrity increases but so does the number of discovered programmers. Sure there's a lot of Javascript slop out there, but thanks to Javascript there have been more and more people discovering programming and starting a wonderful journey. You don't have to start in a lower language to find the joy of programming, and most people who do find it will experiment in many different languages. |
|
| 47 | 47 | ||
| 3 | 3 | publishDate: "05 Jan 2024" |
|
| 4 | 4 | description: "A brief look at my history and how ordinary jobs lead to learning programming and Vim/Neovim" |
|
| 5 | 5 | tags: ["programming", "neovim"] |
|
| 6 | - | ogImage: "/blog-images/cloudinary/v1704512309/Screenshot-Alacritty-01-05-2024-22-16_2x_hdiy9a.png" |
|
| 6 | + | ogImage: "/blog-images/files-stevedylan-dev/alacritty.png" |
|
| 7 | 7 | atUri: "at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.document/3mdzvurxbuf2v" |
|
| 8 | 8 | --- |
|
| 9 | 9 | ||
| 10 | - | > |
|
| 10 | + |  |
|
| 11 | 11 | ||
| 12 | 12 | Something I see pretty consistently when the topic of Vim/Neovim comes up is the inevitable question "Why?" Thanks to years of memes about not being able to quit Vim, it has gained a cult popularity of being "elite," "ancient," or both. Most developers who are comfortable in their VSCode environment can't imagine using something so unfriendly and perhaps ugly. So yeah, "why" is a fair question. I reflected on that question and I realized how my own life experience led me to learn Vim, and perhaps it would be helpful to share that story. |
|
| 13 | 13 |
| 12 | 12 | <h1 class="title mb-6">404 | Oops something went wrong</h1> |
|
| 13 | 13 | <p class="mb-8">Please use the navigation to find your way back</p> |
|
| 14 | 14 | <div class="my-4 grid justify-center"> |
|
| 15 | - | <Image src="/512x512.png" width={256} height={256} alt="logo" loading="eager" /> |
|
| 15 | + | <Image src="/icon.png" width={256} height={256} alt="logo" loading="eager" /> |
|
| 16 | 16 | </div> |
|
| 17 | 17 | </PageLayout> |