Skip to content

Commit

Permalink
Add Farcaster Frame
Browse files Browse the repository at this point in the history
  • Loading branch information
ginesdt committed Sep 9, 2024
1 parent c3fa59a commit a25ec1d
Show file tree
Hide file tree
Showing 14 changed files with 2,285 additions and 1,917 deletions.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@apollo/server": "4.3.0",
"@aws-sdk/client-lambda": "^3.598.0",
"@aws-sdk/credential-providers": "^3.598.0",
"@hono/node-server": "^1.12.0",
"@metamask/eth-sig-util": "5.0.2",
"@react-native-async-storage/async-storage": "^1.23.1",
"@sentry/node": "4.6.6",
Expand All @@ -26,14 +27,17 @@
"expo-modules-core": "^1.12.15",
"expo-web-browser": "^13.0.3",
"express": "4.18.2",
"frog": "^0.15.7",
"graphql": "16.8.2",
"graphql-fields": "2.0.3",
"graphql-tools": "8.3.14",
"highlight.js": "11.5.1",
"hono": "^4.5.3",
"jquery": "3.6.1",
"jsonwebtoken": "9.0.0",
"jwt-decode": "3.1.2",
"pg": "^8.11.5",
"pg-native": "^3.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-infinite": "0.13.0",
Expand Down Expand Up @@ -72,7 +76,7 @@
"karma-cljs-test": "0.1.0",
"normalize.css": "8.0.1",
"sass": "1.63.6",
"shadow-cljs": "2.28.9",
"shadow-cljs": "2.28.11",
"string-replace-loader": "^3.1.0",
"truffle": "5.8.1",
"webpack": "5.92.0",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions shadow-cljs.edn
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
:main streamtide.server.core/-main
:output-dir "server/"
:output-to "server/streamtide_server.js"
:prepend "shadow_esm_import = function(x) { return import(x); };"
:dev {:closure-defines {goog.DEBUG true}
:js-options {:keep-as-import #{"d3"}}}
:release {:compiler-options {:optimizations :simple}}}
Expand Down
5 changes: 1 addition & 4 deletions src/streamtide/server/business_logic.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
This is an intermediate layer between the GraphQL endpoint (or any other API which may come in the future)
and the database functionality to enforce authorization and to enrich or validate input data."
(:require
[cljs.core.async :refer [go]]
[cljs.nodejs :as nodejs]
[clojure.string :as str]
[clojure.string :as string]
[district.shared.async-helpers :refer [safe-go <?]]
[district.shared.error-handling :refer [try-catch-throw]]
[fs]
[path]
[streamtide.server.db :as stdb]
[streamtide.server.notifiers.notifiers :as notifiers]
[streamtide.server.verifiers.twitter-verifier :as twitter]
Expand All @@ -22,8 +21,6 @@
[streamtide.server.notifiers.web-push-notifier]
[streamtide.shared.utils :as shared-utils]))

(def path (nodejs/require "path"))

(defn require-auth [current-user]
"Check if the request comes from an authenticated user. Throws an error otherwise"
(when-not current-user
Expand Down
13 changes: 9 additions & 4 deletions src/streamtide/server/core.cljs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns streamtide.server.core
"Main entry point of the server. Reads the config and starts all modules with mount"
(:require [cljs.nodejs :as nodejs]
(:require ["body-parser" :as body-parser]
[clojure.string :as str]
[district.graphql-utils :as graphql-utils]
[district.server.config :as district.server.config]
Expand All @@ -24,11 +24,10 @@
[streamtide.shared.smart-contracts-dev :as smart-contracts-dev]
[streamtide.shared.smart-contracts-prod :as smart-contracts-prod]
[streamtide.shared.smart-contracts-qa :as smart-contracts-qa]
[streamtide.server.farcaster-frame]
[taoensso.timbre :as log :refer [info warn error]])
(:require-macros [streamtide.shared.utils :refer [get-environment]]))

(def body-parser (nodejs/require "body-parser"))

(defonce resync-count (atom 0))

(def contracts-var
Expand All @@ -46,7 +45,8 @@
#'district.server.web3-events/web3-events
#'district.server.web3/web3
#'streamtide.server.db/streamtide-db
#'streamtide.server.syncer/syncer})
#'streamtide.server.syncer/syncer
#'streamtide.server.farcaster-frame/farcaster-frame})
(mount/with-args
{:config {:default {:logging {:level "info"
:console? false}
Expand Down Expand Up @@ -81,6 +81,11 @@
:url-path "/img/avatar/"}
:verifiers {:twitter {:consumer-key "PLACEHOLDER"
:consumer-secret "PLACEHOLDER"}}
:farcaster-frame {:port 3000
:path "/"
:static-public "resources/public"
:title "StreamTide Farcaster Frame"
:on-error #(js/process.exit 1)}
:web3 {:url "ws://127.0.0.1:8546"
:on-offline (fn []
(log/warn "Ethereum node went offline, stopping syncing modules" {:resyncs @resync-count} ::web3-watcher)
Expand Down
166 changes: 166 additions & 0 deletions src/streamtide/server/farcaster_frame.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
(ns streamtide.server.farcaster-frame
(:require
["@hono/node-server" :refer [serve]]
["@hono/node-server/serve-static" :refer [serveStatic]]
["axios" :as axios]
["hono/jsx" :refer [jsx]]
[bignumber.core :as bn]
[district.server.config :refer [config]]
[district.shared.async-helpers :refer [safe-go <?]]
[mount.core :as mount :refer [defstate]]
[shadow.esm :refer [dynamic-import]]
[streamtide.server.db :as stdb]
[streamtide.server.utils :refer [wrap-as-promise]]
[taoensso.timbre :as log]))

(def TX-API "/tx")
(def FRAME-API "/frame")
(def FINISH-TX-FRAME "/finish")
(def st-contract-abi (js/JSON.parse "[{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"patronAddresses\",\"type\":\"address[]\"},{\"internalType\":\"uint256[]\",\"name\":\"amounts\",\"type\":\"uint256[]\"}],\"name\":\"donate\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\",\"payable\":true}]"))
(def DEFAULT-IMAGE "/img/layout/streamtide-farcaster.png")

(def BUTTON-PRICES
{:1 "10"
:2 "25"
:3 "50"})

(defonce server-instance (atom nil))

(defn- to-jsx [hiccup]
(let [[tag & rest] hiccup
[props & children] (if (map? (first rest)) rest (cons nil rest))
tag-name (if (keyword? tag) (name tag) tag)
props (clj->js props)
children (map (fn [child]
(if (vector? child)
(to-jsx child)
child))
children)]
(apply jsx tag-name props children)))

(defn- get-creator [context]
(or (-> context .-req (.param "creator")) (-> context .-req .query (js->clj :keywordize-keys true) :creator)))

(defn- get-photo [context]
(safe-go
(if (= "1" (-> context .-req .query (js->clj :keywordize-keys true) :profile-pic))
(:user/photo (<? (stdb/get-user (get-creator context))))
DEFAULT-IMAGE)))

(defn- get-tip-value [context]
(or (.-inputText context)
(get BUTTON-PRICES (keyword (str (.-buttonIndex context))))))

(defn- build-frames [frog opts]
(let [Button (.-Button frog)
TextInput (.-TextInput frog)
TransactionButton (.-Transaction Button)
ResetButton (.-Reset Button)
LinkButton (.-Link Button)]
{(str FRAME-API "/:creator")
(fn [c]
(wrap-as-promise
(safe-go
(let [creator (get-creator c)
image (<? (get-photo c))
button-value (.-buttonValue c)]
(if (= button-value "custom-tip")
(.res c (clj->js {:image image
:imageAspectRatio "1:1"
:intents (map
to-jsx
[[TextInput {:placeholder "Amount in $"}]
[TransactionButton
{:target (str TX-API "?creator=" creator)
:action FINISH-TX-FRAME}
"Tip"]
[ResetButton "< Go Back"]])}))
(.res c (clj->js {:image image
:imageAspectRatio "1:1"
:intents (map
to-jsx
(concat (map (fn [amount] [TransactionButton
{:target (str TX-API "?creator=" creator)
:action FINISH-TX-FRAME}
(str "Tip " amount "$")]) (vals BUTTON-PRICES))
[[Button {:value "custom-tip"} "Custom tip"]]))})))))))
FINISH-TX-FRAME
(fn [c]
(wrap-as-promise
(safe-go
(let [tx-id (.-transactionId c)
image (<? (get-photo c))]
(.res c (clj->js {:image image
:imageAspectRatio "1:1"
:intents (map
to-jsx
[[LinkButton {:href (str (:etherscan-tx-url opts) "/" tx-id)} "See transaction status"]
[ResetButton "< Restart"]])}))))))}))

(defn- dollar-to-wei [dollar-amount {:keys [:etherscan-api-key]}]
(safe-go
(let [result (<? (.get axios (str "https://api.etherscan.io/api?module=stats&action=ethprice&apikey=" etherscan-api-key)))
eth-price (-> result .-data .-result .-ethusd)]
(when-not eth-price (throw (js/Error. (str "Cannot fetch current eth price. Details: " (js/JSON.stringify (.-data result))))))
(str (.integerValue (bn/* (bn/pow (js/BigNumber. 10) 18) (bn// (js/BigNumber. dollar-amount) (js/BigNumber. eth-price))))))))

(defn- build-transactions [opts]
{TX-API (fn [c]
(wrap-as-promise
(safe-go
(let [creator (get-creator c)
input-amount (get-tip-value c)
amount-wei (<? (dollar-to-wei input-amount opts))
chain-id (str "eip155:" (:chain-id opts))
st-address (-> @(-> @config :smart-contracts :contracts-var) :streamtide-fwd :address)]
(.contract c (clj->js {:abi st-contract-abi
:chainId chain-id
:functionName "donate"
:args [[creator] [amount-wei]]
:to st-address
:value amount-wei}))))))})

(defn start [{:keys [:hostname :port :path :title :use-devtools? :static-public :on-error :hub :secret] :as opts}]
(-> (js/Promise.all [(dynamic-import "frog") (when use-devtools? (dynamic-import "frog/dev"))])
(.then (fn [[frog frog-dev]]
(try
(let [Frog (.-Frog frog)
app (Frog. #js {:title title
:basePath (or path "/")
:hub hub
:secret secret})]
(when static-public
(.use app "/*" (serveStatic #js {:root static-public})))

; register all frames
(doseq [[path frame-fn] (build-frames frog opts)]
(.frame app path frame-fn))
; register all transactions
(doseq [[path frame-fn] (build-transactions opts)]
(.transaction app path frame-fn))

(when use-devtools?
((.-devtools frog-dev) app #js {:serveStatic serveStatic}))

(let [server (serve #js {:fetch (.-fetch app)
:hostname hostname
:port port})]
(reset! server-instance server)
{}))
(catch :default e
(log/error "Failed load Framecaster Frame" {:error e})
(when on-error (on-error))))))
(.catch (fn [error]
(log/error "Failed to dynamic import frog" {:error error})
(js/process.exit 1)))))


(defn stop []
(if-let [server @server-instance]
(.close server)))


(defstate farcaster-frame
:start (start (merge (:farcaster-frame @config)
(:farcaster-frame (mount/args))))
:stop (stop))
9 changes: 4 additions & 5 deletions src/streamtide/server/graphql/authorization.cljs
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
(ns streamtide.server.graphql.authorization
"Graphql utils for handling authentication. Takes signed data and generates a JWT"
(:require [camel-snake-kebab.core :as csk]
(:require ["jsonwebtoken" :as JsonWebToken]
["siwe" :as siwe]
["viem" :as viem]
[camel-snake-kebab.core :as csk]
[camel-snake-kebab.extras :refer [transform-keys]]
[cljs.nodejs :as nodejs]
[cljs-web3-next.core :as web3-next]
[district.server.config :as config]
[district.shared.async-helpers :refer [safe-go <?]]
[streamtide.shared.utils :as shared-utils]
[taoensso.timbre :as log]))

(defonce JsonWebToken (nodejs/require "jsonwebtoken"))
(defonce viem (nodejs/require "viem"))
(defonce siwe (nodejs/require "siwe"))
(def otp-length 20)
(def otp-time-step 120) ; OTP validity: 2 minutes

Expand Down
12 changes: 2 additions & 10 deletions src/streamtide/server/graphql/graphql_resolvers.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,17 @@
"Defines the resolvers aimed to handle the GraphQL requests.
This namespace does not perform any authorization or any complex logic, but it is mainly an entry point which
delegates the GraphQL calls to the business logic layer."
(:require [cljs.core.async :refer [<! take!]]
[clojure.string :as string]
(:require [clojure.string :as string]
[district.graphql-utils :as graphql-utils]
[district.shared.async-helpers :refer [safe-go <?]]
[district.shared.error-handling :refer [try-catch-throw]]
[eip55.core :as eip55]
[streamtide.server.business-logic :as logic]
[streamtide.server.graphql.authorization :as authorization]
[streamtide.server.utils :refer [wrap-as-promise]]
[taoensso.timbre :as log]))


(defn wrap-as-promise [chanl]
(js/Promise. (fn [resolve reject]
(take! (safe-go (<? chanl))
(fn [v-or-err#]
(if (cljs.core/instance? js/Error v-or-err#)
(reject v-or-err#)
(resolve v-or-err#)))))))

(def enum graphql-utils/kw->gql-name)

(defn- user-id [user]
Expand Down
4 changes: 1 addition & 3 deletions src/streamtide/server/notifiers/discord_notifier.cljs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns streamtide.server.notifiers.discord-notifier
(:require
[cljs.nodejs :as nodejs]
["axios" :as axios]
[cljs.core.async :refer [<!]]
[clojure.string :as string]
[district.server.config :refer [config]]
Expand All @@ -13,8 +13,6 @@
(def notifier-type-kw :discord)
(def notifier-type (name notifier-type-kw))

(defonce axios (nodejs/require "axios"))

(defn- build-content [message]
(str "** " (:title message) " **\n" (:body message)))

Expand Down
3 changes: 1 addition & 2 deletions src/streamtide/server/notifiers/web_push_notifier.cljs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(ns streamtide.server.notifiers.web-push-notifier
(:require
[cljs.nodejs :as nodejs]
["web-push" :as web-push]
[clojure.string :as string]
[district.server.config :refer [config]]
[district.shared.async-helpers :refer [<? safe-go]]
Expand All @@ -11,7 +11,6 @@

(def notifier-type-kw :web-push)
(def notifier-type (name notifier-type-kw))
(defonce web-push (nodejs/require "web-push"))

(def valid-web-push-endpoints-domains #{"android.googleapis.com"
"fcm.googleapis.com"
Expand Down
11 changes: 10 additions & 1 deletion src/streamtide/server/utils.cljs
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
(ns streamtide.server.utils
"Utilities for server"
)
(:require [cljs.core.async :refer [<! take!]]
[district.shared.async-helpers :refer [safe-go <?]]))

(defn wrap-as-promise [chanl]
(js/Promise. (fn [resolve reject]
(take! (safe-go (<? chanl))
(fn [v-or-err#]
(if (cljs.core/instance? js/Error v-or-err#)
(reject v-or-err#)
(resolve v-or-err#)))))))
4 changes: 1 addition & 3 deletions src/streamtide/server/verifiers/discord_verifier.cljs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
(ns streamtide.server.verifiers.discord-verifier
(:require
[cljs.nodejs :as nodejs]
["axios" :as axios]
[cljs.core.async :refer [<!]]
[district.server.config :refer [config]]
[district.shared.async-helpers :refer [<? safe-go]]
[streamtide.server.verifiers.verifiers :as verifiers]))

(defonce axios (nodejs/require "axios"))

(defn- get-token-request [code]
(safe-go
(let [discord-config (-> @config :verifiers :discord)
Expand Down
Loading

0 comments on commit a25ec1d

Please sign in to comment.