├── Dockerfile ├── go.mod ├── README.md ├── hugging-allora.sh ├── run.sh ├── go.sum └── main.go /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use a lightweight base image 2 | FROM golang:1.22-alpine AS builder 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Copy go module files 8 | COPY go.mod go.sum ./ 9 | 10 | # Download dependencies 11 | RUN go mod download 12 | 13 | # Copy the rest of the application code 14 | COPY . . 15 | 16 | # Build the binary 17 | RUN go build -o main . 18 | 19 | # Use a smaller base image for the final image 20 | FROM alpine:latest 21 | 22 | # Copy the binary from the builder stage 23 | COPY --from=builder /app/main /app/ 24 | 25 | # Set the working directory 26 | WORKDIR /app 27 | 28 | # Expose the port your application listens on 29 | EXPOSE 8000 30 | 31 | # Command to run the application 32 | CMD ["./main"] -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module skate 2 | 3 | go 1.21.6 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.11.6 // indirect 12 | github.com/bytedance/sonic/loader v0.1.1 // indirect 13 | github.com/cloudwego/base64x v0.1.4 // indirect 14 | github.com/cloudwego/iasm v0.2.0 // indirect 15 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 16 | github.com/gin-contrib/sse v0.1.0 // indirect 17 | github.com/go-playground/locales v0.14.1 // indirect 18 | github.com/go-playground/universal-translator v0.18.1 // indirect 19 | github.com/go-playground/validator/v10 v10.20.0 // indirect 20 | github.com/goccy/go-json v0.10.2 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 23 | github.com/leodido/go-urn v1.4.0 // indirect 24 | github.com/mattn/go-isatty v0.0.20 // indirect 25 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 26 | github.com/modern-go/reflect2 v1.0.2 // indirect 27 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 28 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 29 | github.com/ugorji/go/codec v1.2.12 // indirect 30 | golang.org/x/arch v0.8.0 // indirect 31 | golang.org/x/crypto v0.23.0 // indirect 32 | golang.org/x/net v0.25.0 // indirect 33 | golang.org/x/sys v0.20.0 // indirect 34 | golang.org/x/text v0.15.0 // indirect 35 | google.golang.org/protobuf v1.34.1 // indirect 36 | gopkg.in/yaml.v3 v3.0.1 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # allora-worker 2 | 3 | ## Install requirements 4 | ``` 5 | sudo apt update && sudo apt upgrade -y 6 | sudo apt install jq -y 7 | 8 | # install docker 9 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg 10 | 11 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 12 | 13 | sudo apt-get update 14 | sudo apt-get install docker-ce docker-ce-cli containerd.io 15 | docker version 16 | 17 | # install docker-compose 18 | VER=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep tag_name | cut -d '"' -f 4) 19 | 20 | curl -L "https://github.com/docker/compose/releases/download/"$VER"/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 21 | 22 | chmod +x /usr/local/bin/docker-compose 23 | docker-compose --version 24 | ``` 25 | 26 | 27 | request some faucet from the [Allora Testnet Faucet](https://faucet.testnet-1.testnet.allora.network/) 28 | 29 | ## Stop the old version 30 | If you've previously run the old version and want to stop it before proceeding, follow these commands 31 | ``` 32 | cd ~ 33 | rm -r ~/allora-worker/ 34 | docker stop custom-inference custom-worker custom-worker-0 custom-worker-1 35 | docker container prune -f 36 | docker rmi custom-inference 37 | ``` 38 | 39 | ## Run the custom model 40 | 1. Create an account and obtain an Upshot ApiKey [here](https://developer.upshot.xyz) 41 | 42 | 2. Clone the git repository 43 | ``` 44 | git clone https://github.com/sarox0987/allora-worker.git 45 | cd allora-worker 46 | ``` 47 | 48 | 3. Run the bash script 49 | ``` 50 | bash run.sh 51 | ``` 52 | * **Index:** Set your worker index.(the index gives you the ability to run multiple workers on one server) 53 | * **Mnemonic Phrase:** Import you menmonic phrase 54 | * **Upshot ApiKey:** Import your upshot apikey 55 | 56 | 57 | make sure both `custom-worker-0` & `custom-inference` containers are running with `docker ps` 58 | Screenshot 2024-09-09 at 3 33 15 PM 59 | 60 | 61 | check the worker container with `docker logs -f custom-worker-0` command 62 | 63 | Screenshot 2024-08-17 at 3 30 29 PM 64 | 65 | make sure `custom-inference` returns correct response 66 | ``` 67 | curl http://localhost:8001/inference/ETH 68 | curl http://localhost:8001/inference/MEME 69 | curl http://localhost:8001/inference/ELECTION 70 | ``` 71 | 72 | ## [DEPRECATED] Run with hugging model and pass your mnemonic phrase to it 73 | ``` 74 | curl -LOs https://raw.githubusercontent.com/sarox0987/allora-worker/main/hugging-allora.sh && bash ./hugging-allora.sh 75 | ``` 76 | 77 | make sure both `hugging-worker` & `hugging-inference` containers are running with `docker ps` 78 | 79 | Screenshot 2024-08-17 at 3 15 37 PM 80 | 81 | check the worker container with `docker logs -f hugging-worker` command 82 | 83 | make sure `hugging-inference` is responsive 84 | ``` 85 | curl http://localhost:8002/inference/ETH 86 | ``` 87 | 88 | -------------------------------------------------------------------------------- /hugging-allora.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get mnemonic phrase from user 4 | read -p "Enter your mnemonic phrase: " mnemonic_phrase 5 | 6 | mkdir hugging-allora 7 | cd hugging-allora 8 | git clone https://github.com/allora-network/allora-huggingface-walkthrough.git 9 | cd allora-huggingface-walkthrough 10 | mkdir worker-data 11 | touch worker-data/env_file 12 | chmod -R 777 worker-data 13 | rm docker-compose.yaml 14 | 15 | cat << EOF > docker-compose.yaml 16 | services: 17 | hugging-inference: 18 | container_name: hugging-inference 19 | build: 20 | context: . 21 | dockerfile: Dockerfile 22 | command: python -u /app/app.py 23 | ports: 24 | - "8002:8000" 25 | 26 | hugging-worker: 27 | container_name: hugging-worker 28 | image: alloranetwork/allora-offchain-node:latest 29 | volumes: 30 | - ./worker-data:/data 31 | depends_on: 32 | - hugging-inference 33 | env_file: 34 | - ./worker-data/env_file 35 | 36 | volumes: 37 | inference-data: 38 | worker-data: 39 | EOF 40 | 41 | 42 | # Write the JSON content to config.json 43 | cat < config.json 44 | { 45 | "wallet": { 46 | "addressKeyName": "test", 47 | "addressRestoreMnemonic": "$mnemonic_phrase", 48 | "alloraHomeDir": "", 49 | "gas": "1000000", 50 | "gasAdjustment": 1.0, 51 | "nodeRpc": "https://allora-rpc.testnet-1.testnet.allora.network/", 52 | "maxRetries": 1, 53 | "delay": 1, 54 | "submitTx": false 55 | }, 56 | "worker": [ 57 | { 58 | "topicId": 1, 59 | "inferenceEntrypointName": "api-worker-reputer", 60 | "loopSeconds": 1, 61 | "parameters": { 62 | "InferenceEndpoint": "http://hugging-inference:8000/inference/{Token}", 63 | "Token": "ETH" 64 | } 65 | }, 66 | { 67 | "topicId": 2, 68 | "inferenceEntrypointName": "api-worker-reputer", 69 | "loopSeconds": 3, 70 | "parameters": { 71 | "InferenceEndpoint": "http://hugging-inference:8000/inference/{Token}", 72 | "Token": "ETH" 73 | } 74 | }, 75 | { 76 | "topicId": 3, 77 | "inferenceEntrypointName": "api-worker-reputer", 78 | "loopSeconds": 5, 79 | "parameters": { 80 | "InferenceEndpoint": "http://hugging-inference:8000/inference/{Token}", 81 | "Token": "BTC" 82 | } 83 | }, 84 | { 85 | "topicId": 4, 86 | "inferenceEntrypointName": "api-worker-reputer", 87 | "loopSeconds": 2, 88 | "parameters": { 89 | "InferenceEndpoint": "http://hugging-inference:8000/inference/{Token}", 90 | "Token": "BTC" 91 | } 92 | }, 93 | { 94 | "topicId": 5, 95 | "inferenceEntrypointName": "api-worker-reputer", 96 | "loopSeconds": 4, 97 | "parameters": { 98 | "InferenceEndpoint": "http://hugging-inference:8000/inference/{Token}", 99 | "Token": "SOL" 100 | } 101 | }, 102 | { 103 | "topicId": 6, 104 | "inferenceEntrypointName": "api-worker-reputer", 105 | "loopSeconds": 5, 106 | "parameters": { 107 | "InferenceEndpoint": "http://hugging-inference:8000/inference/{Token}", 108 | "Token": "SOL" 109 | } 110 | }, 111 | { 112 | "topicId": 7, 113 | "inferenceEntrypointName": "api-worker-reputer", 114 | "loopSeconds": 2, 115 | "parameters": { 116 | "InferenceEndpoint": "http://hugging-inference:8000/inference/{Token}", 117 | "Token": "ETH" 118 | } 119 | }, 120 | { 121 | "topicId": 8, 122 | "inferenceEntrypointName": "api-worker-reputer", 123 | "loopSeconds": 3, 124 | "parameters": { 125 | "InferenceEndpoint": "http://hugging-inference:8000/inference/{Token}", 126 | "Token": "BNB" 127 | } 128 | }, 129 | { 130 | "topicId": 9, 131 | "inferenceEntrypointName": "api-worker-reputer", 132 | "loopSeconds": 5, 133 | "parameters": { 134 | "InferenceEndpoint": "http://hugging-inference:8000/inference/{Token}", 135 | "Token": "ARB" 136 | } 137 | } 138 | 139 | ] 140 | } 141 | EOF 142 | 143 | chmod +x init.config 144 | ./init.config 145 | 146 | # Run docker containers 147 | docker-compose up -d 148 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #rpc="https://allora-rpc.testnet.allora.network" 4 | rpc="https://rpc.ankr.com/allora_testnet" 5 | 6 | read -p "Enter your worker index: " index 7 | read -p "Enter your mnemonic phrase: " mnemonic_phrase 8 | read -p "Enter your upshot apikey: " upshot_apikey 9 | 10 | mkdir worker-data-$index 11 | chmod -R 777 worker-data-$index 12 | 13 | cat << EOF > .env 14 | RPC=$rpc 15 | UPSHOT_APIKEY="$upshot_apikey" 16 | EOF 17 | 18 | 19 | cat << EOF > docker-compose.yaml 20 | services: 21 | custom-inference: 22 | build: . 23 | image: custom-inference 24 | container_name: custom-inference 25 | env_file: .env 26 | ports: 27 | - "8001:8000" 28 | 29 | custom-worker-$index: 30 | container_name: custom-worker-$index 31 | image: alloranetwork/allora-offchain-node:latest 32 | volumes: 33 | - ./worker-data-$index:/data 34 | depends_on: 35 | - custom-inference 36 | env_file: 37 | - ./worker-data-$index/env_file 38 | EOF 39 | 40 | 41 | cat << EOF > init.config 42 | #!/bin/bash 43 | 44 | set -e 45 | 46 | if [ ! -f config.json ]; then 47 | echo "Error: config.json file not found, please provide one" 48 | exit 1 49 | fi 50 | 51 | nodeName=\$(jq -r '.wallet.addressKeyName' config.json) 52 | if [ -z "\$nodeName" ]; then 53 | echo "No wallet name provided for the node, please provide your preferred wallet name. config.json >> wallet.addressKeyName" 54 | exit 1 55 | fi 56 | 57 | # Ensure the worker-data-$index directory exists 58 | mkdir -p ./worker-data-$index 59 | 60 | json_content=\$(cat ./config.json) 61 | stringified_json=\$(echo "\$json_content" | jq -c .) 62 | 63 | mnemonic=\$(jq -r '.wallet.addressRestoreMnemonic' config.json) 64 | if [ -n "\$mnemonic" ]; then 65 | echo "ALLORA_OFFCHAIN_NODE_CONFIG_JSON='\$stringified_json'" > ./worker-data-$index/env_file 66 | echo "NAME=\$nodeName" >> ./worker-data-$index/env_file 67 | echo "ENV_LOADED=true" >> ./worker-data-$index/env_file 68 | 69 | echo "wallet mnemonic already provided by you, loading config.json . Please proceed to run docker compose" 70 | exit 1 71 | fi 72 | 73 | if [ ! -f ./worker-data-$index/env_file ]; then 74 | echo "ENV_LOADED=false" > ./worker-data-$index/env_file 75 | fi 76 | 77 | ENV_LOADED=\$(grep '^ENV_LOADED=' ./worker-data-$index/env_file | cut -d '=' -f 2) 78 | if [ "\$ENV_LOADED" = "false" ]; then 79 | json_content=\$(cat ./config.json) 80 | stringified_json=\$(echo "\$json_content" | jq -c .) 81 | 82 | docker run -it --entrypoint=bash -v \$(pwd)/worker-data-$index:/data -v \$(pwd)/scripts:/scripts -e NAME=\${nodeName}" -e ALLORA_OFFCHAIN_NODE_CONFIG_JSON="\${stringified_json}" alloranetwork/allora-chain:latest -c "bash /scripts/init.sh" 83 | echo "config.json saved to ./worker-data-$index/env_file" 84 | else 85 | echo "config.json is already loaded, skipping the operation. You can set ENV_LOADED variable to false in ./worker-data-$index/env_file to reload the config.json" 86 | fi 87 | EOF 88 | 89 | 90 | cat < config.json 91 | { 92 | "wallet": { 93 | "addressKeyName": "test", 94 | "addressRestoreMnemonic": "$mnemonic_phrase", 95 | "alloraHomeDir": "", 96 | "gas": "1000000", 97 | "gasAdjustment": 1.0, 98 | "nodeRpc": "$rpc", 99 | "maxRetries": 1, 100 | "delay": 1, 101 | "submitTx": true 102 | }, 103 | "worker": [ 104 | { 105 | "topicId": 1, 106 | "inferenceEntrypointName": "api-worker-reputer", 107 | "loopSeconds": 1, 108 | "parameters": { 109 | "InferenceEndpoint": "http://custom-inference:8000/inference/{Token}", 110 | "Token": "ETH" 111 | } 112 | }, 113 | { 114 | "topicId": 2, 115 | "inferenceEntrypointName": "api-worker-reputer", 116 | "loopSeconds": 3, 117 | "parameters": { 118 | "InferenceEndpoint": "http://custom-inference:8000/inference/{Token}", 119 | "Token": "ETH" 120 | } 121 | }, 122 | { 123 | "topicId": 3, 124 | "inferenceEntrypointName": "api-worker-reputer", 125 | "loopSeconds": 5, 126 | "parameters": { 127 | "InferenceEndpoint": "http://custom-inference:8000/inference/{Token}", 128 | "Token": "BTC" 129 | } 130 | }, 131 | { 132 | "topicId": 4, 133 | "inferenceEntrypointName": "api-worker-reputer", 134 | "loopSeconds": 2, 135 | "parameters": { 136 | "InferenceEndpoint": "http://custom-inference:8000/inference/{Token}", 137 | "Token": "BTC" 138 | } 139 | }, 140 | { 141 | "topicId": 5, 142 | "inferenceEntrypointName": "api-worker-reputer", 143 | "loopSeconds": 4, 144 | "parameters": { 145 | "InferenceEndpoint": "http://custom-inference:8000/inference/{Token}", 146 | "Token": "SOL" 147 | } 148 | }, 149 | { 150 | "topicId": 6, 151 | "inferenceEntrypointName": "api-worker-reputer", 152 | "loopSeconds": 5, 153 | "parameters": { 154 | "InferenceEndpoint": "http://custom-inference:8000/inference/{Token}", 155 | "Token": "SOL" 156 | } 157 | }, 158 | { 159 | "topicId": 7, 160 | "inferenceEntrypointName": "api-worker-reputer", 161 | "loopSeconds": 2, 162 | "parameters": { 163 | "InferenceEndpoint": "http://custom-inference:8000/inference/{Token}", 164 | "Token": "ETH" 165 | } 166 | }, 167 | { 168 | "topicId": 8, 169 | "inferenceEntrypointName": "api-worker-reputer", 170 | "loopSeconds": 3, 171 | "parameters": { 172 | "InferenceEndpoint": "http://custom-inference:8000/inference/{Token}", 173 | "Token": "BNB" 174 | } 175 | }, 176 | { 177 | "topicId": 9, 178 | "inferenceEntrypointName": "api-worker-reputer", 179 | "loopSeconds": 5, 180 | "parameters": { 181 | "InferenceEndpoint": "http://custom-inference:8000/inference/{Token}", 182 | "Token": "ARB" 183 | } 184 | }, 185 | { 186 | "topicId": 10, 187 | "inferenceEntrypointName": "api-worker-reputer", 188 | "loopSeconds": 5, 189 | "parameters": { 190 | "InferenceEndpoint": "http://custom-inference:8000/inference/{Token}", 191 | "Token": "MEME" 192 | } 193 | }, 194 | { 195 | "topicId": 11, 196 | "inferenceEntrypointName": "api-worker-reputer", 197 | "loopSeconds": 5, 198 | "parameters": { 199 | "InferenceEndpoint": "http://custom-inference:8000/inference/{Token}", 200 | "Token": "ELECTION" 201 | } 202 | } 203 | 204 | ] 205 | } 206 | EOF 207 | 208 | chmod +x init.config 209 | ./init.config 210 | 211 | # Run docker containers 212 | docker-compose up -d 213 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 2 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 3 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 4 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 5 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 6 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 7 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 13 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 14 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 15 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 16 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 17 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 18 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 19 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 20 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 21 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 22 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 23 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 24 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 25 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 26 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 27 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 28 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 29 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 32 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 33 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 34 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 35 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 36 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 37 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 38 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 39 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 40 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 45 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 46 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 47 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 52 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 53 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 54 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 55 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 57 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 58 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 59 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 60 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 61 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 62 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 63 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 64 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 65 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 66 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 67 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 68 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 69 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 70 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 71 | golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= 72 | golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= 73 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 74 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 75 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 78 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 79 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 80 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 81 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 82 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 84 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 85 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 86 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 87 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 89 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 90 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/gin-gonic/gin" 14 | "golang.org/x/exp/rand" 15 | ) 16 | 17 | type Kline struct { 18 | OpenTime time.Time 19 | CloseTime time.Time 20 | Interval string 21 | Symbol string 22 | Open string 23 | High string 24 | Low string 25 | Close string 26 | Volume string 27 | Closed bool 28 | } 29 | 30 | func main() { 31 | 32 | cfg := &envConfig{ 33 | APIKey: os.Getenv("UPSHOT_APIKEY"), 34 | RPC: os.Getenv("RPC"), 35 | } 36 | 37 | fmt.Println("UPSHOT_APIKEY: ", cfg.APIKey) 38 | fmt.Println("RPC: ", cfg.RPC) 39 | 40 | router := gin.Default() 41 | 42 | router.GET("/inference/:token", func(c *gin.Context) { 43 | token := c.Param("token") 44 | if token == "MEME" { 45 | handleMemeRequest(c, cfg) 46 | return 47 | } 48 | 49 | if token == "ELECTION" { 50 | handleElectRequest(c) 51 | return 52 | } 53 | 54 | symbol := fmt.Sprintf("%sUSDT", token) 55 | 56 | k, err := getLastKlines(symbol, "15m") 57 | if err != nil { 58 | fmt.Println(err) 59 | return 60 | } 61 | 62 | rate, err := calculatePriceChangeRate(*k) 63 | if err != nil { 64 | fmt.Println(err) 65 | return 66 | } 67 | rate = multiplyChangeRate(rate) 68 | close, _ := strconv.ParseFloat(k.Close, 64) 69 | price := close + (close * rate) 70 | 71 | c.String(200, strconv.FormatFloat(price, 'g', -1, 64)) 72 | }) 73 | 74 | router.Run(":8000") 75 | 76 | } 77 | 78 | func handleMemeRequest(c *gin.Context, cfg *envConfig) { 79 | 80 | if cfg.APIKey == "" { 81 | c.String(400, "need api key") 82 | } 83 | 84 | if cfg.RPC == "" { 85 | panic("Invalid env.json file") 86 | } 87 | 88 | lb, err := getLatestBlock(cfg.RPC) 89 | if err != nil { 90 | fmt.Println(err) 91 | return 92 | } 93 | 94 | meme, err := getMemeOracleData(lb, cfg.APIKey) 95 | if err != nil { 96 | fmt.Println(err) 97 | return 98 | } 99 | 100 | mp, err := getMemePrice(meme.Data.Platform, meme.Data.Address) 101 | if err != nil { 102 | fmt.Println(err) 103 | return 104 | } 105 | 106 | fmt.Printf("\nBlockHeight: \"%s\", Meme: \"%s\", Platform: \"%s\", Price: \"%s\"\n\n", 107 | lb, meme.Data.TokenSymbol, meme.Data.Platform, mp) 108 | 109 | mpf, _ := strconv.ParseFloat(mp, 64) 110 | 111 | c.String(http.StatusOK, strconv.FormatFloat(random(mpf), 'g', -1, 64)) 112 | } 113 | 114 | func getLastKlines(symbol, interval string) (*Kline, error) { 115 | 116 | ur, _ := url.Parse("https://api.binance.com/api/v1/klines") 117 | queryParams := url.Values{} 118 | queryParams.Add("endTime", strconv.Itoa(int(time.Now().UnixMilli()))) 119 | queryParams.Add("limit", "1") 120 | queryParams.Add("symbol", symbol) 121 | queryParams.Add("interval", interval) 122 | ur.RawQuery = queryParams.Encode() 123 | resp, err := http.DefaultClient.Get(ur.String()) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | defer resp.Body.Close() 129 | if resp.StatusCode != http.StatusOK { 130 | return nil, fmt.Errorf("status code %d", resp.StatusCode) 131 | } 132 | 133 | var ks [][]interface{} 134 | b, err := io.ReadAll(resp.Body) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | err = json.Unmarshal(b, &ks) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | if len(ks) == 0 { 145 | return nil, err 146 | } 147 | 148 | kline := ks[0] 149 | return &Kline{ 150 | OpenTime: time.UnixMilli(int64(kline[0].(float64))), 151 | Interval: interval, 152 | Symbol: symbol, 153 | Open: kline[1].(string), 154 | High: kline[4].(string), 155 | Low: kline[2].(string), 156 | Close: kline[3].(string), 157 | Volume: kline[5].(string), 158 | }, nil 159 | } 160 | 161 | func calculatePriceChangeRate(kline Kline) (float64, error) { 162 | open, err := strconv.ParseFloat(kline.Open, 64) 163 | if err != nil { 164 | return 0, err 165 | } 166 | close, err := strconv.ParseFloat(kline.Close, 64) 167 | if err != nil { 168 | return 0, err 169 | } 170 | 171 | if open == 0 { 172 | return 0, fmt.Errorf("open price cannot be zero") 173 | } 174 | 175 | priceChangeRate := (close - open) / open 176 | return priceChangeRate, nil 177 | } 178 | 179 | func multiplyChangeRate(changeRate float64) float64 { 180 | r := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) 181 | 182 | multiplier := r.Float64()*0.8 + 0.1 183 | newChangeRate := changeRate * multiplier 184 | return newChangeRate + changeRate 185 | } 186 | 187 | func getMemePrice(network, memeAddress string) (string, error) { 188 | url := fmt.Sprintf("https://api.geckoterminal.com/api/v2/simple/networks/%s/token_price/%s", network, memeAddress) 189 | 190 | req, err := http.NewRequest(http.MethodGet, url, nil) 191 | if err != nil { 192 | return "", fmt.Errorf("failed to create new request: %w", err) 193 | } 194 | req.Header.Set("accept", "application/json") 195 | 196 | client := &http.Client{} 197 | resp, err := client.Do(req) 198 | if err != nil { 199 | return "", fmt.Errorf("failed to execute request: %w", err) 200 | } 201 | defer resp.Body.Close() 202 | 203 | body, err := io.ReadAll(resp.Body) 204 | if err != nil { 205 | return "", fmt.Errorf("failed to read response body: %w", err) 206 | } 207 | 208 | res := &tokenPriceResponse{} 209 | err = json.Unmarshal(body, res) 210 | if err != nil { 211 | return "", fmt.Errorf("failed to unmarshal response: %w", err) 212 | } 213 | 214 | return res.Data.Attributes.TokenPrices[memeAddress], nil 215 | } 216 | 217 | type tokenPriceResponse struct { 218 | Data struct { 219 | Attributes struct { 220 | TokenPrices map[string]string `json:"token_prices"` 221 | } `json:"attributes"` 222 | } `json:"data"` 223 | } 224 | 225 | type latestBlockResponse struct { 226 | Result struct { 227 | SyncInfo struct { 228 | LatestBlockHeight string `json:"latest_block_height"` 229 | } `json:"sync_info"` 230 | } `json:"result"` 231 | } 232 | 233 | func getLatestBlock(rpc string) (string, error) { 234 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/status", rpc), nil) 235 | if err != nil { 236 | return "", fmt.Errorf("failed to create new request: %w", err) 237 | } 238 | 239 | client := &http.Client{} 240 | resp, err := client.Do(req) 241 | if err != nil { 242 | return "", fmt.Errorf("failed to execute request: %w", err) 243 | } 244 | defer resp.Body.Close() 245 | 246 | body, err := io.ReadAll(resp.Body) 247 | if err != nil { 248 | return "", fmt.Errorf("failed to read response body: %w", err) 249 | } 250 | 251 | var response latestBlockResponse 252 | err = json.Unmarshal(body, &response) 253 | if err != nil { 254 | return "", fmt.Errorf("failed to unmarshal response: %w", err) 255 | } 256 | 257 | return response.Result.SyncInfo.LatestBlockHeight, nil 258 | } 259 | 260 | type envConfig struct { 261 | RPC string `json:"rpc"` 262 | APIKey string `json:"api_key"` 263 | } 264 | 265 | type memeOracleResponse struct { 266 | RequestID string `json:"request_id"` 267 | Status bool `json:"status"` 268 | Data struct { 269 | TokenID string `json:"token_id"` 270 | TokenSymbol string `json:"token_symbol"` 271 | Platform string `json:"platform"` 272 | Address string `json:"address"` 273 | } `json:"data"` 274 | } 275 | 276 | func getMemeOracleData(blockHeight string, apiKey string) (*memeOracleResponse, error) { 277 | url := fmt.Sprintf("https://api.upshot.xyz/v2/allora/tokens-oracle/token/%s", blockHeight) 278 | 279 | req, err := http.NewRequest(http.MethodGet, url, nil) 280 | if err != nil { 281 | return nil, fmt.Errorf("failed to create new request: %w", err) 282 | } 283 | req.Header.Set("accept", "application/json") 284 | req.Header.Set("x-api-key", apiKey) 285 | 286 | client := &http.Client{} 287 | resp, err := client.Do(req) 288 | if err != nil { 289 | return nil, err 290 | } 291 | defer resp.Body.Close() 292 | 293 | body, err := io.ReadAll(resp.Body) 294 | if err != nil { 295 | return nil, err 296 | } 297 | 298 | res := &memeOracleResponse{} 299 | err = json.Unmarshal(body, res) 300 | if err != nil { 301 | return nil, fmt.Errorf("failed to unmarshal response: %w", err) 302 | } 303 | 304 | return res, nil 305 | } 306 | 307 | func random(price float64) float64 { 308 | randomPercent := rand.Float64()*6 - 3 309 | 310 | priceChange := price * (randomPercent / 100) 311 | 312 | return price + priceChange 313 | } 314 | 315 | type PriceData struct { 316 | History []struct { 317 | T int64 `json:"t"` 318 | P float64 `json:"p"` 319 | } `json:"history"` 320 | } 321 | 322 | func handleElectRequest(c *gin.Context) { 323 | url := "https://clob.polymarket.com/prices-history?interval=all&market=21742633143463906290569050155826241533067272736897614950488156847949938836455&fidelity=1000" 324 | data, err := getPriceData(url) 325 | if err != nil { 326 | fmt.Println(err) 327 | return 328 | } 329 | 330 | price, err := getRandomPrice(data, 100) 331 | if err != nil { 332 | fmt.Println(err) 333 | return 334 | } 335 | 336 | p := strconv.FormatFloat(random(price*100), 'f', 2, 64) 337 | fmt.Println("Election Response: ", p) 338 | c.String(http.StatusOK, p) 339 | } 340 | 341 | func getPriceData(url string) (*PriceData, error) { 342 | resp, err := http.Get(url) 343 | if err != nil { 344 | return nil, err 345 | } 346 | defer resp.Body.Close() 347 | 348 | body, err := io.ReadAll(resp.Body) 349 | if err != nil { 350 | return nil, err 351 | } 352 | 353 | var priceData PriceData 354 | err = json.Unmarshal(body, &priceData) 355 | if err != nil { 356 | return nil, err 357 | } 358 | 359 | return &priceData, nil 360 | } 361 | 362 | func getRandomPrice(data *PriceData, n int) (float64, error) { 363 | historyLen := len(data.History) 364 | if historyLen == 0 { 365 | return 0, fmt.Errorf("no price data available") 366 | } 367 | 368 | if n > historyLen { 369 | n = historyLen 370 | } 371 | 372 | rand.Seed(uint64(time.Now().UnixNano())) 373 | randomIndex := rand.Intn(n) 374 | fmt.Println(data.History[len(data.History)-1]) 375 | return data.History[historyLen-n+randomIndex].P, nil 376 | } 377 | --------------------------------------------------------------------------------