Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion offchain/src/contracts-url.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export default 'https://bafkreigq7bnsin33zynqbwt45oytfg6nqalur3matsx6u3xuxhdh7qzw4q.ipfs.w3s.link'
export default 'https://lime-urban-salamander-570.mypinata.cloud/ipfs/bafkreigeigtn7vqpp5fkwt5gqgvjlvnpn3nvnshcppjme4mfbvbl2vggo4'
751 changes: 22 additions & 729 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

15 changes: 2 additions & 13 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,10 @@ Development utility scripts which right now just consists of a script to bump th
## Required Tools

- Bun - [bun|(https://bun.sh/docs/installation)]
- Web3 storage cli - `bun install -g @eb3-storage/w3cli`

## Setup web3 storage credentials
## Setup Pinata credentials

You should only need to run these steps once on your machine. The script uses the key and proof for upload permissions.

- Login
- `w3 login <email>`
- Create a new space
- `w3 space create "blah"`
- Create a access key
- `w3 key create `
- Write the private key (starting "Mg...") in the .env file variable `W3_KEY`
- Create a delegate proof and store in proof.txt
- `w3 delegation create <did_from_w3_key_create_above> --can space/blob/add --can space/index/add --can filecoin/offer --can upload/add --base64 > proof.txt`
You should only need to do this once on your machine. Create an **Admin**-scoped API key from the [Pinata dashboard](https://app.pinata.cloud/) and put its JWT in the `PINATA_KEY` environment variable. Also set `PINATA_GATEWAY_DOMAIN` to your account's dedicated gateway domain (Gateways tab in the dashboard, looks like `<random-name>.mypinata.cloud`) — the shared `gateway.pinata.cloud` gateway 404s on freshly pinned content.

## Bump version

Expand Down
Binary file modified scripts/bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@types/semver": "^7.5.8",
"@web3-storage/w3up-client": "^15.0.0",
"pinata": "^2.5.6",
"semver": "^7.6.2",
"smol-toml": "^1.2.2"
}
Expand Down
32 changes: 11 additions & 21 deletions scripts/upload-contract.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as Client from '@web3-storage/w3up-client';
import { Signer } from '@web3-storage/w3up-client/principal/ed25519';
import * as Proof from '@web3-storage/w3up-client/proof';
import { StoreMemory } from '@web3-storage/w3up-client/stores/memory';
import { PinataSDK } from 'pinata';

import { checkVersionsMatch } from './check-version';

const PINATA_GATEWAY = process.env.PINATA_GATEWAY_DOMAIN || 'gateway.pinata.cloud';

export async function uploadContracts() {
const versionsMatch = checkVersionsMatch();

Expand All @@ -13,30 +12,21 @@ export async function uploadContracts() {
'The aiken.toml and the plutus.json version files do not match. Did you run build the contracts after bumping the version?'
);
}

console.log('Uploading latest contracts to IPFS...');
const key = process.env.W3_KEY;
const jwt = process.env.PINATA_KEY;

if (!key) {
throw new Error('key must be set to create a server client to web3.storage');
if (!jwt) {
throw new Error('PINATA_KEY must be set to create a Pinata client');
}

// Loading from file because this proof a very large base64 string that gets truncated
// somewhere (bun or OS) to 4096 characters.
const proof = await Bun.file('proof.txt').text();
const principal = Signer.parse(key);
const store = new StoreMemory();
const client = await Client.create({ principal, store });
const parsed = await Proof.parse(proof);
const space = await client.addSpace(parsed);

await client.setCurrentSpace(space.did());
const pinata = new PinataSDK({ pinataJwt: jwt });

const file = Bun.file('../contracts/plutus.json');
const link = await client.uploadFile(file);
const cid = link.toString();
const upload = await pinata.upload.public.file(new File([await file.arrayBuffer()], 'plutus.json'));
const cid = upload.cid;

console.log(`Latest contracts uploaded with cid: ${cid}`);
const contractFile = Bun.file('../offchain/src/contracts-url.ts');
Bun.write(contractFile, `export default 'https://${cid}.ipfs.w3s.link'`);
Bun.write(contractFile, `export default 'https://${PINATA_GATEWAY}/ipfs/${cid}'`);
}
7 changes: 5 additions & 2 deletions web/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@ SERVICE_WALLET_KEY_PREPROD=
SERVICE_WALLET_ADDRESS_MAINNET=
SERVICE_WALLET_KEY_MAINNET=

# README.md includes instructions on setting up web3 storage space credentials
W3_STORAGE_KEY=
# README.md includes instructions on setting up a Pinata API key
PINATA_JWT=
# Required in practice: the shared gateway.pinata.cloud gateway 404s on freshly pinned
# content, so set this to your account's dedicated gateway domain (Pinata dashboard -> Gateways)
NEXT_PUBLIC_PINATA_GATEWAY_DOMAIN=
34 changes: 7 additions & 27 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,43 +21,23 @@ For Blockfrost you need to set the Preprod or Mainnet API key depending on netwo

You may also need to add a project in the Blockfrost dashboard for the chosen network. Place the API key displayed in the dashboard into `.env.local` under the variable `NEXT_PUBLIC_BLOCKFROST_KEY_PREPROD` or `NEXT_PUBLIC_BLOCKFROST_KEY_MAINNET` depending on which network you chose. You can setup both if you would like to as well.

### Web3 Storage
### Pinata

Web3 storage is used for uploading images to IPFS. To use the API with Mintun you will need to first install the cli tool.
[Pinata](https://pinata.cloud/) is used for uploading images to IPFS. Sign up for a free account (no credit card required), then from the dashboard create an **Admin**-scoped API key. This key is only ever used server-side to mint short-lived, narrowly-scoped upload keys on demand — it never uploads anything directly and is never sent to the browser.

```
pnpm install -g @web3-storage/w3cli
```

Next you need to login. If you don't already have an account this command will create one for you. One drawback is even with free tier you need to enter credit card information. We will see if there is a better solution in the future. The good news is the free tier is plenty of space for testing.

```
w3 login alice@example.com

```

Next create a space and set the CLI to use that space
Copy the key's JWT into `.env.local`:

```
w3 space create
w3 space use <your-space-name>
PINATA_JWT=<your admin jwt>
```

Now you can generate the credentials needed to perform client side upload from mintun.
Then grab your account's dedicated gateway domain from the dashboard (Gateways tab, looks like `<random-name>.mypinata.cloud`) and set it too — the shared `gateway.pinata.cloud` gateway is unreliable for freshly pinned content (returns 404s), so a dedicated gateway is required, not optional:

```
# The following command returns what will be your Agent private key and DID
w3 key create

# ❗️ Store the private key (starting "Mg...") in environment variable W3_STORAGE_KEY

w3 delegation create <did:key:blah-from-create-above> --can space/blob/add --can space/index/add --can filecoin/offer --can upload/add --can store/add --base64 > web/.proof

# ❗️ Create a delegate proof and store it in 'web/.proof'

NEXT_PUBLIC_PINATA_GATEWAY_DOMAIN=<your-name>.mypinata.cloud
```

Now you should be ready to upload. If you get some parse errors when uploading then something probably went wrong with generating the key. The [detailed guide](https://web3.storage/docs/how-to/upload/#using-the-cli) on web3.storage site may be of help.
No CLI tool or manual space/delegation setup is needed beyond that. The app mints a fresh, single-purpose upload key from the admin JWT every time a user uploads images.

## Current Task List

Expand Down
14 changes: 0 additions & 14 deletions web/app/api/storage/delegate/route.ts

This file was deleted.

11 changes: 11 additions & 0 deletions web/app/api/storage/key/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createPinataClient, createScopedKey } from '@/lib/storage/server';

export const dynamic = 'force-dynamic';

// Mints a short-lived, narrowly-scoped Pinata key so the caller can upload directly to IPFS
export async function POST() {
const client = createPinataClient();
const jwt = await createScopedKey(client);

return Response.json({ jwt });
}
6 changes: 2 additions & 4 deletions web/app/collection/create/use-genesis-mint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { CollectionImage, ImagePurpose, MeshContext } from '@ikigai-github/
import { getTime } from 'date-fns';

import { countImages } from '@/lib/image';
import { createWebClient } from '@/lib/storage/client';
import { uploadDirectory } from '@/lib/storage/client';
import { useWallet } from '@/lib/wallet';

import { useCreateCollectionContext } from './context';
Expand Down Expand Up @@ -36,7 +36,6 @@ export default function useGenesisMint() {
if (numImages) {
setUploadProgress(0);

const client = await createWebClient();
const uploaded: CollectionImage[] = [];
const files: File[] = [];
for (const [mode, values] of Object.entries(images)) {
Expand All @@ -60,13 +59,12 @@ export default function useGenesisMint() {
}
}

const result = await client.uploadDirectory(files, {
const cid = await uploadDirectory(files, {
onUploadProgress: (status) => {
setUploadProgress((status.loaded * 100) / status.total);
},
});

const cid = result.toString();
uploaded.forEach((item) => (item.src = item.src.replace('<cid>', cid)));

setUploadProgress(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { DiscordLogoIcon, GlobeIcon, InfoCircledIcon, InstagramLogoIcon, Twitter
import { formatDistanceStrict, formatDistanceToNow } from 'date-fns';
import { useIsMounted } from 'usehooks-ts';

import { getBrandImageUrl } from '@/lib/image';
import { getBrandImageUrls } from '@/lib/image';
import IpfsImage from '@/components/ipfs-image';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';

import { useManageCollectionContext } from './context';
Expand All @@ -17,7 +18,7 @@ import StoreMintingPolicy from './store-minting-policy';

export default function CollectionInfo() {
const { info, state, royalties, policy, drafts, mintReferenceUtxo } = useManageCollectionContext();
const brandUrl = getBrandImageUrl(info);
const brandUrls = getBrandImageUrls(info);
const isMounted = useIsMounted();

const remaining = useMemo(() => {
Expand All @@ -35,8 +36,8 @@ export default function CollectionInfo() {
<div className="bg-background/50 -mt-44 flex flex-col gap-4 rounded-xl border p-4 backdrop-blur">
<div className="flex min-h-36 gap-6">
{info ? (
brandUrl ? (
<img src={brandUrl} alt="Brand Image" className="size-32 rounded-xl object-cover" />
brandUrls ? (
<IpfsImage src={brandUrls} alt="Brand Image" className="size-32 rounded-xl object-cover" />
) : undefined
) : (
<Image src={brandPlaceholder} alt="Brand Image" className="size-32 rounded-xl object-cover" />
Expand Down
9 changes: 5 additions & 4 deletions web/app/collection/manage/[network]/[policy]/draft-nft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { HobbyKnifeIcon, PlusIcon } from '@radix-ui/react-icons';
import { useForm } from 'react-hook-form';
import { uid as createUid } from 'uid';

import { DefaultImageDetail, getWebImageUrl, ImageDetail } from '@/lib/image';
import { DefaultImageDetail, getWebImageUrls, ImageDetail } from '@/lib/image';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Form, FormControl, FormDescription, FormField, FormItem } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import DrawerDialog from '@/components/drawer-dialog';
import ImageDropzone from '@/components/image-dropzone';
import IpfsImage from '@/components/ipfs-image';

import { useManageCollectionContext } from './context';
import DraftTagContent from './draft-tag-content';
Expand Down Expand Up @@ -42,9 +43,9 @@ function DraftNftCard({ uid }: { uid: string }) {

const url = useMemo(() => {
if (typeof image.data !== 'string') {
return URL.createObjectURL(image.data);
return [URL.createObjectURL(image.data)];
} else {
return getWebImageUrl(image.data);
return getWebImageUrls(image.data);
}
}, [image.data]);

Expand All @@ -63,7 +64,7 @@ function DraftNftCard({ uid }: { uid: string }) {
return (
<Card className="hover:bg-foreground/10 h-56 min-w-36 max-w-60 transition-colors">
<div className="relative h-44 overflow-hidden rounded-t-xl">
<img src={url} className="inset-0 size-full object-cover" alt={name} />
<IpfsImage src={url} className="inset-0 size-full object-cover" alt={name || 'Draft NFT'} />
</div>
<div className="flex justify-between p-4">
<div className="font-heading truncate text-left leading-none">{displayName}</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
'use client';

import { useMemo } from 'react';
import Image from 'next/image';
import bannerPlaceholder from '@/public/banner-placeholder.webp';

import { getBannerImageUrl } from '@/lib/image';
import { getBannerImageUrls } from '@/lib/image';
import IpfsImage from '@/components/ipfs-image';

import Collection from './collection';
import CollectionInfo from './collection-info';
Expand All @@ -13,14 +12,14 @@ import { useManageCollectionContext } from './context';
export default function ManageCollection() {
const { info } = useManageCollectionContext();

const bannerUrl = useMemo(() => {
return getBannerImageUrl(info);
const bannerUrls = useMemo(() => {
return getBannerImageUrls(info);
}, [info]);

return (
<>
{bannerUrl ? (
<img className="h-52 max-h-52 min-h-52 rounded-t-xl object-cover" src={bannerUrl} alt="Collection Banner" />
{bannerUrls ? (
<IpfsImage className="h-52 max-h-52 min-h-52 rounded-t-xl object-cover" src={bannerUrls} alt="Collection Banner" />
) : (
<div className="bg-accent h-52 max-h-52 min-h-52 rounded-t-xl object-cover"> </div>
)}
Expand Down
11 changes: 6 additions & 5 deletions web/app/collection/manage/[network]/[policy]/mint-drafts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { MintunNft } from '@ikigai-github/mintun-offchain';
import { CheckIcon } from '@radix-ui/react-icons';

import { getWebImageUrl } from '@/lib/image';
import { getWebImageUrls } from '@/lib/image';
import { timeout } from '@/lib/utils';
import { notifyError, useWallet } from '@/lib/wallet';
import IpfsImage from '@/components/ipfs-image';
import { Button } from '@/components/ui/button';
import TransactionDialog from '@/components/transaction-dialog';

Expand All @@ -13,7 +14,7 @@ import { useManageCollectionContext } from './context';
type DraftOption = {
uid: string;
name: string;
url: string;
url: string[];
selected: boolean;
};

Expand All @@ -27,7 +28,7 @@ export default function MintDrafts() {
useEffect(() => {
const sequence = state?.nextSequence || 0;
const options = drafts.map((draft, index) => {
const url = getWebImageUrl(draft.image.data as string);
const url = getWebImageUrls(draft.image.data as string);
const name = draft.name || `${info?.name} #${sequence + index}`;
const uid = draft.uid;
const selected = index < 10;
Expand Down Expand Up @@ -181,7 +182,7 @@ export default function MintDrafts() {
onClick={() => onSelectToggle(index)}
>
{option.selected ? <CheckIcon className="size-6" /> : <div className="size-6"></div>}
<img src={option.url} alt={option.name} className="size-10 rounded-md" />
<IpfsImage src={option.url} alt={option.name} className="size-10 rounded-md" />
<span>{option.name || 'Generated'}</span>
</li>
);
Expand All @@ -192,7 +193,7 @@ export default function MintDrafts() {
return (
<li key={option.uid} className="flex flex-row items-center gap-2">
<CheckIcon className="size-6" />
<img src={option.url} alt={option.name} className="size-10 rounded-md" />
<IpfsImage src={option.url} alt={option.name} className="size-10 rounded-md" />
<span>{option.name || 'Generated'}</span>
</li>
);
Expand Down
Loading
Loading