diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.env b/.env index 8c82aa2..57989b0 100644 --- a/.env +++ b/.env @@ -1,2 +1,5 @@ STACKLY_API_KEY= -MORALIS_API_KEY= \ No newline at end of file +MORALIS_API_KEY= +PORTALS_API_KEY= +COINGECKO_API_KEY= +MOBULA_API_KEY= \ No newline at end of file diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml new file mode 100644 index 0000000..b0c246e --- /dev/null +++ b/.github/workflows/fly-deploy.yml @@ -0,0 +1,18 @@ +# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ + +name: Fly Deploy +on: + push: + branches: + - main +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + concurrency: deploy-group # optional: ensure only one action runs at a time + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6660423 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +# syntax = docker/dockerfile:1 + +# Adjust BUN_VERSION as desired +ARG BUN_VERSION=1.1.21 +FROM oven/bun:${BUN_VERSION}-slim as base + +LABEL fly_launch_runtime="Next.js" + +# Next.js app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" + + +# Throw-away build stage to reduce size of final image +FROM base as build + +# Install packages needed to build node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential pkg-config python-is-python3 + +# Install node modules +COPY --link bun.lockb package.json ./ +RUN bun install + +# Copy application code +COPY --link . . + +# Build application +RUN bun --bun run build + +# Remove development dependencies +RUN rm -rf node_modules && \ + bun install --ci + + +# Final stage for app image +FROM base + +# Copy built application +COPY --from=build /app /app + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD [ "bun", "run", "start" ] diff --git a/app/api/get-user/route.ts b/app/api/get-user/route.ts index 2434823..269a137 100644 --- a/app/api/get-user/route.ts +++ b/app/api/get-user/route.ts @@ -23,6 +23,11 @@ async function findAsyncSequential( return undefined; } +type DCAOrder = { + amount: string; + sellToken: { address: string; decimals: number }; +}; + export async function POST(request: Request) { const payload = await request.json(); const headers = request.headers; @@ -46,10 +51,73 @@ export async function POST(request: Request) { id: walletAddress.toLowerCase(), startTime_gte: +startTime, })) as { - dcaorders: { - amount: string; - sellToken: { address: string; decimals: number }; - }[]; + dcaorders: DCAOrder[]; + }; + + const getUSDPriceMoralis = async (order: DCAOrder): Promise => { + const response = await Moralis.EvmApi.token.getTokenPrice({ + chain: "0xa4b1", + address: order.sellToken.address, + }); + + return response.raw.usdPrice; + }; + + const getUSDPriceMobula = async (order: DCAOrder): Promise => { + const response = await fetch( + `https://api.mobula.io/api/1/market/data?asset=${order.sellToken.address}&blockchain=42161`, + { + method: "GET", + headers: { + Authorization: process.env.MOBULA_API_KEY || "", + }, + } + ); + + const data = await response.json(); + + return data.data.price; + }; + + const getUSDPriceCoinGecko = async (order: DCAOrder): Promise => { + const url = `https://api.coingecko.com/api/v3/simple/token_price/arbitrum-one?contract_addresses=${order.sellToken.address}&vs_currencies=usd`; + + const response = await fetch(url, { + method: "GET", + headers: { + accept: "application/json", + "x-cg-api-key": process.env.COINGECKO_API_KEY || "", + }, + }); + + const data = await response.json(); + + return data[order.sellToken.address.toLowerCase()].usd; + }; + + const getUSDPricePortals = async (order: DCAOrder): Promise => { + const url = `https://api.portals.fi/v2/tokens?ids=arbitrum:${order.sellToken.address}`; + + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: process.env.PORTALS_API_KEY || "", + }, + }); + + const data = await response.json(); + + return data.tokens[0].price; + }; + + const getPrice = (order: DCAOrder) => { + const optionNumber = Math.floor(Math.random() * 100); + + // use mobula and portals more frequently since they have better plans + if (optionNumber > 50) return getUSDPriceMobula(order); + else if (optionNumber > 25) return getUSDPricePortals(order); + else if (optionNumber > 10) return getUSDPriceCoinGecko(order); + else return getUSDPriceCoinGecko(order); }; const result = await findAsyncSequential( @@ -57,12 +125,9 @@ export async function POST(request: Request) { async (order) => { try { const tokenAmount = formatEther(BigInt(order.amount)); - const response = await Moralis.EvmApi.token.getTokenPrice({ - chain: "0xa4b1", - address: order.sellToken.address, - }); + const usdPrice = await getPrice(order); - const stackValue = response.raw.usdPrice * +tokenAmount; + const stackValue = usdPrice * +tokenAmount; if (stackValue >= +minmumValue) return true; return false; diff --git a/bun.lockb b/bun.lockb index 503669c..87517ce 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..8e9a08d --- /dev/null +++ b/fly.toml @@ -0,0 +1,22 @@ +# fly.toml app configuration file generated for zealy-api on 2024-08-05T11:12:59+01:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'zealy-api' +primary_region = 'mad' + +[build] + +[http_service] + internal_port = 3000 + force_https = true + auto_stop_machines = 'stop' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + memory = '1gb' + cpu_kind = 'shared' + cpus = 1 diff --git a/package.json b/package.json index a1800aa..13a4130 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,12 @@ "viem": "^2.16.5" }, "devDependencies": { - "typescript": "^5", + "@flydotio/dockerfile": "latest", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "eslint": "^8", - "eslint-config-next": "14.2.4" + "eslint-config-next": "14.2.4", + "typescript": "^5" } }