├── packages ├── indexer │ ├── pnpm-workspace.yaml │ ├── README.md │ ├── .npmrc │ ├── .gitignore │ ├── tsconfig.json │ ├── src │ │ └── utils │ │ │ ├── ipfs.ts │ │ │ ├── mapping.ts │ │ │ └── client.ts │ ├── package.json │ ├── config.yaml │ ├── schema.graphql │ └── index.html ├── blog │ ├── .eslintrc.json │ ├── src │ │ ├── assets │ │ │ └── globals.css │ │ ├── utils │ │ │ ├── types.ts │ │ │ └── data.ts │ │ ├── app │ │ │ ├── robots.ts │ │ │ ├── page.tsx │ │ │ ├── sitemap.ts │ │ │ ├── opengraph-image.tsx │ │ │ ├── layout.tsx │ │ │ └── [slug] │ │ │ │ └── page.tsx │ │ └── components │ │ │ ├── Header.tsx │ │ │ ├── Card.tsx │ │ │ ├── Footer.tsx │ │ │ └── Layout.tsx │ ├── public │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── sup.png │ │ │ └── friendship-ended-with-kickback.jpg │ │ └── icons │ │ │ ├── favicon.ico │ │ │ ├── sup-192x192.png │ │ │ ├── sup-512x512.png │ │ │ ├── sup-maskable.png │ │ │ ├── apple-touch-icon.png │ │ │ └── apple-touch-icon-180x180.png │ ├── next.config.js │ ├── postcss.config.js │ ├── README.md │ ├── tailwind.config.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ └── data │ │ └── posts │ │ ├── faq-for-attendees.md │ │ ├── faq-for-event-organizers.md │ │ ├── introducing-show-up-protocol.md │ │ └── 5-tips-to-reduce-no-shows-at-your-event.md ├── app │ ├── src │ │ ├── assets │ │ │ └── globals.css │ │ ├── app │ │ │ ├── favicon.ico │ │ │ ├── actions │ │ │ │ └── cache.ts │ │ │ ├── notifications │ │ │ │ └── page.tsx │ │ │ ├── profile │ │ │ │ ├── page.tsx │ │ │ │ └── components │ │ │ │ │ ├── Login.tsx │ │ │ │ │ └── Profile.tsx │ │ │ ├── opengraph-image.tsx │ │ │ ├── robots.ts │ │ │ ├── past │ │ │ │ └── page.tsx │ │ │ ├── create │ │ │ │ ├── page.tsx │ │ │ │ └── components │ │ │ │ │ ├── CreateEventContext.tsx │ │ │ │ │ ├── Info.tsx │ │ │ │ │ └── ImageUpload.tsx │ │ │ ├── components │ │ │ │ ├── Tabs.tsx │ │ │ │ ├── Overview.tsx │ │ │ │ └── Card.tsx │ │ │ ├── page.tsx │ │ │ ├── api │ │ │ │ └── upload │ │ │ │ │ └── route.ts │ │ │ ├── manifest.ts │ │ │ ├── tickets │ │ │ │ ├── page.tsx │ │ │ │ └── components │ │ │ │ │ ├── Overview.tsx │ │ │ │ │ └── Ticket.tsx │ │ │ ├── events │ │ │ │ ├── [slug] │ │ │ │ │ ├── admin │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── opengraph-image.tsx │ │ │ │ └── components │ │ │ │ │ └── Admin │ │ │ │ │ ├── Actions.tsx │ │ │ │ │ └── Settle.tsx │ │ │ ├── sitemap.ts │ │ │ ├── [id] │ │ │ │ ├── page.tsx │ │ │ │ ├── opengraph-image.tsx │ │ │ │ └── components │ │ │ │ │ └── Profile.tsx │ │ │ └── layout.tsx │ │ ├── components │ │ │ ├── Connect.tsx │ │ │ ├── Loading.tsx │ │ │ ├── Empty.tsx │ │ │ ├── Header.tsx │ │ │ ├── Protected.tsx │ │ │ ├── Date.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Layout.tsx │ │ │ ├── MobileLayout.tsx │ │ │ ├── OpenGraph.tsx │ │ │ ├── LinkComponent.tsx │ │ │ ├── Alert.tsx │ │ │ ├── Testnet.tsx │ │ │ ├── SelectBox.tsx │ │ │ ├── Navbar.tsx │ │ │ ├── Hero.tsx │ │ │ ├── Notifications.tsx │ │ │ └── ActionDrawer.tsx │ │ ├── utils │ │ │ ├── abi.d.ts │ │ │ ├── format.ts │ │ │ ├── site.ts │ │ │ ├── config.ts │ │ │ ├── dates.ts │ │ │ ├── types.ts │ │ │ └── network.ts │ │ ├── hooks │ │ │ ├── useProfile.ts │ │ │ ├── useEvent.ts │ │ │ ├── useTickets.ts │ │ │ ├── useEvents.ts │ │ │ └── useAllowance.ts │ │ ├── context │ │ │ ├── Data.tsx │ │ │ ├── Web3.tsx │ │ │ ├── Notification.tsx │ │ │ └── EventData.tsx │ │ └── services │ │ │ └── storage.ts │ ├── public │ │ ├── images │ │ │ ├── seats.jpg │ │ │ ├── stage.jpg │ │ │ ├── audience.jpg │ │ │ └── conference.jpg │ │ └── icons │ │ │ ├── favicon.ico │ │ │ ├── sup-192x192.png │ │ │ ├── sup-512x512.png │ │ │ ├── sup-maskable.png │ │ │ ├── apple-touch-icon.png │ │ │ └── apple-touch-icon-180x180.png │ ├── postcss.config.js │ ├── README.md │ ├── .eslintrc.json │ ├── .prettierrc.json │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── next.config.js │ ├── wagmi.config.ts │ ├── .gitignore │ └── package.json ├── protocol │ ├── overview.png │ ├── .env.example │ ├── tsconfig.json │ ├── contracts │ │ ├── mocks │ │ │ ├── Token.sol │ │ │ ├── TrueMock.sol │ │ │ ├── FalseMock.sol │ │ │ ├── FalseCreateMock.sol │ │ │ └── FalseSettleMock.sol │ │ ├── Common.sol │ │ ├── interfaces │ │ │ ├── IConditionModule.sol │ │ │ └── IShowHub.sol │ │ └── conditions │ │ │ ├── SplitEther.sol │ │ │ ├── RecipientEther.sol │ │ │ ├── SplitToken.sol │ │ │ └── RecipientToken.sol │ ├── test │ │ └── utils │ │ │ └── types.ts │ ├── package.json │ ├── deployments.json │ ├── scripts │ │ ├── deploy-token.ts │ │ └── verify.ts │ └── hardhat.config.ts └── subgraph │ ├── tsconfig.json │ ├── networks.json │ ├── generated │ └── templates.ts │ ├── package.json │ ├── src │ └── metadata.ts │ ├── subgraph.yaml │ ├── schema.graphql │ └── abis │ └── IConditionModule.json ├── .vscode ├── extensions.json └── settings.json ├── package.json ├── .gitignore ├── LICENSE └── README.md /packages/indexer/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - ./* 3 | -------------------------------------------------------------------------------- /packages/blog/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/indexer/README.md: -------------------------------------------------------------------------------- 1 | ## Show Up Indexer 2 | 3 | Built using [Envio](https://docs.envio.dev) 4 | -------------------------------------------------------------------------------- /packages/app/src/assets/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/blog/src/assets/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/protocol/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/protocol/overview.png -------------------------------------------------------------------------------- /packages/app/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/app/src/app/favicon.ico -------------------------------------------------------------------------------- /packages/blog/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/blog/public/favicon.ico -------------------------------------------------------------------------------- /packages/app/public/images/seats.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/app/public/images/seats.jpg -------------------------------------------------------------------------------- /packages/app/public/images/stage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/app/public/images/stage.jpg -------------------------------------------------------------------------------- /packages/blog/public/images/sup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/blog/public/images/sup.png -------------------------------------------------------------------------------- /packages/app/public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/app/public/icons/favicon.ico -------------------------------------------------------------------------------- /packages/app/public/images/audience.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/app/public/images/audience.jpg -------------------------------------------------------------------------------- /packages/blog/public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/blog/public/icons/favicon.ico -------------------------------------------------------------------------------- /packages/indexer/.npmrc: -------------------------------------------------------------------------------- 1 | # Needed for ts build folder to have 2 | # access to rescript node_modules 3 | shamefully-hoist=true 4 | -------------------------------------------------------------------------------- /packages/app/public/icons/sup-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/app/public/icons/sup-192x192.png -------------------------------------------------------------------------------- /packages/app/public/icons/sup-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/app/public/icons/sup-512x512.png -------------------------------------------------------------------------------- /packages/app/public/images/conference.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/app/public/images/conference.jpg -------------------------------------------------------------------------------- /packages/app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/app/public/icons/sup-maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/app/public/icons/sup-maskable.png -------------------------------------------------------------------------------- /packages/blog/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /packages/blog/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/blog/public/icons/sup-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/blog/public/icons/sup-192x192.png -------------------------------------------------------------------------------- /packages/blog/public/icons/sup-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/blog/public/icons/sup-512x512.png -------------------------------------------------------------------------------- /packages/blog/public/icons/sup-maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/blog/public/icons/sup-maskable.png -------------------------------------------------------------------------------- /packages/app/public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/app/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/app/src/components/Connect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function Connect() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /packages/app/src/utils/abi.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'abitype' { 2 | export interface Register { 3 | AddressType: `0x${string}` 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/blog/public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/blog/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/subgraph/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@graphprotocol/graph-ts/types/tsconfig.base.json", 3 | "include": ["src", "tests"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/app/public/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/app/public/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /packages/blog/public/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/blog/public/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /packages/app/README.md: -------------------------------------------------------------------------------- 1 | # Show Up App 2 | 3 | Onchain RSVP and Event management that allows you to earn $$ by showing up at events! 4 | 5 | - https://www.showup.events/ 6 | -------------------------------------------------------------------------------- /packages/blog/README.md: -------------------------------------------------------------------------------- 1 | # Show Up Blog 2 | 3 | Onchain RSVP and Event management that allows you to earn $$ by showing up at events! 4 | 5 | - https://www.showup.events/ 6 | -------------------------------------------------------------------------------- /packages/blog/public/images/friendship-ended-with-kickback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wslyvh/show-up/HEAD/packages/blog/public/images/friendship-ended-with-kickback.jpg -------------------------------------------------------------------------------- /packages/blog/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface Post { 2 | slug: string 3 | title: string 4 | description: string 5 | date: number 6 | body: string 7 | } 8 | -------------------------------------------------------------------------------- /packages/app/src/app/actions/cache.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { revalidatePath } from 'next/cache' 4 | 5 | export async function revalidateAll() { 6 | revalidatePath('/', 'layout') 7 | } 8 | -------------------------------------------------------------------------------- /packages/app/src/app/notifications/page.tsx: -------------------------------------------------------------------------------- 1 | import { Notifications } from '@/components/Notifications' 2 | 3 | export default function NotificationsPage() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "[solidity]": { 5 | "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "prettier" 5 | ], 6 | "plugins": [ 7 | "prettier" 8 | ], 9 | "rules": { 10 | "prettier/prettier": ["error"] 11 | } 12 | } -------------------------------------------------------------------------------- /packages/app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "singleQuote": true, 5 | "jsxSingleQuote": true, 6 | "printWidth": 120, 7 | "bracketSameLine": true, 8 | "useTabs": false, 9 | "tabWidth": 2 10 | } 11 | -------------------------------------------------------------------------------- /packages/protocol/.env.example: -------------------------------------------------------------------------------- 1 | DEPLOYER_KEY=required private key to deploy 2 | 3 | INFURA_API_KEY=optional depending on RPC config 4 | ETHERSCAN_API_KEY=optional for smart contract verification 5 | OPTIMISTIC_API_KEY=optional for smart contract verification 6 | -------------------------------------------------------------------------------- /packages/app/src/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react' 2 | import { Profile } from './components/Profile' 3 | 4 | export default function ProfilePage() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /packages/protocol/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog/src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { BLOG_URL } from 'app/src/utils/site' 2 | import { MetadataRoute } from 'next' 3 | 4 | export default function robots(): MetadataRoute.Robots { 5 | return { 6 | rules: { 7 | userAgent: '*', 8 | allow: '/', 9 | }, 10 | sitemap: `${BLOG_URL}/sitemap.xml`, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/blog/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | theme: {}, 5 | content: [ 6 | './src/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | plugins: [require("@tailwindcss/typography"), require("daisyui")], 9 | daisyui: { 10 | themes: ['night'], 11 | }, 12 | } 13 | export default config 14 | -------------------------------------------------------------------------------- /packages/subgraph/networks.json: -------------------------------------------------------------------------------- 1 | { 2 | "sepolia": { 3 | "Registry": { 4 | "address": "0x7Cc8E0633021b9DF8D2F01d9287C3b8e29f4eDe2", 5 | "startBlock": 4629005 6 | } 7 | }, 8 | "optimism": { 9 | "Registry": { 10 | "address": "0x7Cc8E0633021b9DF8D2F01d9287C3b8e29f4eDe2", 11 | "startBlock": 111754660 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/blog/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@/components/Card" 2 | import { GetPosts } from "@/utils/data" 3 | 4 | export default async function Home() { 5 | const posts = GetPosts() 6 | 7 | return ( 8 |
9 | {posts.map((post) => { 10 | return 11 | })} 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /packages/app/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | theme: {}, 5 | content: [ 6 | './src/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | plugins: [require("@tailwindcss/typography"), require("daisyui")], 9 | daisyui: { 10 | // More details at https://daisyui.com/docs/config/ 11 | themes: ['night'], 12 | }, 13 | } 14 | export default config 15 | -------------------------------------------------------------------------------- /packages/app/src/hooks/useProfile.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import { GetUser } from '@/services/showhub' 3 | 4 | export function useProfile(address: string) { 5 | const { data, isError, isPending } = useQuery({ 6 | queryKey: ['user', address], 7 | queryFn: () => GetUser(address), 8 | }) 9 | 10 | return { 11 | data, 12 | isPending, 13 | isError, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/indexer/.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.obj 3 | *.out 4 | *.compile 5 | *.native 6 | *.byte 7 | *.cmo 8 | *.annot 9 | *.cmi 10 | *.cmx 11 | *.cmt 12 | *.cmti 13 | *.cma 14 | *.a 15 | *.cmxa 16 | *.obj 17 | *~ 18 | *.annot 19 | *.cmj 20 | *.bak 21 | lib/* 22 | *.mlast 23 | *.mliast 24 | .vscode 25 | .merlin 26 | .bsb.lock 27 | /node_modules/ 28 | benchmarks/ 29 | artifacts 30 | cache 31 | generated 32 | logs 33 | *.bs.js 34 | build 35 | -------------------------------------------------------------------------------- /packages/indexer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "lib": ["es6"], 6 | "allowJs": true, 7 | "checkJs": false, 8 | "outDir": "build", 9 | "rootDirs": ["src", "generated"], 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "esModuleInterop": true, 13 | "resolveJsonModule": true, 14 | "skipLibCheck": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/app/src/utils/format.ts: -------------------------------------------------------------------------------- 1 | import slugify from 'slugify' 2 | 3 | export function Slugify(value: string) { 4 | return slugify(value, { strict: true, lower: true }) 5 | } 6 | 7 | export function TruncateMiddle(text: string, length: number = 5) { 8 | if (text?.length > length * 2 + 1) { 9 | return `${text.substring(0, length)}...${text.substring(text.length - length, text.length)}` 10 | } 11 | 12 | return text 13 | } 14 | -------------------------------------------------------------------------------- /packages/app/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props { 4 | text?: string 5 | } 6 | 7 | export function Loading(props: Props) { 8 | return ( 9 |
10 | 11 |

{props.text ?? 'Loading..'}

12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /packages/app/src/app/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | import { defaultOpenGraphImage } from '@/components/OpenGraph' 2 | import { SITE_NAME } from '@/utils/site' 3 | 4 | // Route segment config 5 | export const runtime = 'edge' 6 | 7 | // Image metadata 8 | export const alt = SITE_NAME 9 | export const size = { width: 1200, height: 630 } 10 | export const contentType = 'image/png' 11 | 12 | export default async function Image() { 13 | return defaultOpenGraphImage() 14 | } 15 | -------------------------------------------------------------------------------- /packages/app/src/components/Empty.tsx: -------------------------------------------------------------------------------- 1 | import { InboxIcon } from '@heroicons/react/24/outline' 2 | import React from 'react' 3 | 4 | interface Props { 5 | text?: string 6 | } 7 | 8 | export function Empty(props: Props) { 9 | return ( 10 |
11 | 12 |

{props.text ?? 'No data..'}

13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/next/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "moduleResolution": "node", 6 | "plugins": [ 7 | { 8 | "name": "next" 9 | } 10 | ], 11 | "paths": { 12 | "@/*": ["./src/*"] 13 | } 14 | }, 15 | "ts-node": { 16 | "require": ["tsconfig-paths/register"] 17 | }, 18 | "include": ["next-env.d.ts", "src", ".next/types/**/*.ts"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/app/src/hooks/useEvent.ts: -------------------------------------------------------------------------------- 1 | import { GetEventBySlug } from '@/services/showhub' 2 | import { useQuery } from '@tanstack/react-query' 3 | 4 | interface Props { 5 | id: string 6 | } 7 | 8 | export function useEvent(props: Props) { 9 | const { data, isError, isPending, refetch } = useQuery({ 10 | queryKey: ['events', props.id], 11 | queryFn: () => GetEventBySlug(props.id), 12 | }) 13 | 14 | return { 15 | data, 16 | isPending, 17 | isError, 18 | refetch, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/subgraph/generated/templates.ts: -------------------------------------------------------------------------------- 1 | // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | 3 | import { DataSourceTemplate, DataSourceContext } from "@graphprotocol/graph-ts"; 4 | 5 | export class Event extends DataSourceTemplate { 6 | static create(cid: string): void { 7 | DataSourceTemplate.create("Event", [cid]); 8 | } 9 | 10 | static createWithContext(cid: string, context: DataSourceContext): void { 11 | DataSourceTemplate.createWithContext("Event", [cid], context); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/app/src/hooks/useTickets.ts: -------------------------------------------------------------------------------- 1 | import { useAccount } from 'wagmi' 2 | import { useQuery } from '@tanstack/react-query' 3 | import { GetEventsByRegistration } from '@/services/showhub' 4 | 5 | export function useTickets() { 6 | const { address } = useAccount() 7 | const { data, isError, isPending } = useQuery({ 8 | queryKey: ['tickets', address], 9 | queryFn: () => GetEventsByRegistration(address), 10 | enabled: !!address, 11 | }) 12 | 13 | return { 14 | data, 15 | isPending, 16 | isError, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/app/src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { CONFIG } from '@/utils/config' 2 | import { SITE_URL } from '@/utils/site' 3 | import { MetadataRoute } from 'next' 4 | 5 | export default function robots(): MetadataRoute.Robots { 6 | if (CONFIG.NETWORK_ENV === 'test') { 7 | return { 8 | rules: { 9 | userAgent: '*', 10 | disallow: '/', 11 | }, 12 | } 13 | } 14 | 15 | return { 16 | rules: { 17 | userAgent: '*', 18 | allow: '/', 19 | }, 20 | sitemap: `${SITE_URL}/sitemap.xml`, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/blog/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /packages/protocol/contracts/mocks/Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; 5 | 6 | import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; 7 | import '@openzeppelin/contracts/access/Ownable.sol'; 8 | 9 | contract Token is ERC20, Ownable { 10 | constructor() ERC20('SUP Test Token', 'SUP') Ownable(msg.sender) { 11 | _mint(msg.sender, 10000 ether); 12 | } 13 | 14 | function mint(address to, uint256 amount) public onlyOwner { 15 | _mint(to, amount); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/app/src/app/past/page.tsx: -------------------------------------------------------------------------------- 1 | import { Overview } from '../components/Overview' 2 | import { GetPastEvents } from '@/services/showhub' 3 | import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query' 4 | 5 | export default async function Home() { 6 | const queryClient = new QueryClient() 7 | 8 | await queryClient.prefetchQuery({ 9 | queryKey: ['events', 'past'], 10 | queryFn: GetPastEvents, 11 | }) 12 | 13 | return ( 14 | 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/app/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LinkComponent } from './LinkComponent' 3 | import { SITE_EMOJI } from '@/utils/site' 4 | 5 | export function Header() { 6 | return ( 7 |
8 | 9 |

{SITE_EMOJI}

10 |
11 | 12 | 13 | 14 | 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/app/src/app/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { CreateForm } from '@/app/create/components/Create' 2 | import { Protected } from '@/components/Protected' 3 | import { GetConditionModules } from '@/services/showhub' 4 | import CreateEventProvider from './components/CreateEventContext' 5 | 6 | export default async function Home() { 7 | const modules = await GetConditionModules() 8 | 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/app/src/components/Protected.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { PropsWithChildren, useEffect } from 'react' 4 | import { usePathname, useRouter } from 'next/navigation' 5 | import { useAccount } from 'wagmi' 6 | 7 | export function Protected(props: PropsWithChildren) { 8 | const router = useRouter() 9 | const pathname = usePathname() 10 | const { isConnected } = useAccount() 11 | 12 | useEffect(() => { 13 | if (!isConnected) { 14 | router.push(`/profile?redirect=${pathname}`) 15 | } 16 | }, [router, pathname, isConnected]) 17 | 18 | return <>{props.children} 19 | } 20 | -------------------------------------------------------------------------------- /packages/app/src/app/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | options: string[] 3 | selected?: string 4 | onSelect: (option: string) => void 5 | } 6 | 7 | export function Tabs(props: Props) { 8 | return ( 9 |
10 | {props.options.map((option) => ( 11 | props.onSelect(option)}> 16 | {option} 17 | 18 | ))} 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "show-up", 3 | "version": "0.2.0", 4 | "license": "MIT", 5 | "private": true, 6 | "workspaces": [ 7 | "packages/app", 8 | "packages/blog", 9 | "packages/protocol" 10 | ], 11 | "scripts": { 12 | "dev": "yarn workspaces app run dev", 13 | "deploy": "yarn workspace protocol deploy", 14 | "run:node": "yarn workspace protocol hardhat node", 15 | "run:wagmi": "yarn workspace app wagmi", 16 | "build": "yarn workspaces -pt run build", 17 | "test": "yarn workspaces -pt run test" 18 | }, 19 | "devDependencies": { 20 | "knip": "^4.2.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/indexer/src/utils/ipfs.ts: -------------------------------------------------------------------------------- 1 | export async function TryFetchIpfsFile(contentHash: string) { 2 | try { 3 | const response = await fetch(`https://ipfs.io/ipfs/${contentHash}`); 4 | if (response.ok) { 5 | return await response.json(); 6 | } 7 | } catch (e) { 8 | console.error("Unable to fetch from IPFS"); 9 | } 10 | try { 11 | const response = await fetch(`https://dweb.link/ipfs/${contentHash}`); 12 | if (response.ok) { 13 | return await response.json(); 14 | } 15 | } catch (e) { 16 | console.error("Unable to fetch from DWeb"); 17 | } 18 | 19 | return null; 20 | } 21 | -------------------------------------------------------------------------------- /packages/app/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { GetUpcomingEvents } from '@/services/showhub' 2 | import { Overview } from './components/Overview' 3 | import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query' 4 | import { Hero } from '@/components/Hero' 5 | 6 | export default async function Home() { 7 | const queryClient = new QueryClient() 8 | 9 | await queryClient.prefetchQuery({ 10 | queryKey: ['events', 'upcoming'], 11 | queryFn: GetUpcomingEvents, 12 | }) 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/app/next.config.js: -------------------------------------------------------------------------------- 1 | const withPWA = require('@ducanh2912/next-pwa').default({ 2 | dest: 'public', 3 | }) 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | reactStrictMode: true, 8 | staticPageGenerationTimeout: 180, 9 | webpack: (config) => { 10 | config.resolve.fallback = { fs: false, net: false, tls: false } 11 | config.externals.push('pino-pretty', 'lokijs', 'encoding') 12 | return config 13 | }, 14 | images: { 15 | remotePatterns: [ 16 | { protocol: 'https', hostname: '*.showup.events' }, 17 | { protocol: 'https', hostname: 'ipfs.io' }, 18 | ], 19 | }, 20 | } 21 | 22 | module.exports = withPWA(nextConfig) 23 | -------------------------------------------------------------------------------- /packages/app/src/components/Date.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | interface Props { 4 | date: string | number | Date 5 | } 6 | 7 | export function DateCard(props: Props) { 8 | const date = dayjs(props.date) 9 | 10 | return ( 11 |
12 |
13 | {date.format('MMM')} 14 |
15 |
16 | {date.format('DD')} 17 | {date.format('ddd')} 18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/blog/src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { GetPosts } from '@/utils/data' 2 | import { BLOG_URL } from 'app/src/utils/site' 3 | import { MetadataRoute } from 'next' 4 | 5 | export default async function sitemap(): Promise { 6 | const posts = GetPosts() 7 | const pages = [ 8 | { 9 | url: BLOG_URL, 10 | lastModified: new Date(), 11 | changeFrequency: 'weekly', 12 | priority: 1, 13 | }, 14 | ...posts.map((i) => { 15 | return { 16 | url: `${BLOG_URL}/${i.slug}`, 17 | lastModified: new Date(i.date), 18 | changeFrequency: 'never', 19 | priority: 0.6, 20 | } as any 21 | }), 22 | ] 23 | 24 | return pages 25 | } 26 | -------------------------------------------------------------------------------- /packages/app/src/app/create/components/CreateEventContext.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { PropsWithChildren, createContext, useContext } from 'react' 4 | import { ConditionModule } from '@/utils/types' 5 | 6 | interface Props extends PropsWithChildren { 7 | modules: ConditionModule[] 8 | } 9 | 10 | export const useCreateEvent = () => useContext(CreateEventContext) 11 | 12 | const CreateEventContext = createContext({ 13 | modules: [], 14 | }) 15 | 16 | export default function CreateEventProvider(props: Props) { 17 | return ( 18 | 22 | {props.children} 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/blog/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/app/src/context/Data.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { PropsWithChildren, useState } from 'react' 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 5 | import { DEFAULT_STALE_TIME } from '@/utils/site' 6 | 7 | export default function DataProvider(props: PropsWithChildren) { 8 | const [queryClient] = useState( 9 | () => 10 | new QueryClient({ 11 | defaultOptions: { 12 | queries: { 13 | // With SSR, needs a value above 0 to avoid refetching immediately on the client 14 | staleTime: DEFAULT_STALE_TIME, 15 | }, 16 | }, 17 | }) 18 | ) 19 | 20 | return {props.children} 21 | } 22 | -------------------------------------------------------------------------------- /packages/app/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SITE_DESCRIPTION, SOCIAL_GITHUB, SOCIAL_TWITTER } from '@/utils/site' 3 | import { FaGithub, FaTwitter } from 'react-icons/fa6' 4 | import { LinkComponent } from './LinkComponent' 5 | 6 | export function Footer() { 7 | return ( 8 |
9 |

{SITE_DESCRIPTION}

10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/blog/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LinkComponent } from 'app/src/components/LinkComponent' 3 | import { SITE_EMOJI, SITE_URL } from 'app/src/utils/site' 4 | 5 | export function Header() { 6 | return ( 7 |
8 | 9 |

10 | {SITE_EMOJI} 11 | Blog 12 |

13 |
14 | 15 | 16 | 17 | 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/app/src/hooks/useEvents.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import { GetEventsByOwner, GetPastEvents, GetUpcomingEvents } from '@/services/showhub' 3 | 4 | export function useEvents(past: boolean = false) { 5 | const { data, isError, isPending } = useQuery({ 6 | queryKey: ['events', past ? 'past' : 'upcoming'], 7 | queryFn: () => (past ? GetPastEvents() : GetUpcomingEvents()), 8 | }) 9 | 10 | return { 11 | data, 12 | isPending, 13 | isError, 14 | } 15 | } 16 | 17 | export function useMyEvents(address: string) { 18 | const { data, isError, isPending } = useQuery({ 19 | queryKey: ['events', address], 20 | queryFn: () => GetEventsByOwner(address), 21 | }) 22 | 23 | return { 24 | data, 25 | isPending, 26 | isError, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/app/src/app/api/upload/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse, NextRequest } from 'next/server' 2 | 3 | export async function POST(req: NextRequest) { 4 | const formData = await req.formData() 5 | const file = formData.get('file') as File 6 | console.log('IPFS Upload Request', file.name) 7 | 8 | const data = new FormData() 9 | data.append('file', file) 10 | data.append('pinataMetadata', JSON.stringify({ name: file.name })) 11 | console.log('POST files', file.name) 12 | 13 | const res = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', { 14 | method: 'POST', 15 | headers: { 16 | Authorization: `Bearer ${process.env.PINATA_JWT}`, 17 | }, 18 | body: data, 19 | }) 20 | 21 | const result = await res.json() 22 | return NextResponse.json(result, { status: 200 }) 23 | } 24 | -------------------------------------------------------------------------------- /packages/app/wagmi.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@wagmi/cli' 2 | import { actions, hardhat } from '@wagmi/cli/plugins' 3 | 4 | export default defineConfig({ 5 | out: 'src/abis.ts', 6 | contracts: [], 7 | plugins: [ 8 | actions({ 9 | getContract: true, 10 | readContract: true, 11 | prepareWriteContract: true, 12 | watchContractEvent: false, 13 | }), 14 | hardhat({ 15 | project: '../protocol', 16 | deployments: { 17 | ShowHub: { 18 | 10: '0x27d81f79D12327370cdB18DdEa03080621AEAadC', 19 | 8453: '0x27d81f79D12327370cdB18DdEa03080621AEAadC', 20 | 84532: '0x27d81f79D12327370cdB18DdEa03080621AEAadC', 21 | 11155111: '0x27d81f79D12327370cdB18DdEa03080621AEAadC', 22 | }, 23 | }, 24 | }), 25 | ], 26 | }) 27 | -------------------------------------------------------------------------------- /packages/blog/src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import Link from 'next/link' 3 | import { Post } from '@/utils/types' 4 | 5 | interface Props { 6 | post: Post 7 | } 8 | 9 | export function Card({ post }: Props) { 10 | return ( 11 | 12 |
13 |
14 |

{dayjs(post.date).format('ddd MMM DD')}

15 |

{post.title}

16 |

{post.description}

17 |
18 |
19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/app/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react' 2 | import { Header } from './Header' 3 | import { Footer } from './Footer' 4 | 5 | export function Layout(props: PropsWithChildren) { 6 | const containerClass = 'container mx-auto max-w-4xl' 7 | 8 | return ( 9 |
10 |
11 |
12 |
13 |
14 |
15 | 16 |
{props.children}
17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/blog/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SITE_INFO, SOCIAL_GITHUB, SOCIAL_TWITTER } from 'app/src/utils/site' 3 | import { FaGithub, FaTwitter } from 'react-icons/fa6' 4 | import { LinkComponent } from 'app/src/components/LinkComponent' 5 | 6 | export function Footer() { 7 | return ( 8 |
9 |

{SITE_INFO}

10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/app/src/context/Web3.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { PropsWithChildren } from 'react' 4 | import { State, WagmiProvider } from 'wagmi' 5 | import { WAGMI_CONFIG } from '@/utils/network' 6 | import DataProvider from './Data' 7 | import { CONFIG } from '@/utils/config' 8 | import { createWeb3Modal } from '@web3modal/wagmi/react' 9 | 10 | interface Props extends PropsWithChildren { 11 | initialState?: State 12 | } 13 | 14 | createWeb3Modal({ 15 | wagmiConfig: WAGMI_CONFIG, 16 | projectId: CONFIG.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, 17 | enableAnalytics: true, 18 | }) 19 | 20 | export function Web3Provider(props: Props) { 21 | return ( 22 | <> 23 | 24 | {props.children} 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/indexer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sup-indexer", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "clean": "tsc --clean", 6 | "build": "tsc --build", 7 | "watch": "tsc --watch", 8 | "mocha": "ts-mocha test/**/*.ts", 9 | "codegen": "envio codegen", 10 | "dev": "envio dev", 11 | "test": "pnpm mocha", 12 | "start": "ts-node generated/src/Index.bs.js" 13 | }, 14 | "devDependencies": { 15 | "@types/expect": "^24.3.0", 16 | "@types/mocha": "10.0.6", 17 | "@types/node": "20.8.8", 18 | "mocha": "10.2.0", 19 | "ts-mocha": "^10.0.0", 20 | "ts-node": "10.9.1", 21 | "typescript": "5.2.2" 22 | }, 23 | "dependencies": { 24 | "@ensdomains/ensjs": "^3.4.2", 25 | "dayjs": "^1.11.10", 26 | "envio": "0.0.28", 27 | "ethers": "6.8.0", 28 | "slugify": "^1.6.6", 29 | "viem": "^1.20.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/app/src/utils/site.ts: -------------------------------------------------------------------------------- 1 | export const SITE_EMOJI = '😎👋' 2 | export const SITE_NAME = 'Show Up' 3 | export const SITE_SHORT_NAME = 'Sup' 4 | export const SITE_DESCRIPTION = 'Earn $$ by showing up!' 5 | export const SITE_INFO = 'Onchain RSVP and Event management' 6 | export const SITE_DOMAIN = 'showup.events' 7 | export const SITE_URL = 8 | process.env.NETWORK_ENV === 'test' ? `https://test.${SITE_DOMAIN}` : `https://www.${SITE_DOMAIN}` 9 | 10 | export const BLOG_NAME = 'Show Up Blog' 11 | export const BLOG_DOMAIN = 'blog.showup.events' 12 | export const BLOG_URL = `https://${BLOG_DOMAIN}` 13 | 14 | export const SOCIAL_TWITTER = 'wslyvh' 15 | export const SOCIAL_GITHUB = 'wslyvh/show-up' 16 | 17 | export const DEFAULT_REVALIDATE_PERIOD = 600 // in seconds // 600 = 10 mins // 3600 = 1 hour 18 | export const DEFAULT_STALE_TIME = 60 * 1000 // in milliseconds // 60 * 1000 = 1 min 19 | -------------------------------------------------------------------------------- /packages/app/src/app/manifest.ts: -------------------------------------------------------------------------------- 1 | import { SITE_DESCRIPTION, SITE_NAME, SITE_SHORT_NAME } from '@/utils/site' 2 | import { MetadataRoute } from 'next' 3 | 4 | export default function manifest(): MetadataRoute.Manifest { 5 | return { 6 | name: SITE_NAME, 7 | short_name: SITE_SHORT_NAME, 8 | description: SITE_DESCRIPTION, 9 | lang: 'en', 10 | start_url: '/', 11 | display: 'standalone', 12 | background_color: '#0f1729', 13 | theme_color: '#000000', 14 | icons: [ 15 | { src: '/icons/favicon.ico', sizes: 'any', type: 'image/x-icon' }, 16 | { src: '/icons/sup-192x192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, 17 | { src: '/icons/sup-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, 18 | { src: '/icons/sup-maskable', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, 19 | ], 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/app/src/app/tickets/page.tsx: -------------------------------------------------------------------------------- 1 | import { Protected } from '@/components/Protected' 2 | import { Overview } from './components/Overview' 3 | import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query' 4 | import { GetEventsByRegistration } from '@/services/showhub' 5 | import { Suspense } from 'react' 6 | 7 | export default async function TicketsPage() { 8 | const queryClient = new QueryClient() 9 | 10 | const defaultAddress = '0x' // Pre-fetch default empty address 11 | await queryClient.prefetchQuery({ 12 | queryKey: ['tickets', defaultAddress], 13 | queryFn: () => GetEventsByRegistration(defaultAddress), 14 | }) 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "app": "0.2.0", 13 | "dayjs": "^1.11.10", 14 | "gray-matter": "^4.0.3", 15 | "marked": "^11.0.0", 16 | "next": "14.0.3", 17 | "next-plausible": "^3.12.0", 18 | "react": "^18", 19 | "react-dom": "^18" 20 | }, 21 | "devDependencies": { 22 | "@tailwindcss/typography": "^0.5.10", 23 | "@types/node": "^20", 24 | "@types/react": "^18", 25 | "@types/react-dom": "^18", 26 | "autoprefixer": "^10.0.1", 27 | "daisyui": "^4.4.19", 28 | "eslint": "^8", 29 | "eslint-config-next": "14.0.3", 30 | "postcss": "^8", 31 | "tailwindcss": "^3.3.0", 32 | "typescript": "^5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/app/src/hooks/useAllowance.ts: -------------------------------------------------------------------------------- 1 | import { AddressZero } from '@/utils/network' 2 | import { useEffect } from 'react' 3 | import { erc20Abi } from 'viem' 4 | import { useBlockNumber, useReadContract } from 'wagmi' 5 | 6 | export function useAllowance(owner: string, spender: string, tokenAddress: string = AddressZero) { 7 | const { data: blockNumber } = useBlockNumber({ watch: true }) 8 | const { 9 | data: allowance, 10 | refetch, 11 | isLoading, 12 | isError, 13 | } = useReadContract({ 14 | address: tokenAddress, 15 | abi: erc20Abi, 16 | functionName: 'allowance', 17 | args: [owner, spender], 18 | query: { 19 | enabled: tokenAddress !== AddressZero, 20 | }, 21 | }) 22 | 23 | useEffect(() => { 24 | refetch() 25 | }, [blockNumber]) 26 | 27 | return { 28 | allowance: BigInt((allowance as string) ?? 0), 29 | refetch, 30 | isLoading, 31 | isError, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/app/src/services/storage.ts: -------------------------------------------------------------------------------- 1 | import { CONFIG } from '@/utils/config' 2 | import PinataClient from '@pinata/sdk' 3 | 4 | export async function Store(name: string, data: any) { 5 | console.log('Store Data file to IPFS', name) 6 | const pinata = PinataClient(CONFIG.NEXT_PUBLIC_PINATA_API_KEY, CONFIG.NEXT_PUBLIC_PINATA_API_SECRET) 7 | const res = await pinata.pinJSONToIPFS(data, { 8 | pinataMetadata: { 9 | name: name, 10 | }, 11 | pinataOptions: { 12 | cidVersion: 0, 13 | }, 14 | }) 15 | 16 | return res.IpfsHash 17 | } 18 | 19 | export async function Upload(file: File) { 20 | console.log('Upload file over API', file.name) 21 | const formData = new FormData() 22 | formData.append('file', file, file.name) 23 | 24 | const res = await fetch('/api/upload', { 25 | method: 'POST', 26 | body: formData, 27 | }) 28 | 29 | const data = await res.json() 30 | return data.IpfsHash 31 | } 32 | -------------------------------------------------------------------------------- /packages/blog/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react' 2 | import { Header } from './Header' 3 | import { Footer } from './Footer' 4 | 5 | export function BlogLayout(props: PropsWithChildren) { 6 | const containerClass = 'container mx-auto max-w-4xl' 7 | 8 | return ( 9 |
10 |
11 |
12 |
13 |
14 |
15 | 16 |
{props.children}
17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/app/src/components/MobileLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react' 2 | import { Header } from './Header' 3 | import { Navbar } from './Navbar' 4 | import { TestnetAlert } from './Testnet' 5 | 6 | const containerClass = 'container mx-auto max-w-4xl' 7 | 8 | export function MobileLayout(props: PropsWithChildren) { 9 | return ( 10 |
11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
{props.children}
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /packages/app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # hardhat 38 | contracts/node_modules 39 | contracts/.env 40 | contracts/coverage 41 | contracts/coverage.json 42 | contracts/typechain 43 | contracts/typechain-types 44 | 45 | contracts/cache 46 | contracts/artifacts 47 | 48 | # wagmi recommends to ignore their generated file 49 | # /src/abis.ts 50 | 51 | # subgraph 52 | subgraph/node_modules 53 | subgraph/build 54 | 55 | **public/sw.js 56 | **public/sw* 57 | **public/workbox-** -------------------------------------------------------------------------------- /packages/protocol/contracts/Common.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | error AccessDenied(); 5 | error AlreadyRegistered(); 6 | error AlreadyStarted(); 7 | error InactiveRecord(); 8 | error IncorrectValue(); 9 | error InvalidAddress(); 10 | error InvalidDate(); 11 | error LimitReached(); 12 | error NoAttendees(); 13 | error NotFound(); 14 | error NotWhitelisted(); 15 | error UnexpectedConditionModuleError(); 16 | 17 | enum Status { 18 | Active, 19 | Cancelled, 20 | Settled 21 | } 22 | 23 | struct Record { 24 | uint256 id; 25 | uint256 endDate; 26 | uint256 limit; 27 | address owner; 28 | Status status; 29 | string contentUri; 30 | address conditionModule; 31 | // 32 | uint256 totalRegistrations; 33 | uint256 totalAttendees; 34 | mapping(uint256 => address) registrationIndex; 35 | mapping(address => Registrations) registrations; 36 | } 37 | 38 | struct Registrations { 39 | bool registered; 40 | bool attended; 41 | } 42 | -------------------------------------------------------------------------------- /packages/subgraph/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subgraph", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "codegen": "graph codegen", 7 | "build": "graph build --network optimism", 8 | "build:sepolia": "graph build --network sepolia", 9 | "deploy": "graph deploy --node https://api.studio.thegraph.com/deploy/ show-up-optimism", 10 | "deploy:sepolia": "graph deploy --node https://api.studio.thegraph.com/deploy/ show-up-sepolia --network sepolia", 11 | "create-local": "graph create --node http://localhost:8020/ show-up-protocol", 12 | "remove-local": "graph remove --node http://localhost:8020/ show-up-protocol", 13 | "deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 show-up-protocol", 14 | "test": "graph test" 15 | }, 16 | "dependencies": { 17 | "@graphprotocol/graph-cli": "^0.60.0", 18 | "@graphprotocol/graph-ts": "^0.31.0" 19 | }, 20 | "devDependencies": { 21 | "matchstick-as": "0.5.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # hardhat 40 | **/protocol/node_modules 41 | **/protocol/.env 42 | **/protocol/coverage 43 | **/protocol/coverage.json 44 | **/protocol/typechain 45 | **/protocol/typechain-types 46 | 47 | **/protocol/cache 48 | **/protocol/artifacts 49 | 50 | # subgraph 51 | **/subgraph/node_modules 52 | **/subgraph/build 53 | 54 | # wagmi recommends to ignore their generated file 55 | # /src/abis.ts 56 | 57 | # subgraph 58 | **/subgraph/node_modules 59 | **/subgraph/build -------------------------------------------------------------------------------- /packages/protocol/contracts/interfaces/IConditionModule.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import '../Common.sol'; 5 | 6 | interface IConditionModule { 7 | function initialize(uint256 id, bytes calldata data) external returns (bool); 8 | 9 | function cancel( 10 | uint256 id, 11 | address owner, 12 | address[] calldata registrations, 13 | bytes calldata data 14 | ) external returns (bool); 15 | 16 | function fund(uint256 id, address sender, bytes calldata data) external payable returns (bool); 17 | 18 | function register( 19 | uint256 id, 20 | address participant, 21 | address sender, 22 | bytes calldata data 23 | ) external payable returns (bool); 24 | 25 | function checkin(uint256 id, address[] calldata attendees, bytes calldata data) external returns (bool); 26 | 27 | function settle(uint256 id, address[] calldata attendees, bytes calldata data) external returns (bool); 28 | 29 | function name() external view returns (string memory); 30 | } 31 | -------------------------------------------------------------------------------- /packages/app/src/app/events/[slug]/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import { GetEventBySlug } from '@/services/showhub' 2 | import EventDataProvider from '@/context/EventData' 3 | import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query' 4 | import { CheckinOverview } from '../../components/Admin/CheckinOverview' 5 | import { Protected } from '@/components/Protected' 6 | 7 | interface Params { 8 | params: { slug: string } 9 | searchParams: { [key: string]: string | string[] | undefined } 10 | } 11 | 12 | export default async function EventsPage({ params }: Params) { 13 | const queryClient = new QueryClient() 14 | 15 | await queryClient.prefetchQuery({ 16 | queryKey: ['events', params.slug], 17 | queryFn: () => GetEventBySlug(params.slug), 18 | }) 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /packages/app/src/components/OpenGraph.tsx: -------------------------------------------------------------------------------- 1 | import { SITE_EMOJI, SITE_INFO, SITE_SHORT_NAME } from '@/utils/site' 2 | import { ImageResponse } from 'next/og' 3 | 4 | export function defaultOpenGraphImage() { 5 | return new ImageResponse( 6 | ( 7 |
18 |

19 | {SITE_EMOJI} {SITE_SHORT_NAME} 20 |

21 |

Onchain Events & RSVP

22 |

Increase event participation. Reward attendees.

23 |
24 | ) 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/app/src/components/LinkComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import Link from 'next/link' 3 | 4 | interface Props { 5 | href: string 6 | children: ReactNode 7 | download?: boolean | string 8 | ariaLabel?: string 9 | isExternal?: boolean 10 | className?: string 11 | } 12 | 13 | export function LinkComponent(props: Props) { 14 | if (!props.href) return <>{props.children} 15 | const className = props.className ?? '' 16 | const isExternal = props.href.match(/^([a-z0-9]*:|.{0})\/\/.*$/) || props.isExternal 17 | 18 | if (isExternal) { 19 | return ( 20 | 27 | {props.children} 28 | 29 | ) 30 | } 31 | 32 | return ( 33 | 34 | {props.children} 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /packages/blog/src/app/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | import { BLOG_NAME, SITE_EMOJI, SITE_NAME } from 'app/src/utils/site' 2 | import { ImageResponse } from 'next/og' 3 | 4 | // Route segment config 5 | export const runtime = 'edge' 6 | 7 | // Image metadata 8 | export const alt = SITE_NAME 9 | export const size = { width: 1200, height: 630 } 10 | export const contentType = 'image/png' 11 | 12 | export default async function Image() { 13 | return new ImageResponse( 14 |
24 | 25 |

{SITE_EMOJI}

26 | {SITE_NAME} 27 | B L O G 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /packages/app/src/app/events/components/Admin/Actions.tsx: -------------------------------------------------------------------------------- 1 | import { LinkComponent } from '@/components/LinkComponent' 2 | import { Record } from '@/utils/types' 3 | import { Cancel } from './Cancel' 4 | import { Settle } from './Settle' 5 | import { useEventData } from '@/context/EventData' 6 | 7 | interface Props { 8 | record: Record 9 | } 10 | 11 | export function AdminActions({ record }: Props) { 12 | const eventData = useEventData() 13 | 14 | return ( 15 |
16 | 17 | {eventData.isActive && ( 18 | 19 | 22 | 23 | )} 24 | {!eventData.isActive && ( 25 | 28 | )} 29 | 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /packages/app/src/app/profile/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import { Connect } from '@/components/Connect' 2 | import React from 'react' 3 | 4 | export function Login() { 5 | return ( 6 |
7 |
8 | 11 | 12 | 13 |
14 | 15 |
OR
16 | 17 |
18 |
19 | 22 | 23 |
24 | 25 |
26 | 29 |
30 |
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /packages/app/src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { GetAllEvents } from '@/services/showhub' 2 | import { SITE_URL } from '@/utils/site' 3 | import { Status } from '@/utils/types' 4 | import { MetadataRoute } from 'next' 5 | 6 | export default async function sitemap(): Promise { 7 | const events = await GetAllEvents() 8 | const pages = [ 9 | { 10 | url: SITE_URL, 11 | lastModified: new Date(), 12 | changeFrequency: 'always', 13 | priority: 1, 14 | }, 15 | { 16 | url: `${SITE_URL}/past`, 17 | lastModified: new Date(), 18 | changeFrequency: 'always', 19 | priority: 0.8, 20 | }, 21 | ...events.map((i) => { 22 | const isActive = i.status == Status.Active 23 | const isCancelled = i.status == Status.Cancelled 24 | 25 | return { 26 | url: `${SITE_URL}/events/${i.id}`, 27 | lastModified: new Date(i.createdAt), 28 | changeFrequency: isActive ? 'daily' : 'never', 29 | priority: isCancelled ? 0.1 : isActive ? 0.6 : 0.4, 30 | } as any 31 | }), 32 | ] 33 | 34 | return pages 35 | } 36 | -------------------------------------------------------------------------------- /packages/app/src/components/Alert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | CheckCircleIcon, 4 | ExclamationCircleIcon, 5 | ExclamationTriangleIcon, 6 | InformationCircleIcon, 7 | } from '@heroicons/react/24/outline' 8 | 9 | interface Props { 10 | type: 'success' | 'info' | 'warning' | 'error' 11 | message: string 12 | className?: string 13 | } 14 | 15 | export function Alert(props: Props) { 16 | let className = `alert alert-${props.type} flex flex-row text-left items-start` 17 | if (props.className) className += ` ${props.className}` 18 | const iconClassName = `stroke-${props.type} shrink-0 h-6 w-6 text-${props.type}-400` 19 | 20 | return ( 21 |
22 | {props.type === 'success' && } 23 | {props.type === 'info' && } 24 | {props.type === 'warning' && } 25 | {props.type === 'error' && } 26 | {props.message} 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 wslyvh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/app/src/components/Testnet.tsx: -------------------------------------------------------------------------------- 1 | import { CONFIG } from '@/utils/config' 2 | import React from 'react' 3 | import { LinkComponent } from './LinkComponent' 4 | import { SITE_URL } from '@/utils/site' 5 | 6 | export function TestnetAlert() { 7 | if (CONFIG.NETWORK_ENV === 'test') { 8 | return ( 9 |
10 | 15 | 20 | 21 |
22 |

Show Up Testnet

23 |
Please note that you're connected to a test network on Sepolia.
24 |
25 | 26 | Go to Main 27 | 28 |
29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/app/src/app/components/Overview.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Card } from './Card' 4 | import { useEvents } from '@/hooks/useEvents' 5 | import { Empty } from '@/components/Empty' 6 | import { Tabs } from './Tabs' 7 | import { usePathname, useRouter } from 'next/navigation' 8 | 9 | export function Overview() { 10 | const router = useRouter() 11 | const pathname = usePathname() 12 | const pastEvents = pathname === '/past' 13 | let { data, isError } = useEvents(pastEvents) 14 | 15 | if (!data || isError) return 16 | 17 | const onSelect = (option: string) => { 18 | if (option === 'Upcoming') router.push('/') 19 | if (option === 'Past') router.push('/past') 20 | } 21 | 22 | return ( 23 | <> 24 |
25 | 26 |
27 | 28 |
29 | {data.map((event) => ( 30 | 31 | ))} 32 |
33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /packages/blog/src/utils/data.ts: -------------------------------------------------------------------------------- 1 | 2 | import fs from 'fs' 3 | import { join } from 'path' 4 | import matter from 'gray-matter' 5 | import { Post } from './types' 6 | 7 | const baseFolder = 'data' 8 | 9 | export function GetPosts() { 10 | const dir = join(process.cwd(), baseFolder, 'posts') 11 | const files = fs.readdirSync(dir, { withFileTypes: true }) 12 | .filter(i => i.isFile() && i.name.endsWith('.md')) 13 | 14 | const items = files.map(i => { 15 | const fullPath = join(dir, i.name) 16 | const content = fs.readFileSync(fullPath, 'utf8') 17 | if (!content) { 18 | console.log('File has no content..', i.name) 19 | } 20 | 21 | if (content) { 22 | const doc = matter(content) 23 | return { 24 | ...doc.data, 25 | slug: i.name.replace('.md', ''), 26 | date: new Date(doc.data.date as string).getTime(), 27 | body: doc.content 28 | } 29 | } 30 | }).filter(i => !!i) as Array 31 | 32 | return items.filter(i => i.date < new Date().getTime()).sort((a, b) => b.date - a.date) 33 | } 34 | -------------------------------------------------------------------------------- /packages/protocol/test/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, ethers } from "ethers" 2 | import dayjs from "dayjs" 3 | 4 | export enum Status { 5 | Active = 0, 6 | Cancelled = 1, 7 | Settled = 2, 8 | } 9 | 10 | export const defaultContentUri = 'ipfs://bafkreiacvvoznsgbabpj6cz27iratlwp7kdsyu7buiaenzzaemxqbwolvm' 11 | export const defaultDepositFee = ethers.utils.parseUnits('0.01', 18) // 0.02 ether 12 | export const defaultTokenDepositFee = ethers.utils.parseUnits('10', 18) // 10 tokens 13 | export const defaultFundFee = ethers.utils.parseUnits('1', 18) // 1 ether 14 | export const defaultTokenFee = BigNumber.from('2000000000000000000') // 2 ether 15 | export const defaultTokenMint = BigNumber.from('100000000000000000000') // 100 ether 16 | 17 | export const defaultMaxParticipants = 100 18 | 19 | export const eventMetadata = { 20 | appId: "showup-test", 21 | title: "Test Event", 22 | description: "Lorem ipsum dolor sit amet..", 23 | start: dayjs().add(1, 'day').toISOString(), 24 | end: dayjs().add(5, 'day').toISOString(), 25 | timezone: "Europe/Amsterdam", 26 | location: "0xOnline", 27 | website: "https://www.showup.events/", 28 | imageUrl: "ipfs://bafybeick4wvngyahuhaw5qbwzfs2m7opmptj2cgypvjpho2o225o5tzhxa", 29 | links: [], 30 | tags: [] 31 | } -------------------------------------------------------------------------------- /packages/indexer/src/utils/mapping.ts: -------------------------------------------------------------------------------- 1 | import slugify from "slugify" 2 | 3 | export function GetStatusName(number: number = 0) { 4 | switch (number) { 5 | case 0: return 'Active' 6 | case 1: return 'Cancelled' 7 | case 2: return 'Settled' 8 | } 9 | 10 | return 'Active' 11 | } 12 | 13 | export function GetStatusId(status: 'Active' | 'Cancelled' | 'Settled' = 'Active') { 14 | switch (status) { 15 | case 'Active': return 0 16 | case 'Cancelled': return 1 17 | case 'Settled': return 2 18 | } 19 | } 20 | 21 | export function GetVisibilityName(number: number = 0) { 22 | switch (number) { 23 | case 0: return 'Public' 24 | case 1: return 'Unlisted' 25 | } 26 | 27 | return 'Public' 28 | } 29 | 30 | export function GetVisibilityId(visibility: string | number) { 31 | if (typeof visibility === 'number') { 32 | return visibility 33 | } 34 | 35 | switch (visibility) { 36 | case 'Public': return 0 37 | case 'Unlisted': return 1 38 | } 39 | 40 | return 0 41 | } 42 | 43 | export function TruncateMiddle(text: string, length: number = 5) { 44 | if (text?.length > length * 2 + 1) { 45 | return `${text.substring(0, length)}...${text.substring(text.length - length, text.length)}` 46 | } 47 | 48 | return text 49 | } 50 | 51 | export function Slugify(value: string) { 52 | return slugify(value, { strict: true, lower: true }) 53 | } -------------------------------------------------------------------------------- /packages/protocol/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "protocol", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "hardhat compile", 7 | "deploy": "npx hardhat run scripts/deploy.ts", 8 | "deploy:token": "npx hardhat run scripts/deploy-token.ts", 9 | "run:node": "hardhat node", 10 | "run:create": "npx hardhat run scripts/create.ts", 11 | "run:verify": "npx hardhat run scripts/verify.ts", 12 | "coverage": "hardhat coverage", 13 | "test": "REPORT_GAS=true hardhat test" 14 | }, 15 | "devDependencies": { 16 | "@ethersproject/abi": "^5.7.0", 17 | "@ethersproject/providers": "^5.7.2", 18 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.0", 19 | "@nomicfoundation/hardhat-network-helpers": "^1.0.0", 20 | "@nomicfoundation/hardhat-toolbox": "^2.0.0", 21 | "@nomiclabs/hardhat-ethers": "^2.2.3", 22 | "@nomiclabs/hardhat-etherscan": "^3.1.7", 23 | "@openzeppelin/contracts": "^5.0.0", 24 | "@typechain/ethers-v5": "^10.1.0", 25 | "@typechain/hardhat": "^6.1.2", 26 | "@types/chai": "^4.2.0", 27 | "@types/dotenv": "^8.2.0", 28 | "@types/mocha": ">=9.1.0", 29 | "@types/node": ">=12.0.0", 30 | "chai": "^4.2.0", 31 | "dotenv": "^16.3.1", 32 | "ethers": "5.7.2", 33 | "hardhat": "^2.19.4", 34 | "hardhat-gas-reporter": "^1.0.9", 35 | "solidity-coverage": "^0.8.5", 36 | "ts-node": "^10.9.1", 37 | "typechain": "^8.3.1", 38 | "typescript": "^5.2.2" 39 | }, 40 | "dependencies": { 41 | "dayjs": "^1.11.10" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/app/src/app/tickets/components/Overview.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Tabs } from '@/app/components/Tabs' 4 | import { Ticket } from './Ticket' 5 | import { Empty } from '@/components/Empty' 6 | import { useTickets } from '@/hooks/useTickets' 7 | import { useAccount } from 'wagmi' 8 | import { useRouter, useSearchParams } from 'next/navigation' 9 | import dayjs from 'dayjs' 10 | 11 | export function Overview() { 12 | const router = useRouter() 13 | const searchQuery = useSearchParams() 14 | const { address } = useAccount() 15 | const tickets = useTickets() 16 | const pastEvents = searchQuery.get('filter') === 'past' 17 | 18 | if (tickets.isError || tickets.data?.length === 0) return 19 | 20 | const onSelect = (option: string) => { 21 | if (option === 'Upcoming') router.push('?filter=upcoming') 22 | if (option === 'Past') router.push('?filter=past') 23 | } 24 | 25 | return ( 26 | <> 27 |
28 | 29 |
30 | 31 |
32 | {tickets.data 33 | ?.filter((i) => 34 | // TODO: Filter on server/client integration 35 | pastEvents ? dayjs(i.metadata?.end).isBefore(dayjs()) : dayjs(i.metadata?.end).isAfter(dayjs()) 36 | ) 37 | .map((ticket) => )} 38 |
39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /packages/protocol/contracts/mocks/TrueMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; 5 | import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; 6 | import '../Common.sol'; 7 | 8 | contract TrueMock is Ownable { 9 | constructor(address owner) Ownable(owner) {} 10 | 11 | function initialize(uint256 id, bytes calldata data) external virtual onlyOwner returns (bool) { 12 | return true; 13 | } 14 | 15 | function cancel( 16 | uint256 id, 17 | address owner, 18 | address[] calldata registrations, 19 | bytes calldata data 20 | ) external virtual onlyOwner returns (bool) { 21 | return true; 22 | } 23 | 24 | function fund(uint256 id, address sender, bytes calldata data) external payable virtual onlyOwner returns (bool) { 25 | return true; 26 | } 27 | 28 | function register( 29 | uint256 id, 30 | address participant, 31 | address sender, 32 | bytes calldata data 33 | ) external payable virtual onlyOwner returns (bool) { 34 | return true; 35 | } 36 | 37 | function checkin( 38 | uint256 id, 39 | address[] calldata attendees, 40 | bytes calldata data 41 | ) external virtual onlyOwner returns (bool) { 42 | return true; 43 | } 44 | 45 | function settle( 46 | uint256 id, 47 | address[] calldata attendees, 48 | bytes calldata data 49 | ) external virtual onlyOwner returns (bool) { 50 | return true; 51 | } 52 | 53 | function name() external view returns (string memory) { 54 | return 'TrueMock'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/protocol/contracts/mocks/FalseMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; 5 | import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; 6 | import '../Common.sol'; 7 | 8 | contract FalseMock is Ownable { 9 | constructor(address owner) Ownable(owner) {} 10 | 11 | function initialize(uint256 id, bytes calldata data) external virtual onlyOwner returns (bool) { 12 | return false; 13 | } 14 | 15 | function cancel( 16 | uint256 id, 17 | address owner, 18 | address[] calldata registrations, 19 | bytes calldata data 20 | ) external virtual onlyOwner returns (bool) { 21 | return false; 22 | } 23 | 24 | function fund(uint256 id, address sender, bytes calldata data) external payable virtual onlyOwner returns (bool) { 25 | return false; 26 | } 27 | 28 | function register( 29 | uint256 id, 30 | address participant, 31 | address sender, 32 | bytes calldata data 33 | ) external payable virtual onlyOwner returns (bool) { 34 | return false; 35 | } 36 | 37 | function checkin( 38 | uint256 id, 39 | address[] calldata attendees, 40 | bytes calldata data 41 | ) external virtual onlyOwner returns (bool) { 42 | return false; 43 | } 44 | 45 | function settle( 46 | uint256 id, 47 | address[] calldata attendees, 48 | bytes calldata data 49 | ) external virtual onlyOwner returns (bool) { 50 | return false; 51 | } 52 | 53 | function name() external view returns (string memory) { 54 | return 'FalseMock'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/protocol/deployments.json: -------------------------------------------------------------------------------- 1 | { 2 | "10": { 3 | "ShowHub": "0x27d81f79D12327370cdB18DdEa03080621AEAadC", 4 | "RecipientEther": "0x855ac352180E70A091c078B330031Fa3029200f5", 5 | "RecipientToken": "0xBB345dce41213ceC911D75854582f943fBF7D8dD", 6 | "SplitEther": "0x805164435e4188675056299f50E81Fc63994AC0E", 7 | "SplitToken": "0x561701F67B3BdC6fb1A3905a4B5DEDC27F19Ec56" 8 | }, 9 | "8453": { 10 | "ShowHub": "0x27d81f79D12327370cdB18DdEa03080621AEAadC", 11 | "RecipientEther": "0x855ac352180E70A091c078B330031Fa3029200f5", 12 | "RecipientToken": "0xBB345dce41213ceC911D75854582f943fBF7D8dD", 13 | "SplitEther": "0x805164435e4188675056299f50E81Fc63994AC0E", 14 | "SplitToken": "0x561701F67B3BdC6fb1A3905a4B5DEDC27F19Ec56" 15 | }, 16 | "84532": { 17 | "ShowHub": "0x27d81f79D12327370cdB18DdEa03080621AEAadC", 18 | "RecipientEther": "0x855ac352180E70A091c078B330031Fa3029200f5", 19 | "RecipientToken": "0xBB345dce41213ceC911D75854582f943fBF7D8dD", 20 | "SplitEther": "0x805164435e4188675056299f50E81Fc63994AC0E", 21 | "SplitToken": "0x561701F67B3BdC6fb1A3905a4B5DEDC27F19Ec56", 22 | "Token": "0x555B9c3B79EF437776F7E0833c234c802D741771" 23 | }, 24 | "11155111": { 25 | "ShowHub": "0x27d81f79D12327370cdB18DdEa03080621AEAadC", 26 | "RecipientEther": "0x855ac352180e70a091c078b330031fa3029200f5", 27 | "RecipientToken": "0xBB345dce41213ceC911D75854582f943fBF7D8dD", 28 | "SplitEther": "0x805164435e4188675056299f50E81Fc63994AC0E", 29 | "SplitToken": "0x561701f67b3bdc6fb1a3905a4b5dedc27f19ec56", 30 | "Token": "0x796b9850Be63Ffa903eD2854164c21189DbB4B89" 31 | } 32 | } -------------------------------------------------------------------------------- /packages/protocol/contracts/mocks/FalseCreateMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; 5 | import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; 6 | import '../Common.sol'; 7 | 8 | contract FalseCreateMock is Ownable { 9 | constructor(address owner) Ownable(owner) {} 10 | 11 | function initialize(uint256 id, bytes calldata data) external virtual onlyOwner returns (bool) { 12 | return true; 13 | } 14 | 15 | function cancel( 16 | uint256 id, 17 | address owner, 18 | address[] calldata registrations, 19 | bytes calldata data 20 | ) external virtual onlyOwner returns (bool) { 21 | return false; 22 | } 23 | 24 | function fund(uint256 id, address sender, bytes calldata data) external payable virtual onlyOwner returns (bool) { 25 | return false; 26 | } 27 | 28 | function register( 29 | uint256 id, 30 | address participant, 31 | address sender, 32 | bytes calldata data 33 | ) external payable virtual onlyOwner returns (bool) { 34 | return false; 35 | } 36 | 37 | function checkin( 38 | uint256 id, 39 | address[] calldata attendees, 40 | bytes calldata data 41 | ) external virtual onlyOwner returns (bool) { 42 | return false; 43 | } 44 | 45 | function settle( 46 | uint256 id, 47 | address[] calldata attendees, 48 | bytes calldata data 49 | ) external virtual onlyOwner returns (bool) { 50 | return false; 51 | } 52 | 53 | function name() external view returns (string memory) { 54 | return 'FalseCreateMock'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/protocol/contracts/mocks/FalseSettleMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; 5 | import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; 6 | import '../Common.sol'; 7 | 8 | contract FalseSettleMock is Ownable { 9 | constructor(address owner) Ownable(owner) {} 10 | 11 | function initialize(uint256 id, bytes calldata data) external virtual onlyOwner returns (bool) { 12 | return true; 13 | } 14 | 15 | function cancel( 16 | uint256 id, 17 | address owner, 18 | address[] calldata registrations, 19 | bytes calldata data 20 | ) external virtual onlyOwner returns (bool) { 21 | return true; 22 | } 23 | 24 | function fund(uint256 id, address sender, bytes calldata data) external payable virtual onlyOwner returns (bool) { 25 | return true; 26 | } 27 | 28 | function register( 29 | uint256 id, 30 | address participant, 31 | address sender, 32 | bytes calldata data 33 | ) external payable virtual onlyOwner returns (bool) { 34 | return true; 35 | } 36 | 37 | function checkin( 38 | uint256 id, 39 | address[] calldata attendees, 40 | bytes calldata data 41 | ) external virtual onlyOwner returns (bool) { 42 | return true; 43 | } 44 | 45 | function settle( 46 | uint256 id, 47 | address[] calldata attendees, 48 | bytes calldata data 49 | ) external virtual onlyOwner returns (bool) { 50 | return false; 51 | } 52 | 53 | function name() external view returns (string memory) { 54 | return 'FalseSettleMock'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/app/src/app/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { GetEventsByOwner, GetUser } from '@/services/showhub' 2 | import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query' 3 | import { Profile } from './components/Profile' 4 | import { SITE_NAME, SITE_URL } from '@/utils/site' 5 | 6 | interface Params { 7 | params: { id: string } 8 | searchParams: { [key: string]: string | string[] | undefined } 9 | } 10 | 11 | export async function generateMetadata({ params }: Params) { 12 | const owner = await GetUser(params.id) 13 | if (!owner) return {} 14 | 15 | const baseUri = new URL(`${SITE_URL}/${params.id}`) 16 | const title = `${owner.name} @ ${SITE_NAME}` 17 | const description = `Check out all the events of ${owner.name} on Show Up.` 18 | 19 | return { 20 | title: title, 21 | description: description, 22 | metadataBase: new URL(baseUri), 23 | openGraph: { 24 | title: title, 25 | description: description, 26 | images: owner.avatar ?? `${baseUri}/opengraph-image`, 27 | }, 28 | twitter: { 29 | title: title, 30 | description: description, 31 | images: owner.avatar ?? `${baseUri}/opengraph-image`, 32 | card: 'summary', 33 | }, 34 | } 35 | } 36 | 37 | export default async function OrganizerPage({ params }: Params) { 38 | const queryClient = new QueryClient() 39 | 40 | await queryClient.prefetchQuery({ 41 | queryKey: ['events', 'owner', params.id], 42 | queryFn: () => GetEventsByOwner(params.id), 43 | }) 44 | 45 | return ( 46 | 47 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /packages/app/src/components/SelectBox.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | import { Combobox } from '@headlessui/react' 5 | 6 | interface Props { 7 | id?: string 8 | options: string[] 9 | defaultValue?: string 10 | onChange: (value: string) => void 11 | } 12 | 13 | export function SelectBox(props: Props) { 14 | const [value, setValue] = useState(props.defaultValue) 15 | const [query, setQuery] = useState('') 16 | 17 | const filtered = 18 | query === '' 19 | ? props.options 20 | : props.options.filter((i: string) => { 21 | return i.toLowerCase().includes(query.toLowerCase()) 22 | }) 23 | 24 | function handleChange(value: string) { 25 | setValue(value) 26 | props.onChange(value) 27 | } 28 | 29 | return ( 30 | 31 | <> 32 | setQuery(event.target.value)} 34 | className='input input-sm input-bordered w-full' 35 | /> 36 | 37 | {filtered.map((i) => { 38 | let className = 'py-2 px-4 text-sm cursor-pointer hover:bg-base-100' 39 | if (i === value) className += ' text-white font-bold' 40 | 41 | return ( 42 | 43 | {i} 44 | 45 | ) 46 | })} 47 | 48 | 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **NOTE** Show Up Protocol has been thoughtfully designed, built and reviewed by external developers. However, it has not been audited yet. Please check the [contracts and documentation](./packages/protocol/) and use at your own risk. 2 | 3 | # Sup 😎👋 4 | 5 | Onchain RSVP and Event management protocol designed to reshape event participation 6 | 7 | - https://www.showup.events/ 8 | 9 | ## Packages 📦 10 | 11 | - [App](./packages/app) - Next.js Mobile PWA 12 | - [Protocol](./packages/protocol) - Smart Contract layer (Hardhat) 13 | - [Subgraph](./packages/subgraph) - The Graph indexer 14 | 15 | Built using [Nexth](https://github.com/wslyvh/nexth/) starter kit. 16 | 17 | ## Protocol 18 | 19 | The Smart contracts are deployed on 20 | 21 | - Optimism 22 | - Base 23 | 24 | **Testnet** 25 | 26 | - Sepolia 27 | - Base Sepolia 28 | 29 | => [More details](./packages/protocol/) 30 | 31 | ## Development 🛠️ 32 | 33 | ```bash 34 | npm run dev 35 | # or 36 | yarn dev 37 | ``` 38 | 39 | ## Deploy on Vercel 🚢 40 | 41 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwslyvh%2Fnexth) 42 | 43 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=nexth&filter=next.js&utm_source=nexth&utm_campaign=nexth-readme) from the creators of Next.js. 44 | 45 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 46 | 47 | ## Acknowledgements 48 | 49 | Show Up has been built on the ideas and experiences from [@wearekickback](https://github.com/wearekickback/) 50 | 51 | Shout/out to @makoto @jefflau @hiddentao 👏👏 52 | -------------------------------------------------------------------------------- /packages/subgraph/src/metadata.ts: -------------------------------------------------------------------------------- 1 | import { json, Bytes, dataSource, log } from '@graphprotocol/graph-ts' 2 | import { Event } from '../generated/schema'; 3 | 4 | export function handleEventMetadata(content: Bytes): void { 5 | log.debug('ShowUp.Protocol - EventMetaData', []) 6 | let metadata = new Event(dataSource.stringParam()) 7 | const value = json.fromBytes(content).toObject() 8 | 9 | if (value) { 10 | const appId = value.get('appId') 11 | const title = value.get('title') 12 | const description = value.get('description') 13 | const start = value.get('start') 14 | const end = value.get('end') 15 | const timezone = value.get('timezone') 16 | const location = value.get('location') 17 | const website = value.get('website') 18 | const imageUrl = value.get('imageUrl') 19 | const visibility = value.get('visibility') 20 | 21 | if (appId) metadata.appId = appId.toString() 22 | if (title) metadata.title = title.toString() 23 | if (description) metadata.description = description.toString() 24 | if (start) metadata.start = start.toString() 25 | if (end) metadata.end = end.toString() 26 | if (timezone) metadata.timezone = timezone.toString() 27 | if (location) metadata.location = location.toString() 28 | if (website) metadata.website = website.toString() 29 | if (imageUrl) metadata.imageUrl = imageUrl.toString() 30 | if (visibility) { 31 | metadata.visibility = visibility.toString() 32 | } else { 33 | metadata.visibility = 'Public' 34 | } 35 | 36 | metadata.save() 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /packages/app/src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { BellIcon, HomeIcon, TicketIcon, UserIcon } from '@heroicons/react/24/outline' 5 | import { LinkComponent } from './LinkComponent' 6 | import { usePathname } from 'next/navigation' 7 | import { useNotifications } from '@/context/Notification' 8 | 9 | export function Navbar() { 10 | const notifications = useNotifications() 11 | const pathname = usePathname() 12 | const iconClassName = 'h-6 w-6' 13 | 14 | return ( 15 |
16 | 22 | 23 | 24 | 25 | 26 | 27 | 31 |

32 | 33 | {notifications.new && } 34 |

35 |
36 | 37 | 38 | 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /packages/blog/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from 'next' 2 | import { PropsWithChildren } from 'react' 3 | import { BLOG_DOMAIN, BLOG_NAME, BLOG_URL, SITE_DESCRIPTION, SITE_NAME, SOCIAL_TWITTER } from "app/src/utils/site" 4 | import PlausibleProvider from 'next-plausible' 5 | import { BlogLayout } from '@/components/Layout' 6 | import '../assets/globals.css' 7 | 8 | export const metadata: Metadata = { 9 | applicationName: BLOG_NAME, 10 | title: { 11 | default: BLOG_NAME, 12 | template: `%s · ${BLOG_NAME}`, 13 | }, 14 | metadataBase: new URL(BLOG_URL), 15 | description: SITE_DESCRIPTION, 16 | openGraph: { 17 | type: 'website', 18 | title: BLOG_NAME, 19 | siteName: BLOG_NAME, 20 | description: SITE_DESCRIPTION, 21 | url: BLOG_URL, 22 | images: `${BLOG_URL}/opengraph-image`, 23 | }, 24 | twitter: { 25 | card: 'summary_large_image', 26 | site: SOCIAL_TWITTER, 27 | title: BLOG_NAME, 28 | description: SITE_DESCRIPTION, 29 | images: `${BLOG_URL}/opengraph-image`, 30 | }, 31 | } 32 | 33 | export const viewport: Viewport = { 34 | width: 'device-width', 35 | height: 'device-height', 36 | initialScale: 1.0, 37 | viewportFit: 'cover', 38 | themeColor: '#000000', 39 | } 40 | 41 | export default function RootLayout(props: PropsWithChildren) { 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {props.children} 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /packages/app/src/app/create/components/Info.tsx: -------------------------------------------------------------------------------- 1 | import { ActionDrawer } from '@/components/ActionDrawer' 2 | import { LinkComponent } from '@/components/LinkComponent' 3 | import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline' 4 | 5 | export function InfoDrawer() { 6 | return ( 7 | }> 10 |
11 |

Show Up aligns the incentives between event organizers and attendees

12 | 13 |
    14 |
  • Event organizers request a deposit fee to register
  • 15 |
  • Attendees get rewarded by showing up
  • 16 |
17 | 18 |

win/win for everyone 🤝

19 | 20 |
    21 |
  • Show Up is a decentralized App and has no control of your event(s)
  • 22 |
  • Metadata is stored on IPFS. The conditions and payments are managed onchain via smart contracts
  • 23 |
  • Once an event is created, it cannot be edited. You can cancel an event at any time
  • 24 |
  • Anyone can register for your event by paying the deposit fee
  • 25 |
  • The event host is required to keep track and manage check ins
  • 26 |
  • The event host needs to settle the event after its end date to distribute the funds
  • 27 |
28 | 29 |

30 | Check{' '} 31 | 32 | Github 33 | {' '} 34 | for more details. 35 |

36 |
37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /packages/indexer/config.yaml: -------------------------------------------------------------------------------- 1 | name: sup-indexer 2 | description: Show Up Protocol indexer 3 | contracts: 4 | - name: ShowHub 5 | handler: src/EventHandlers.ts 6 | events: 7 | - event: ConditionModuleWhitelisted(address indexed conditionModule, string name, bool indexed whitelisted, address sender, uint256 timestamp) 8 | - event: Created(uint256 indexed id, string contentUri, uint256 endDate, uint256 limit,address indexed conditionModule, bytes data, address sender, uint256 timestamp) 9 | isAsync: true 10 | - event: Updated(uint256 indexed id, string contentUri, uint256 limit, address owner, address sender, uint256 timestamp) 11 | isAsync: true 12 | - event: Canceled(uint256 indexed id, string reason, bytes data, address sender, uint256 timestamp) 13 | isAsync: true 14 | - event: Funded(uint256 indexed id, bytes data, address sender, uint256 timestamp) 15 | isAsync: true 16 | - event: Registered(uint256 indexed id, address indexed participant, bytes data, address sender, uint256 timestamp) 17 | isAsync: true 18 | - event: CheckedIn(uint256 indexed id, address[] attendees, bytes data, address sender, uint256 timestamp) 19 | isAsync: true 20 | - event: Settled(uint256 indexed id, bytes data, address sender, uint256 timestamp) 21 | isAsync: true 22 | unordered_multichain_mode: true 23 | networks: 24 | - id: 10 # Optimism 25 | start_block: 0 26 | contracts: 27 | - name: ShowHub 28 | address: 0x27d81f79D12327370cdB18DdEa03080621AEAadC 29 | - id: 8453 # Base 30 | start_block: 0 31 | contracts: 32 | - name: ShowHub 33 | address: 0x27d81f79D12327370cdB18DdEa03080621AEAadC 34 | - id: 11155111 # Sepolia 35 | start_block: 0 36 | contracts: 37 | - name: ShowHub 38 | address: 0x27d81f79D12327370cdB18DdEa03080621AEAadC 39 | -------------------------------------------------------------------------------- /packages/blog/src/app/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { GetPosts } from "@/utils/data" 2 | import { LinkComponent } from "app/src/components/LinkComponent" 3 | import { marked } from "marked" 4 | import { metadata as LayoutMetadata } from "../layout" 5 | import { BLOG_NAME, BLOG_URL } from "app/src/utils/site" 6 | import dayjs from "dayjs" 7 | 8 | interface Params { 9 | params: { slug: string } 10 | searchParams: { [key: string]: string | string[] | undefined } 11 | } 12 | 13 | export async function generateMetadata({ params }: Params) { 14 | const posts = GetPosts() 15 | const post = posts.find((post) => post.slug === params.slug) 16 | if (!post) return {} 17 | 18 | return { 19 | ...LayoutMetadata, 20 | title: post.title, 21 | description: post.description, 22 | openGraph: { 23 | title: `${post.title} · ${BLOG_NAME}`, 24 | description: post.description, 25 | images: `${BLOG_URL}/opengraph-image`, 26 | }, 27 | twitter: { 28 | title: `${post.title} · ${BLOG_NAME}`, 29 | description: post.description, 30 | images: `${BLOG_URL}/opengraph-image`, 31 | }, 32 | } 33 | } 34 | 35 | export default async function BlogPost({ params }: Params) { 36 | const posts = GetPosts() 37 | const post = posts.find((post) => post.slug === params.slug) 38 | 39 | if (!post) return
404
40 | 41 | return ( 42 |
43 |

{post.title}

44 |

{dayjs(post.date).format('ddd MMM DD')}

45 | 46 |
47 | 48 |
49 | 50 |
51 | ← Back to overview 52 |
53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /packages/subgraph/subgraph.yaml: -------------------------------------------------------------------------------- 1 | specVersion: 0.0.5 2 | schema: 3 | file: ./schema.graphql 4 | features: 5 | - fullTextSearch 6 | dataSources: 7 | - kind: ethereum 8 | name: Registry 9 | network: optimism 10 | source: 11 | abi: Registry 12 | address: "0x7Cc8E0633021b9DF8D2F01d9287C3b8e29f4eDe2" 13 | startBlock: 111754660 14 | mapping: 15 | kind: ethereum/events 16 | apiVersion: 0.0.7 17 | language: wasm/assemblyscript 18 | entities: 19 | - Record 20 | - Participant 21 | - User 22 | - ConditionModule 23 | - Condition 24 | abis: 25 | - name: Registry 26 | file: ./abis/Registry.json 27 | - name: IConditionModule 28 | file: ./abis/IConditionModule.json 29 | - name: Token 30 | file: ./abis/Token.json 31 | eventHandlers: 32 | - event: Canceled(indexed uint256,string,bytes,address,uint256) 33 | handler: handleCanceled 34 | - event: CheckedIn(indexed uint256,address[],bytes,address,uint256) 35 | handler: handleCheckedIn 36 | - event: ConditionModuleWhitelisted(indexed address,indexed bool,address,uint256) 37 | handler: handleConditionModuleWhitelisted 38 | - event: Created(indexed uint256,string,indexed address,bytes,address,uint256) 39 | handler: handleCreated 40 | - event: Registered(indexed uint256,indexed address,bytes,address,uint256) 41 | handler: handleRegistered 42 | - event: Settled(indexed uint256,bytes,address,uint256) 43 | handler: handleSettled 44 | file: ./src/registry.ts 45 | templates: 46 | - kind: file/ipfs 47 | name: Event 48 | mapping: 49 | apiVersion: 0.0.7 50 | language: wasm/assemblyscript 51 | file: ./src/metadata.ts 52 | handler: handleEventMetadata 53 | entities: 54 | - Event 55 | abis: 56 | - name: Registry 57 | file: ./abis/Registry.json 58 | network: optimism 59 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.2.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "test": "echo \"Error: no tests specified\"", 9 | "start": "next start", 10 | "lint": "next lint --fix", 11 | "prettier": "prettier './src' --write", 12 | "wagmi": "wagmi generate" 13 | }, 14 | "husky": { 15 | "hooks": { 16 | "pre-commit": "lint-staged" 17 | } 18 | }, 19 | "lint-staged": { 20 | "./src": [ 21 | "lint", 22 | "prettier" 23 | ] 24 | }, 25 | "dependencies": { 26 | "@ducanh2912/next-pwa": "^9.7.2", 27 | "@headlessui/react": "^1.7.17", 28 | "@heroicons/react": "^2.0.18", 29 | "@pinata/sdk": "^1.2.1", 30 | "@tanstack/react-query": "^5.24.1", 31 | "@web3modal/wagmi": "^4.0.10", 32 | "abitype": "^0.10.1", 33 | "dayjs": "^1.11.10", 34 | "ethereum-blockies-base64": "^1.0.2", 35 | "frog": "latest", 36 | "hono": "^4", 37 | "marked": "^11.0.1", 38 | "next": "^14.0.4", 39 | "next-plausible": "^3.11.2", 40 | "qrcode.react": "^3.1.0", 41 | "react": "^18.2.0", 42 | "react-dom": "^18.2.0", 43 | "react-dropzone": "^14.2.3", 44 | "react-icons": "^4.11.0", 45 | "slugify": "^1.6.6", 46 | "viem": "^2.7.15", 47 | "wagmi": "^2.5.7" 48 | }, 49 | "devDependencies": { 50 | "@tailwindcss/typography": "^0.5.10", 51 | "@tsconfig/next": "^2.0.0", 52 | "@types/node": "^20", 53 | "@types/react": "^18", 54 | "@types/react-dom": "^18", 55 | "@wagmi/cli": "^2.1.1", 56 | "autoprefixer": "^10", 57 | "daisyui": "^4.6.0", 58 | "eslint": "^8", 59 | "eslint-config-next": "^14.0.4", 60 | "eslint-config-prettier": "^9.0.0", 61 | "eslint-plugin-prettier": "^5.0.0", 62 | "husky": "^8.0.3", 63 | "lint-staged": "^14.0.1", 64 | "postcss": "^8", 65 | "prettier": "^3.0.3", 66 | "tailwindcss": "^3", 67 | "typescript": "^5", 68 | "workbox-window": "^7.0.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/app/src/components/Hero.tsx: -------------------------------------------------------------------------------- 1 | import { SITE_EMOJI, SITE_SHORT_NAME } from '@/utils/site' 2 | import React from 'react' 3 | import { LinkComponent } from './LinkComponent' 4 | 5 | export function Hero() { 6 | return ( 7 |
12 |
13 |
14 |
15 |
16 |

17 | {SITE_SHORT_NAME} {SITE_EMOJI} 18 |

19 |

Onchain Events & RSVP

20 |

Increase event participation. Reward attendees.

21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 |
33 | ) 34 | 35 | // return ( 36 | //
37 | //
38 | //
39 | //

Hello there

40 | //

41 | // Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda excepturi exercitationem quasi. In 42 | // deleniti eaque aut repudiandae et a id nisi. 43 | //

44 | // 45 | //
46 | //
47 | //
48 | // ) 49 | } 50 | -------------------------------------------------------------------------------- /packages/subgraph/schema.graphql: -------------------------------------------------------------------------------- 1 | type _Schema_ 2 | @fulltext( 3 | name: "eventSearch" 4 | language: en 5 | algorithm: rank 6 | include: [{ entity: "Event", fields: [{ name: "title" }, { name: "description" }, { name: "location" }] }] 7 | ) 8 | 9 | enum Status { 10 | Active 11 | Cancelled 12 | Settled 13 | } 14 | 15 | type ConditionModule @entity { 16 | id: Bytes! # address 17 | name: String! 18 | createdAt: BigInt! 19 | createdBy: Bytes! # address 20 | whitelisted: Boolean! # true 21 | blockNumber: BigInt! 22 | transactionHash: Bytes! 23 | } 24 | 25 | type Record @entity { 26 | id: String! 27 | createdAt: BigInt! 28 | createdBy: Bytes! 29 | updatedAt: BigInt 30 | blockNumber: BigInt! 31 | transactionHash: Bytes! 32 | status: Status! # Active 33 | message: String 34 | 35 | conditionModule: Bytes! # address 36 | condition: Condition 37 | 38 | contentUri: String! 39 | metadata: Event 40 | 41 | participants: [Participant!]! @derivedFrom(field: "record") 42 | } 43 | 44 | type User @entity { 45 | id: Bytes! # address 46 | participations: [Participant!] @derivedFrom(field: "user") 47 | } 48 | 49 | type Participant @entity { 50 | id: String! # Set to `record.id.concat(user.id)` 51 | createdAt: BigInt! 52 | createdBy: Bytes! 53 | transactionHash: Bytes! 54 | address: Bytes! # address 55 | checkedIn: Boolean! # false 56 | user: User! 57 | record: Record! 58 | } 59 | 60 | type Event @entity { 61 | id: ID! 62 | appId: String 63 | title: String! 64 | description: String 65 | start: String! 66 | end: String! 67 | timezone: String! 68 | location: String! 69 | website: String 70 | imageUrl: String 71 | visibility: String 72 | } 73 | 74 | type Condition @entity(immutable: true) { 75 | id: String! # Set to `record.id.concat(conditionModule.id)` 76 | address: Bytes! # address 77 | name: String! 78 | owner: Bytes! # address 79 | endDate: BigInt! 80 | depositFee: BigInt! 81 | maxParticipants: BigInt! 82 | tokenAddress: Bytes! # address 83 | tokenSymbol: String 84 | tokenName: String 85 | tokenDecimals: BigInt 86 | } 87 | -------------------------------------------------------------------------------- /packages/indexer/schema.graphql: -------------------------------------------------------------------------------- 1 | type ConditionModule { 2 | id: ID! # Address 3 | address: String! 4 | chainId: Int! 5 | createdAt: BigInt! 6 | createdBy: Bytes! # address 7 | blockNumber: BigInt! 8 | transactionHash: Bytes! 9 | 10 | name: String! 11 | whitelisted: Boolean! # true 12 | } 13 | 14 | type Record { 15 | id: ID! # Chain + Record ID 16 | recordId: String! 17 | chainId: Int! 18 | slug: String! 19 | createdAt: BigInt! 20 | createdBy: Bytes! 21 | blockNumber: BigInt! 22 | transactionHash: Bytes! 23 | 24 | endDate: BigInt! 25 | limit: BigInt! 26 | owner: User! 27 | status: Int! 28 | message: String 29 | 30 | contentUri: String! 31 | metadata: Event 32 | 33 | conditionModule: ConditionModule! 34 | conditionModuleData: ConditionModuleData! 35 | 36 | totalRegistrations: BigInt! 37 | totalAttendees: BigInt! 38 | totalFunded: BigInt! 39 | registrations: [Registration!]! @derivedFrom(field: "record") 40 | } 41 | 42 | type Registration { 43 | id: ID! # Chain + Record + User address 44 | createdAt: BigInt! 45 | createdBy: Bytes! 46 | blockNumber: BigInt! 47 | transactionHash: Bytes! 48 | 49 | participated: Boolean! # false 50 | record: Record! 51 | user: User! 52 | } 53 | 54 | type User { 55 | id: ID! # address 56 | name: String! 57 | 58 | avatar: String 59 | description: String 60 | website: String 61 | email: String 62 | twitter: String 63 | github: String 64 | discord: String 65 | telegram: String 66 | 67 | registrations: [Registration!]! @derivedFrom(field: "user") 68 | } 69 | 70 | type Event { 71 | id: ID! # Content Hash 72 | appId: String 73 | title: String! 74 | description: String 75 | start: BigInt! 76 | end: BigInt! 77 | timezone: String! 78 | location: String! 79 | website: String 80 | imageUrl: String 81 | visibility: Int! 82 | } 83 | 84 | type ConditionModuleData { 85 | id: ID! # Chain + Record + ConditionModule 86 | conditionModule: String! # Address 87 | depositFee: BigInt! 88 | recipient: Bytes 89 | 90 | tokenAddress: Bytes 91 | tokenSymbol: String 92 | tokenName: String 93 | tokenDecimals: Int 94 | } 95 | -------------------------------------------------------------------------------- /packages/app/src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import { sepolia, baseSepolia, optimism, base, Chain } from 'viem/chains' 2 | 3 | const networkEnv = process.env.NEXT_PUBLIC_NETWORK_ENV ?? 'test' 4 | const chains: [Chain, ...Chain[]] = networkEnv === 'main' ? [optimism, base] : [sepolia] 5 | const appId = (process.env.NEXT_PUBLIC_DEFAULT_APP_ID ?? networkEnv === 'main') ? 'showup-events' : 'showup-test' 6 | 7 | export const CONFIG = { 8 | NODE_ENV: process.env.NODE_ENV || 'development', 9 | NETWORK_ENV: networkEnv, 10 | 11 | NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID ?? '', 12 | NEXT_PUBLIC_INFURA_KEY: process.env.NEXT_PUBLIC_INFURA_KEY ?? '', 13 | NEXT_PUBLIC_ALCHEMY_KEY_BASE: process.env.NEXT_PUBLIC_ALCHEMY_KEY_BASE ?? '', 14 | NEXT_PUBLIC_ALCHEMY_KEY_BASE_SEPOLIA: process.env.NEXT_PUBLIC_ALCHEMY_KEY_BASE_SEPOLIA ?? '', 15 | 16 | NEXT_PUBLIC_PINATA_API_KEY: process.env.NEXT_PUBLIC_PINATA_API_KEY ?? '', 17 | NEXT_PUBLIC_PINATA_API_SECRET: process.env.NEXT_PUBLIC_PINATA_API_SECRET ?? '', 18 | NEXT_PUBLIC_PINATA_JWT: process.env.NEXT_PUBLIC_PINATA_JWT ?? '', 19 | 20 | DEFAULT_IPFS_GATEWAY: process.env.NEXT_PUBLIC_DEFAULT_IPFS_GATEWAY ?? 'https://ipfs.io/ipfs', 21 | DEFAULT_APP_ID: appId, 22 | DEFAULT_CHAINS: chains, 23 | } 24 | ;(() => { 25 | if (!process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID) { 26 | console.error('You need to provide a NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID env variable') 27 | } 28 | if (!process.env.NEXT_PUBLIC_ALCHEMY_KEY_BASE) { 29 | console.error('You need to provide a NEXT_PUBLIC_ALCHEMY_KEY_BASE env variable') 30 | } 31 | if (!process.env.NEXT_PUBLIC_ALCHEMY_KEY_BASE_SEPOLIA) { 32 | console.error('You need to provide a NEXT_PUBLIC_ALCHEMY_KEY_BASE_SEPOLIA env variable') 33 | } 34 | if (!process.env.NEXT_PUBLIC_INFURA_KEY) { 35 | console.error('You need to provide a NEXT_PUBLIC_INFURA_KEY env variable') 36 | } 37 | if (!process.env.NEXT_PUBLIC_PINATA_API_KEY) { 38 | console.error('NEXT_PUBLIC_PINATA_API_KEY is not defined') 39 | } 40 | if (!process.env.NEXT_PUBLIC_PINATA_API_SECRET) { 41 | console.error('NEXT_PUBLIC_PINATA_API_SECRET is not defined') 42 | } 43 | })() 44 | -------------------------------------------------------------------------------- /packages/app/src/utils/dates.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import { Record } from './types' 3 | import { SITE_DOMAIN, SITE_URL } from './site' 4 | 5 | export function GenerateGoogleCalendarLink(record: Record) { 6 | const calendarLink = new URL(`https://www.google.com/calendar/render?action=TEMPLATE`) 7 | 8 | calendarLink.searchParams.append('text', `${record.metadata.title}`) 9 | calendarLink.searchParams.append('dates', `${dateFormat(record.metadata.start)}/${dateFormat(record.metadata.end)}`) 10 | calendarLink.searchParams.append('location', `${record.metadata.location}`) 11 | calendarLink.searchParams.append('details', generateDescription(record)) 12 | 13 | return calendarLink.href 14 | } 15 | 16 | export function GenerateIcsFileLink(record: Record) { 17 | const ics = [`BEGIN:VCALENDAR`, `VERSION:2.0`, `PRODID:${SITE_DOMAIN}`] 18 | ics.push( 19 | `BEGIN:VEVENT`, 20 | `UID:${record.slug}`, 21 | `DTSTAMP:${dateFormat()}`, 22 | `DTSTART:${dateFormat(record.metadata.start)}`, 23 | `DTEND:${dateFormat(record.metadata.end)}`, 24 | `SUMMARY:${record.metadata.title}`, 25 | `DESCRIPTION:RSVP - ${getShowUpEventLink(record)}`, 26 | `LOCATION:${record.metadata.location}`, 27 | `END:VEVENT` 28 | ) 29 | ics.push(`END:VCALENDAR`) 30 | 31 | const file = new Blob([ics.filter((row: string) => !!row).join('\n')], { type: 'text/calendar' }) 32 | return URL.createObjectURL(file) 33 | } 34 | 35 | function dateFormat(date?: string | number) { 36 | return (!date ? dayjs() : dayjs(date)).toISOString().replace(/-|:|\.\d\d\d/g, '') 37 | } 38 | 39 | function generateDescription(record: Record) { 40 | let description = `EVENT INFORMATION IN YOUR CALENDAR MIGHT BE OUT OF DATE. MAKE SURE TO VISIT THE WEBSITE FOR THE LATEST INFORMATION.\n` 41 | description += '====================\n\n' 42 | 43 | description += `RSVP: ${getShowUpEventLink(record)}\n` 44 | if (record.metadata.website) { 45 | description += `Website: ${record.metadata.website}\n` 46 | } 47 | 48 | description += `\n\n` 49 | description += record.metadata.description 50 | 51 | return description 52 | } 53 | 54 | function getShowUpEventLink(record: Record) { 55 | return `${SITE_URL}/events/${record.slug}` 56 | } 57 | -------------------------------------------------------------------------------- /packages/app/src/app/events/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { GetAllEvents, GetEventBySlug } from '@/services/showhub' 2 | import { EventDetails } from '../components/Details' 3 | import EventDataProvider from '@/context/EventData' 4 | import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query' 5 | import { SITE_NAME, SITE_URL } from '@/utils/site' 6 | import { CONFIG } from '@/utils/config' 7 | import { getFrameMetadata } from 'frog/next' 8 | 9 | interface Params { 10 | params: { slug: string } 11 | searchParams: { [key: string]: string | string[] | undefined } 12 | } 13 | 14 | export async function generateStaticParams() { 15 | if (CONFIG.NETWORK_ENV !== 'main') return [] 16 | 17 | const events = await GetAllEvents() 18 | 19 | return events.map((i) => ({ 20 | slug: i.slug, 21 | })) 22 | } 23 | 24 | export async function generateMetadata({ params }: Params) { 25 | const event = await GetEventBySlug(params.slug) 26 | if (!event?.metadata) return {} 27 | 28 | const url = CONFIG.NODE_ENV === 'development' ? 'http://localhost:3000' : SITE_URL 29 | const frameMetadata = await getFrameMetadata(`${url}/api/events/${params.slug}`) 30 | const baseUri = new URL(`${SITE_URL}/events/${params.slug}`) 31 | 32 | return { 33 | title: event.metadata.title, 34 | description: event.metadata.description, 35 | metadataBase: new URL(baseUri), 36 | openGraph: { 37 | title: `${SITE_NAME} @ ${event.metadata.title}`, 38 | description: event.metadata.description, 39 | images: event.metadata.imageUrl ?? `${baseUri}/opengraph-image`, 40 | }, 41 | twitter: { 42 | title: `${SITE_NAME} @ ${event.metadata.title}`, 43 | description: event.metadata.description, 44 | images: event.metadata.imageUrl ?? `${baseUri}/opengraph-image`, 45 | }, 46 | other: frameMetadata, 47 | } 48 | } 49 | 50 | export default async function EventsPage({ params }: Params) { 51 | const queryClient = new QueryClient() 52 | 53 | await queryClient.prefetchQuery({ 54 | queryKey: ['events', params.slug], 55 | queryFn: () => GetEventBySlug(params.slug), 56 | }) 57 | 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /packages/app/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata, Viewport } from 'next/types' 2 | import { PropsWithChildren } from 'react' 3 | import { 4 | DEFAULT_REVALIDATE_PERIOD, 5 | SITE_DESCRIPTION, 6 | SITE_DOMAIN, 7 | SITE_NAME, 8 | SITE_URL, 9 | SOCIAL_TWITTER, 10 | } from '@/utils/site' 11 | import { MobileLayout } from '@/components/MobileLayout' 12 | import { NotificationProvider } from '@/context/Notification' 13 | import PlausibleProvider from 'next-plausible' 14 | import { Web3Provider } from '@/context/Web3' 15 | import '../assets/globals.css' 16 | 17 | export const metadata: Metadata = { 18 | applicationName: SITE_NAME, 19 | title: { 20 | default: `${SITE_NAME} Events`, 21 | template: `${SITE_NAME} @ %s`, 22 | }, 23 | metadataBase: new URL(SITE_URL), 24 | description: SITE_DESCRIPTION, 25 | manifest: '/manifest.json', 26 | appleWebApp: { 27 | title: SITE_NAME, 28 | capable: true, 29 | statusBarStyle: 'black-translucent', 30 | }, 31 | openGraph: { 32 | type: 'website', 33 | title: SITE_NAME, 34 | siteName: SITE_NAME, 35 | description: SITE_DESCRIPTION, 36 | url: SITE_URL, 37 | images: '/opengraph-image', 38 | }, 39 | twitter: { 40 | card: 'summary_large_image', 41 | site: SOCIAL_TWITTER, 42 | title: SITE_NAME, 43 | description: SITE_DESCRIPTION, 44 | images: '/opengraph-image', 45 | }, 46 | } 47 | 48 | export const viewport: Viewport = { 49 | width: 'device-width', 50 | height: 'device-height', 51 | initialScale: 1.0, 52 | viewportFit: 'cover', 53 | themeColor: '#000000', 54 | } 55 | 56 | export const revalidate = DEFAULT_REVALIDATE_PERIOD 57 | 58 | export default function RootLayout(props: PropsWithChildren) { 59 | return ( 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | {props.children} 71 | 72 | 73 | 74 | 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /packages/indexer/index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | GraphiQL 12 | 24 | 25 | 32 | 37 | 42 | 47 | 48 | 49 | 50 | 51 |
Loading...
52 | 56 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /packages/app/src/app/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { MapPinIcon, UserIcon } from '@heroicons/react/24/outline' 2 | import { LinkComponent } from '@/components/LinkComponent' 3 | import { Record } from '@/utils/types' 4 | import Image from 'next/image' 5 | import dayjs from 'dayjs' 6 | 7 | interface Props { 8 | event: Record 9 | } 10 | 11 | export function Card({ event }: Props) { 12 | return ( 13 | 14 |
15 |
16 |

17 | {dayjs(event.metadata?.start).format('ddd MMM DD · HH:mm')} 18 |

19 |
20 |

{event.metadata?.title}

21 | {event.totalFunded > 0 && funded} 22 |
23 |
24 | {event.metadata?.location} 25 |
26 |
27 | {event.registrations.length} going 28 | {event.limit > 0 && ( 29 | <> 30 | · 31 | {event.limit - event.registrations.length} left 32 | 33 | )} 34 |
35 |
36 | 37 | {event.metadata?.imageUrl && ( 38 |
39 |
40 | {event.metadata?.title} 50 |
51 |
52 | )} 53 |
54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /packages/protocol/scripts/deploy-token.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network, run } from 'hardhat' 2 | import { defaultTokenMint } from '../test/utils/types' 3 | import deployments from '../deployments.json' 4 | import fs from 'fs' 5 | 6 | export async function main() { 7 | console.log('Deploying Show Up Protocol..') 8 | const [owner, attendee1, attendee2, attendee3, attendee4, attendee5] = await ethers.getSigners() 9 | 10 | console.log('NETWORK ID', network.config.chainId) 11 | if (!network.config.chainId) { 12 | throw new Error('INVALID_NETWORK_ID') 13 | } 14 | 15 | const Token = await ethers.getContractFactory('Token') 16 | const token = await Token.deploy() 17 | console.log('Token:', token.address) 18 | 19 | if (network.config.chainId === 31337) { 20 | await token.mint(attendee1.address, defaultTokenMint) 21 | await token.mint(attendee2.address, defaultTokenMint) 22 | await token.mint(attendee3.address, defaultTokenMint) 23 | await token.mint(attendee4.address, defaultTokenMint) 24 | await token.mint(attendee5.address, defaultTokenMint) 25 | } 26 | 27 | console.log(`Write Token address to file..`) 28 | const data = { 29 | ...deployments, 30 | [network.config.chainId]: { 31 | ...(deployments as any)[network.config.chainId], 32 | Token: token.address, 33 | } 34 | } 35 | fs.writeFileSync('./deployments.json', JSON.stringify(data, null, 2)) 36 | 37 | if (network.config.chainId == 84532) { 38 | // no auto verification on Base Sepolia 39 | return 40 | } 41 | 42 | // no need to verify on localhost or hardhat 43 | if (network.config.chainId != 31337 && process.env.ETHERSCAN_API_KEY) { 44 | console.log(`Waiting for block confirmations..`) 45 | await token.deployTransaction.wait(10) // last contract deployed 46 | 47 | console.log('Verifying Token contract..') 48 | try { 49 | run('verify:verify', { 50 | address: token.address, 51 | constructorArguments: [], 52 | contract: 'contracts/mocks/Token.sol:Token', 53 | }) 54 | } catch (e) { 55 | console.log(e) 56 | } 57 | } 58 | } 59 | 60 | // We recommend this pattern to be able to use async/await everywhere 61 | // and properly handle errors. 62 | main().catch((error) => { 63 | console.error(error) 64 | process.exitCode = 1 65 | }) 66 | -------------------------------------------------------------------------------- /packages/protocol/contracts/interfaces/IShowHub.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import '../Common.sol'; 5 | 6 | interface IShowHub { 7 | // Hub events 8 | event ConditionModuleWhitelisted( 9 | address indexed conditionModule, 10 | string name, 11 | bool indexed whitelisted, 12 | address sender, 13 | uint256 timestamp 14 | ); 15 | 16 | // Registry events 17 | event Created( 18 | uint256 indexed id, 19 | string contentUri, 20 | uint256 endDate, 21 | uint256 limit, 22 | address indexed conditionModule, 23 | bytes data, 24 | address sender, 25 | uint256 timestamp 26 | ); 27 | event Updated(uint256 indexed id, string contentUri, uint256 limit, address owner, address sender, uint256 timestamp); 28 | event Canceled(uint256 indexed id, string reason, bytes data, address sender, uint256 timestamp); 29 | event Funded(uint256 indexed id, bytes data, address sender, uint256 timestamp); 30 | event Registered(uint256 indexed id, address indexed participant, bytes data, address sender, uint256 timestamp); 31 | event CheckedIn(uint256 indexed id, address[] attendees, bytes data, address sender, uint256 timestamp); 32 | event Settled(uint256 indexed id, bytes data, address sender, uint256 timestamp); 33 | 34 | // Hub functions 35 | function whitelistConditionModule(address conditionModule, bool enable) external; 36 | 37 | // Registry functions 38 | function create( 39 | string calldata contentUri, 40 | uint256 endDate, 41 | uint256 limit, 42 | address conditionModule, 43 | bytes calldata conditionModuleData 44 | ) external; 45 | 46 | function updateContentUri(uint256 id, string calldata contentUri) external; 47 | 48 | function updateLimit(uint256 id, uint256 limit) external; 49 | 50 | function updateOwner(uint256 id, address owner) external; 51 | 52 | function cancel(uint256 id, string calldata reason, bytes calldata conditionModuleData) external; 53 | 54 | function fund(uint256 id, bytes calldata conditionModuleData) external payable; 55 | 56 | function register(uint256 id, address participant, bytes calldata conditionModuleData) external payable; 57 | 58 | function checkin(uint256 id, address[] calldata attendees, bytes calldata conditionModuleData) external; 59 | 60 | function settle(uint256 id, bytes calldata conditionModuleData) external; 61 | 62 | // View functions 63 | function isConditionModuleWhitelisted(address conditionModule) external view returns (bool); 64 | } 65 | -------------------------------------------------------------------------------- /packages/protocol/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from 'hardhat/config' 2 | import { join } from 'path' 3 | import dotenv from 'dotenv' 4 | import '@nomicfoundation/hardhat-toolbox' 5 | 6 | dotenv.config() // project root 7 | dotenv.config({ path: join(process.cwd(), '../../.env') }) // workspace root 8 | 9 | const deployerKey = process.env.DEPLOYER_KEY 10 | if (!deployerKey) { 11 | console.warn('DEPLOYER_KEY not found in .env file. Running with default config') 12 | } 13 | const etherscanApiKey = process.env.ETHERSCAN_API_KEY ?? '' 14 | if (!etherscanApiKey) { 15 | console.warn('ETHERSCAN_API_KEY not found in .env file. Will skip Etherscan verification') 16 | } 17 | const infuraApiKey = process.env.INFURA_API_KEY ?? '' 18 | if (!infuraApiKey) { 19 | console.warn('INFURA_API_KEY not found in .env file.') 20 | } 21 | const optimisticApiKey = process.env.OPTIMISTIC_API_KEY ?? etherscanApiKey ?? '' 22 | if (!optimisticApiKey) { 23 | console.warn('OPTIMISTIC_API_KEY not found in .env file. Will skip Etherscan verification') 24 | } 25 | 26 | const config: HardhatUserConfig = { 27 | defaultNetwork: 'hardhat', 28 | solidity: { 29 | version: '0.8.22', 30 | settings: { 31 | optimizer: { 32 | enabled: true, 33 | runs: 200 34 | } 35 | }, 36 | }, 37 | etherscan: { 38 | apiKey: { 39 | mainnet: etherscanApiKey, 40 | sepolia: etherscanApiKey, 41 | optimisticEthereum: optimisticApiKey, 42 | "base-sepolia": etherscanApiKey 43 | }, 44 | }, 45 | networks: { 46 | hardhat: { 47 | chainId: 31337, 48 | }, 49 | localhost: { 50 | chainId: 31337, 51 | url: 'http://127.0.0.1:8545', 52 | }, 53 | sepolia: { 54 | chainId: 11155111, 55 | url: 'https://rpc.sepolia.org/', // https://rpc-sepolia.rockx.com/ || https://rpc.sepolia.org/ || infuraApiKey ? `https://sepolia.infura.io/v3/${infuraApiKey}` 56 | accounts: [deployerKey as string], 57 | }, 58 | "base-sepolia": { 59 | chainId: 84532, 60 | url: "https://sepolia.base.org", 61 | accounts: [deployerKey as string], 62 | // apiURL: "https://api-sepolia.basescan.org/api", 63 | // browserURL: "https://sepolia.basescan.org" 64 | }, 65 | optimism: { 66 | chainId: 10, 67 | url: 'https://mainnet.optimism.io/', 68 | accounts: [deployerKey as string], 69 | }, 70 | base: { 71 | chainId: 8453, 72 | url: 'https://mainnet.base.org', 73 | accounts: [deployerKey as string], 74 | }, 75 | }, 76 | } 77 | 78 | export default config 79 | -------------------------------------------------------------------------------- /packages/app/src/components/Notifications.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useEffect } from 'react' 4 | import { useNotifications } from '@/context/Notification' 5 | import { 6 | ArrowUpRightIcon, 7 | CheckCircleIcon, 8 | ExclamationCircleIcon, 9 | ExclamationTriangleIcon, 10 | InformationCircleIcon, 11 | } from '@heroicons/react/24/outline' 12 | import { LinkComponent } from './LinkComponent' 13 | import dayjs from 'dayjs' 14 | import relativeTime from 'dayjs/plugin/relativeTime' 15 | import { TruncateMiddle } from '@/utils/format' 16 | import { Empty } from './Empty' 17 | dayjs.extend(relativeTime) 18 | 19 | export function Notifications() { 20 | const { notifications, MarkAsRead } = useNotifications() 21 | 22 | useEffect(() => { 23 | MarkAsRead() 24 | }, []) 25 | 26 | if (notifications.length === 0) return 27 | 28 | return ( 29 |
30 | {notifications 31 | .sort((a, b) => b.created - a.created) 32 | .map((i, index) => { 33 | const id = `${index}_${i.type}_notification` 34 | const iconClassName = `stroke-${i.type} shrink-0 h-6 w-6 text-${i.type}-400` 35 | 36 | return ( 37 |
38 |
39 | {i.type === 'success' && } 40 | {i.type === 'info' && } 41 | {i.type === 'warning' && } 42 | {i.type === 'error' && } 43 |
44 |
45 | {i.message} 46 | 47 | {dayjs().to(dayjs(i.created))} 48 | {i.from && ( 49 | <> 50 | · 51 | {i.from.endsWith('.eth') ? i.from : TruncateMiddle(i.from)} 52 | 53 | )} 54 | 55 |
56 | {i.cta && ( 57 | 58 | 59 | 60 | )} 61 |
62 | ) 63 | })} 64 |
65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /packages/app/src/app/create/components/ImageUpload.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react' 2 | import { CloudArrowUpIcon, TrashIcon } from '@heroicons/react/24/outline' 3 | import { useDropzone } from 'react-dropzone' 4 | 5 | interface Props { 6 | onUpload: (file?: File) => void 7 | } 8 | 9 | export function ImageUpload(props: Props) { 10 | const [filePreview, setFilePreview] = useState('') 11 | 12 | const onDrop = useCallback( 13 | (acceptedFiles: File[]) => { 14 | if (acceptedFiles?.length > 0) { 15 | const file = acceptedFiles[0] 16 | const reader = new FileReader() 17 | reader.readAsDataURL(file) 18 | reader.onload = async function () { 19 | setFilePreview(reader.result as string) 20 | props.onUpload(file) 21 | } 22 | } 23 | }, 24 | [props] 25 | ) 26 | 27 | const { getRootProps, getInputProps } = useDropzone({ 28 | onDrop, 29 | multiple: false, 30 | accept: { 31 | 'image/png': ['.png'], 32 | 'image/jpeg': ['.jpeg'], 33 | }, 34 | }) 35 | 36 | function clearFile() { 37 | setFilePreview('') 38 | props.onUpload() 39 | } 40 | 41 | return ( 42 |
43 | {!filePreview && ( 44 |
45 | 60 |
61 | )} 62 | 63 | {filePreview && ( 64 |
65 | 68 | Image preview 69 |
70 | )} 71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /packages/app/src/components/ActionDrawer.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Fragment, PropsWithChildren, ReactElement, cloneElement, useState } from 'react' 4 | import { Dialog, Transition } from '@headlessui/react' 5 | import { XMarkIcon } from '@heroicons/react/24/outline' 6 | 7 | interface Props extends PropsWithChildren { 8 | title: string 9 | actionComponent: ReactElement 10 | } 11 | 12 | export function ActionDrawer(props: Props) { 13 | const [open, setOpen] = useState(false) 14 | 15 | return ( 16 | <> 17 | {cloneElement(props.actionComponent, { onClick: () => setOpen(true) })} 18 | 19 | 20 | 21 | 29 |
30 | 31 | 32 |
33 |
34 |
35 | 43 | 44 |
45 | 48 | 49 |
50 | {props.title} 51 | 52 |
{props.children}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /packages/app/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface State { 2 | loading: boolean 3 | data?: T 4 | error?: string 5 | } 6 | 7 | export interface LoadingStateData { 8 | isLoading: boolean 9 | message: string 10 | type: 'error' | 'success' | 'info' | '' 11 | data?: any 12 | } 13 | 14 | export interface LoadingState { 15 | isLoading: boolean 16 | message: string 17 | type: 'error' | 'success' | 'info' | '' 18 | } 19 | 20 | export enum Status { 21 | Active, 22 | Cancelled, 23 | Settled, 24 | } 25 | 26 | export enum Visibility { 27 | Public, 28 | Unlisted, 29 | } 30 | 31 | export interface Record { 32 | id: string 33 | chainId: number 34 | recordId: string 35 | slug: string 36 | createdAt: string | number 37 | createdBy: string 38 | endDate: string | number 39 | limit: number 40 | status: Status 41 | message?: string 42 | 43 | ownerId: string 44 | owner: UserProfile 45 | 46 | conditionModuleId: string 47 | conditionModule: ConditionModule 48 | conditionModuleData: ConditionModuleData 49 | 50 | contentUri: string 51 | metadata: EventMetadata 52 | 53 | registrations: Registration[] 54 | 55 | totalRegistrations: number 56 | totalAttendees: number 57 | totalFunded: number 58 | } 59 | 60 | export interface EventMetadata { 61 | appId?: string 62 | title: string 63 | description: string 64 | start: string | number 65 | end: string | number 66 | timezone: string 67 | location: string 68 | website: string 69 | imageUrl: string 70 | visibility: Visibility 71 | links: string[] 72 | tags: string[] 73 | } 74 | 75 | export interface UserProfile { 76 | id: string 77 | name: string 78 | avatar: string 79 | 80 | description?: string 81 | website?: string 82 | email?: string 83 | twitter?: string 84 | github?: string 85 | discord?: string 86 | telegram?: string 87 | } 88 | 89 | export interface Registration extends UserProfile { 90 | createdAt: string | number 91 | createdBy: string 92 | participated: boolean 93 | transactionHash: string 94 | } 95 | 96 | export interface ConditionModule { 97 | id: string 98 | address: string 99 | chainId: number 100 | name: 'RecipientEther' | 'RecipientToken' | 'SplitEther' | 'SplitToken' 101 | whitelisted: boolean 102 | } 103 | 104 | export interface ConditionModuleData extends ConditionModule { 105 | depositFee: bigint 106 | tokenAddress?: string 107 | tokenSymbol?: string 108 | tokenName?: string 109 | tokenDecimals?: number 110 | } 111 | 112 | export interface CreateEventData { 113 | chainId: number 114 | endDate: string | number 115 | customEndDate: boolean 116 | limit: number 117 | depositFee: number 118 | recipient?: string 119 | tokenAddress?: string 120 | } 121 | -------------------------------------------------------------------------------- /packages/blog/data/posts/faq-for-attendees.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FAQ for Attendees 3 | description: Discover the details of Show Up in this comprehensive FAQ guide. Find answers to common questions about events, registering, checking in, and earning money by showing up. 4 | date: 2023-12-18T14:25:20.138Z 5 | --- 6 | 7 | Discover the details of Show Up in this comprehensive FAQ guide. Find answers to common questions about events, registering, checking in, and earning money by showing up. 8 | 9 | ### What is Show Up? 10 | 11 | Show Up is an on-chain RSVP and Event management protocol designed to reshape event participation. It introduces a novel staking mechanism that aligns the incentives between event organizers and attendees. [More info here](https://blog.showup.events/introducing-show-up-protocol). 12 | 13 | ### How can I join an event? 14 | 15 | Any event on [Show Up](https://www.showup.events/) is public. You can join any event by paying the deposit fee on the event page to secure your spot. 16 | 17 | ### Can I edit my registration after signing up? 18 | 19 | No, registrations are final. Make sure to double-check the event details before confirming your attendance. 20 | 21 | ### What if I can't attend after registering? 22 | 23 | If you're unable to attend, make sure to contact your event organizer on time to inform them. This may free up space for others to join. Your deposit will be lost and distributed among checked-in attendees at the end of the event. 24 | 25 | ### How do I check in at an event? 26 | 27 | Simply show up at the event, and the organizer will check you in. Make sure to bring your ticket or proof of registration. 28 | 29 | ### How much is the deposit fee? 30 | 31 | The deposit fee is set by the organizer. Check the event details to find the specified amount. 32 | 33 | ### How do I pay for the deposit fee? 34 | 35 | The currency and fee are set by the organizer. Show Up currently supports native Ether or ERC20 stablecoins, like USDC, USDT, and DAI. It's only deployed on Optimism; make sure your funds are available there in order to register for an event. 36 | 37 | ### How can I make money? 38 | 39 | If you show up at the event, your deposit will be returned to you along with a share of the unclaimed deposits. 40 | 41 | ### Joined an event but didn't get money back? 42 | 43 | The organizer checks and settles the event to distribute funds. This process takes time and can only happen after the event has ended, so please be patient. When in doubt, make sure to contact the event organizer. Show Up is not able to retrieve, settle or collect any of the funds. 44 | 45 | ### I have another question. How can I get support for Show Up? 46 | 47 | Check the [blog](https://blog.showup.events/) for more information, or head over to [GitHub](https://github.com/wslyvh/show-up) to read the technical documentation or open an issue, or support-/feature request. 48 | -------------------------------------------------------------------------------- /packages/app/src/app/[id]/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | import { defaultOpenGraphImage } from '@/components/OpenGraph' 2 | import { GetUser } from '@/services/showhub' 3 | import { SITE_EMOJI, SITE_NAME } from '@/utils/site' 4 | import { ImageResponse } from 'next/og' 5 | 6 | // Route segment config 7 | export const runtime = 'edge' 8 | 9 | // Image metadata 10 | export const alt = SITE_NAME 11 | export const size = { width: 1200, height: 630 } 12 | 13 | export const contentType = 'image/png' 14 | 15 | export default async function Image({ params }: { params: { id: string } }) { 16 | const owner = await GetUser(params.id) 17 | if (!owner) return defaultOpenGraphImage() 18 | 19 | const title = owner.name 20 | let fontSize = 96 21 | if (title.length > 50) fontSize = 72 22 | if (title.length > 100) fontSize = 60 23 | if (title.length > 150) fontSize = 48 24 | 25 | return new ImageResponse( 26 | ( 27 |
37 |

{title}

38 |
46 | 53 | 58 | 59 |

 

60 |
61 | 62 |

63 | {SITE_EMOJI} 64 |

65 |
66 | ) 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /packages/subgraph/abis/IConditionModule.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint256", 6 | "name": "recordId", 7 | "type": "uint256" 8 | }, 9 | { 10 | "internalType": "bytes", 11 | "name": "data", 12 | "type": "bytes" 13 | } 14 | ], 15 | "name": "cancel", 16 | "outputs": [], 17 | "stateMutability": "nonpayable", 18 | "type": "function" 19 | }, 20 | { 21 | "inputs": [ 22 | { 23 | "internalType": "uint256", 24 | "name": "recordId", 25 | "type": "uint256" 26 | }, 27 | { 28 | "internalType": "address[]", 29 | "name": "attendees", 30 | "type": "address[]" 31 | }, 32 | { 33 | "internalType": "bytes", 34 | "name": "data", 35 | "type": "bytes" 36 | } 37 | ], 38 | "name": "checkin", 39 | "outputs": [ 40 | { 41 | "internalType": "address[]", 42 | "name": "", 43 | "type": "address[]" 44 | } 45 | ], 46 | "stateMutability": "nonpayable", 47 | "type": "function" 48 | }, 49 | { 50 | "inputs": [ 51 | { 52 | "internalType": "uint256", 53 | "name": "recordId", 54 | "type": "uint256" 55 | }, 56 | { 57 | "internalType": "bytes", 58 | "name": "data", 59 | "type": "bytes" 60 | } 61 | ], 62 | "name": "initialize", 63 | "outputs": [], 64 | "stateMutability": "nonpayable", 65 | "type": "function" 66 | }, 67 | { 68 | "inputs": [], 69 | "name": "name", 70 | "outputs": [ 71 | { 72 | "internalType": "string", 73 | "name": "", 74 | "type": "string" 75 | } 76 | ], 77 | "stateMutability": "view", 78 | "type": "function" 79 | }, 80 | { 81 | "inputs": [ 82 | { 83 | "internalType": "uint256", 84 | "name": "recordId", 85 | "type": "uint256" 86 | }, 87 | { 88 | "internalType": "address", 89 | "name": "participant", 90 | "type": "address" 91 | }, 92 | { 93 | "internalType": "address", 94 | "name": "sender", 95 | "type": "address" 96 | }, 97 | { 98 | "internalType": "bytes", 99 | "name": "data", 100 | "type": "bytes" 101 | } 102 | ], 103 | "name": "register", 104 | "outputs": [], 105 | "stateMutability": "payable", 106 | "type": "function" 107 | }, 108 | { 109 | "inputs": [ 110 | { 111 | "internalType": "uint256", 112 | "name": "recordId", 113 | "type": "uint256" 114 | }, 115 | { 116 | "internalType": "bytes", 117 | "name": "data", 118 | "type": "bytes" 119 | } 120 | ], 121 | "name": "settle", 122 | "outputs": [], 123 | "stateMutability": "nonpayable", 124 | "type": "function" 125 | } 126 | ] 127 | -------------------------------------------------------------------------------- /packages/app/src/app/profile/components/Profile.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { useAccount, useDisconnect, useEnsAvatar, useEnsName } from 'wagmi' 5 | import { Login } from './Login' 6 | import Image from 'next/image' 7 | import makeBlockie from 'ethereum-blockies-base64' 8 | import { TruncateMiddle } from '@/utils/format' 9 | import { LinkComponent } from '@/components/LinkComponent' 10 | import { CalendarIcon, PlusIcon } from '@heroicons/react/24/outline' 11 | import { useMyEvents } from '@/hooks/useEvents' 12 | import { TicketBadge } from '@/app/tickets/components/Ticket' 13 | import { useRouter, useSearchParams } from 'next/navigation' 14 | import { normalize } from 'viem/ens' 15 | 16 | export function Profile() { 17 | const { address } = useAccount() 18 | const { disconnect } = useDisconnect() 19 | const { isConnected } = useAccount() 20 | const { data: name } = useEnsName({ address, chainId: 1 }) 21 | const { data: avatar } = useEnsAvatar({ name: normalize(name as string), chainId: 1 }) 22 | const { data: events } = useMyEvents(address) 23 | 24 | const router = useRouter() 25 | const searchParams = useSearchParams() 26 | const redirect = searchParams.get('redirect') 27 | 28 | if (!isConnected) { 29 | return 30 | } 31 | 32 | if (isConnected && !!redirect) { 33 | router.push(redirect) 34 | } 35 | 36 | const displayName = name ?? TruncateMiddle(address, 6) 37 | const imageUrl = avatar ?? makeBlockie(address ?? '') 38 | 39 | return ( 40 |
41 |
42 | {displayName} 43 |
44 | {displayName} 45 | 48 |
49 |
50 | 51 |
52 | 53 |
54 | 55 | <> 56 | Create Event 57 | 58 | 59 | 60 |
61 | My Events 62 | 63 |
64 | 65 | {events && events.length > 0 && ( 66 |
    67 | {events?.map((event) => { 68 | return ( 69 |
  • 70 | 71 | · {event.metadata?.title} 72 | 73 | 74 |
  • 75 | ) 76 | })} 77 |
78 | )} 79 |
80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /packages/app/src/app/events/[slug]/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | import { defaultOpenGraphImage } from '@/components/OpenGraph' 2 | import { GetEventBySlug } from '@/services/showhub' 3 | import { SITE_EMOJI, SITE_NAME } from '@/utils/site' 4 | import { ImageResponse } from 'next/og' 5 | import dayjs from 'dayjs' 6 | 7 | // Route segment config 8 | export const runtime = 'edge' 9 | 10 | // Image metadata 11 | export const alt = SITE_NAME 12 | export const size = { width: 1200, height: 630 } 13 | 14 | export const contentType = 'image/png' 15 | 16 | export default async function Image({ params }: { params: { slug: string } }) { 17 | const record = await GetEventBySlug(params.slug) 18 | if (!record || !record.metadata) return defaultOpenGraphImage() 19 | 20 | const sameDay = dayjs(record.metadata.start).isSame(record.metadata.end, 'day') 21 | const title = record.metadata.title 22 | let fontSize = 96 23 | if (title.length > 50) fontSize = 72 24 | if (title.length > 100) fontSize = 60 25 | if (title.length > 150) fontSize = 48 26 | 27 | return new ImageResponse( 28 | ( 29 |
39 |

{title}

40 |
48 | 55 | 60 | 61 |

62 | {dayjs(record.metadata.start).format('ddd MMM DD · HH:mm')} 63 | {' → '} 64 | {dayjs(record.metadata.end).format(sameDay ? 'HH:mm' : 'ddd MMM DD · HH:mm')} 65 |

66 |
67 | 68 |

69 | {SITE_EMOJI} 70 |

71 |
72 | ) 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /packages/protocol/scripts/verify.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network, run } from 'hardhat' 2 | import deployments from '../deployments.json' 3 | 4 | export async function main() { 5 | console.log('Verifying Show Up Protocol..') 6 | 7 | console.log('NETWORK ID', network.config.chainId) 8 | if (!network.config.chainId) { 9 | console.error('Invalid Network ID') 10 | return 11 | } 12 | 13 | const contracts = (deployments as any)[network.config.chainId] 14 | const showhub = contracts.ShowHub 15 | const recipientEther = contracts.RecipientEther 16 | const recipientToken = contracts.RecipientToken 17 | const splitEther = contracts.SplitEther 18 | const splitToken = contracts.SplitToken 19 | 20 | if (!showhub || !recipientEther || !recipientToken || !splitEther || !splitToken) { 21 | console.log('Contracts not found') 22 | return 23 | } 24 | 25 | console.log('Deployment addresses:') 26 | console.log('showhub:', showhub) 27 | console.log('recipientEther:', recipientEther) 28 | console.log('recipientToken:', recipientToken) 29 | console.log('splitEther:', splitEther) 30 | console.log('splitToken:', splitToken) 31 | 32 | // no need to verify on localhost or hardhat 33 | if (network.config.chainId != 31337 && process.env.ETHERSCAN_API_KEY) { 34 | console.log('Verifying contracts..') 35 | 36 | try { 37 | run('verify:verify', { 38 | address: showhub, 39 | constructorArguments: [], 40 | contract: 'contracts/ShowHub.sol:ShowHub', 41 | }) 42 | } catch (e) { 43 | console.log(e) 44 | } 45 | 46 | console.log('Verifying RecipientEther module..') 47 | try { 48 | run('verify:verify', { 49 | address: recipientEther, 50 | constructorArguments: [showhub], 51 | contract: 'contracts/conditions/RecipientEther.sol:RecipientEther', 52 | }) 53 | } catch (e) { 54 | console.log(e) 55 | } 56 | 57 | console.log('Verifying RecipientToken module..') 58 | try { 59 | run('verify:verify', { 60 | address: recipientToken, 61 | constructorArguments: [showhub], 62 | contract: 'contracts/conditions/RecipientToken.sol:RecipientToken', 63 | }) 64 | } catch (e) { 65 | console.log(e) 66 | } 67 | 68 | console.log('Verifying SplitEther module..') 69 | try { 70 | run('verify:verify', { 71 | address: splitEther, 72 | constructorArguments: [showhub], 73 | contract: 'contracts/conditions/SplitEther.sol:SplitEther', 74 | }) 75 | } catch (e) { 76 | console.log(e) 77 | } 78 | 79 | console.log('Verifying SplitToken module..') 80 | try { 81 | run('verify:verify', { 82 | address: splitToken, 83 | constructorArguments: [showhub], 84 | contract: 'contracts/conditions/SplitToken.sol:SplitToken', 85 | }) 86 | } catch (e) { 87 | console.log(e) 88 | } 89 | } 90 | } 91 | 92 | // We recommend this pattern to be able to use async/await everywhere 93 | // and properly handle errors. 94 | main().catch((error) => { 95 | console.error(error) 96 | process.exitCode = 1 97 | }) 98 | -------------------------------------------------------------------------------- /packages/app/src/app/tickets/components/Ticket.tsx: -------------------------------------------------------------------------------- 1 | import { Record, Status } from '@/utils/types' 2 | import { QRCodeSVG } from 'qrcode.react' 3 | import { useAccount } from 'wagmi' 4 | import { formatEther, formatUnits } from 'viem/utils' 5 | import dayjs from 'dayjs' 6 | 7 | interface Props { 8 | record: Record 9 | } 10 | 11 | interface StatusProps { 12 | status: Status 13 | size?: 'xs' | 'sm' | 'md' | 'lg' 14 | className?: string 15 | } 16 | 17 | export function TicketBadge(props: StatusProps) { 18 | let className = 'badge badge-outline self-start mt-2 ml-2 shrink-0' 19 | if (props.status == Status.Active) className += ' badge-info' 20 | if (props.status == Status.Cancelled) className += ' badge-error' 21 | if (props.status == Status.Settled) className += ' badge-success' 22 | if (props.size) className += ` badge-${props.size}` 23 | if (props.className) className += ` ${props.className}` 24 | 25 | return {props.status} 26 | } 27 | 28 | export function Ticket(props: Props) { 29 | const { address } = useAccount() 30 | const ticket = props.record.registrations.find((p) => p.id.toLowerCase() == address.toLowerCase()) 31 | 32 | if (!ticket) return null 33 | 34 | return ( 35 |
36 |
37 | 38 |
39 | 40 |
41 |
42 |
43 | 44 |
45 |
46 | {props.record.metadata?.title} 47 | 48 |
49 | {props.record.metadata?.location} 50 | 51 |
52 | Date 53 | {dayjs(props.record.metadata?.start).format('DD/MMM/YYYY')} 54 |
55 | 56 |
57 |
58 | Address 59 | {address} 60 |
61 |
62 | Deposit 63 | 64 | {!props.record.conditionModuleData.tokenAddress && ( 65 | <>{formatEther(BigInt(props.record.conditionModuleData.depositFee))} ETH 66 | )} 67 | {props.record.conditionModuleData.tokenAddress && ( 68 | <> 69 | {formatUnits( 70 | BigInt(props.record.conditionModuleData.depositFee), 71 | props.record.conditionModuleData.tokenDecimals ?? 18 72 | )}{' '} 73 | {props.record.conditionModuleData.tokenSymbol} 74 | 75 | )} 76 | 77 |
78 |
79 |
80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /packages/protocol/contracts/conditions/SplitEther.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; 5 | import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; 6 | import '../Common.sol'; 7 | 8 | contract SplitEther is Ownable { 9 | struct Conditions { 10 | uint256 depositFee; 11 | } 12 | 13 | mapping(uint256 => Conditions) internal _conditions; 14 | mapping(uint256 => uint256) internal _totalDeposits; 15 | mapping(uint256 => uint256) internal _totalFunded; 16 | 17 | string internal _name; 18 | 19 | constructor(address owner) Ownable(owner) { 20 | _name = 'SplitEther'; 21 | } 22 | 23 | function initialize(uint256 id, bytes calldata data) external virtual onlyOwner returns (bool) { 24 | Conditions memory conditions = abi.decode(data, (Conditions)); 25 | 26 | _conditions[id] = conditions; 27 | 28 | return true; 29 | } 30 | 31 | function cancel( 32 | uint256 id, 33 | address owner, 34 | address[] calldata registrations, 35 | bytes calldata data 36 | ) external virtual onlyOwner returns (bool) { 37 | for (uint256 i = 0; i < registrations.length; i++) { 38 | payable(registrations[i]).transfer(_conditions[id].depositFee); 39 | } 40 | _totalDeposits[id] = 0; 41 | 42 | if (_totalFunded[id] > 0) { 43 | payable(owner).transfer(_totalFunded[id]); 44 | _totalFunded[id] = 0; 45 | } 46 | 47 | return true; 48 | } 49 | 50 | function fund(uint256 id, address sender, bytes calldata data) external payable virtual onlyOwner returns (bool) { 51 | if (msg.value == 0) revert IncorrectValue(); 52 | 53 | _totalFunded[id] += msg.value; 54 | 55 | return true; 56 | } 57 | 58 | function register( 59 | uint256 id, 60 | address participant, 61 | address sender, 62 | bytes calldata data 63 | ) external payable virtual onlyOwner returns (bool) { 64 | if (_conditions[id].depositFee != msg.value) revert IncorrectValue(); 65 | 66 | _totalDeposits[id] += _conditions[id].depositFee; 67 | 68 | return true; 69 | } 70 | 71 | function checkin( 72 | uint256 id, 73 | address[] calldata attendees, 74 | bytes calldata data 75 | ) external virtual onlyOwner returns (bool) { 76 | return true; 77 | } 78 | 79 | function settle( 80 | uint256 id, 81 | address[] calldata attendees, 82 | bytes calldata data 83 | ) external virtual onlyOwner returns (bool) { 84 | uint256 totalFunds = _totalDeposits[id] + _totalFunded[id]; 85 | (bool success, uint256 attendanceFee) = Math.tryDiv(totalFunds, attendees.length); 86 | if (!success) revert IncorrectValue(); 87 | 88 | for (uint256 i = 0; i < attendees.length; i++) { 89 | payable(attendees[i]).transfer(attendanceFee); 90 | } 91 | 92 | _totalDeposits[id] = 0; 93 | _totalFunded[id] = 0; 94 | 95 | return true; 96 | } 97 | 98 | // View functions 99 | // ======================= 100 | 101 | function name() external view returns (string memory) { 102 | return _name; 103 | } 104 | 105 | function getConditions(uint256 id) external view returns (Conditions memory) { 106 | return _conditions[id]; 107 | } 108 | 109 | function getTotalDeposits(uint256 id) external view returns (uint256) { 110 | return _totalDeposits[id]; 111 | } 112 | 113 | function getTotalFunded(uint256 id) external view returns (uint256) { 114 | return _totalFunded[id]; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/app/src/app/[id]/components/Profile.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useMyEvents } from '@/hooks/useEvents' 4 | import { Card } from '../../components/Card' 5 | import { Empty } from '@/components/Empty' 6 | import { FaDiscord, FaEthereum, FaGithub, FaTelegram, FaTwitter } from 'react-icons/fa6' 7 | import makeBlockie from 'ethereum-blockies-base64' 8 | import { LinkComponent } from '@/components/LinkComponent' 9 | import { useProfile } from '@/hooks/useProfile' 10 | 11 | interface Props { 12 | id: string 13 | } 14 | 15 | export function Profile(props: Props) { 16 | const { data: owner, isError: isOwnerError } = useProfile(props.id) 17 | const { data: events, isError: isEventsError } = useMyEvents(props.id) 18 | if (!owner) return null // loading 19 | if (isOwnerError || isEventsError) return 20 | 21 | const onSelect = (option: string) => { 22 | // filter 23 | } 24 | 25 | return ( 26 | <> 27 |
28 |
29 |
30 |
31 |
32 | {owner.name} 33 |
34 |
35 |
36 |
37 | 38 |
39 |

{owner.name}

40 |
41 | {owner.twitter && ( 42 | 43 | 44 | 45 | )} 46 | 47 | {owner.telegram && ( 48 | 49 | 50 | 51 | )} 52 | 53 | {owner.discord && ( 54 | 55 | 56 | 57 | )} 58 | 59 | {owner.github && ( 60 | 61 | 62 | 63 | )} 64 | 65 | {owner.id && ( 66 | 67 | 68 | 69 | )} 70 |
71 | 72 | {owner.website && ( 73 |
74 | 75 | 78 | 79 |
80 | )} 81 | 82 |
{owner.description}
83 |
84 |
85 | 86 | {/*
87 | 88 |
*/} 89 | 90 |
91 | {events && events.length > 0 && events.map((event) => )} 92 |
93 | 94 | {events && events.length === 0 && } 95 | 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /packages/app/src/utils/network.ts: -------------------------------------------------------------------------------- 1 | import { defaultWagmiConfig } from '@web3modal/wagmi/react/config' 2 | import { http, createStorage, cookieStorage } from 'wagmi' 3 | import { SITE_NAME, SITE_DESCRIPTION, SITE_URL } from './site' 4 | import { base, baseSepolia, optimism, sepolia } from 'viem/chains' 5 | import { CONFIG } from './config' 6 | import { Transport } from 'viem' 7 | 8 | export const AddressZero = '0x0000000000000000000000000000000000000000' 9 | export const DefaultDepositFee = 0.01 10 | 11 | const transports: Record = 12 | CONFIG.NETWORK_ENV === 'main' 13 | ? { 14 | [optimism.id]: http(`https://optimism-mainnet.infura.io/v3/${CONFIG.NEXT_PUBLIC_INFURA_KEY}`), 15 | [base.id]: http(`https://base-mainnet.g.alchemy.com/v2/${CONFIG.NEXT_PUBLIC_ALCHEMY_KEY_BASE}`), 16 | } 17 | : { 18 | [sepolia.id]: http(`https://sepolia.infura.io/v3/${CONFIG.NEXT_PUBLIC_INFURA_KEY}`), 19 | [baseSepolia.id]: http(`https://base-sepolia.g.alchemy.com/v2/${CONFIG.NEXT_PUBLIC_ALCHEMY_KEY_BASE_SEPOLIA}`), 20 | } 21 | 22 | // export const WAGMI_CONFIG = createConfig({ 23 | // chains: CONFIG.DEFAULT_CHAINS, 24 | // transports: transports, 25 | // }) 26 | 27 | // Web3Modal config 28 | export const WAGMI_CONFIG = defaultWagmiConfig({ 29 | chains: CONFIG.DEFAULT_CHAINS, 30 | transports: transports, 31 | 32 | projectId: CONFIG.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, 33 | ssr: true, 34 | // storage: createStorage({ 35 | // storage: cookieStorage, 36 | // }), 37 | 38 | metadata: { 39 | name: SITE_NAME, 40 | description: SITE_DESCRIPTION, 41 | url: SITE_URL, 42 | icons: [], 43 | }, 44 | }) 45 | 46 | export function GetNetworkColor(chainId: number) { 47 | if (chainId === 1) return 'green' 48 | if (chainId === 10) return 'red' 49 | if (chainId === 137) return 'purple' 50 | if (chainId === 42161) return 'blue' 51 | if (chainId === 534352) return 'yellow' 52 | 53 | return 'grey' 54 | } 55 | 56 | export function GetNetworkName(chainId: number) { 57 | if (chainId === 1) return 'mainnet' 58 | if (chainId === 11155111) return 'sepolia' 59 | if (chainId === 42161) return 'arbitrum' 60 | if (chainId === 10) return 'optimism' 61 | 62 | return 'mainnet' 63 | } 64 | 65 | export const WHITELISTED_TOKENS = [ 66 | { chainId: 1, symbol: 'DAI', address: '0x6b175474e89094c44da98b954eedeac495271d0f', decimals: 18 }, 67 | { chainId: 1, symbol: 'USDC', address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: 6 }, 68 | { chainId: 1, symbol: 'USDT', address: '0xdac17f958d2ee523a2206206994597c13d831ec7', decimals: 6 }, 69 | 70 | { chainId: 10, symbol: 'OP', address: '0x4200000000000000000000000000000000000042', decimals: 18 }, 71 | { chainId: 10, symbol: 'DAI', address: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', decimals: 18 }, 72 | { chainId: 10, symbol: 'USDT', address: '0x94b008aa00579c1307b0ef2c499ad98a8ce58e58', decimals: 6 }, 73 | { chainId: 10, symbol: 'USDC', address: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', decimals: 6 }, 74 | 75 | { chainId: 11155111, symbol: 'SUP-DAI', address: '0x7ef7024B76791BD1f31Ac482724c76f0e24a2dD0', decimals: 18 }, 76 | ] 77 | 78 | export function GetTokenSymbol(address?: string) { 79 | if (!address || address == AddressZero) return 'ETH' 80 | 81 | return WHITELISTED_TOKENS.find((t) => t.address.toLowerCase() === address.toLowerCase())?.symbol || 'ETH' 82 | } 83 | 84 | export function GetTokenDecimals(address?: string) { 85 | if (!address || address == AddressZero) return 18 86 | 87 | return WHITELISTED_TOKENS.find((t) => t.address.toLowerCase() === address.toLowerCase())?.decimals || 18 88 | } 89 | -------------------------------------------------------------------------------- /packages/indexer/src/utils/client.ts: -------------------------------------------------------------------------------- 1 | import { Chain, createPublicClient, http } from "viem"; 2 | import { normalize } from "viem/ens"; 3 | import { baseSepolia, sepolia, optimism, mainnet, base } from "viem/chains"; 4 | import { TruncateMiddle } from "./mapping"; 5 | import { addEnsContracts, ensPublicActions } from "@ensdomains/ensjs"; 6 | import { getName } from "@ensdomains/ensjs/public"; 7 | 8 | export function GetClient(chainId: number) { 9 | let chain: Chain = optimism; 10 | if (chainId == 10) chain = optimism; 11 | if (chainId == 8453) chain = base; 12 | if (chainId == 11155111) chain = sepolia; 13 | if (chainId == 84532) chain = baseSepolia; 14 | 15 | return createPublicClient({ 16 | chain: chain, 17 | transport: http(), 18 | }); 19 | } 20 | 21 | export async function GetEnsProfile(address: string) { 22 | const client = createPublicClient({ 23 | chain: addEnsContracts(mainnet), 24 | transport: http(), 25 | }).extend(ensPublicActions); 26 | 27 | let name = TruncateMiddle(address); 28 | try { 29 | // console.log("Get ENS Name", address); 30 | const nameResult = await getName(client, { 31 | address: address as any, 32 | }); 33 | 34 | if (nameResult) { 35 | name = nameResult.name; 36 | // console.log("Get records for", name); 37 | const ensAvatar = await client.getEnsAvatar({ 38 | name: normalize(name), 39 | }); 40 | const records = await client.getRecords({ 41 | name: nameResult.name, 42 | coins: ["ETH"], 43 | texts: [ 44 | "name", 45 | "description", 46 | "url", 47 | "location", 48 | "avatar", 49 | "email", 50 | "com.twitter", 51 | "com.github", 52 | "com.discord", 53 | "com.telegram", 54 | ], 55 | }); 56 | 57 | // Parse records 58 | const description = 59 | records.texts.find((r) => r.key === "description")?.value ?? null; 60 | const url = records.texts.find((r) => r.key === "url")?.value ?? null; 61 | const avatar = 62 | records.texts.find((r) => r.key === "avatar")?.value ?? null; 63 | const email = records.texts.find((r) => r.key === "email")?.value ?? null; 64 | const twitter = 65 | records.texts 66 | .find((r) => r.key === "com.twitter") 67 | ?.value?.replace("https://twitter.com/", "") ?? null; 68 | const github = 69 | records.texts 70 | .find((r) => r.key === "com.github") 71 | ?.value?.replace("https://github.com/", "") ?? null; 72 | const discord = 73 | records.texts.find((r) => r.key === "com.discord")?.value ?? null; 74 | const telegram = 75 | records.texts 76 | .find((r) => r.key === "com.telegram") 77 | ?.value?.replace("https://t.me/", "") ?? null; 78 | 79 | // Parse records 80 | return { 81 | id: address, 82 | name: name, 83 | avatar: ensAvatar ?? avatar, 84 | description: description, 85 | website: url, 86 | email: email, 87 | twitter: twitter, 88 | github: github, 89 | discord: discord, 90 | telegram: telegram, 91 | }; 92 | } 93 | } catch (e) { 94 | // ignore 95 | console.log("Error fetching ENS Records", e); 96 | } 97 | 98 | return { 99 | id: address, 100 | name: name, 101 | avatar: null, 102 | description: null, 103 | website: null, 104 | email: null, 105 | twitter: null, 106 | github: null, 107 | discord: null, 108 | telegram: null, 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /packages/protocol/contracts/conditions/RecipientEther.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; 5 | import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; 6 | import '../Common.sol'; 7 | 8 | contract RecipientEther is Ownable { 9 | struct Conditions { 10 | uint256 depositFee; 11 | address recipient; 12 | } 13 | 14 | mapping(uint256 => Conditions) internal _conditions; 15 | mapping(uint256 => uint256) internal _totalDeposits; 16 | mapping(uint256 => uint256) internal _totalFunded; 17 | 18 | string internal _name; 19 | 20 | constructor(address owner) Ownable(owner) { 21 | _name = 'RecipientEther'; 22 | } 23 | 24 | function initialize(uint256 id, bytes calldata data) external virtual onlyOwner returns (bool) { 25 | Conditions memory conditions = abi.decode(data, (Conditions)); 26 | 27 | _conditions[id] = conditions; 28 | 29 | return true; 30 | } 31 | 32 | function cancel( 33 | uint256 id, 34 | address owner, 35 | address[] calldata registrations, 36 | bytes calldata data 37 | ) external virtual onlyOwner returns (bool) { 38 | for (uint256 i = 0; i < registrations.length; i++) { 39 | payable(registrations[i]).transfer(_conditions[id].depositFee); 40 | } 41 | _totalDeposits[id] = 0; 42 | 43 | if (_totalFunded[id] > 0) { 44 | payable(owner).transfer(_totalFunded[id]); 45 | _totalFunded[id] = 0; 46 | } 47 | 48 | return true; 49 | } 50 | 51 | function fund(uint256 id, address sender, bytes calldata data) external payable virtual onlyOwner returns (bool) { 52 | if (msg.value == 0) revert IncorrectValue(); 53 | 54 | _totalFunded[id] += msg.value; 55 | 56 | return true; 57 | } 58 | 59 | function register( 60 | uint256 id, 61 | address participant, 62 | address sender, 63 | bytes calldata data 64 | ) external payable virtual onlyOwner returns (bool) { 65 | if (_conditions[id].depositFee != msg.value) revert IncorrectValue(); 66 | 67 | _totalDeposits[id] += _conditions[id].depositFee; 68 | 69 | return true; 70 | } 71 | 72 | function checkin( 73 | uint256 id, 74 | address[] calldata attendees, 75 | bytes calldata data 76 | ) external virtual onlyOwner returns (bool) { 77 | return true; 78 | } 79 | 80 | function settle( 81 | uint256 id, 82 | address[] calldata attendees, 83 | bytes calldata data 84 | ) external virtual onlyOwner returns (bool) { 85 | (bool success, uint256 fundFee) = Math.tryDiv(_totalFunded[id], attendees.length); 86 | if (!success) revert IncorrectValue(); 87 | 88 | uint256 totalPayouts = 0; 89 | uint256 settlementFee = _conditions[id].depositFee + fundFee; 90 | for (uint256 i = 0; i < attendees.length; i++) { 91 | payable(attendees[i]).transfer(settlementFee); 92 | totalPayouts += _conditions[id].depositFee; 93 | } 94 | 95 | if (_totalDeposits[id] > totalPayouts) { 96 | uint256 recipientFee = _totalDeposits[id] - totalPayouts; 97 | payable(_conditions[id].recipient).transfer(recipientFee); 98 | } 99 | 100 | _totalDeposits[id] = 0; 101 | _totalFunded[id] = 0; 102 | 103 | return true; 104 | } 105 | 106 | // View functions 107 | // ======================= 108 | 109 | function name() external view returns (string memory) { 110 | return _name; 111 | } 112 | 113 | function getConditions(uint256 id) external view returns (Conditions memory) { 114 | return _conditions[id]; 115 | } 116 | 117 | function getTotalDeposits(uint256 id) external view returns (uint256) { 118 | return _totalDeposits[id]; 119 | } 120 | 121 | function getTotalFunded(uint256 id) external view returns (uint256) { 122 | return _totalFunded[id]; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /packages/blog/data/posts/faq-for-event-organizers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FAQ for Event organizers 3 | description: Discover the details of Show Up in this comprehensive FAQ guide. Find answers to common questions about event creation, check-ins, RSVPs, deposits, and distributing funds. 4 | date: 2023-12-14T09:15:25.138Z 5 | --- 6 | 7 | Discover the details of Show Up in this comprehensive FAQ guide. Find answers to common questions about event creation, check-ins, RSVPs, deposits, and distributing funds. 8 | 9 | ### What is Show Up? 10 | 11 | Show Up is an on-chain RSVP and Event management protocol designed to reshape event participation. It introduces a novel staking mechanism that aligns the incentives between event organizers and attendees. [More info here](https://blog.showup.events/introducing-show-up-protocol). 12 | 13 | ### Who can create an event? 14 | 15 | Anyone can create an event. The protocol is fully open and permissionless. Simply connect your account, go to ["Create Event"](https://www.showup.events/create), fill in the details, set the deposit fee, and publish your event. It's that easy! 16 | 17 | ### How do I update or manage my event? 18 | 19 | Events are currently immutable to avoid changing conditions after people sign up. However, you can cancel and create a new event at any time. As an organizer, you are also responsible for checking people in and settling the event. 20 | 21 | ### What are the costs associated with managing an event? 22 | 23 | You only pay for transaction fees when creating or managing your event. Show Up is currently only deployed on Optimism, keeping costs low. Creating an event is ~0.02ct, and managing check-ins is ~0.01ct. The protocol does not charge any fees yet. 24 | 25 | ### Can I charge money for tickets? 26 | 27 | No, Show Up is not meant as a replacement for your ticketing solution but focuses specifically on RSVPs with its staking mechanism. This works great for, e.g., requesting a fee to incentivize hackers to submit their projects at a hackathon or fostering dedicated participants in educational bootcamps, courses, etc. Find more info and use-cases [here](https://blog.showup.events/introducing-show-up-protocol). 28 | 29 | ### How much is the registration fee? 30 | 31 | As an organizer, you decide the deposit fee for your event. It's recommended to keep it accessible enough so people can register but have some skin in the game to actually show up. A recommended fee is an equivalent of $5-20 depending on the size of your event. 32 | 33 | ### Which currency is used for the deposit? 34 | 35 | As an organizer, you can decide which currency to use for registrations. Show Up currently supports native Ether deposits or ERC20 stablecoins like USDC, USDT, and DAI. Keep in mind that the protocol is currently only deployed on Optimism, meaning all transactions and funds need to be bridged and available there. 36 | 37 | ### What happens with all the deposits? 38 | 39 | All deposits are secured using a set of smart contracts until after the event ends. Show Up nor the organizer can retrieve or collect these funds. During the event, the organizer can check in people. After the event ends, the organizer can settle the event, distributing all funds to those who showed up. 40 | 41 | ### Who decides who showed up? 42 | 43 | It's the organizer's responsibility to check in people during the event when they show up. You can use the registration list on the App to help manage check-ins. After the event ends, the organizer can settle the event, distributing all funds to those who showed up. 44 | 45 | ### What happens if an attendee doesn't show up? 46 | 47 | Their deposit is distributed among checked-in attendees at the end of the event. 48 | 49 | ### I have another question. How can I get support for Show Up? 50 | 51 | Check the [blog](https://blog.showup.events/) for more information, or head over to [GitHub](https://github.com/wslyvh/show-up) to read the technical documentation or open an issue, or support-/feature request. 52 | -------------------------------------------------------------------------------- /packages/protocol/contracts/conditions/SplitToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; 5 | import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 6 | import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; 7 | import '../Common.sol'; 8 | 9 | contract SplitToken is Ownable { 10 | struct Conditions { 11 | uint256 depositFee; 12 | address tokenAddress; 13 | } 14 | 15 | string internal _name; 16 | mapping(uint256 => Conditions) internal _conditions; 17 | mapping(uint256 => uint256) internal _totalDeposits; 18 | mapping(uint256 => uint256) internal _totalFunded; 19 | 20 | constructor(address owner) Ownable(owner) { 21 | _name = 'SplitToken'; 22 | } 23 | 24 | function initialize(uint256 id, bytes calldata data) external virtual onlyOwner returns (bool) { 25 | Conditions memory conditions = abi.decode(data, (Conditions)); 26 | 27 | _conditions[id] = conditions; 28 | 29 | return true; 30 | } 31 | 32 | function cancel( 33 | uint256 id, 34 | address owner, 35 | address[] calldata registrations, 36 | bytes calldata data 37 | ) external virtual onlyOwner returns (bool) { 38 | IERC20 token = IERC20(_conditions[id].tokenAddress); 39 | 40 | for (uint256 i = 0; i < registrations.length; i++) { 41 | require(token.transfer(registrations[i], _conditions[id].depositFee)); 42 | } 43 | _totalDeposits[id] = 0; 44 | 45 | if (_totalFunded[id] > 0) { 46 | require(token.transfer(owner, _totalFunded[id])); 47 | _totalFunded[id] = 0; 48 | } 49 | 50 | return true; 51 | } 52 | 53 | function fund(uint256 id, address sender, bytes calldata data) external payable virtual onlyOwner returns (bool) { 54 | if (msg.value > 0) revert IncorrectValue(); 55 | uint256 amount = abi.decode(data, (uint256)); 56 | if (amount == 0) revert IncorrectValue(); 57 | 58 | IERC20 token = IERC20(_conditions[id].tokenAddress); 59 | require(token.transferFrom(sender, address(this), amount)); 60 | 61 | _totalFunded[id] += amount; 62 | 63 | return true; 64 | } 65 | 66 | function register( 67 | uint256 id, 68 | address participant, 69 | address sender, 70 | bytes calldata data 71 | ) external payable virtual onlyOwner returns (bool) { 72 | if (msg.value > 0) revert IncorrectValue(); 73 | 74 | IERC20 token = IERC20(_conditions[id].tokenAddress); 75 | require(token.transferFrom(sender, address(this), _conditions[id].depositFee)); 76 | 77 | _totalDeposits[id] += _conditions[id].depositFee; 78 | 79 | return true; 80 | } 81 | 82 | function checkin( 83 | uint256 id, 84 | address[] calldata attendees, 85 | bytes calldata data 86 | ) external virtual onlyOwner returns (bool) { 87 | return true; 88 | } 89 | 90 | function settle( 91 | uint256 id, 92 | address[] calldata attendees, 93 | bytes calldata data 94 | ) external virtual onlyOwner returns (bool) { 95 | uint256 totalFunds = _totalDeposits[id] + _totalFunded[id]; 96 | (bool success, uint256 attendanceFee) = Math.tryDiv(totalFunds, attendees.length); 97 | if (!success) revert IncorrectValue(); 98 | 99 | IERC20 token = IERC20(_conditions[id].tokenAddress); 100 | for (uint256 i = 0; i < attendees.length; i++) { 101 | require(token.transfer(attendees[i], attendanceFee)); 102 | } 103 | 104 | _totalDeposits[id] = 0; 105 | _totalFunded[id] = 0; 106 | 107 | return true; 108 | } 109 | 110 | // View functions 111 | // ======================= 112 | 113 | function name() external view returns (string memory) { 114 | return _name; 115 | } 116 | 117 | function getConditions(uint256 id) external view returns (Conditions memory) { 118 | return _conditions[id]; 119 | } 120 | 121 | function getTotalDeposits(uint256 id) external view returns (uint256) { 122 | return _totalDeposits[id]; 123 | } 124 | 125 | function getTotalFunded(uint256 id) external view returns (uint256) { 126 | return _totalFunded[id]; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /packages/app/src/context/Notification.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { WAGMI_CONFIG } from '@/utils/network' 4 | import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react' 5 | import { getEnsName, waitForTransactionReceipt } from 'wagmi/actions' 6 | 7 | interface Notification { 8 | created: number 9 | type: 'success' | 'error' | 'warning' | 'info' 10 | message: string 11 | from?: string 12 | cta?: { label: string; href: string } 13 | data?: any 14 | } 15 | 16 | interface NotificationContext { 17 | new: boolean 18 | notifications: Notification[] 19 | Add: (notification: Notification) => Promise 20 | MarkAsRead: () => void 21 | Clear: () => void 22 | } 23 | 24 | const defaultState: NotificationContext = { 25 | new: false, 26 | notifications: [], 27 | Add: () => Promise.resolve(), 28 | MarkAsRead: () => {}, 29 | Clear: () => {}, 30 | } 31 | 32 | export const useNotifications = () => useContext(NotificationContext) 33 | 34 | const NotificationContext = createContext(defaultState) 35 | const localStorageKey = 'showup.notifications' 36 | 37 | export function NotificationProvider(props: PropsWithChildren) { 38 | const [state, setState] = useState({ 39 | ...defaultState, 40 | Add, 41 | MarkAsRead, 42 | Clear, 43 | }) 44 | 45 | useEffect(() => { 46 | if (typeof window !== 'undefined' && window.localStorage) { 47 | const notifications = localStorage.getItem(localStorageKey) 48 | if (notifications) { 49 | setState((state) => ({ ...state, notifications: JSON.parse(notifications) })) 50 | } 51 | } 52 | }, []) 53 | 54 | async function Add(notification: Notification) { 55 | await saveNotification(notification) 56 | 57 | if (notification.data?.hash) { 58 | console.log('Wait for transaction', notification.data.hash) 59 | try { 60 | await waitForTxNotification(notification.data.hash, notification) 61 | } catch (e) { 62 | // will re-try once, as its most likely a RPC issue 63 | try { 64 | await waitForTxNotification(notification.data.hash, notification) 65 | } catch (e) { 66 | // 67 | } 68 | } 69 | } 70 | } 71 | 72 | async function waitForTxNotification(hash: string, notification: Notification) { 73 | try { 74 | const data = await waitForTransactionReceipt(WAGMI_CONFIG, { 75 | hash: notification.data.hash, 76 | }) 77 | 78 | if (data.status === 'success') { 79 | return saveNotification({ 80 | ...notification, 81 | type: 'success', 82 | message: 'Transaction completed', 83 | }) 84 | } 85 | 86 | if (data.status === 'reverted') { 87 | return saveNotification({ 88 | ...notification, 89 | type: 'error', 90 | message: 'Transaction failed', 91 | }) 92 | } 93 | } catch (e) { 94 | console.log('Unable to wait for transaction') 95 | } 96 | } 97 | 98 | function MarkAsRead() { 99 | setState((state) => ({ 100 | ...state, 101 | new: false, 102 | })) 103 | } 104 | 105 | function Clear() { 106 | console.log('Clear Notifications') 107 | 108 | state.notifications = [] 109 | if (typeof window !== 'undefined' && window.localStorage) { 110 | localStorage.removeItem(localStorageKey) 111 | } 112 | } 113 | 114 | async function saveNotification(notification: Notification) { 115 | if (notification.from) { 116 | try { 117 | const name = await getEnsName(WAGMI_CONFIG, { 118 | address: notification.from, 119 | }) 120 | 121 | if (name) notification.from = name 122 | } catch (e) { 123 | // Unable to fetch ENS name (unsupported chain) 124 | } 125 | } 126 | 127 | const notifications = [...state.notifications, notification] 128 | if (typeof window !== 'undefined' && window.localStorage) { 129 | localStorage.setItem(localStorageKey, JSON.stringify(notifications)) 130 | } 131 | 132 | setState((state) => ({ 133 | ...state, 134 | new: true, 135 | notifications: notifications, 136 | })) 137 | } 138 | 139 | if (typeof window === 'undefined') { 140 | return <>{props.children} 141 | } 142 | 143 | return {props.children} 144 | } 145 | -------------------------------------------------------------------------------- /packages/app/src/context/EventData.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { PropsWithChildren, createContext, useContext } from 'react' 4 | import { useAccount, useBalance } from 'wagmi' 5 | import { EventMetadata, Record, Status } from '@/utils/types' 6 | import dayjs from 'dayjs' 7 | import { Alert } from '@/components/Alert' 8 | import { useEvent } from '@/hooks/useEvent' 9 | import { Chain } from 'viem/chains' 10 | import { CONFIG } from '@/utils/config' 11 | 12 | interface EventDataContext { 13 | record: Record 14 | event: EventMetadata 15 | chain: Chain 16 | sameDay: boolean 17 | hasEnded: boolean 18 | hasParticipants: boolean 19 | hasAttendees: boolean 20 | hasBalance: boolean 21 | isActive: boolean 22 | isCancelled: boolean 23 | isSettled: boolean 24 | isAdmin: boolean 25 | isParticipant: boolean 26 | canRegister: boolean 27 | canCancel: boolean 28 | canSettle: boolean 29 | refetch: () => void 30 | } 31 | 32 | const defaultState: EventDataContext = { 33 | record: {} as Record, 34 | event: {} as EventMetadata, 35 | chain: {} as Chain, 36 | sameDay: false, 37 | hasEnded: false, 38 | hasParticipants: false, 39 | hasAttendees: false, 40 | hasBalance: false, 41 | isActive: false, 42 | isCancelled: false, 43 | isSettled: false, 44 | isAdmin: false, 45 | isParticipant: false, 46 | canRegister: false, 47 | canCancel: false, 48 | canSettle: false, 49 | refetch: () => console.log('defaultState refetch()'), 50 | } 51 | 52 | interface Props extends PropsWithChildren { 53 | id: string 54 | } 55 | 56 | export const useEventData = () => useContext(EventDataContext) 57 | 58 | const EventDataContext = createContext(defaultState) 59 | 60 | export default function EventDataProvider(props: Props) { 61 | const { data: record, refetch } = useEvent(props) 62 | const { address } = useAccount() 63 | const balanceRequest: any = { address: address, watch: true } 64 | if (record?.conditionModuleData.tokenAddress) { 65 | balanceRequest.token = record.conditionModuleData.tokenAddress 66 | } 67 | const { data: balance } = useBalance(balanceRequest) 68 | const chain = CONFIG.DEFAULT_CHAINS.find((i) => i.id === record?.conditionModule.chainId) 69 | 70 | if (!record || !chain) return null 71 | 72 | const event = record.metadata! 73 | const sameDay = dayjs(event.start).isSame(event.end, 'day') 74 | const hasEnded = dayjs().isAfter(dayjs(record.endDate)) 75 | const hasAttendees = record.registrations.filter((i) => !!i.participated).length > 0 76 | const hasBalance = (balance && balance.value > BigInt(record.conditionModuleData.depositFee)) || false 77 | const isCancelled = record.status == Status.Cancelled 78 | const isActive = record.status == Status.Active 79 | const isSettled = record.status == Status.Settled 80 | const isAdmin = record.createdBy.toLowerCase() === address?.toLowerCase() 81 | const isParticipant = record.registrations.map((i) => i.id.toLowerCase()).includes(address?.toLowerCase()) 82 | const canRegister = !isActive || hasEnded || isParticipant || !hasBalance 83 | const canCancel = isActive && isAdmin && !hasEnded && !hasAttendees 84 | const canSettle = hasEnded && isActive && hasAttendees && !isSettled 85 | 86 | return ( 87 | 0, 95 | hasAttendees, 96 | hasBalance, 97 | isActive, 98 | isCancelled, 99 | isSettled, 100 | isAdmin, 101 | isParticipant, 102 | canRegister, 103 | canCancel, 104 | canSettle, 105 | refetch, 106 | }}> 107 | <> 108 | {isCancelled && ( 109 | 110 | )} 111 | {hasEnded && isActive && ( 112 | 113 | )} 114 | {isSettled && ( 115 | 116 | )} 117 | {props.children} 118 | 119 | 120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /packages/protocol/contracts/conditions/RecipientToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; 5 | import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 6 | import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; 7 | import '../Common.sol'; 8 | 9 | contract RecipientToken is Ownable { 10 | struct Conditions { 11 | uint256 depositFee; 12 | address tokenAddress; 13 | address recipient; 14 | } 15 | 16 | mapping(uint256 => Conditions) internal _conditions; 17 | mapping(uint256 => uint256) internal _totalDeposits; 18 | mapping(uint256 => uint256) internal _totalFunded; 19 | 20 | string internal _name; 21 | 22 | constructor(address owner) Ownable(owner) { 23 | _name = 'RecipientToken'; 24 | } 25 | 26 | function initialize(uint256 id, bytes calldata data) external virtual onlyOwner returns (bool) { 27 | Conditions memory conditions = abi.decode(data, (Conditions)); 28 | 29 | _conditions[id] = conditions; 30 | 31 | return true; 32 | } 33 | 34 | function cancel( 35 | uint256 id, 36 | address owner, 37 | address[] calldata registrations, 38 | bytes calldata data 39 | ) external virtual onlyOwner returns (bool) { 40 | IERC20 token = IERC20(_conditions[id].tokenAddress); 41 | 42 | for (uint256 i = 0; i < registrations.length; i++) { 43 | require(token.transfer(registrations[i], _conditions[id].depositFee)); 44 | } 45 | _totalDeposits[id] = 0; 46 | 47 | if (_totalFunded[id] > 0) { 48 | require(token.transfer(owner, _totalFunded[id])); 49 | _totalFunded[id] = 0; 50 | } 51 | 52 | return true; 53 | } 54 | 55 | function fund(uint256 id, address sender, bytes calldata data) external payable virtual onlyOwner returns (bool) { 56 | if (msg.value > 0) revert IncorrectValue(); 57 | uint256 amount = abi.decode(data, (uint256)); 58 | if (amount == 0) revert IncorrectValue(); 59 | 60 | IERC20 token = IERC20(_conditions[id].tokenAddress); 61 | require(token.transferFrom(sender, address(this), amount)); 62 | 63 | _totalFunded[id] += amount; 64 | 65 | return true; 66 | } 67 | 68 | function register( 69 | uint256 id, 70 | address participant, 71 | address sender, 72 | bytes calldata data 73 | ) external payable virtual onlyOwner returns (bool) { 74 | if (msg.value > 0) revert IncorrectValue(); 75 | 76 | IERC20 token = IERC20(_conditions[id].tokenAddress); 77 | require(token.transferFrom(sender, address(this), _conditions[id].depositFee)); 78 | 79 | _totalDeposits[id] += _conditions[id].depositFee; 80 | 81 | return true; 82 | } 83 | 84 | function checkin( 85 | uint256 id, 86 | address[] calldata attendees, 87 | bytes calldata data 88 | ) external virtual onlyOwner returns (bool) { 89 | return true; 90 | } 91 | 92 | function settle( 93 | uint256 id, 94 | address[] calldata attendees, 95 | bytes calldata data 96 | ) external virtual onlyOwner returns (bool) { 97 | (bool success, uint256 fundFee) = Math.tryDiv(_totalFunded[id], attendees.length); 98 | if (!success) revert IncorrectValue(); 99 | 100 | uint256 totalPayouts = 0; 101 | uint256 settlementFee = _conditions[id].depositFee + fundFee; 102 | IERC20 token = IERC20(_conditions[id].tokenAddress); 103 | for (uint256 i = 0; i < attendees.length; i++) { 104 | require(token.transfer(attendees[i], settlementFee)); 105 | totalPayouts += _conditions[id].depositFee; 106 | } 107 | 108 | if (_totalDeposits[id] > totalPayouts) { 109 | uint256 recipientFee = _totalDeposits[id] - totalPayouts; 110 | require(token.transfer(_conditions[id].recipient, recipientFee)); 111 | } 112 | 113 | _totalDeposits[id] = 0; 114 | _totalFunded[id] = 0; 115 | 116 | return true; 117 | } 118 | 119 | // View functions 120 | // ======================= 121 | 122 | function name() external view returns (string memory) { 123 | return _name; 124 | } 125 | 126 | function getConditions(uint256 id) external view returns (Conditions memory) { 127 | return _conditions[id]; 128 | } 129 | 130 | function getTotalDeposits(uint256 id) external view returns (uint256) { 131 | return _totalDeposits[id]; 132 | } 133 | 134 | function getTotalFunded(uint256 id) external view returns (uint256) { 135 | return _totalFunded[id]; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /packages/app/src/app/events/components/Admin/Settle.tsx: -------------------------------------------------------------------------------- 1 | import { showHubAddress, simulateShowHub, writeShowHub } from '@/abis' 2 | import { ActionDrawer } from '@/components/ActionDrawer' 3 | import { useEventData } from '@/context/EventData' 4 | import { useNotifications } from '@/context/Notification' 5 | import { CONFIG } from '@/utils/config' 6 | import { LoadingState } from '@/utils/types' 7 | import { useAccount } from 'wagmi' 8 | import { useState } from 'react' 9 | import { revalidateAll } from '@/app/actions/cache' 10 | import { Alert } from '@/components/Alert' 11 | import { WAGMI_CONFIG } from '@/utils/network' 12 | import { switchChain, waitForTransactionReceipt } from 'wagmi/actions' 13 | import { useQueryClient } from '@tanstack/react-query' 14 | 15 | export function Settle() { 16 | const eventData = useEventData() 17 | const notifications = useNotifications() 18 | const queryClient = useQueryClient() 19 | const chain = CONFIG.DEFAULT_CHAINS.find((i) => i.id === eventData.record.conditionModule.chainId) 20 | const { address, chainId } = useAccount() 21 | const [state, setState] = useState({ 22 | isLoading: false, 23 | type: '', 24 | message: '', 25 | }) 26 | const actionButton = ( 27 | 30 | ) 31 | 32 | async function Settle() { 33 | if (!address || !chain) { 34 | setState({ ...state, isLoading: false, type: 'error', message: 'Not connected' }) 35 | return 36 | } 37 | 38 | setState({ ...state, isLoading: true, type: 'info', message: `Settling event. Sign transaction` }) 39 | 40 | if (chainId !== eventData.chain.id) { 41 | try { 42 | console.log(`Switching chains ${chainId} -> ${eventData.chain.id}`) 43 | await switchChain(WAGMI_CONFIG, { chainId: eventData.chain.id }) 44 | } catch (e) { 45 | console.log('Unable to switch chains', e) 46 | } 47 | } 48 | 49 | try { 50 | const txConfig = await simulateShowHub(WAGMI_CONFIG, { 51 | chainId: eventData.record.conditionModule.chainId, 52 | address: showHubAddress, 53 | functionName: 'settle', 54 | args: [eventData.record.recordId, '0x'], 55 | }) 56 | 57 | const hash = await writeShowHub(WAGMI_CONFIG, txConfig.request) 58 | 59 | setState({ ...state, isLoading: true, type: 'info', message: 'Transaction sent. Awaiting confirmation' }) 60 | 61 | const data = await waitForTransactionReceipt(WAGMI_CONFIG, { hash: hash }) 62 | 63 | if (data.status == 'success') { 64 | setState({ ...state, isLoading: false, type: 'success', message: 'Event settled' }) 65 | 66 | await notifications.Add({ 67 | created: Date.now(), 68 | type: 'success', 69 | message: `Event settled`, 70 | from: address, 71 | cta: { 72 | label: 'View transaction', 73 | href: `${chain?.blockExplorers?.default.url}/tx/${hash}`, 74 | }, 75 | data: { hash }, 76 | }) 77 | 78 | await revalidateAll() 79 | queryClient.invalidateQueries({ queryKey: ['events'] }) 80 | eventData.refetch() 81 | 82 | return 83 | } 84 | 85 | setState({ ...state, isLoading: false, type: 'error', message: 'Unable to settle event' }) 86 | } catch (e) { 87 | setState({ ...state, isLoading: false, type: 'error', message: 'Unable to settle event' }) 88 | } 89 | } 90 | 91 | return ( 92 | 93 |
94 |
95 |

96 | Settling an event is final and no changes can be made after. Make sure you have checked in all attendees. 97 |

98 |
99 | 100 |
101 | {state.message && } 102 | 103 | 116 |
117 |
118 |
119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /packages/blog/data/posts/introducing-show-up-protocol.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introducing Show Up Protocol 3 | description: Show Up—an onchain RSVP and Event management protocol that reshapes event participation, by introduceing a novel staking mechanism that aligns the incentives. 4 | date: 2023-12-04T12:05:26.138Z 5 | --- 6 | 7 | ## Sup 😎👋 8 | 9 | In a space buzzing with events and gatherings, the excitement can sometimes be overshadowed by a common challenge—no-shows. Event organizers invest time, effort, and resources into creating memorable experiences, only to face the disappointment of empty seats. This not only impacts the organizer but also affects the overall atmosphere and dynamics for other attendees. A typical coordination problem. 10 | 11 | Enter [Show Up](https://www.showup.events/)—an onchain RSVP and Event management protocol designed to reshape event participation. It introduces a novel staking mechanism that aligns the incentives between event organizers and attendees. 12 | 13 | - Higher event participation for event hosts 14 | - Reward active participation for attendees 15 | - A decentralized, open and permissionless protocol 16 | 17 | win/win for everyone 🤝 18 | 19 | ![Show Up Protocol](/images/sup.png) 20 | 21 | ### How does it work? 22 | 23 | The Show Up Protocol is a set of decentralized, open, and permissionless smart contracts built on the Ethereum protocol. These contracts manage commitments and the conditions for keeping them. It is currently deployed on Optimism (and Sepolia testnet). The event metadata is stored on IPFS, ensuring the platform has no control over your events, funds, or payments. 24 | 25 | As an event organizer, you decide the deposit fee, which can be in Ether or any ERC20 token. Once an event is created, it cannot be edited, although you can cancel an event at any time. Anyone can register for your event once published if they pay the deposit fee. The event host keeps track and manages check-ins. At the end of the event, the host settles the event, automatically distributing the funds among all checked attendees. 26 | 27 | More information on the protocol can be found on [Github](https://github.com/wslyvh/show-up/tree/main/packages/protocol). 28 | 29 | ## Use-cases 30 | 31 | The Show Up App (mobile PWA) focuses specifically on RSVP and reducing no-shows for events. It is not a full-fledged ticketing solution but works great for: 32 | 33 | - Requesting a registration fee for a hackathon to incentivize project submissions. 34 | - Educational bootcamps, courses, or workshops to foster dedicated and motivated participants. 35 | - Creating engaged communities by transforming simple RSVPs into a meaningful commitments. 36 | 37 | The protocol is fully open and permissionless, allowing the creation of commitments and events outside the App. The protocol uses a modular set of condition modules designed to be extensible, facilitating various other needs and use-cases, including: 38 | 39 | - **Fitness challenges**: Individuals stake on fitness goals, promoting healthier lifestyles. 40 | - **Habit tracking**: Rewarding positive habits and personal growth. 41 | - **Language learning groups**: Encouraging consistency and dedication to regular practice sessions. 42 | - **Skill development**: Users commit to developing new skills or hobbies, from learning a musical instrument to mastering a programming language. 43 | 44 | The modularity of Show Up's commitment-driven protocol makes it a valuable tool across diverse contexts, encouraging commitment, participation, and achievement. 45 | 46 | ## Security 47 | 48 | Show Up is completly open-source and available on Github. While Show Up Protocol has been thoughtfully designed, built and reviewed by external developers, it has not been audited yet. Please check the contracts and documentation and use at your own risk. Depending on the deposit fees, the risk per event should be relatively low. 49 | 50 | ### Optimism 51 | 52 | - Registry [0x7Cc8E0633021b9DF8D2F01d9287C3b8e29f4eDe2](https://optimistic.etherscan.io/address/0x7Cc8E0633021b9DF8D2F01d9287C3b8e29f4eDe2) 53 | - BasicEther [0x33FF944E8504B674835A5BEd88f10f11bEC92c2c](https://optimistic.etherscan.io/address/0x33FF944E8504B674835A5BEd88f10f11bEC92c2c) 54 | - BasicToken [0x33132fE88fe8316881474b551CA2DDD277A320a0](https://optimistic.etherscan.io/address/0x33132fE88fe8316881474b551CA2DDD277A320a0) 55 | 56 | ### Sepolia 57 | 58 | - Registry [0x7Cc8E0633021b9DF8D2F01d9287C3b8e29f4eDe2](https://sepolia.etherscan.io/address/0x7Cc8E0633021b9DF8D2F01d9287C3b8e29f4eDe2) 59 | - BasicEther [0x33FF944E8504B674835A5BEd88f10f11bEC92c2c](https://sepolia.etherscan.io/address/0x33FF944E8504B674835A5BEd88f10f11bEC92c2c) 60 | - BasicToken [0x33132fE88fe8316881474b551CA2DDD277A320a0](https://sepolia.etherscan.io/address/0x33132fE88fe8316881474b551CA2DDD277A320a0) 61 | 62 | ## Links 63 | 64 | - Website https://www.showup.events/ 65 | - Testnet https://test.showup.events/ 66 | - Github https://github.com/wslyvh/show-up 67 | 68 | ### Acknowledgements 69 | 70 | ![Friendship Ended with Kickback](/images/friendship-ended-with-kickback.jpg) 71 | 72 | Show Up has been built on the ideas and experiences from [@wearekickback](https://twitter.com/wearekickback/) 👏 73 | -------------------------------------------------------------------------------- /packages/blog/data/posts/5-tips-to-reduce-no-shows-at-your-event.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 5 tips to reduce no-shows at your event 3 | description: Reducing no-shows at events can be challenging, but implementing certain strategies can help improve attendance rates. Follow these tips to minimize no-shows at your next event. 4 | date: 2024-01-12T15:15:15.138Z 5 | --- 6 | 7 | Reducing no-shows at events can be challenging, but implementing certain strategies can help improve attendance rates. 8 | 9 | Here are some tips to minimize no-shows: 10 | 11 | ## 1. Keep your community engaged 12 | 13 | Events are not just about the content presented; they're also about the connections made. Building a sense of community around your event creates a supportive environment. Pre-event interactions through forums or social media groups allow attendees to network, discuss, and build relationships that can continue and grow even after your event. This creates a richer experience for everyone involved. 14 | 15 | Consider getting attendees involved in organizing the event itself, like an unconference or participant-driven event. Let attendees suggest talks, speakers, or discussion items. What would they like to see during the event? Or request feedback or ideas on how they can contribute to the success of the event. When you keep them engaged, they are more likely to honor their commitment. 16 | 17 | Most importantly, make sure to keep attendees informed and excited leading up to the event. Share important information, confirmed speakers or talks, announce sponsors or other activities. But ensure to save something for during the event itself. Having fun, interesting, or unexpected activities during the event ensures people share and tweet to build FOMO (fear of missing out) and excitement for future events. 18 | 19 | ## 2. Set a ticket price 20 | 21 | Free events have by far the biggest no-show rates, as they have no financial investment in your event. While free events might seem to be more accessible and attract people who would otherwise not be able to make it, the absence of skin in the game increases the likelihood of no-shows. And no event is actually free. As an event organizer, you invest a lot of time, effort, and resources. Reducing no-shows ensures those resources are properly utilized and reduces unnecessary waste, leftovers, or other unused materials. 22 | 23 | Even a small fee increases the value an attendee gives to your event and thereby increases participation rates. It also offsets some of the financial costs for the organizers. 24 | 25 | ## 3. Create a waiting list 26 | 27 | Most physical events are limited in some capacity, whether by resources, venue limitations, or other factors. If your event has a cap, make sure to establish a waiting list. This gives you a good indication when planning the event. It might be that your venue is too small, or you're underutilizing some of your resources. But it also ensures that whenever someone cancels, you can immediately invite others from the waiting list. This ensures that every spot is filled. 28 | 29 | A (partial) refund policy for attendees who cancel ahead of time could give better guarantees for attendees to buy their tickets in the first place. This adds flexibility and may encourage responsible cancellations rather than no-shows. 30 | 31 | ## 4. Make it easy to refund or cancel 32 | 33 | Receiving a cancellation or refund request is not fun. But the easier and sooner people are able to do this, the faster you can invite others. Make sure to provide the information during the ticketing process and remind people of these during confirmation and follow-ups. 34 | 35 | Especially for free events, make it clear that you have a waiting list and that cancellations could free up a spot for others on the list. This provides a bit of an incentive for attendees to cancel registration if they're no longer able to make it. 36 | 37 | If you're charging for tickets, you might not be able to provide full refunds, as the costs and planning of your event are ongoing already. Providing alternatives, like transferring a ticket or allowing secondary markets or platforms to resell tickets, could provide options that still ensure your seats are filled. 38 | 39 | ## 5. Offer incentives for (early) check-ins 40 | 41 | The last tip is to offer incentives or perks for attendees who check in to the event, such as limited merchandise, special privileges, exclusive access, or discounts for the next events. Early check-ins can set a positive tone for the event. 42 | 43 | Consider leveraging a mechanisms like Show Up protocol, where attendees make a deposit during registration, which is returned upon attendance. This can provide a good balance between an accessible event while still requesting a commitment from your attendees. Make sure you set an appropriate deposit fee. Too low, and attendees may still not feel committed; too high, and it might create a financial barrier, losing potential participants. 44 | 45 | --- 46 | 47 | Reducing no-shows is an ongoing process. Implementing a combination of these tips will not only boost attendance but also enhance the overall event experience for both organizers and participants. Make sure that you gather feedback from attendees after the event. Understand what they liked or did not like about the event, but also follow up with people who did not show up to understand their reasons to improve future events. 48 | 49 | Happy hosting! 50 | --------------------------------------------------------------------------------