# Project export: EstiMate

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2024
- Tagline: Friends Fueled Fun: Betting Made Easy and Secure
- Devpost: https://devpost.com/software/estimate-0nqzfx
- GitHub: https://github.com/underthestars-zhy/EstiMate
- Demo: https://github.com/ryantinder/treehacks-2024
- Video: https://www.youtube.com/embed/SdZH-XBvShM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Taisu Ventures: Amazing Web3 Gaming Award ($500 Cash); Caldera: Best Use of Caldera ($1k ETH); Ava Labs / Avalanche: Best Consumer Use Case for Blockchain ($1.5k Cash))
- Team: 1 GitHub contributor(s) — Underthestars-zhy (11 commits)

## Devpost submission (written by the team)

### Inspiration

🌱 We got this idea after noticing a common annoyance—tracking casual bets was a mess. Whether it was predicting the snow day or daring each other over who could do the craziest things, our bets were often forgotten, leaving behind a trail of "who said what." We created EstiMate, an app that makes betting with friends easy and secure. Our aim is to solidify those playful moments into lasting memories. Check out the fun video that inspired us!

### What it does

💡 EstiMate lets you set up bets and share them with your friends, whether it's a one-on-one challenge or a wager within your group, club, or frat. After creating a bet, you can pick how long the bet lasts, how much is at stake, and which side you're on. Inviting friends is super easy – just send a link or hold your phones close to each other to share with AirDrop. EstiMate is built on blockchain, allowing for seamless, protected, and transparent transactions.

### How we built it

🛠️ In EstiMate, when a user logs in using their Apple ID, they are taken to their dashboard where all ongoing bets are displayed. After creating a bet, the frontend sends a request to our backend, which is built on Bun-powered Express, with the necessary details including userId, bet name, bet amount, and timeframe. This request is processed by our backend, which interacts with the Convex database to register the bet and retrieve the user's private key. Next, we use Solidity for writing smart contracts that handle the logic of holding bets on the Caldera chain. These smart contracts are crucial for ensuring the integrity and security of each transaction. Using Ethers, we obtain private keys from Convex to broadcast transactions from clients' accounts on the Caldera chain.

### Challenges we ran into

🌃 One of the main technical challenges we faced was managing the complexity of integrating multiple components, including iOS for the frontend, express.js and ethers.js for the backend, and on-chain technology for the blockchain functionality. Coordinating these distinct areas required careful planning and collaboration among team members specialized in each domain. Additionally, understanding user needs and behaviors presented another hurdle. To address this, we conducted market validation surveys, interviewing over 10 individuals. This effort helped us segment our target market—college students—more effectively, allowing us to tailor EstiMate to better meet their preferences and betting habits.

### What we learned

📝 While developing EstiMate, we learned the critical importance of adopting a user-centered approach. Transforming a traditional, pen-and-paper practice into a digital social application highlighted the need to understand and prioritize user needs and behaviors — ensuring the app's design and functionality are both intuitive and engaging. Additionally, given the diverse technological backgrounds of our team members and the variety of APIs and development environments we utilized, meshing these different elements helped to hone both our technical and teamwork skills.

### What's next

? 🚀 Looking ahead, we plan to expand EstiMate's capabilities by supporting multichain blockchain technology. We aim to transform the platform into a public marketplace for campus and institution-wide betting, allowing us to leverage tokenomics within the app. Additionally, our next steps include evaluating partnerships with campus organizations like SideChat and Fizz to enhance the social aspect of creating and sharing bets.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 27 recognized source files, 59 KB.
- Swift (language) — detected in the code
- Express (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- Solidity (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (44 of 44)

```
.DS_Store
EstiMate.xcodeproj/project.pbxproj
EstiMate.xcodeproj/project.xcworkspace/contents.xcworkspacedata
EstiMate.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
EstiMate.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
EstiMate.xcodeproj/project.xcworkspace/xcuserdata/tinder.xcuserdatad/UserInterfaceState.xcuserstate
EstiMate.xcodeproj/xcuserdata/tinder.xcuserdatad/xcschemes/xcschememanagement.plist
EstiMate.xcodeproj/xcuserdata/zhuhaoyu.xcuserdatad/xcschemes/xcschememanagement.plist
EstiMate/API/AllBets.swift
EstiMate/API/BetInvited.swift
EstiMate/API/WebAPI.swift
EstiMate/Assets.xcassets/AccentColor.colorset/Contents.json
EstiMate/Assets.xcassets/AppIcon.appiconset/Contents.json
EstiMate/Assets.xcassets/Contents.json
EstiMate/Assets.xcassets/icon.imageset/Contents.json
EstiMate/Assets.xcassets/launch.imageset/Contents.json
EstiMate/Bets/Bet.swift
EstiMate/Bets/BetIndicator.swift
EstiMate/Bets/BetItem.swift
EstiMate/Bets/BetResult.swift
EstiMate/Bets/BetStatus.swift
EstiMate/BetSheet/BeetSheet.swift
EstiMate/BetSheet/CreateBet.swift
EstiMate/BetSheet/SheatResult.swift
EstiMate/BetSheet/SheetAmount.swift
EstiMate/BetSheet/SheetBottom.swift
EstiMate/BetSheet/SheetContainer.swift
EstiMate/BetSheet/SheetInput.swift
EstiMate/BetSheet/SheetInvite.swift
EstiMate/BetSheet/SheetRecieve.swift
EstiMate/BetSheet/SheetSide.swift
EstiMate/BetSheet/SheetStatus.swift
EstiMate/BetSheet/SheetTime.swift
EstiMate/BetSheet/SheetVote.swift
EstiMate/ContentView.swift
EstiMate/EstiMate.entitlements
EstiMate/EstiMateApp.swift
EstiMate/Extensions/MeasureSize.swift
EstiMate/Info.plist
EstiMate/Launch Screen.storyboard
EstiMate/Navigation/NavBar.swift
EstiMate/Preview Content/Preview Assets.xcassets/Contents.json
EstiMate/Welcome/GuideItem.swift
EstiMate/Welcome/WelcomeView.swift
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- make WebAPI base url configurable
- ani
- animation
- result view
- vote function & refresh
- set side
- change bet coint amount
- receive invitation
- create link
- Welcome View
- launch screens
- Initial Commit

## Key source files (fetched from GitHub, selected and truncated for size)

### EstiMate/ContentView.swift

```swift
//
//  ContentView.swift
//  EstiMate
//
//  Created by 朱浩宇 on 2024/2/17.
//

import SwiftUI
import SwiftDate
import SwiftUIPullToRefresh

struct ContentView: View {
    @State var allBets = AllBets.shared

    var body: some View {
        RefreshableScrollView { done in
            Task {
                try await allBets.refresh()
                done()
            }
        } progress: { state in
            ProgressView().progressViewStyle(.circular)
                .controlSize(.large)
                .padding(.top, 148)
        } content: {
            VStack(spacing: 40) {
                ForEach(allBets.bets) { bet in
                    BetItem(bet: bet)
                        .padding(.horizontal, 30)
                }
            }
            .padding(.top, 168)
            .animation(.easeInOut, value: allBets.bets)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .overlay {
            VStack {
                NavBar()

                Spacer()
            }
        }
        .overlay {
            SheetContainer()
        }
        .task {
            do {
                try await allBets.refresh()
            } catch {
                print(error)
            }
        }
    }
}

#Preview {
    ContentView()
        .ignoresSafeArea()
}

```

### EstiMate/EstiMateApp.swift

```swift
//
//  EstiMateApp.swift
//  EstiMate
//
//  Created by 朱浩宇 on 2024/2/17.
//

import SwiftUI
import UserNotifications

@main
struct EstiMateApp: App {
    @UIApplicationDelegateAdaptor var delegate: AppDelegate
    @AppStorage("userID") var userID: String = ""

    var body: some Scene {
        WindowGroup {
            VStack {
                if userID.isEmpty {
                    WelcomeView()
                } else {
                    ContentView()
                }
            }
            .task {
                do {
                    //                    userID = ""
                    try await WebAPI.checkPermission()
                } catch {
                    print(error)
                }
            }
            .ignoresSafeArea(.all)
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        UNUserNotificationCenter.current().delegate = self

        registerForPushNotifications()

        UIApplication.shared.registerForRemoteNotifications()

        return true
    }

    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        registryToken(deviceToken: deviceToken)
    }

    func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        print("Failed to register: \(error)")
    }

    func registryToken(deviceToken: Data) {
        let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
        let token = tokenParts.joined()
        let pastToken = UserDefaults.standard.string(forKey: "pushNotificationToken")
        if let pastToken {
            print("Past Token: \(pastToken)")
            if pastToken != token {
                UserDefaults.standard.set(token, forKey: "pushNotificationToken")
                Task.detached { try await WebAPI.setDeviceToken(token: token) }
            }
        } else {
            UserDefaults.standard.set(token, forKey: "pushNotificationToken")
        }
        print("Device Token: \(token)")
    }

    func registerForPushNotifications() {
        UNUserNotificationCenter.current()
            .requestAuthorization(options: [.alert]) { granted, _ in
                print("Permission granted: \(granted)")
            }
    }

    func application(
        _ application: UIApplication,
        didReceiveRemoteNotification userInfo: [AnyHashable: Any],
        fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
    ) {
        print("receive notification")
        print(userInfo)

        switch userInfo["type"] as? String {
        case "join":
            if let name = userInfo["name"] as? String {
                BetInvited.shared.list.append(name)
            }
        case "results":
            if let data = userInfo["data"] as? [AnyHashable: Any],
               let betID = data["betId"] as? String,
               let change = data["change"] as? Double,
               let userSide = data["userside"] as? Bool,
               let voteSide = data["vote"] as? Bool,
               let balance = data["amount"] as? Double {
                let r = BetResult(betID: betID, change: change, userSide: userSide, voteSide: voteSide, balance: balance)
                SheetStatusPublisher.shared.status = .result(betR: r)
            }
        default: break
        }
    }

}

```

### EstiMate/Bets/BetStatus.swift

```swift
//
//  BetStatus.swift
//  EstiMate
//
//  Created by 朱浩宇 on 2024/2/17.
//

import Foundation

enum BetStatus {
    case inProgress
    case voted
    case done
    case canceled
}

```

### EstiMate/API/BetInvited.swift

```swift
//
//  BetInvited.swift
//  EstiMate
//
//  Created by 朱浩宇 on 2024/2/17.
//

import Foundation

@Observable
class BetInvited {
    static let shared = BetInvited()

    var list = [String]()

    func reset(with name: String) {
        list = [name]
    }
}

```

### EstiMate/Bets/Bet.swift

```swift
//
//  Bet.swift
//  EstiMate
//
//  Created by 朱浩宇 on 2024/2/17.
//

import SwiftUI

struct Bet: Identifiable, Equatable {
    let id: UUID
    let title: String
    let emoji: String
    let start: Date
    let end: Date
    let amount: Double
    var status: BetStatus
}

```

### EstiMate/Bets/BetResult.swift

```swift
//
//  BetResult.swift
//  EstiMate
//
//  Created by 朱浩宇 on 2024/2/18.
//

import Foundation

struct BetResult: Equatable {
    let betID: String
    let change: Double
    let userSide: Bool
    let voteSide: Bool
    let balance: Double

    var bet: Bet {
        AllBets.shared.getBet(by: betID)!
    }
}

```

### EstiMate/BetSheet/SheetStatus.swift

```swift
//
//  SheetStatus.swift
//  EstiMate
//
//  Created by 朱浩宇 on 2024/2/17.
//

import Foundation
import SwiftUI
import SwiftUIX

enum SheetStatus: Equatable {
    case bottom
    case input
    case time
    case amount
    case invite
    case receive(betID: String)
    case side(bet: Bet)
    case vote(bet: Bet)
    case result(betR: BetResult)

    var needPadding: Bool {
        switch self {
        case .bottom, .input: false
        default: true
        }
    }
}

```

### EstiMate/BetSheet/CreateBet.swift

```swift
//
//  CreateBet.swift
//  EstiMate
//
//  Created by 朱浩宇 on 2024/2/17.
//

import Foundation

@Observable
class CreateBet {
    static let shared = CreateBet()

    var title = ""
    var emoji = ""
    var end = Date()
    var amount = 10

    func reset() {
        title = ""
        emoji = ""
        end = Date()
        amount = 10
    }

    func createBet() -> Bet {
        Bet(id: UUID(), title: title, emoji: emoji, start: Date(), end: end, amount: Double(amount), status: .inProgress)
    }
}

```

### EstiMate/Extensions/MeasureSize.swift

```swift
//
//  MeasureSize.swift
//  EstiMate
//
//  Created by 朱浩宇 on 2024/2/17.
//

import SwiftUI
import SwiftUIX

struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = Screen.size

    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

struct MeasureSizeModifier: ViewModifier {
    func body(content: Content) -> some View {
        content.background(GeometryReader { geometry in
            Color.clear.preference(key: SizePreferenceKey.self,
                                   value: geometry.frame(in: .local).size)
        })
    }
}

extension View {
    func measureSize(perform action: @escaping (CGSize) -> Void) -> some View {
        modifier(MeasureSizeModifier())
            .onPreferenceChange(SizePreferenceKey.self, perform: action)
    }
}

```

### EstiMate/BetSheet/SheetBottom.swift

```swift
//
//  SheetBottom.swift
//  EstiMate
//
//  Created by 朱浩宇 on 2024/2/17.
//

import SwiftUI
import SwiftUIX

struct SheetBottom: View {
    let namespace: Namespace.ID
    
    var body: some View {
        VStack {
            HStack(spacing: 12) {
                Image(systemName: "plus.circle.fill")
                    .foregroundStyle(.accent)
                    .font(.system(size: 32).weight(.bold))

                Group {
                    Text("New Bet ")
                        .foregroundStyle(.black)
                        .font(.system(size: 20).weight(.bold))
                    +
                    Text("(Long Press to Speak)")
                        .foregroundStyle(Color(hexadecimal: "808080"))
                        .font(.system(size: 14).weight(.bold))
                }

                Spacer()
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding(.horizontal, 30)
            .padding(.top, 40)

            Spacer()
        }
    }
}

```

[17 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]