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
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Black Check ($BLKCHK)

**An ERC20 token backed by Checks Originals NFTs**
**An experimental ERC20 artwork connected to Checks Originals NFTs**

Black Check (`$BLKCHK`) is an experimental digital artwork that creates a fungible token representation of [Checks Originals](https://etherscan.io/address/0x036721e5A769Cc48B3189EFbb9ccE4471E8A48B1) NFTs. The contract accepts Check NFTs and mints tokens proportional to their checks count, with a maximum supply of 1 token (1 × 10^18 wei).
Black Check (`$BLKCHK`) is experimental digital artwork that creates a fungible protocol representation of [Checks Originals](https://etherscan.io/address/0x036721e5A769Cc48B3189EFbb9ccE4471E8A48B1) NFTs. The contract accepts Checks and mints tokens according to their checks count, with a maximum supply of exactly 1 token.

📄 **[Read the Full Whitepaper](https://github.com/visualizevalue/vvhitepapers/blob/main/projects/BLACK_CHECK.md)**
📄 **[Read the Full Whitepaper](https://github.com/visualizevalue/vvhitepapers/blob/main/projects/black-check.md)**

> **⚠️ Important Notice**
>
> Participation in this project involves engagement with experimental digital artworks and is undertaken entirely at your own risk. This work does not constitute an offer to sell or the solicitation of an offer to buy any security, commodity, or financial instrument in any jurisdiction. It is a creative exploration of ownership, value, and representation, not an investment vehicle. No guarantees are made regarding liquidity, market value, or future performance.
> Participation involves engagement with experimental digital artworks on Ethereum. Mint, extract, and composite are creative acts—not financial transactions or investments. No offer to sell or buy any security or financial instrument is made or implied. Participation is voluntary and at your own risk. No guarantees exist regarding liquidity, value, availability, or performance.

## Token Allocations

Expand Down Expand Up @@ -95,6 +95,18 @@ one(tokenIds)

## Development

### Frontend

The participant interface lives in `frontend/`.

```shell
cd frontend
npm install
npm run dev
```

The interface reads live state from Ethereum and supports deposit, extract, and composite actions through an injected wallet.

### Running Tests

```shell
Expand Down
40 changes: 40 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/.vinext/
/out/

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

/dist/
/.wrangler/
/outputs/
/work/
5 changes: 5 additions & 0 deletions frontend/.openai/hosting.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"project_id": "appgprj_6a86ecdc13a88191a54f36c678f4903f",
"d1": null,
"r2": null
}
26 changes: 26 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Black Check frontend

The participant interface for Black Check ($BLKCHK), an experimental onchain artwork connected to Checks Originals.

## Run locally

Requires Node.js 22.13 or newer.

```bash
npm install
npm run dev
```

Use `npm test` for the production build and integration checks.

## Live data

- Ethereum state is read from the deployed Black Check and Checks Originals contracts.
- Connected-wallet ownership is indexed through Alchemy's public NFT endpoint.
- Market data is read from the exact BLKCHK/ETH Uniswap v4 pool through GeckoTerminal.

Third-party market data is indicative only. It is not a valuation, a guarantee of executable value, or investment guidance.

## Deployment

The app builds with vinext for Cloudflare Workers-compatible hosting. The Sites project identifier is stored in `.openai/hosting.json`; secrets and hosted runtime values do not belong in the repository.
60 changes: 60 additions & 0 deletions frontend/app/api/checks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const CHECKS = "0x036721e5A769Cc48B3189EFbb9ccE4471E8A48B1";
const ADDRESS = /^0x[a-fA-F0-9]{40}$/;

type AlchemyNft = {
tokenId?: string;
name?: string;
image?: {
originalUrl?: string;
contentType?: string;
thumbnailUrl?: string;
pngUrl?: string;
cachedUrl?: string;
};
raw?: {
metadata?: {
attributes?: Array<{ trait_type?: string; value?: string | number }>;
};
};
};

export async function GET(request: Request) {
const owner = new URL(request.url).searchParams.get("owner") ?? "";
if (!ADDRESS.test(owner)) {
return Response.json({ error: "A valid wallet address is required." }, { status: 400 });
}

const query = new URLSearchParams({
owner,
withMetadata: "true",
pageSize: "100",
});
query.append("contractAddresses[]", CHECKS);

try {
const response = await fetch(
`https://eth-mainnet.g.alchemy.com/nft/v3/demo/getNFTsForOwner?${query}`,
{ headers: { accept: "application/json" } },
);
if (!response.ok) throw new Error(`Ownership index returned ${response.status}.`);

const data = (await response.json()) as { ownedNfts?: AlchemyNft[]; totalCount?: number };
const checks = (data.ownedNfts ?? []).map((nft) => ({
tokenId: nft.tokenId ?? "",
name: nft.name ?? `Check #${nft.tokenId ?? "—"}`,
image:
nft.image?.contentType === "image/svg+xml" && nft.image.originalUrl
? nft.image.originalUrl
: nft.image?.pngUrl ?? nft.image?.cachedUrl ?? nft.image?.thumbnailUrl ?? null,
checksCount:
nft.raw?.metadata?.attributes?.find((attribute) => attribute.trait_type === "Checks")?.value ?? null,
}));

return Response.json(
{ checks, totalCount: data.totalCount ?? checks.length },
{ headers: { "cache-control": "private, no-store" } },
);
} catch {
return Response.json({ error: "Checks are temporarily unavailable." }, { status: 502 });
}
}
60 changes: 60 additions & 0 deletions frontend/app/api/market/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const TOKEN = "0x718477c471b335ee0ca29b9f4b95edd26d2ede54";
const POOL = "0x132c1c936dcb7837f24cc558745bbd4df0f393c6422eff8d1d406cd8004ded43";

type PoolResponse = {
data?: {
id?: string;
attributes?: {
address?: string;
name?: string;
base_token_price_usd?: string;
base_token_price_quote_token?: string;
reserve_in_usd?: string;
volume_usd?: { h24?: string };
price_change_percentage?: { h24?: string };
transactions?: { h24?: { buys?: number; sells?: number } };
};
relationships?: {
base_token?: { data?: { id?: string } };
quote_token?: { data?: { id?: string } };
};
};
};

export async function GET() {
try {
const response = await fetch(
`https://api.geckoterminal.com/api/v2/networks/eth/pools/${POOL}?include=base_token,quote_token`,
{ headers: { accept: "application/json;version=20230203" } },
);
if (!response.ok) throw new Error(`Market index returned ${response.status}.`);

const data = (await response.json()) as PoolResponse;
const pool = data.data;
const attributes = pool?.attributes;
if (!attributes) throw new Error("Market pool not found.");
if (attributes.address?.toLowerCase() !== POOL) throw new Error("Unexpected market pool.");

const baseTokenId = pool.relationships?.base_token?.data?.id?.toLowerCase();
if (baseTokenId !== `eth_${TOKEN}`) throw new Error("Unexpected market base token.");

const buys = attributes.transactions?.h24?.buys ?? 0;
const sells = attributes.transactions?.h24?.sells ?? 0;
return Response.json(
{
pair: attributes.name ?? "$BLKCHK / ETH",
priceUsd: attributes.base_token_price_usd ?? null,
priceEth: attributes.base_token_price_quote_token ?? null,
liquidityUsd: attributes.reserve_in_usd ?? null,
volume24hUsd: attributes.volume_usd?.h24 ?? null,
change24h: attributes.price_change_percentage?.h24 ?? null,
transactions24h: buys + sells,
sourceUrl: `https://www.geckoterminal.com/eth/pools/${POOL}`,
updatedAt: new Date().toISOString(),
},
{ headers: { "cache-control": "public, max-age=15, s-maxage=30, stale-while-revalidate=300" } },
);
} catch {
return Response.json({ error: "Market data is temporarily unavailable." }, { status: 502 });
}
}
90 changes: 90 additions & 0 deletions frontend/app/chatgpt-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export type ChatGPTUser = {
userId: string;
displayName: string;
email: string;
fullName: string | null;
};

const USER_ID_HEADER = "oai-authenticated-user-id";
const USER_EMAIL_HEADER = "oai-authenticated-user-email";
const USER_FULL_NAME_HEADER = "oai-authenticated-user-full-name";
const USER_FULL_NAME_ENCODING_HEADER =
"oai-authenticated-user-full-name-encoding";
const PERCENT_ENCODED_UTF8 = "percent-encoded-utf-8";
const SIGN_IN_PATH = "/signin-with-chatgpt";
const SIGN_OUT_PATH = "/signout-with-chatgpt";
const CALLBACK_PATH = "/callback";

export async function getChatGPTUser(): Promise<ChatGPTUser | null> {
const requestHeaders = await headers();
const userId = requestHeaders.get(USER_ID_HEADER);
const email = requestHeaders.get(USER_EMAIL_HEADER);
if (!userId || !email) return null;

const encodedFullName = requestHeaders.get(USER_FULL_NAME_HEADER);
const fullName =
encodedFullName &&
requestHeaders.get(USER_FULL_NAME_ENCODING_HEADER) === PERCENT_ENCODED_UTF8
? safeDecodeURIComponent(encodedFullName)
: null;

return {
userId,
displayName: fullName ?? email,
email,
fullName,
};
}

export async function requireChatGPTUser(
returnTo: string,
): Promise<ChatGPTUser> {
const user = await getChatGPTUser();
if (user) return user;

redirect(chatGPTSignInPath(returnTo));
}

export function chatGPTSignInPath(returnTo: string): string {
const safeReturnTo = safeRelativeReturnPath(returnTo);
return `${SIGN_IN_PATH}?return_to=${encodeURIComponent(safeReturnTo)}`;
}

export function chatGPTSignOutPath(returnTo = "/"): string {
const safeReturnTo = safeRelativeReturnPath(returnTo);
return `${SIGN_OUT_PATH}?return_to=${encodeURIComponent(safeReturnTo)}`;
}

function safeRelativeReturnPath(value: string): string {
if (!value.startsWith("/") || value.startsWith("//")) return "/";

let url: URL;
try {
url = new URL(value, "https://app.local");
} catch {
return "/";
}
if (url.origin !== "https://app.local") return "/";
if (isReservedAuthPath(url.pathname)) return "/";

return `${url.pathname}${url.search}${url.hash}`;
}

function isReservedAuthPath(pathname: string): boolean {
return (
pathname === SIGN_IN_PATH ||
pathname === SIGN_OUT_PATH ||
pathname === CALLBACK_PATH
);
}

function safeDecodeURIComponent(value: string): string | null {
try {
return decodeURIComponent(value);
} catch {
return null;
}
}
Loading