├── 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 | Create event
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 |
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 |
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 | Go to Events →
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 |
15 |
16 |
{props.children}
17 |
18 |
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 |
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 |
15 |
16 |
{props.children}
17 |
18 |
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 |
17 |
18 |
19 | {props.children}
20 |
21 |
22 |
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 |
20 | Check-in Attendees
21 |
22 |
23 | )}
24 | {!eventData.isActive && (
25 |
26 | Check-in Attendees
27 |
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 |
9 | Sign-in with Ethereum
10 |
11 |
12 |
13 |
14 |
15 |
OR
16 |
17 |
18 |
19 |
20 | Sign-in with Email
21 |
22 |
23 |
24 |
25 |
26 |
27 | Coming soon
28 |
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 | [](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 |
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 | Create event
25 |
26 |
27 | Learn more
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 | //
Get Started
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 |
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 |
48 |
49 |
50 |
51 | Click to upload or drag and drop
52 |
53 |
54 | .png or .jpeg format
55 | recommended size of 1200:600 (2:1 ratio)
56 |
57 |
58 |
59 |
60 |
61 | )}
62 |
63 | {filePreview && (
64 |
65 |
66 |
67 |
68 |
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 |
setOpen(false)}>
46 |
47 |
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 |
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 |
43 |
44 | {displayName}
45 | disconnect()}>
46 | Disconnect
47 |
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 |
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 |
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 |
76 | Website
77 |
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 |
28 | Settle Event
29 |
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 |
108 | {state.isLoading && (
109 | <>
110 | Loading
111 |
112 | >
113 | )}
114 | {!state.isLoading && <>Settle Event>}
115 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------