From 94a871438ebd0e46b47e7e9d4087adf48e2dcdae Mon Sep 17 00:00:00 2001 From: nsjames Date: Mon, 22 Jul 2024 12:37:05 +0100 Subject: [PATCH 1/2] working unperformant version --- .gitignore | 1 + bun.lockb | Bin 118377 -> 169453 bytes package.json | 3 + src/lib/components/ApyChart.svelte | 4 +- src/lib/components/ClaimRewards.svelte | 2 +- src/lib/components/EarnApyChart.svelte | 4 +- src/lib/components/Navbar.svelte | 2 +- src/lib/components/Stake.svelte | 4 +- src/lib/components/Stats.svelte | 6 +- src/lib/components/Unstake.svelte | 2 +- src/lib/components/UserHistory.svelte | 237 ++++++++++++++++++ src/lib/models/Persistent.ts | 18 ++ src/lib/models/RexDateState.ts | 18 ++ src/lib/models/ServerResponse.ts | 23 ++ src/lib/models/UserRexDateState.ts | 19 ++ src/lib/services/database.backend.ts | 55 ++++ src/lib/services/firebase.backend.ts | 118 +++++++++ src/lib/services/history.ts | 207 +++++++++++++++ src/lib/{ => services}/wharf.ts | 2 +- src/lib/utils/api.ts | 42 ++++ src/lib/{ => utils}/networks.ts | 0 .../api/get-rex-account-history/+server.ts | 71 ++++++ .../api/get-rex-price-history/+server.ts | 20 ++ .../api/process-rex-states/+server.ts | 82 ++++++ src/routes/+page.svelte | 2 +- 25 files changed, 931 insertions(+), 11 deletions(-) create mode 100644 src/lib/components/UserHistory.svelte create mode 100644 src/lib/models/Persistent.ts create mode 100644 src/lib/models/RexDateState.ts create mode 100644 src/lib/models/ServerResponse.ts create mode 100644 src/lib/models/UserRexDateState.ts create mode 100644 src/lib/services/database.backend.ts create mode 100644 src/lib/services/firebase.backend.ts create mode 100644 src/lib/services/history.ts rename src/lib/{ => services}/wharf.ts (99%) create mode 100644 src/lib/utils/api.ts rename src/lib/{ => utils}/networks.ts (100%) create mode 100644 src/routes/(backend)/api/get-rex-account-history/+server.ts create mode 100644 src/routes/(backend)/api/get-rex-price-history/+server.ts create mode 100644 src/routes/(backend)/api/process-rex-states/+server.ts diff --git a/.gitignore b/.gitignore index 3a47f86..fdfd433 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ tmp # environment variable files .env +.env.* # executables *.apk diff --git a/bun.lockb b/bun.lockb index 9f184736b859a48ba2fce9a376b12550c2f32a5b..2b20af5b7eed3c50fc6d4c8ae599d78d674024e9 100755 GIT binary patch delta 50569 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

Ja?Am&HlBy-nZC_ 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 import Chart from 'chart.js/auto'; import {onMount} from "svelte"; - import WharfService, {rexpool} from "$lib/wharf"; + import WharfService, {rexpool} from "$lib/services/wharf"; let clazz:string = ''; @@ -102,7 +102,7 @@

- +
diff --git a/src/lib/components/ClaimRewards.svelte b/src/lib/components/ClaimRewards.svelte index a106bdf..ad4d7a6 100644 --- a/src/lib/components/ClaimRewards.svelte +++ b/src/lib/components/ClaimRewards.svelte @@ -1,5 +1,5 @@ +
diff --git a/src/lib/components/Stats.svelte b/src/lib/components/Stats.svelte index c4d6af4..56319bf 100644 --- a/src/lib/components/Stats.svelte +++ b/src/lib/components/Stats.svelte @@ -1,10 +1,12 @@ diff --git a/src/lib/components/Unstake.svelte b/src/lib/components/Unstake.svelte index 715df42..ae1d98c 100644 --- a/src/lib/components/Unstake.svelte +++ b/src/lib/components/Unstake.svelte @@ -1,5 +1,5 @@ + +
+
+
+
You've made
+

{earnedYieldTotal} EOS

+
over the past 30 days
+
+ + + + + + + + +
+ +
+ +
+ + + + {#if $rexStateHistory.length > 0} + + {/if} +
+ diff --git a/src/lib/models/Persistent.ts b/src/lib/models/Persistent.ts new file mode 100644 index 0000000..2160026 --- /dev/null +++ b/src/lib/models/Persistent.ts @@ -0,0 +1,18 @@ +import { v4 } from 'uuid'; + +export class Persistent { + public id:string; + public docType:string; + public createdAt:number; + public updatedAt:number|null = null; + + constructor(docType:string){ + this.id = v4(); + this.docType = docType; + this.createdAt = +new Date(); + } + + key():string{ + return `${this.docType}::${this.id}`; + } +} diff --git a/src/lib/models/RexDateState.ts b/src/lib/models/RexDateState.ts new file mode 100644 index 0000000..59716a3 --- /dev/null +++ b/src/lib/models/RexDateState.ts @@ -0,0 +1,18 @@ +import {Persistent} from "$lib/models/Persistent"; + +export class RexDateState extends Persistent { + // the date in the format of "YYYY-MM-DD" + public id: string = ""; + public rexPrice: number = 0; + public apy: number = 0; + public tvl: number = 0; + + constructor(json: Partial = {}) { + super('rex_date_state'); + Object.assign(this, json); + } + + static key(id: string): string { + return new RexDateState({id}).key(); + } +} \ No newline at end of file diff --git a/src/lib/models/ServerResponse.ts b/src/lib/models/ServerResponse.ts new file mode 100644 index 0000000..7df99a9 --- /dev/null +++ b/src/lib/models/ServerResponse.ts @@ -0,0 +1,23 @@ +export class ServerResponse { + public data:any = null; + public error:boolean = false; + + constructor(json?:Partial){ + (Object).assign(this, json); + } + + static ok(data: any): ServerResponse { + return new ServerResponse({ + error: false, + data, + }); + } + + static fail(data: any): ServerResponse { + return new ServerResponse({ + error: true, + data, + }); + } + +} \ No newline at end of file diff --git a/src/lib/models/UserRexDateState.ts b/src/lib/models/UserRexDateState.ts new file mode 100644 index 0000000..00c5bbe --- /dev/null +++ b/src/lib/models/UserRexDateState.ts @@ -0,0 +1,19 @@ +import {Persistent} from "$lib/models/Persistent"; + +export class UserRexDateState extends Persistent { + // "account-YYYY-MM-DD" + public id: string = ""; + // the date in the format of "YYYY-MM-DD" + public date: string = ""; + public account: string = ""; + public rex: number = 0; + + constructor(json: Partial = {}) { + super('user_rex_date_state'); + Object.assign(this, json); + } + + static key(id: string): string { + return new UserRexDateState({id}).key(); + } +} \ No newline at end of file diff --git a/src/lib/services/database.backend.ts b/src/lib/services/database.backend.ts new file mode 100644 index 0000000..a3a7ab4 --- /dev/null +++ b/src/lib/services/database.backend.ts @@ -0,0 +1,55 @@ +import { FireStore } from "$lib/services/firebase.backend"; + +export class DatabaseBackend { + static async insert(model:any, collection:string|null = null):Promise { + model.updatedAt = Date.now(); + if(await FireStore.exists(model.key(), collection)) throw new Error('Document already exists'); + return FireStore.set(model.key(), JSON.parse(JSON.stringify(model)), collection); + } + static async upsert(model:any, collection:string|null = null){ + model.updatedAt = Date.now(); + return FireStore.set(model.key(), JSON.parse(JSON.stringify(model)), collection); + } + + static async upsertMany(models:any[], collection:string|null = null){ + const data:any = {}; + for(const model of models.filter(x => !!x)){ + model.updatedAt = Date.now(); + data[model.key()] = JSON.parse(JSON.stringify(model)); + } + return FireStore.setMany(data, collection); + } + + static async upsertRaw(key:any, data:any, collection:string|null = null){ + if(data.hasOwnProperty('updatedAt')) data.updatedAt = Date.now(); + return FireStore.set(key, typeof data === 'string' ? JSON.parse(JSON.stringify(data)) : data, collection); + } + + static async get(key:string, Model:any = null, collection:string|null = null): Promise { + return FireStore.get(key, collection).then(x => { + if(!x) return null; + return Model ? new Model(x) : x; + }); + } + + static async update(key:string, data:any, collection:string|null = null): Promise { + if(data.hasOwnProperty('updatedAt')) data.updatedAt = Date.now(); + return FireStore.update(key, data, collection); + } + + static async remove(key:any, collection:string|null = null){ + return FireStore.delete(key, collection); + } + + static async exists(key:any, collection:string|null = null){ + return FireStore.exists(key, collection); + } + + static async query(func:(collection:any) => {}, Model:any = null, _collection:string|null = null){ + return FireStore.query(func, _collection).then(x => Model ? x.map((y:any) => new Model(y)) : x); + } + + static async rawQuery(_collection:string|null = null){ + return FireStore.rawQuery(_collection); + } +} \ No newline at end of file diff --git a/src/lib/services/firebase.backend.ts b/src/lib/services/firebase.backend.ts new file mode 100644 index 0000000..cf84950 --- /dev/null +++ b/src/lib/services/firebase.backend.ts @@ -0,0 +1,118 @@ +import { initializeApp, cert, getApp } from 'firebase-admin/app'; +import { getFirestore } from 'firebase-admin/firestore'; + +import {FIREBASE_SERVICE_ACCOUNT, FIRESTORE_DEFAULT_COLLECTION} from "$env/static/private" +if(!FIREBASE_SERVICE_ACCOUNT) { + throw new Error('Please set your Firebase environment variables'); +} + +const serviceAccount = JSON.parse(FIREBASE_SERVICE_ACCOUNT!.toString()); + +let app; +try { + app = getApp(); +} catch (e) { + app = initializeApp({ + credential: cert(serviceAccount) + }); +} + +const db = getFirestore(); +const collectionName = (collection:string|null = null) => { + return collection || FIRESTORE_DEFAULT_COLLECTION || "default"; +} + +export class FireStore { + + static async get(index: string, collection: string|null = null): Promise { + try { + const doc = await db.collection(collectionName(collection)).doc(index).get(); + return doc.data(); + } catch (error) { + console.error(error); + return null; + } + } + + static async set(index: string, data: any, collection: string|null = null): Promise { + try { + const write = await db.collection(collectionName(collection)).doc(index).set(data); + return !!write && !!write.writeTime; + } catch (error) { + console.error(error); + return false; + } + } + + static async update(index: string, data: any, collection: string|null = null): Promise { + try { + const write = await db.collection(collectionName(collection)).doc(index).update(data); + return !!write && !!write.writeTime; + } catch (error) { + console.error(error); + return false; + } + } + + static async setMany(data:Array<{[key:string]:any}>, collection: string|null = null): Promise { + try { + const batch = db.batch(); + for(const key in data){ + const ref = db.collection(collectionName(collection)).doc(key); + batch.set(ref, data[key]); + } + const write = await batch.commit(); + return !!write && write.length > 0; + } catch (error) { + console.error(error); + return false; + } + } + + static async delete(index: string, collection: string|null = null): Promise { + try { + const write = await db.collection(collectionName(collection)).doc(index).delete(); + return !!write && !!write.writeTime; + } catch (error) { + console.error(error); + return false; + } + } + + static async exists(index: string, collection: string|null = null): Promise { + try { + const doc = await db.collection(collectionName(collection)).doc(index).get(); + return doc.exists; + } catch (error) { + console.error(error); + return false; + } + } + + // this doesn't cover OR queries: https://firebase.google.com/docs/firestore/query-data/queries + static async query(func:(collection:any) => {}, _collection: string|null = null): Promise { + try { + const preparedQuery:any = func(db.collection(collectionName(_collection))); + const query = await preparedQuery.get(); + if(query.empty) return []; + + try { + return query.docs.map((doc: any) => doc.data()); + } catch(e){} + + try { + return query.data(); + } catch(e){} + + console.error('Unknown query result'); + return []; + } catch (error) { + console.error(error); + return []; + } + } + + static async rawQuery(_collection: string|null = null): Promise { + return db.collection(collectionName(_collection)); + } +} diff --git a/src/lib/services/history.ts b/src/lib/services/history.ts new file mode 100644 index 0000000..77c34ee --- /dev/null +++ b/src/lib/services/history.ts @@ -0,0 +1,207 @@ +import {get, writable, type Writable} from "svelte/store"; +import { toast } from 'svelte-sonner' +import WharfService, {account} from "$lib/services/wharf"; +import type {RexDateState} from "$lib/models/RexDateState"; + +export interface PurchaseOrSell { + amount: number; + trx_id: string; + timestamp: string; + block_id: string; + block_num: number; +} + +export let purchases: Writable = writable([]); +export let sells: Writable = writable([]); +export let blockPrices: Writable = writable({}); +export let blockBalances: Writable = writable({}); +export let blockTimestamps: Writable = writable({}); +export let rexStateHistory: Writable = writable([]); + +const HYPERION_BASE = 'https://eos.hyperion.eosrio.io'; + +export class HistoryService { + static async getHistory(){ + if(!get(account)) return; + await this.getPurchases(get(account)); + await this.getSells(get(account)); + } + + static async getPurchases(account:string){ + const url = new URL(`${HYPERION_BASE}/v2/history/get_actions`); + url.searchParams.append('account', account); + url.searchParams.append('filter', 'eosio:buyrex'); + url.searchParams.append('skip', '0'); + url.searchParams.append('limit', '100'); + url.searchParams.append('sort', 'desc'); + const result = await fetch(url.toString()).then(res => res.json()); + if(result.actions && result.actions.length){ + purchases.set(result.actions.map((action: any) => { + const data = action.act.data; + blockTimestamps.update((timestamps) => { + timestamps[action.block_num] = +new Date(action.timestamp); + return timestamps; + }); + return { + amount: parseFloat(data.amount.split(' ')[0]), + trx_id: action.trx_id, + timestamp: action.timestamp, + block_id: action.block_id, + block_num: action.block_num + } + })); + + for(let i = 0; i < get(purchases).length; i++){ + const block = get(purchases)[i].block_num; + await this.fillBlockData(block); + } + } + } + + static async getSells(account:string){ + const url = new URL(`${HYPERION_BASE}/v2/history/get_actions`); + url.searchParams.append('account', account); + url.searchParams.append('filter', 'eosio:sellrex'); + url.searchParams.append('skip', '0'); + url.searchParams.append('limit', '100'); + url.searchParams.append('sort', 'desc'); + const result = await fetch(url.toString()).then(res => res.json()); + if(result.actions && result.actions.length){ + sells.set(result.actions.map((action: any) => { + const data = action.act.data; + blockTimestamps.update((timestamps) => { + timestamps[action.block_num] = +new Date(action.timestamp); + return timestamps; + }); + return { + amount: parseFloat(data.rex.split(' ')[0]), + trx_id: action.trx_id, + timestamp: action.timestamp, + block_id: action.block_id, + block_num: action.block_num + } + })); + + for(let i = 0; i < get(sells).length; i++){ + const block = get(sells)[i].block_num; + await this.fillBlockData(block); + } + } + } + + static async fillBlockData(block:number){ + if(!get(blockPrices)[block]){ + const price = await this.getPriceForBlock(block); + if(price){ + blockPrices.update((prices) => { + prices[block] = price; + return prices; + }); + } + } + + if(!get(blockBalances)[block]){ + const balance = await this.getBalanceForBlock(block, get(account)); + if(balance){ + blockBalances.update((balances) => { + balances[block] = balance; + return balances; + }); + } + } + } + + static async getBalanceForBlock(block:number, account:string){ + const url = new URL(`${HYPERION_BASE}/v2/history/get_deltas`); + url.searchParams.append('code', 'eosio'); + url.searchParams.append('table', 'rexbal'); + url.searchParams.append('payer', account); + url.searchParams.append('block_num', block.toString()); + const result = await fetch(url.toString()).then(res => res.json()); + + if(result.deltas && result.deltas.length){ + const balance = result.deltas[0].data; + return parseFloat(balance.rex_balance.split(' ')[0]); + } + } + + static async getBlockRexState(buyrexBlock:number, donaterexBlock:number) { + + let pool; + let retpool; + + { + const url = new URL(`${HYPERION_BASE}/v2/history/get_deltas`); + url.searchParams.append('code', 'eosio'); + url.searchParams.append('table', 'rexpool'); + url.searchParams.append('block_num', buyrexBlock.toString()); + const result = await fetch(url.toString()).then(res => res.json()); + + if(result.deltas && result.deltas.length){ + pool = result.deltas[0].data; + } + } + + { + const url = new URL(`${HYPERION_BASE}/v2/history/get_deltas`); + url.searchParams.append('code', 'eosio'); + url.searchParams.append('table', 'rexretpool'); + url.searchParams.append('block_num', donaterexBlock.toString()); + const result = await fetch(url.toString()).then(res => res.json()); + + if(result.deltas && result.deltas.length){ + retpool = result.deltas[0].data; + } + } + + return { + pool, + retpool + } + + } + + static async getPriceForBlock(block:number){ + const url = new URL(`${HYPERION_BASE}/v2/history/get_deltas`); + url.searchParams.append('code', 'eosio'); + url.searchParams.append('table', 'rexpool'); + url.searchParams.append('block_num', block.toString()); + const result = await fetch(url.toString()).then(res => res.json()); + + if(result.deltas && result.deltas.length){ + const pool = result.deltas[0].data; + const S0 = parseFloat(pool.total_lendable.split(' ')[0]); + const R0 = parseFloat(pool.total_rex.split(' ')[0]); + const R1 = R0 + 1; + const S1 = (S0 * R1) / R0; + return parseFloat(parseFloat((S1 - S0).toString()).toFixed(10)); + } + + return null; + } + + static async findBlockForDate(date: number, filter:string = 'eosio:buyrex'){ + // use /v2/history/get_actions to find a block that has a buyrex action between the start of the date day, and the end of the date day + const url = new URL(`${HYPERION_BASE}/v2/history/get_actions`); + url.searchParams.append('account', 'eosio'); + url.searchParams.append('filter', filter); + url.searchParams.append('skip', '0'); + url.searchParams.append('limit', '1'); + + // get the 00:00:00 utc start time + const startOfDay = new Date(date); + startOfDay.setUTCHours(0, 0, 0, 0); + url.searchParams.append('after', startOfDay.toISOString()); // ISO8601 format dates + + // get the 23:59:59 utc end time + const endOfDay = new Date(date); + endOfDay.setUTCHours(23, 59, 59, 999); + url.searchParams.append('before', endOfDay.toISOString()); + + const result = await fetch(url.toString()).then(res => res.json()); + if(!result.actions || !result.actions.length) return null; + return result.actions[0].block_num; + } + + +} \ No newline at end of file diff --git a/src/lib/wharf.ts b/src/lib/services/wharf.ts similarity index 99% rename from src/lib/wharf.ts rename to src/lib/services/wharf.ts index b42fdf1..272377d 100644 --- a/src/lib/wharf.ts +++ b/src/lib/services/wharf.ts @@ -4,7 +4,7 @@ import {get, writable, type Writable} from "svelte/store"; import {TransactPluginResourceProvider} from "@wharfkit/transact-plugin-resource-provider"; import { WalletPluginAnchor } from "@wharfkit/wallet-plugin-anchor" import { toast } from 'svelte-sonner' -import {showConfetti} from "$lib/index"; +import {showConfetti} from "$lib"; export let account:Writable = writable(null); export let eosBalance:Writable = writable(0); diff --git a/src/lib/utils/api.ts b/src/lib/utils/api.ts new file mode 100644 index 0000000..d656458 --- /dev/null +++ b/src/lib/utils/api.ts @@ -0,0 +1,42 @@ +export class Api { + static async get(path:string) { + const response = await fetch(path); + return await response.json(); + } + static async post(path:string, data:any = {}) { + const response = await fetch(path, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + return await response.json(); + } + static async put(path:string, data:any = {}) { + const response = await fetch(path, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + return await response.json(); + } + static async delete(path:string) { + const response = await fetch(path, { + method: 'DELETE' + }); + return await response.json(); + } + static async patch(path:string, data:any = {}) { + const response = await fetch(path, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); + return await response.json(); + } +} \ No newline at end of file diff --git a/src/lib/networks.ts b/src/lib/utils/networks.ts similarity index 100% rename from src/lib/networks.ts rename to src/lib/utils/networks.ts diff --git a/src/routes/(backend)/api/get-rex-account-history/+server.ts b/src/routes/(backend)/api/get-rex-account-history/+server.ts new file mode 100644 index 0000000..6b43e7e --- /dev/null +++ b/src/routes/(backend)/api/get-rex-account-history/+server.ts @@ -0,0 +1,71 @@ +import { json } from '@sveltejs/kit'; +import {ServerResponse} from "$lib/models/ServerResponse"; +import { dev } from '$app/environment'; +import {DatabaseBackend} from "$lib/services/database.backend"; +import {HistoryService} from "$lib/services/history"; +import {Asset, Int64} from "@wharfkit/antelope"; +import {RexDateState} from "$lib/models/RexDateState"; +import {UserRexDateState} from "$lib/models/UserRexDateState"; + +export async function POST({ request }) { + // if(!dev) return json(ServerResponse.fail('Not in dev mode')); + + const { account } = await request.json(); + if(!account || !account.trim().length) return json(ServerResponse.fail('No account provided')); + + const dateKey = (date) => `${account}:${date}`; + + try { + const earliestDate = '2024-07-07'; + const earliestTimestamp = +new Date(earliestDate); + const currentDate = new Date().toISOString().split('T')[0]; + const daysBetween = Math.abs(Math.floor((Date.parse(earliestDate) - Date.parse(currentDate)) / 86400000)); + + for(let i = 0; i < daysBetween; i++){ + const date = new Date(earliestTimestamp + (i * 86400000)).toISOString().split('T')[0]; + console.log('Processing:', date); + + const rexDateState = new UserRexDateState({ + id: dateKey(date), + }); + + const exists = await DatabaseBackend.exists(rexDateState.key()); + if(exists) { + console.log('exists', date); + continue; + } + + const buyrexBlock = await HistoryService.findBlockForDate(earliestTimestamp + (i * 86400000)); + if(!buyrexBlock) { + console.error('buyrexBlock not found', i); + continue; + } + + let donaterexBlock = await HistoryService.findBlockForDate(earliestTimestamp + (i * 86400000), 'eosio:donatetorex'); + if(!donaterexBlock) { + // get yesterday's block + // this is a hack for when there isn't a donation to trigger an update to the rexretpool in this day + donaterexBlock = await HistoryService.findBlockForDate(earliestTimestamp + ((i - 1) * 86400000), 'eosio:donatetorex'); + if(!donaterexBlock) { + console.error('donaterexBlock not found', i); + continue; + } + } + + const rexState = await HistoryService.getBlockRexState(buyrexBlock, donaterexBlock); + + rexDateState.rexPrice = getPrice(rexState.pool); + rexDateState.apy = getApy(rexState.pool, rexState.retpool); + rexDateState.tvl = parseFloat(Asset.fromString(rexState.pool.total_lendable).toString().split(' ')[0]); + + await DatabaseBackend.upsert(rexDateState); + + } + + + return json(ServerResponse.ok({})); + } catch (e:any) { + console.error(e); + return json(ServerResponse.fail('Failed to get admin data')); + } +} diff --git a/src/routes/(backend)/api/get-rex-price-history/+server.ts b/src/routes/(backend)/api/get-rex-price-history/+server.ts new file mode 100644 index 0000000..b67c665 --- /dev/null +++ b/src/routes/(backend)/api/get-rex-price-history/+server.ts @@ -0,0 +1,20 @@ +import { json } from '@sveltejs/kit'; +import {ServerResponse} from "$lib/models/ServerResponse"; +import {DatabaseBackend} from "$lib/services/database.backend"; +import {RexDateState} from "$lib/models/RexDateState"; + +export async function GET({ request }) { + + try { + const earliestDate = '2024-07-07'; + + const docs = await DatabaseBackend.query(collection => { + return collection.where('docType', '==', 'rex_date_state') + }, RexDateState) + + return json(ServerResponse.ok(docs)); + } catch (e:any) { + console.error(e); + return json(ServerResponse.fail('Failed to get admin data')); + } +} diff --git a/src/routes/(backend)/api/process-rex-states/+server.ts b/src/routes/(backend)/api/process-rex-states/+server.ts new file mode 100644 index 0000000..4b48ad6 --- /dev/null +++ b/src/routes/(backend)/api/process-rex-states/+server.ts @@ -0,0 +1,82 @@ +import { json } from '@sveltejs/kit'; +import {ServerResponse} from "$lib/models/ServerResponse"; +import { dev } from '$app/environment'; +import {DatabaseBackend} from "$lib/services/database.backend"; +import {HistoryService} from "$lib/services/history"; +import {Asset, Int64} from "@wharfkit/antelope"; +import {RexDateState} from "$lib/models/RexDateState"; + +export async function GET({ request }) { + if(!dev) return json(ServerResponse.fail('Not in dev mode')); + + const getPrice = (pool) => { + const S0 = parseFloat(pool.total_lendable.split(' ')[0]); + const R0 = parseFloat(pool.total_rex.split(' ')[0]); + const R1 = R0 + 1; + const S1 = (S0 * R1) / R0; + return parseFloat(parseFloat((S1 - S0).toString()).toFixed(10)); + } + + const getApy = (pool, retpool) => { + const total_lendable = Asset.fromString(pool.total_lendable).units.toNumber(); + const current_rate_of_increase = Int64.from(retpool.current_rate_of_increase).toNumber(); + const proceeds = Int64.from(retpool.proceeds).toNumber(); + return parseFloat(parseFloat( + (((proceeds + current_rate_of_increase) / 30 * 365) / total_lendable * 100).toString() + ).toFixed(2)); + } + + try { + const earliestDate = '2024-07-07'; + const earliestTimestamp = +new Date(earliestDate); + const currentDate = new Date().toISOString().split('T')[0]; + const daysBetween = Math.abs(Math.floor((Date.parse(earliestDate) - Date.parse(currentDate)) / 86400000)); + + for(let i = 0; i < daysBetween; i++){ + const date = new Date(earliestTimestamp + (i * 86400000)).toISOString().split('T')[0]; + console.log('Processing:', date); + + const rexDateState = new RexDateState({ + id: date, + }); + + const exists = await DatabaseBackend.exists(rexDateState.key()); + if(exists) { + console.log('exists', date); + continue; + } + + const buyrexBlock = await HistoryService.findBlockForDate(earliestTimestamp + (i * 86400000)); + if(!buyrexBlock) { + console.error('buyrexBlock not found', i); + continue; + } + + let donaterexBlock = await HistoryService.findBlockForDate(earliestTimestamp + (i * 86400000), 'eosio:donatetorex'); + if(!donaterexBlock) { + // get yesterday's block + // this is a hack for when there isn't a donation to trigger an update to the rexretpool in this day + donaterexBlock = await HistoryService.findBlockForDate(earliestTimestamp + ((i - 1) * 86400000), 'eosio:donatetorex'); + if(!donaterexBlock) { + console.error('donaterexBlock not found', i); + continue; + } + } + + const rexState = await HistoryService.getBlockRexState(buyrexBlock, donaterexBlock); + + rexDateState.rexPrice = getPrice(rexState.pool); + rexDateState.apy = getApy(rexState.pool, rexState.retpool); + rexDateState.tvl = parseFloat(Asset.fromString(rexState.pool.total_lendable).toString().split(' ')[0]); + + await DatabaseBackend.upsert(rexDateState); + + } + + + return json(ServerResponse.ok({})); + } catch (e:any) { + console.error(e); + return json(ServerResponse.fail('Failed to get admin data')); + } +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 6177801..cad0a95 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,7 +1,7 @@ - +
diff --git a/src/lib/components/Stats.svelte b/src/lib/components/Stats.svelte index 56319bf..a6f3e93 100644 --- a/src/lib/components/Stats.svelte +++ b/src/lib/components/Stats.svelte @@ -1,12 +1,13 @@ - { + $: balanceByDate = (() => { const balances = []; - for(let i = 0; i < Object.keys($blockTimestamps).length; i++){ - const block:number = Object.keys($blockTimestamps)[i]; - const timestamp = Object.values($blockTimestamps)[i]; - const balance = $blockBalances[block]; - balances.push({balance, date: new Date(timestamp).toISOString().split('T')[0]}); + + const ascendingActions = $userActions.sort((a, b) => +new Date(a.date) - +new Date(b.date)); + + for(let action of ascendingActions){ + if(action.type === UserRexActionType.Purchase){ + const balance = balances.length ? balances[balances.length - 1].balance + action.rex : action.rex; + const eosBalance = balances.length ? balances[balances.length - 1].eos + action.eos : action.eos; + balances.push({balance, eos:eosBalance, date: new Date(action.date).toISOString().split('T')[0]}); + } else if(action.type === UserRexActionType.Sell){ + const balance = balances.length ? balances[balances.length - 1].balance - action.rex : 0; + const eosBalance = balances.length ? balances[balances.length - 1].eos - action.eos : 0; + balances.push({balance, eos:eosBalance, date: new Date(action.date).toISOString().split('T')[0]}); + } + } + + // if there are multiple actions on the same day, remove all but the last one + for(let i = 0; i < balances.length; i++){ + const current = balances[i]; + const next = balances[i + 1]; + if(next && current.date === next.date){ + balances.splice(i, 1); + i--; + } } + return balances; })(); $: rawEosPurchases = (() => { - // return 1; - return $purchases.reduce((acc, purchase) => { - return acc + purchase.amount; + return $userActions.filter(x => x.type === UserRexActionType.Purchase).reduce((acc, purchase) => { + return acc + purchase.eos; }, 0); })(); $: earnedYieldTotal = (() => { if(!$rawRexBalance) return 0; - // return 1; + if(!$userActions) return 0; + if(!rawEosPurchases) return 0; - // total from sells - const totalFromSells = $sells.reduce((acc, sell) => { - return acc + sell.amount; + const totalFromSells = $userActions.filter(x => x.type === UserRexActionType.Sell).reduce((acc, sell) => { + return acc + sell.rex; }, 0); + return parseFloat( + (WharfService.convertRexToEos( + parseFloat($rawRexBalance.rex_balance.split(' ')[0]) + totalFromSells + ) - parseFloat(rawEosPurchases)).toString() + ).toFixed(4); + })(); + + $: rexStateHistoryWithToday = (() => { + if(!$rexStateHistory) return []; + if(!$rexpool) return $rexStateHistory; + + let history:any = $rexStateHistory; + const todaysDate = new Date().toISOString().split('T')[0]; + const index = history.findIndex(x => x.id === todaysDate); + if(index === -1){ + const price = WharfService.convertRexToEos(1); + history.push({id: todaysDate, rexPrice: price}); + } - return parseFloat(WharfService.convertRexToEos(parseFloat($rawRexBalance.rex_balance.split(' ')[0]) + totalFromSells) - parseFloat(rawEosPurchases)).toFixed(4); + return history; })(); const getChartData = () => { - const timeBoxedStateHistory = $rexStateHistory.filter((state) => { + const timeBoxedStateHistory = rexStateHistoryWithToday.filter((state) => { const date = new Date(state.id); return date >= startDate && date <= endDate; }); @@ -68,9 +105,25 @@ return date.toLocaleDateString(); }); - const datasets = timeBoxedStateHistory.map((state) => { - const balance = reducedBalancesByDate.find((balance) => new Date(balance.date) >= new Date(state.id)); - return balance ? balance.balance * state.rexPrice : 0; + const datasets = timeBoxedStateHistory.map((state, index) => { + const balances = balanceByDate + // find all dates higher than the current state date + .filter((balance) => +new Date(balance.date) <= +new Date(state.id)) + // sort by date + .sort((a, b) => +new Date(a.date) - +new Date(b.date)); + const balance = balances.length ? balances[balances.length-1].balance * state.rexPrice : 0; + const eosBalance = balances.length ? balances[balances.length-1].eos : 0; + + if(!showOnlyRewards){ + return balance; + } + // just show the current earned yield if we're at the last state + // as it's sometimes incorrect due to precision issues + if(index === timeBoxedStateHistory.length - 1){ + return earnedYieldTotal; + } + + return balance - eosBalance; }); return { @@ -98,36 +151,43 @@ }, 200); } - purchases.subscribe((data) => { - refresh(); - }); - - sells.subscribe((data) => { + userActions.subscribe((data) => { refresh(); }); rexStateHistory.subscribe((data) => { if(data.length > 0){ startDate = new Date(data[0].id); - endDate = new Date(data[data.length - 1].id); } refresh(); }); - // account.subscribe(async (data) => { - // if(data){ - // await HistoryService.getHistory(); - // refresh(); - // } - // }); + const getUserHistory = async () => { + if($account){ + const result = await Api.post('/api/get-rex-account-history', {account: $account}); + if(result.error){ + console.error(result); + toast.error(result.data); + return; + } - onMount(async () => { + userActions.set(result.data); + refresh(); + } + } + + account.subscribe(async (data) => { + await getUserHistory(); + }); + onMount(async () => { const history = await Api.get('/api/get-rex-price-history'); if(!history.error){ rexStateHistory.set(history.data); + } else { + toast.error(history.data); + return; } - // await HistoryService.getHistory(); ctx = canvas.getContext('2d'); @@ -145,7 +205,7 @@ // only show middle, first and last tick callback: function(value, index, ticks) { const totalTicks = ticks.length; - const middleTick = Math.floor(totalTicks / 2); + const middleTick = Math.floor(totalTicks / 2)+1; if (index === 0 || index === middleTick-1 || index === totalTicks - 1) { return this.getLabelForValue(value); } @@ -200,29 +260,23 @@ + +
You've made

{earnedYieldTotal} EOS

-
over the past 30 days
+ {#if $rexStateHistory.length} +
since {new Date($rexStateHistory[0].id).toLocaleDateString()}
+ {/if}
- - - - - - - -
- - {#if $rexStateHistory.length > 0} + +
+ +
Show only earned from yield
+
{/if}
- diff --git a/src/lib/models/UserRexAction.ts b/src/lib/models/UserRexAction.ts new file mode 100644 index 0000000..c923026 --- /dev/null +++ b/src/lib/models/UserRexAction.ts @@ -0,0 +1,26 @@ +import {Persistent} from "$lib/models/Persistent"; + +export enum UserRexActionType { + Purchase = 0, + Sell = 1, +} + +export class UserRexAction extends Persistent { + // trx_id + public id: string = ""; + public block_num: number = 0; + public date: string = ""; + public account: string = ""; + public rex: number = 0; + public eos: number = 0; + public type: UserRexActionType = UserRexActionType.Purchase; + + constructor(json: Partial = {}) { + super('user_rex_action'); + Object.assign(this, json); + } + + static key(id: string): string { + return new UserRexAction({id}).key(); + } +} \ No newline at end of file diff --git a/src/lib/models/UserRexDateState.ts b/src/lib/models/UserRexDateState.ts deleted file mode 100644 index 00c5bbe..0000000 --- a/src/lib/models/UserRexDateState.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {Persistent} from "$lib/models/Persistent"; - -export class UserRexDateState extends Persistent { - // "account-YYYY-MM-DD" - public id: string = ""; - // the date in the format of "YYYY-MM-DD" - public date: string = ""; - public account: string = ""; - public rex: number = 0; - - constructor(json: Partial = {}) { - super('user_rex_date_state'); - Object.assign(this, json); - } - - static key(id: string): string { - return new UserRexDateState({id}).key(); - } -} \ No newline at end of file diff --git a/src/lib/services/database.backend.ts b/src/lib/services/database.backend.ts index a3a7ab4..ac9bcb7 100644 --- a/src/lib/services/database.backend.ts +++ b/src/lib/services/database.backend.ts @@ -1,5 +1,9 @@ import { FireStore } from "$lib/services/firebase.backend"; +export enum COLLECTIONS { + RexActions = 'rex_actions', +} + export class DatabaseBackend { static async insert(model:any, collection:string|null = null):Promise { model.updatedAt = Date.now(); diff --git a/src/lib/services/history.backend.ts b/src/lib/services/history.backend.ts new file mode 100644 index 0000000..1818dc8 --- /dev/null +++ b/src/lib/services/history.backend.ts @@ -0,0 +1,63 @@ +import {HistoryService} from "$lib/services/history"; +import {COLLECTIONS, DatabaseBackend} from "$lib/services/database.backend"; +import {UserRexAction, UserRexActionType} from "$lib/models/UserRexAction"; +import {ServerResponse} from "$lib/models/ServerResponse"; + +export class HistoryBackendService { + + static async processAccountHistory(account:string){ + try { + const earliestDate = '2024-07-07'; + const earliestTimestamp = +new Date(earliestDate); + + const purchases = await HistoryService.getPurchases(account); + + for(let purchase of purchases) { + if(await DatabaseBackend.exists(UserRexAction.key(purchase.trx_id), COLLECTIONS.RexActions)) { + continue; + } + + const price = await HistoryService.getPriceForBlock(purchase.block_num); + const rex = parseFloat((purchase.amount / price).toString()).toFixed(4); + const action = new UserRexAction({ + id: purchase.trx_id, + block_num: purchase.block_num, + date: purchase.timestamp, + account, + rex: parseFloat(rex), + eos: purchase.amount, + type: UserRexActionType.Purchase, + }); + + await DatabaseBackend.upsert(action, COLLECTIONS.RexActions); + } + + const sells = await HistoryService.getSells(account); + for(let sell of sells) { + if(await DatabaseBackend.exists(UserRexAction.key(sell.trx_id), COLLECTIONS.RexActions)) { + continue; + } + + const price = await HistoryService.getPriceForBlock(sell.block_num); + const eos = parseFloat((sell.amount * price).toString()).toFixed(4); + const action = new UserRexAction({ + id: sell.trx_id, + block_num: sell.block_num, + date: sell.timestamp, + account, + rex: sell.amount, + eos: parseFloat(eos), + type: UserRexActionType.Sell, + }); + + await DatabaseBackend.upsert(action, COLLECTIONS.RexActions); + } + + return ServerResponse.ok({}); + } catch (e:any) { + console.error(e); + return ServerResponse.fail('Failed to get process account history'); + } + } + +} \ No newline at end of file diff --git a/src/lib/services/history.ts b/src/lib/services/history.ts index 77c34ee..a2ddfed 100644 --- a/src/lib/services/history.ts +++ b/src/lib/services/history.ts @@ -11,21 +11,13 @@ export interface PurchaseOrSell { block_num: number; } -export let purchases: Writable = writable([]); -export let sells: Writable = writable([]); -export let blockPrices: Writable = writable({}); -export let blockBalances: Writable = writable({}); -export let blockTimestamps: Writable = writable({}); export let rexStateHistory: Writable = writable([]); +export let userActions: Writable = writable([]); + const HYPERION_BASE = 'https://eos.hyperion.eosrio.io'; export class HistoryService { - static async getHistory(){ - if(!get(account)) return; - await this.getPurchases(get(account)); - await this.getSells(get(account)); - } static async getPurchases(account:string){ const url = new URL(`${HYPERION_BASE}/v2/history/get_actions`); @@ -36,12 +28,8 @@ export class HistoryService { url.searchParams.append('sort', 'desc'); const result = await fetch(url.toString()).then(res => res.json()); if(result.actions && result.actions.length){ - purchases.set(result.actions.map((action: any) => { + return result.actions.map((action: any) => { const data = action.act.data; - blockTimestamps.update((timestamps) => { - timestamps[action.block_num] = +new Date(action.timestamp); - return timestamps; - }); return { amount: parseFloat(data.amount.split(' ')[0]), trx_id: action.trx_id, @@ -49,13 +37,10 @@ export class HistoryService { block_id: action.block_id, block_num: action.block_num } - })); - - for(let i = 0; i < get(purchases).length; i++){ - const block = get(purchases)[i].block_num; - await this.fillBlockData(block); - } + }); } + + return []; } static async getSells(account:string){ @@ -63,66 +48,22 @@ export class HistoryService { url.searchParams.append('account', account); url.searchParams.append('filter', 'eosio:sellrex'); url.searchParams.append('skip', '0'); - url.searchParams.append('limit', '100'); + url.searchParams.append('limit', '200'); url.searchParams.append('sort', 'desc'); const result = await fetch(url.toString()).then(res => res.json()); if(result.actions && result.actions.length){ - sells.set(result.actions.map((action: any) => { + return result.actions.map((action: any) => { const data = action.act.data; - blockTimestamps.update((timestamps) => { - timestamps[action.block_num] = +new Date(action.timestamp); - return timestamps; - }); return { amount: parseFloat(data.rex.split(' ')[0]), trx_id: action.trx_id, timestamp: action.timestamp, - block_id: action.block_id, block_num: action.block_num } - })); - - for(let i = 0; i < get(sells).length; i++){ - const block = get(sells)[i].block_num; - await this.fillBlockData(block); - } - } - } - - static async fillBlockData(block:number){ - if(!get(blockPrices)[block]){ - const price = await this.getPriceForBlock(block); - if(price){ - blockPrices.update((prices) => { - prices[block] = price; - return prices; - }); - } - } - - if(!get(blockBalances)[block]){ - const balance = await this.getBalanceForBlock(block, get(account)); - if(balance){ - blockBalances.update((balances) => { - balances[block] = balance; - return balances; - }); - } + }); } - } - - static async getBalanceForBlock(block:number, account:string){ - const url = new URL(`${HYPERION_BASE}/v2/history/get_deltas`); - url.searchParams.append('code', 'eosio'); - url.searchParams.append('table', 'rexbal'); - url.searchParams.append('payer', account); - url.searchParams.append('block_num', block.toString()); - const result = await fetch(url.toString()).then(res => res.json()); - if(result.deltas && result.deltas.length){ - const balance = result.deltas[0].data; - return parseFloat(balance.rex_balance.split(' ')[0]); - } + return []; } static async getBlockRexState(buyrexBlock:number, donaterexBlock:number) { diff --git a/src/lib/services/wharf.ts b/src/lib/services/wharf.ts index 272377d..a61d031 100644 --- a/src/lib/services/wharf.ts +++ b/src/lib/services/wharf.ts @@ -5,6 +5,7 @@ import {TransactPluginResourceProvider} from "@wharfkit/transact-plugin-resource import { WalletPluginAnchor } from "@wharfkit/wallet-plugin-anchor" import { toast } from 'svelte-sonner' import {showConfetti} from "$lib"; +import {Api} from "$lib/utils/api"; export let account:Writable = writable(null); export let eosBalance:Writable = writable(0); @@ -78,6 +79,10 @@ export default class WharfService { account.set(session.actor.toString()) setTimeout(() => WharfService.refresh()); } + + try { + setTimeout(() => Api.get('/api/process-rex-today'),1000); + } catch (e) {} } static async login(){ @@ -155,7 +160,7 @@ export default class WharfService { const R0 = parseFloat(pool.total_rex.split(' ')[0]); const R1 = R0 + rex; const S1 = (S0 * R1) / R0; - return parseFloat(parseFloat((S1 - S0).toString()).toFixed(4)); + return parseFloat(parseFloat((S1 - S0).toString()).toFixed(9)); } static async getEosBalance(){ diff --git a/src/routes/(backend)/api/get-rex-account-history/+server.ts b/src/routes/(backend)/api/get-rex-account-history/+server.ts index 6b43e7e..aee29c0 100644 --- a/src/routes/(backend)/api/get-rex-account-history/+server.ts +++ b/src/routes/(backend)/api/get-rex-account-history/+server.ts @@ -1,71 +1,25 @@ import { json } from '@sveltejs/kit'; import {ServerResponse} from "$lib/models/ServerResponse"; -import { dev } from '$app/environment'; -import {DatabaseBackend} from "$lib/services/database.backend"; -import {HistoryService} from "$lib/services/history"; -import {Asset, Int64} from "@wharfkit/antelope"; +import {COLLECTIONS, DatabaseBackend} from "$lib/services/database.backend"; import {RexDateState} from "$lib/models/RexDateState"; -import {UserRexDateState} from "$lib/models/UserRexDateState"; +import {HistoryBackendService} from "$lib/services/history.backend"; +import {UserRexAction} from "$lib/models/UserRexAction"; export async function POST({ request }) { - // if(!dev) return json(ServerResponse.fail('Not in dev mode')); - - const { account } = await request.json(); - if(!account || !account.trim().length) return json(ServerResponse.fail('No account provided')); - - const dateKey = (date) => `${account}:${date}`; + const {account} = await request.json(); + if(!account) return json(ServerResponse.fail('No account provided')); try { - const earliestDate = '2024-07-07'; - const earliestTimestamp = +new Date(earliestDate); - const currentDate = new Date().toISOString().split('T')[0]; - const daysBetween = Math.abs(Math.floor((Date.parse(earliestDate) - Date.parse(currentDate)) / 86400000)); - - for(let i = 0; i < daysBetween; i++){ - const date = new Date(earliestTimestamp + (i * 86400000)).toISOString().split('T')[0]; - console.log('Processing:', date); - - const rexDateState = new UserRexDateState({ - id: dateKey(date), - }); - - const exists = await DatabaseBackend.exists(rexDateState.key()); - if(exists) { - console.log('exists', date); - continue; - } - - const buyrexBlock = await HistoryService.findBlockForDate(earliestTimestamp + (i * 86400000)); - if(!buyrexBlock) { - console.error('buyrexBlock not found', i); - continue; - } - - let donaterexBlock = await HistoryService.findBlockForDate(earliestTimestamp + (i * 86400000), 'eosio:donatetorex'); - if(!donaterexBlock) { - // get yesterday's block - // this is a hack for when there isn't a donation to trigger an update to the rexretpool in this day - donaterexBlock = await HistoryService.findBlockForDate(earliestTimestamp + ((i - 1) * 86400000), 'eosio:donatetorex'); - if(!donaterexBlock) { - console.error('donaterexBlock not found', i); - continue; - } - } - - const rexState = await HistoryService.getBlockRexState(buyrexBlock, donaterexBlock); - - rexDateState.rexPrice = getPrice(rexState.pool); - rexDateState.apy = getApy(rexState.pool, rexState.retpool); - rexDateState.tvl = parseFloat(Asset.fromString(rexState.pool.total_lendable).toString().split(' ')[0]); - - await DatabaseBackend.upsert(rexDateState); - - } + const processed = await HistoryBackendService.processAccountHistory(account); + if(processed.error) return json(processed); + const docs = await DatabaseBackend.query(collection => { + return collection.where('account', '==', account) + }, UserRexAction, COLLECTIONS.RexActions) - return json(ServerResponse.ok({})); + return json(ServerResponse.ok(docs)); } catch (e:any) { console.error(e); - return json(ServerResponse.fail('Failed to get admin data')); + return json(ServerResponse.fail('Failed to get account data')); } } diff --git a/src/routes/(backend)/api/get-rex-price-history/+server.ts b/src/routes/(backend)/api/get-rex-price-history/+server.ts index b67c665..9d6ac35 100644 --- a/src/routes/(backend)/api/get-rex-price-history/+server.ts +++ b/src/routes/(backend)/api/get-rex-price-history/+server.ts @@ -6,8 +6,6 @@ import {RexDateState} from "$lib/models/RexDateState"; export async function GET({ request }) { try { - const earliestDate = '2024-07-07'; - const docs = await DatabaseBackend.query(collection => { return collection.where('docType', '==', 'rex_date_state') }, RexDateState) diff --git a/src/routes/(backend)/api/process-rex-account-history/+server.ts b/src/routes/(backend)/api/process-rex-account-history/+server.ts new file mode 100644 index 0000000..4a30563 --- /dev/null +++ b/src/routes/(backend)/api/process-rex-account-history/+server.ts @@ -0,0 +1,12 @@ +import { json } from '@sveltejs/kit'; +import {ServerResponse} from "$lib/models/ServerResponse"; +import {HistoryBackendService} from "$lib/services/history.backend"; + +export async function POST({ request }) { + const { account } = await request.json(); + if(!account || !account.trim().length) return json(ServerResponse.fail('No account provided')); + + const dateKey = (date) => `${account}:${date}`; + + return json(await HistoryBackendService.processAccountHistory(account)); +} diff --git a/src/routes/(backend)/api/process-rex-states/+server.ts b/src/routes/(backend)/api/process-rex-states/+server.ts index 4b48ad6..043dfc8 100644 --- a/src/routes/(backend)/api/process-rex-states/+server.ts +++ b/src/routes/(backend)/api/process-rex-states/+server.ts @@ -42,7 +42,6 @@ export async function GET({ request }) { const exists = await DatabaseBackend.exists(rexDateState.key()); if(exists) { - console.log('exists', date); continue; } diff --git a/src/routes/(backend)/api/process-rex-today/+server.ts b/src/routes/(backend)/api/process-rex-today/+server.ts new file mode 100644 index 0000000..ed0f1de --- /dev/null +++ b/src/routes/(backend)/api/process-rex-today/+server.ts @@ -0,0 +1,77 @@ +import { json } from '@sveltejs/kit'; +import {ServerResponse} from "$lib/models/ServerResponse"; +import { dev } from '$app/environment'; +import {DatabaseBackend} from "$lib/services/database.backend"; +import {HistoryService} from "$lib/services/history"; +import {Asset, Int64} from "@wharfkit/antelope"; +import {RexDateState} from "$lib/models/RexDateState"; + +export async function GET({ request }) { + if(!dev) return json(ServerResponse.fail('Not in dev mode')); + + const getPrice = (pool) => { + const S0 = parseFloat(pool.total_lendable.split(' ')[0]); + const R0 = parseFloat(pool.total_rex.split(' ')[0]); + const R1 = R0 + 1; + const S1 = (S0 * R1) / R0; + return parseFloat(parseFloat((S1 - S0).toString()).toFixed(10)); + } + + const getApy = (pool, retpool) => { + const total_lendable = Asset.fromString(pool.total_lendable).units.toNumber(); + const current_rate_of_increase = Int64.from(retpool.current_rate_of_increase).toNumber(); + const proceeds = Int64.from(retpool.proceeds).toNumber(); + return parseFloat(parseFloat( + (((proceeds + current_rate_of_increase) / 30 * 365) / total_lendable * 100).toString() + ).toFixed(2)); + } + + try { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setUTCHours(0, 0, 0, 0); + const yesterdayTimestamp = +yesterday; + + const dayBeforeYesterday = new Date(); + dayBeforeYesterday.setDate(dayBeforeYesterday.getDate() - 2); + dayBeforeYesterday.setUTCHours(0, 0, 0, 0); + const dayBeforeYesterdayTimestamp = +dayBeforeYesterday; + + const rexDateState = new RexDateState({ + id: yesterday.toISOString().split('T')[0], + }); + + const exists = await DatabaseBackend.exists(rexDateState.key()); + if(exists) { + return json(ServerResponse.ok(true)); + } + + const buyrexBlock = await HistoryService.findBlockForDate(yesterdayTimestamp); + if(!buyrexBlock) { + return json(ServerResponse.fail('buyrexBlock not found')); + } + + let donaterexBlock = await HistoryService.findBlockForDate(yesterdayTimestamp, 'eosio:donatetorex'); + if(!donaterexBlock) { + // get yesterday's block + // this is a hack for when there isn't a donation to trigger an update to the rexretpool in this day + donaterexBlock = await HistoryService.findBlockForDate(dayBeforeYesterdayTimestamp, 'eosio:donatetorex'); + if(!donaterexBlock) { + return json(ServerResponse.fail('donaterexBlock not found')); + } + } + + const rexState = await HistoryService.getBlockRexState(buyrexBlock, donaterexBlock); + + rexDateState.rexPrice = getPrice(rexState.pool); + rexDateState.apy = getApy(rexState.pool, rexState.retpool); + rexDateState.tvl = parseFloat(Asset.fromString(rexState.pool.total_lendable).toString().split(' ')[0]); + + await DatabaseBackend.upsert(rexDateState); + + return json(ServerResponse.ok(true)); + } catch (e:any) { + console.error(e); + return json(ServerResponse.fail('Failed to get admin data')); + } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 75fb2e7..42f2d67 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,10 +2,12 @@ import "$lib/app.scss"; import { Toaster } from 'svelte-sonner' import GoogleAnalytics from "$lib/components/analytics/GoogleAnalytics.svelte"; + import {onMount} from "svelte"; const title = 'EOS Staking' const description = 'Stake your EOS tokens and earn rewards' const url = 'https://stake.eosnetwork.com' +