diff --git a/.github/workflows/api-dev.yaml b/.github/workflows/api-dev.yaml index 6f267d4..b9d1c01 100644 --- a/.github/workflows/api-dev.yaml +++ b/.github/workflows/api-dev.yaml @@ -6,18 +6,13 @@ on: workflow_dispatch: env: - DOCKER_TAGS: dfxswiss/deuro-api:beta - AZURE_RESOURCE_GROUP: rg-dfx-api-dev - AZURE_CONTAINER_APP: ca-dfx-dea-dev - DEPLOY_INFO: ${{ github.ref_name }}-${{ github.sha }} + DOCKER_TAGS: deurocom/api:beta + DEPLOY_SERVICE: deuro-api jobs: build-and-deploy: - name: Build, test and deploy to DEV - runs-on: ubuntu-latest - defaults: - run: - working-directory: . + name: Build and deploy to DEV + runs-on: ubuntu-24.04-arm steps: - name: Checkout uses: actions/checkout@v4 @@ -37,19 +32,20 @@ jobs: context: . push: true tags: ${{ env.DOCKER_TAGS }} + platforms: linux/arm64 - - name: Log in to Azure - uses: azure/login@v2 - with: - creds: ${{ secrets.DFX_DEV_CREDENTIALS }} - - - name: Update Azure Container App - uses: azure/CLI@v2 - with: - inlineScript: | - az containerapp update --resource-group ${{ env.AZURE_RESOURCE_GROUP }} --name ${{ env.AZURE_CONTAINER_APP }} --image ${{ env.DOCKER_TAGS }} --set-env-vars DEPLOY_INFO=${{ env.DEPLOY_INFO }} + - name: Install cloudflared + run: | + curl -fsSL https://github.com/cloudflare/cloudflared/releases/download/2025.4.0/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared + chmod +x /usr/local/bin/cloudflared - - name: Logout from Azure + - name: Deploy to server run: | - az logout - if: always() + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_DEV_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + echo "${{ secrets.DEPLOY_DEV_SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts + ssh -i ~/.ssh/deploy_key \ + -o ProxyCommand="cloudflared access ssh --hostname ${{ secrets.DEPLOY_DEV_HOST }}" \ + ${{ secrets.DEPLOY_DEV_USER }}@${{ secrets.DEPLOY_DEV_HOST }} \ + "${{ env.DEPLOY_SERVICE }}" diff --git a/.github/workflows/api-prd.yaml b/.github/workflows/api-prd.yaml index adc968f..d08d52e 100644 --- a/.github/workflows/api-prd.yaml +++ b/.github/workflows/api-prd.yaml @@ -6,18 +6,13 @@ on: workflow_dispatch: env: - DOCKER_TAGS: dfxswiss/deuro-api:latest - AZURE_RESOURCE_GROUP: rg-dfx-api-prd - AZURE_CONTAINER_APP: ca-dfx-dea-prd - DEPLOY_INFO: ${{ github.ref_name }}-${{ github.sha }} + DOCKER_TAGS: deurocom/api:latest + DEPLOY_SERVICE: deuro-api jobs: build-and-deploy: name: Build, test and deploy to PRD - runs-on: ubuntu-latest - defaults: - run: - working-directory: . + runs-on: ubuntu-24.04-arm steps: - name: Checkout uses: actions/checkout@v4 @@ -37,19 +32,20 @@ jobs: context: . push: true tags: ${{ env.DOCKER_TAGS }} + platforms: linux/arm64 - - name: Log in to Azure - uses: azure/login@v2 - with: - creds: ${{ secrets.DFX_PRD_CREDENTIALS }} - - - name: Update Azure Container App - uses: azure/CLI@v2 - with: - inlineScript: | - az containerapp update --resource-group ${{ env.AZURE_RESOURCE_GROUP }} --name ${{ env.AZURE_CONTAINER_APP }} --image ${{ env.DOCKER_TAGS }} --set-env-vars DEPLOY_INFO=${{ env.DEPLOY_INFO }} + - name: Install cloudflared + run: | + curl -fsSL https://github.com/cloudflare/cloudflared/releases/download/2025.4.0/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared + chmod +x /usr/local/bin/cloudflared - - name: Logout from Azure + - name: Deploy to server run: | - az logout - if: always() + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_PRD_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + echo "${{ secrets.DEPLOY_PRD_SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts + ssh -i ~/.ssh/deploy_key \ + -o ProxyCommand="cloudflared access ssh --hostname ${{ secrets.DEPLOY_PRD_HOST }}" \ + ${{ secrets.DEPLOY_PRD_USER }}@${{ secrets.DEPLOY_PRD_HOST }} \ + "${{ env.DEPLOY_SERVICE }}" diff --git a/.npmignore b/.npmignore index 597877b..3041e7d 100644 --- a/.npmignore +++ b/.npmignore @@ -5,4 +5,6 @@ node_modules .env .env.local -.env.**.local \ No newline at end of file +.env.**.local + +.claude/ diff --git a/analytics/analytics.service.ts b/analytics/analytics.service.ts index 4b590a2..919ed39 100644 --- a/analytics/analytics.service.ts +++ b/analytics/analytics.service.ts @@ -153,6 +153,7 @@ export class AnalyticsService { const equityAdjusted: number = expo.general.equityInReserve; const otherContributions: number = equityAdjusted - minterProposalFees - investFees - redeemFees - positionProposalFees - otherProfitClaims; + const savingsInfo = this.save.getInfo(); return { minterProposalFees, @@ -162,7 +163,7 @@ export class AnalyticsService { otherProfitClaims, otherContributions, - savingsInterestCosts: this.save.getInfo().totalInterest, + savingsInterestCosts: savingsInfo.totalInterest, otherLossClaims: this.deps.getEcosystemDepsInfo().earnings.loss, }; } diff --git a/api.config.ts b/api.config.ts index ddd8609..488bb74 100644 --- a/api.config.ts +++ b/api.config.ts @@ -1,4 +1,5 @@ -import { Chain, createPublicClient, http } from 'viem'; +import { Address, Chain, createPublicClient, http, zeroAddress } from 'viem'; +import { ADDRESS } from '@deuro/eurocoin'; import { mainnet, polygon } from 'viem/chains'; import { Logger } from '@nestjs/common'; @@ -89,3 +90,14 @@ export const COINGECKO_CLIENT = (query: string) => { const uri: string = `https://pro-api.coingecko.com${query}`; return fetch(`${uri}${hasParams ? '&' : '?'}x_cg_pro_api_key=${CONFIG.coingeckoApiKey}`); }; + +// Contract addresses for the active chain +export const ADDR = ADDRESS[CONFIG.chain.id]; + +export function isDeployed(addr: string | undefined): addr is Address { + return !!addr && addr !== zeroAddress; +} + +export function isV3Hub(hubAddress: Address): boolean { + return isDeployed(ADDR.mintingHub) && hubAddress.toLowerCase() === ADDR.mintingHub.toLowerCase(); +} diff --git a/api.service.ts b/api.service.ts index 3b718df..efac084 100644 --- a/api.service.ts +++ b/api.service.ts @@ -76,6 +76,7 @@ export class ApiService { await timeTask('updateBidV2s', () => this.challenges.updateBidV2s()).catch(() => {}), await timeTask('updateChallengesPrices', () => this.challenges.updateChallengesPrices()).catch(() => {}), await timeTask('updateSavingsUserLeaderboard', () => this.savings.updateSavingsUserLeaderboard()).catch(() => {}), + await timeTask('updateSavingsBalances', () => this.savings.updateSavingsBalances()).catch(() => {}), ]; await Promise.all(promises); diff --git a/challenges/challenges.service.ts b/challenges/challenges.service.ts index 5aac60b..b989923 100644 --- a/challenges/challenges.service.ts +++ b/challenges/challenges.service.ts @@ -28,7 +28,8 @@ import { ChallengesQueryStatus, } from './challenges.types'; import { Address } from 'viem'; -import { ADDRESS, MintingHubGatewayABI } from '@deuro/eurocoin'; +import { MintingHubGatewayV2ABI, MintingHubV3ABI } from '@deuro/eurocoin'; +import { isV3Hub } from '../api.config'; @Injectable() export class ChallengesService { @@ -178,16 +179,21 @@ export class ChallengesService { // mapping active challenge -> prices const challengesPrices: ChallengesPricesMapping = {}; - const id = VIEM_CONFIG.chain.id; for (const c of active) { - const price = await VIEM_CONFIG.readContract({ - abi: MintingHubGatewayABI, - address: ADDRESS[id].mintingHubGateway, - functionName: 'price', - args: [parseInt(c.number.toString())], - }); + try { + const abi = isV3Hub(c.mintingHubAddress) ? MintingHubV3ABI : MintingHubGatewayV2ABI; - challengesPrices[c.id] = price.toString(); + const price = await VIEM_CONFIG.readContract({ + abi, + address: c.mintingHubAddress, + functionName: 'price', + args: [BigInt(c.number)], + }); + + challengesPrices[c.id] = price.toString(); + } catch (e) { + this.logger.warn(`Failed to fetch price for challenge ${c.id}: ${e.message}`); + } } // upsert @@ -205,6 +211,7 @@ export class ChallengesService { items { id position + mintingHubAddress number challenger start @@ -232,7 +239,7 @@ export class ChallengesService { const mapped: ChallengesQueryItemMapping = {}; for (const i of list) { mapped[i.id] = i; - mapped[i.id].version = 2; + mapped[i.id].version = isV3Hub(i.mintingHubAddress) ? 3 : 2; } // upsert @@ -250,6 +257,7 @@ export class ChallengesService { items { id position + mintingHubAddress number numberBid bidder @@ -276,7 +284,7 @@ export class ChallengesService { const mapped: BidsQueryItemMapping = {}; for (const i of list) { mapped[i.id] = i; - mapped[i.id].version = 2; + mapped[i.id].version = isV3Hub(i.mintingHubAddress) ? 3 : 2; } // upsert diff --git a/challenges/challenges.types.ts b/challenges/challenges.types.ts index 5dc4aeb..2c1c023 100644 --- a/challenges/challenges.types.ts +++ b/challenges/challenges.types.ts @@ -7,6 +7,7 @@ export type ChallengesQueryItem = { id: ChallengesId; position: Address; + mintingHubAddress: Address; number: bigint; challenger: Address; @@ -27,6 +28,7 @@ export type BidsQueryItem = { id: BidsId; position: Address; + mintingHubAddress: Address; number: bigint; numberBid: bigint; diff --git a/ecosystem/ecosystem.deps.service.ts b/ecosystem/ecosystem.deps.service.ts index 1b668ed..d67ce0b 100644 --- a/ecosystem/ecosystem.deps.service.ts +++ b/ecosystem/ecosystem.deps.service.ts @@ -54,7 +54,7 @@ export class EcosystemDepsService { fetchPolicy: 'no-cache', query: gql` query GetDEPS { - dEPSs(orderBy: "id", limit: 1000) { + depss(orderBy: "id", limit: 1000) { items { id profits @@ -65,12 +65,12 @@ export class EcosystemDepsService { `, }); - if (!profitLossPonder.data || !profitLossPonder.data.dEPSs.items.length) { + if (!profitLossPonder.data || !profitLossPonder.data.depss.items.length) { this.logger.warn('No profitLossPonder data found.'); return; } - const d = profitLossPonder.data.dEPSs.items.at(0); + const d = profitLossPonder.data.depss.items.at(0); const unrealizedProfit = this.getUnrealizedProfit(); const earningsData: ApiEcosystemDepsInfo['earnings'] = { profit: parseFloat(formatUnits(d.profits, 18)), diff --git a/frontendcode/frontendcode.service.ts b/frontendcode/frontendcode.service.ts index 4a4bd4d..cb71114 100644 --- a/frontendcode/frontendcode.service.ts +++ b/frontendcode/frontendcode.service.ts @@ -41,6 +41,7 @@ export class FrontendCodeService { where: { created_gt: "${checkTimestamp}" amount_gte: "${minAmount}" + frontendCode_not: null } ) { items { diff --git a/package.json b/package.json index 68bf120..3857858 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@deuro/api", - "version": "0.3.3", + "version": "0.3.4", "private": false, "license": "MIT", "homepage": "https://api.deuro.com", @@ -24,7 +24,7 @@ }, "dependencies": { "@apollo/client": "^3.10.5", - "@deuro/eurocoin": "^1.0.13", + "@deuro/eurocoin": "^2.1.0", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", diff --git a/positions/positions.service.ts b/positions/positions.service.ts index d3e7c76..77da399 100644 --- a/positions/positions.service.ts +++ b/positions/positions.service.ts @@ -1,9 +1,9 @@ import { gql } from '@apollo/client/core'; -import { ADDRESS, PositionV2ABI, SavingsABI } from '@deuro/eurocoin'; +import { PositionV2ABI, SavingsGatewayV2ABI } from '@deuro/eurocoin'; import { Injectable, Logger } from '@nestjs/common'; import { FIVEDAYS_MS } from 'utils/const-helper'; import { Address, erc20Abi, getAddress } from 'viem'; -import { CONFIG, VIEM_CONFIG } from '../api.config'; +import { ADDR, isV3Hub, VIEM_CONFIG } from '../api.config'; import { PONDER_CLIENT } from '../api.apollo.config'; import { ApiMintingUpdateListing, @@ -119,6 +119,7 @@ export class PositionsService { fixedAnnualRatePPM principal + mintingHubAddress } } } @@ -136,9 +137,10 @@ export class PositionsService { const virtualPriceDataPromises: Promise[] = []; const interestPromises: Promise[] = []; - const leadrate = await VIEM_CONFIG.readContract({ - address: ADDRESS[CONFIG.chain.id].savingsGateway, - abi: SavingsABI, + // V2 leadrate must succeed — failure aborts the update so stale-but-correct data is served + const v2Leadrate = await VIEM_CONFIG.readContract({ + address: ADDR.savingsGateway, + abi: SavingsGatewayV2ABI, functionName: 'currentRatePPM', }); @@ -196,13 +198,16 @@ export class PositionsService { const v = (virtualPriceData[idx] as PromiseFulfilledResult).value; const i = (interestData[idx] as PromiseFulfilledResult).value; + const annualInterestPPM = isV3Hub(p.mintingHubAddress) ? p.fixedAnnualRatePPM : v2Leadrate + p.riskPremiumPPM; + const entry: PositionQuery = { - version: 2, + version: isV3Hub(p.mintingHubAddress) ? 3 : 2, position: getAddress(p.position), owner: getAddress(p.owner), deuro: getAddress(p.deuro), collateral: getAddress(p.collateral), + mintingHubAddress: getAddress(p.mintingHubAddress), price: p.price, created: p.created, @@ -213,7 +218,7 @@ export class PositionsService { original: getAddress(p.original), minimumCollateral: p.minimumCollateral, - annualInterestPPM: leadrate + p.riskPremiumPPM, + annualInterestPPM, riskPremiumPPM: p.riskPremiumPPM, reserveContribution: p.reserveContribution, start: p.start, @@ -297,6 +302,7 @@ export class PositionsService { feeTimeframe feePPM feePaid + mintingHubAddress } } } @@ -318,7 +324,7 @@ export class PositionsService { if (list[k] === undefined) list[k] = []; const entry: MintingUpdateQuery = { - version: 2, + version: isV3Hub(m.mintingHubAddress) ? 3 : 2, id: m.id, txHash: m.txHash, @@ -343,6 +349,7 @@ export class PositionsService { feeTimeframe: m.feeTimeframe, feePPM: m.feePPM, feePaid: m.feePaid, + mintingHubAddress: getAddress(m.mintingHubAddress), }; list[k].push(entry); diff --git a/positions/positions.types.ts b/positions/positions.types.ts index 3145bf2..a3ed825 100644 --- a/positions/positions.types.ts +++ b/positions/positions.types.ts @@ -2,12 +2,13 @@ import { Address } from 'viem'; // ---------------------------------------------------------------------------------- // Ponder export type PositionQuery = { - version: 2; + version: 2 | 3; position: Address; owner: Address; deuro: Address; collateral: Address; + mintingHubAddress: Address; price: string; created: number; @@ -47,7 +48,7 @@ export type PositionQuery = { export type MintingUpdateQueryId = `${Address}-${number}`; export type MintingUpdateQuery = { - version: 2; + version: 2 | 3; id: MintingUpdateQueryId; txHash: string; @@ -72,6 +73,7 @@ export type MintingUpdateQuery = { feeTimeframe: number; feePPM: number; feePaid: string; + mintingHubAddress: Address; }; // ---------------------------------------------------------------------------------- diff --git a/savings/savings.core.service.ts b/savings/savings.core.service.ts index 05e9b8c..4126ef1 100644 --- a/savings/savings.core.service.ts +++ b/savings/savings.core.service.ts @@ -1,8 +1,8 @@ import { gql } from '@apollo/client/core'; -import { ADDRESS, SavingsGatewayABI } from '@deuro/eurocoin'; +import { ERC20ABI, SavingsGatewayV2ABI, SavingsV3ABI } from '@deuro/eurocoin'; import { Injectable, Logger } from '@nestjs/common'; import { PONDER_CLIENT } from 'api.apollo.config'; -import { VIEM_CONFIG } from 'api.config'; +import { ADDR, isDeployed, VIEM_CONFIG } from 'api.config'; import { EcosystemStablecoinService } from 'ecosystem/ecosystem.stablecoin.service'; import { Address, formatUnits, zeroAddress } from 'viem'; import { ApiSavingsInfo, ApiSavingsUserLeaderboard, ApiSavingsUserTable } from './savings.core.types'; @@ -12,6 +12,7 @@ import { SavingsLeadrateService } from './savings.leadrate.service'; export class SavingsCoreService { private readonly logger = new Logger(this.constructor.name); private fetchedSavingsUserLeaderboard: ApiSavingsUserLeaderboard[] = []; + private fetchedSavingsBalances: { v2: bigint; v3: bigint } = { v2: 0n, v3: 0n }; constructor( private readonly fc: EcosystemStablecoinService, @@ -22,25 +23,55 @@ export class SavingsCoreService { const totalSavedRaw = this.fc.getEcosystemStablecoinKeyValues()?.['Savings:TotalSaved']?.amount || 0n; const totalInterestRaw = this.fc.getEcosystemStablecoinKeyValues()?.['Savings:TotalInterestCollected']?.amount || 0n; const totalWithdrawnRaw = this.fc.getEcosystemStablecoinKeyValues()?.['Savings:TotalWithdrawn']?.amount || 0n; - const rate = this.lead.getInfo().rate; + const info = this.lead.getInfo(); + const { v2: v2BalanceRaw, v3: v3BalanceRaw } = this.fetchedSavingsBalances; const totalSaved: number = parseFloat(formatUnits(totalSavedRaw, 18)); const totalInterest: number = parseFloat(formatUnits(totalInterestRaw, 18)); const totalWithdrawn: number = parseFloat(formatUnits(totalWithdrawnRaw, 18)); + const totalBalance: number = parseFloat(formatUnits(v2BalanceRaw + v3BalanceRaw, 18)); const totalSupply: number = this.fc.getEcosystemStablecoinInfo()?.total?.supply || 1; - const ratioOfSupply: number = totalSaved / totalSupply; + const ratioOfSupply: number = totalBalance / totalSupply; return { totalSaved, totalWithdrawn, - totalBalance: totalSaved - totalWithdrawn, + totalBalance, totalInterest, - rate, + rate: info.v3.rate || info.v2.rate, + rateV2: info.v2.rate, + rateV3: info.v3.rate, ratioOfSupply, }; } + async updateSavingsBalances(): Promise { + this.logger.debug('Updating SavingsBalances'); + const results = await Promise.allSettled([ + VIEM_CONFIG.readContract({ + address: ADDR.decentralizedEURO, + abi: ERC20ABI, + functionName: 'balanceOf', + args: [ADDR.savingsGateway], + }), + isDeployed(ADDR.savings) + ? VIEM_CONFIG.readContract({ + address: ADDR.decentralizedEURO, + abi: ERC20ABI, + functionName: 'balanceOf', + args: [ADDR.savings], + }) + : Promise.resolve(0n), + ]); + + if (results[0].status === 'fulfilled') this.fetchedSavingsBalances.v2 = results[0].value; + else this.logger.warn(`Failed to fetch V2 savings balance: ${results[0].reason}`); + + if (results[1].status === 'fulfilled') this.fetchedSavingsBalances.v3 = results[1].value; + else this.logger.warn(`Failed to fetch V3 savings balance: ${results[1].reason}`); + } + async updateSavingsUserLeaderboard(): Promise { this.logger.debug('Updating SavingsUserLeaderboard'); @@ -63,17 +94,39 @@ export class SavingsCoreService { const mapped = await Promise.all( items.map(async (item) => { - const unrealizedInterest = await VIEM_CONFIG.readContract({ - address: ADDRESS[VIEM_CONFIG.chain.id].savingsGateway, - abi: SavingsGatewayABI, - functionName: 'accruedInterest', - args: [item.id], - }); + const results = await Promise.allSettled([ + VIEM_CONFIG.readContract({ + address: ADDR.savingsGateway, + abi: SavingsGatewayV2ABI, + functionName: 'accruedInterest', + args: [item.id], + }), + isDeployed(ADDR.savings) + ? VIEM_CONFIG.readContract({ + address: ADDR.savings, + abi: SavingsV3ABI, + functionName: 'accruedInterest', + args: [item.id], + }) + : Promise.resolve(0n), + isDeployed(ADDR.savings) + ? VIEM_CONFIG.readContract({ + address: ADDR.savings, + abi: SavingsV3ABI, + functionName: 'claimableInterest', + args: [item.id], + }) + : Promise.resolve(0n), + ]); + + const v2 = results[0].status === 'fulfilled' ? results[0].value : 0n; + const v3Accrued = results[1].status === 'fulfilled' ? results[1].value : 0n; + const v3Claimable = results[2].status === 'fulfilled' ? results[2].value : 0n; return { account: item.id, amountSaved: item.amountSaved, - unrealizedInterest: unrealizedInterest.toString(), + unrealizedInterest: (BigInt(v2) + BigInt(v3Accrued) + BigInt(v3Claimable)).toString(), interestReceived: item.interestReceived, }; }) @@ -88,6 +141,8 @@ export class SavingsCoreService { async getUserTables(userAddress: Address, limit: number = 15): Promise { const user: Address = userAddress == zeroAddress ? zeroAddress : (userAddress.toLowerCase() as Address); + const userWhere = user == zeroAddress ? '' : `where: { account: "${user}" }`; + const ownerWhere = user == zeroAddress ? '' : `where: { owner: "${user}" }`; const savedFetched = await PONDER_CLIENT.query({ fetchPolicy: 'no-cache', query: gql` @@ -95,7 +150,7 @@ export class SavingsCoreService { savingsSaveds( orderBy: "blockheight" orderDirection: "desc" - ${user == zeroAddress ? '' : `where: { account: "${user}" }`} + ${userWhere} limit: ${limit} ) { items { @@ -121,7 +176,7 @@ export class SavingsCoreService { savingsWithdrawns( orderBy: "blockheight" orderDirection: "desc" - ${user == zeroAddress ? '' : `where: { account: "${user}" }`} + ${userWhere} limit: ${limit} ) { items { @@ -147,7 +202,7 @@ export class SavingsCoreService { savingsInterests( orderBy: "blockheight" orderDirection: "desc" - ${user == zeroAddress ? '' : `where: { account: "${user}" }`} + ${userWhere} limit: ${limit} ) { items { @@ -166,10 +221,60 @@ export class SavingsCoreService { `, }); + const vaultDepositsFetched = await PONDER_CLIENT.query({ + fetchPolicy: 'no-cache', + query: gql` + query GetSavingsVaultDeposit { + savingsVaultDeposits( + orderBy: "blockheight" + orderDirection: "desc" + ${ownerWhere} + limit: ${limit} + ) { + items { + id + vault + owner + assets + blockheight + timestamp + txHash + } + } + } + `, + }); + + const vaultWithdrawsFetched = await PONDER_CLIENT.query({ + fetchPolicy: 'no-cache', + query: gql` + query GetSavingsVaultWithdraw { + savingsVaultWithdraws( + orderBy: "blockheight" + orderDirection: "desc" + ${ownerWhere} + limit: ${limit} + ) { + items { + id + vault + owner + assets + blockheight + timestamp + txHash + } + } + } + `, + }); + return { save: savedFetched?.data?.savingsSaveds?.items ?? [], interest: interestFetched?.data?.savingsInterests?.items ?? [], withdraw: withdrawnFetched?.data?.savingsWithdrawns?.items ?? [], + vaultSave: vaultDepositsFetched?.data?.savingsVaultDeposits?.items ?? [], + vaultWithdraw: vaultWithdrawsFetched?.data?.savingsVaultWithdraws?.items ?? [], }; } } diff --git a/savings/savings.core.types.ts b/savings/savings.core.types.ts index 133bff0..e838f10 100644 --- a/savings/savings.core.types.ts +++ b/savings/savings.core.types.ts @@ -40,6 +40,26 @@ export type SavingsWithdrawQuery = { total: string; balance: string; }; + +export type SavingsVaultDepositQuery = { + id: string; + vault: Address; + owner: Address; + assets: string; + blockheight: number; + timestamp: number; + txHash: string; +}; + +export type SavingsVaultWithdrawQuery = { + id: string; + vault: Address; + owner: Address; + assets: string; + blockheight: number; + timestamp: number; + txHash: string; +}; // -------------------------------------------------------------------------- // Service @@ -51,6 +71,8 @@ export type ApiSavingsInfo = { totalBalance: number; totalInterest: number; rate: number; + rateV2: number; + rateV3: number; ratioOfSupply: number; }; @@ -58,6 +80,8 @@ export type ApiSavingsUserTable = { save: SavingsSavedQuery[]; interest: SavingsInterestQuery[]; withdraw: SavingsWithdrawQuery[]; + vaultSave: SavingsVaultDepositQuery[]; + vaultWithdraw: SavingsVaultWithdrawQuery[]; }; export type ApiSavingsUserLeaderboard = { diff --git a/savings/savings.leadrate.service.ts b/savings/savings.leadrate.service.ts index 949060c..0e61162 100644 --- a/savings/savings.leadrate.service.ts +++ b/savings/savings.leadrate.service.ts @@ -5,6 +5,7 @@ import { ApiLeadrateInfo, ApiLeadrateProposed, ApiLeadrateRate, + ApiLeadrateVersionInfo, LeadrateProposed, LeadrateRateObjectArray, LeadrateRateProposedObjectArray, @@ -19,46 +20,68 @@ export class SavingsLeadrateService { private fetchedProposals: LeadrateRateProposedObjectArray = {}; getRates(): ApiLeadrateRate { - const l = Object.values(this.fetchedRates); - const h = l.sort((a, b) => b.blockheight - a.blockheight); - const n = h.length === 0; + const l = Object.values(this.fetchedRates).sort((a, b) => b.blockheight - a.blockheight); + const n = l.length === 0; return { - created: n ? 0 : h[0].created, - blockheight: n ? 0 : h[0].blockheight, - rate: n ? 0 : h[0].approvedRate, + created: n ? 0 : l[0].created, + blockheight: n ? 0 : l[0].blockheight, + rate: n ? 0 : l[0].approvedRate, num: l.length, list: l, }; } getProposals(): ApiLeadrateProposed { - const l = Object.values(this.fetchedProposals); - const h = l.sort((a, b) => b.blockheight - a.blockheight); - const n = h.length === 0; + const l = Object.values(this.fetchedProposals).sort((a, b) => b.blockheight - a.blockheight); + const n = l.length === 0; return { - created: n ? 0 : h[0]?.created || 0, - blockheight: n ? 0 : h[0]?.blockheight || 0, - nextRate: n ? 0 : h[0]?.nextRate, - nextchange: n ? 0 : h[0]?.nextChange, + created: n ? 0 : l[0]?.created || 0, + blockheight: n ? 0 : l[0]?.blockheight || 0, + nextRate: n ? 0 : l[0]?.nextRate, + nextchange: n ? 0 : l[0]?.nextChange, num: l.length, list: l, }; } getInfo(): ApiLeadrateInfo { - const r = this.getRates(); - const p = this.getProposals(); - const isProposal = r.rate != p.nextRate; - const isPending = p.nextchange * 1000 >= Date.now(); return { - rate: r.rate, - nextRate: isProposal ? p.nextRate : undefined, - nextchange: isProposal ? p.nextchange : undefined, + v2: this.getVersionInfo('v2'), + v3: this.getVersionInfo('v3'), + }; + } + + private getVersionInfo(source: string): ApiLeadrateVersionInfo { + const rates = this.getRatesBySource(source); + const proposals = this.getProposalsBySource(source); + + const rate = rates.length > 0 ? rates[0].approvedRate : 0; + const latestProposal = proposals.length > 0 ? proposals[0] : null; + + const isProposal = latestProposal != null && rate != latestProposal.nextRate; + const isPending = latestProposal != null && latestProposal.nextChange * 1000 >= Date.now(); + + return { + rate, + nextRate: isProposal ? latestProposal!.nextRate : undefined, + nextchange: isProposal ? latestProposal!.nextChange : undefined, isProposal, isPending, }; } + private getRatesBySource(source: string): LeadrateRateQuery[] { + return Object.values(this.fetchedRates) + .filter((r) => r.source === source) + .sort((a, b) => b.blockheight - a.blockheight); + } + + private getProposalsBySource(source: string): LeadrateProposed[] { + return Object.values(this.fetchedProposals) + .filter((p) => p.source === source) + .sort((a, b) => b.blockheight - a.blockheight); + } + async updateLeadrateRates() { this.logger.debug('Updating leadrate rates'); const { data } = await PONDER_CLIENT.query({ @@ -72,6 +95,7 @@ export class SavingsLeadrateService { blockheight txHash approvedRate + source } } } @@ -91,6 +115,7 @@ export class SavingsLeadrateService { blockheight: parseInt(r.blockheight as any), txHash: r.txHash, approvedRate: r.approvedRate, + source: r.source, } as LeadrateRateQuery; } @@ -117,6 +142,7 @@ export class SavingsLeadrateService { proposer nextRate nextChange + source } } } @@ -138,6 +164,7 @@ export class SavingsLeadrateService { proposer: r.proposer as Address, nextRate: r.nextRate, nextChange: r.nextChange, + source: r.source, } as LeadrateProposed; } diff --git a/savings/savings.leadrate.types.ts b/savings/savings.leadrate.types.ts index 261af0b..311d57f 100644 --- a/savings/savings.leadrate.types.ts +++ b/savings/savings.leadrate.types.ts @@ -7,6 +7,7 @@ export type LeadrateRateQuery = { blockheight: number; txHash: string; approvedRate: number; + source: string; }; export type LeadrateProposed = { @@ -17,6 +18,7 @@ export type LeadrateProposed = { proposer: Address; nextRate: number; nextChange: number; + source: string; }; // -------------------------------------------------------------------------- @@ -31,14 +33,19 @@ export type LeadrateRateProposedObjectArray = { // -------------------------------------------------------------------------- // Api -export type ApiLeadrateInfo = { +export type ApiLeadrateVersionInfo = { rate: number; - nextRate: number; - nextchange: number; + nextRate?: number; + nextchange?: number; isProposal: boolean; isPending: boolean; }; +export type ApiLeadrateInfo = { + v2: ApiLeadrateVersionInfo; + v3: ApiLeadrateVersionInfo; +}; + export type ApiLeadrateRate = { created: number; blockheight: number; diff --git a/socialmedia/socialmedia.helper.ts b/socialmedia/socialmedia.helper.ts index 1b1c7b0..9455118 100644 --- a/socialmedia/socialmedia.helper.ts +++ b/socialmedia/socialmedia.helper.ts @@ -13,17 +13,19 @@ export function delay(ms: number): Promise { } export function createRefCodeLabelLink(frontendCode: string): string { + if (!frontendCode) return ''; + if (frontendCode.toLowerCase() === DEURO_WALLET_FRONTEND_CODE.toLowerCase()) { return `dEURO Wallet`; } const refCode = createRefCode(frontendCode); if (!refCode) return ''; - + // Special case for Cake Wallet if (refCode === 'Cake Wallet') { return `[${refCode}](https://cakewallet.com/)`; } - + return `[${refCode}](https://app.deuro.com?ref=${refCode})`; } diff --git a/socialmedia/socialmedia.service.ts b/socialmedia/socialmedia.service.ts index 2dd8a84..a93afe4 100644 --- a/socialmedia/socialmedia.service.ts +++ b/socialmedia/socialmedia.service.ts @@ -74,7 +74,7 @@ export class SocialMediaService { await value.doSendUpdates(); } } catch (e) { - this.logger.error('Error while sending updates:', e); + this.logger.error(`Error while sending updates: ${e?.message ?? e}`, e?.stack); } } @@ -94,7 +94,7 @@ export class SocialMediaService { } } } catch (e) { - this.logger.error('Error while sending saving updates:', e); + this.logger.error(`Error while sending saving updates: ${e?.message ?? e}`, e?.stack); } } @@ -113,7 +113,7 @@ export class SocialMediaService { } } } catch (e) { - this.logger.error('Error while sending frontend code updates:', e); + this.logger.error(`Error while sending frontend code updates: ${e?.message ?? e}`, e?.stack); } } @@ -137,7 +137,7 @@ export class SocialMediaService { } } } catch (e) { - this.logger.error('Error while sending trade updates:', e); + this.logger.error(`Error while sending trade updates: ${e?.message ?? e}`, e?.stack); } } @@ -160,7 +160,7 @@ export class SocialMediaService { } } } catch (e) { - this.logger.error('Error while sending bridge updates:', e); + this.logger.error(`Error while sending bridge updates: ${e?.message ?? e}`, e?.stack); } } @@ -181,7 +181,7 @@ export class SocialMediaService { } } } catch (e) { - this.logger.error('Error while sending mint updates:', e); + this.logger.error(`Error while sending mint updates: ${e?.message ?? e}`, e?.stack); } } } diff --git a/yarn.lock b/yarn.lock index 816633e..49453d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -421,12 +421,11 @@ enabled "2.0.x" kuler "^2.0.0" -"@deuro/eurocoin@^1.0.13": - version "1.0.13" - resolved "https://registry.yarnpkg.com/@deuro/eurocoin/-/eurocoin-1.0.13.tgz#05e9c6ff13d4bfcaa8ad14aa377b2815709cfc80" - integrity sha512-NrAHpgLrlx662BgTcgkxBXNvNPl+bUeQPaVGwOsDrJXyVsJJVfpBwPcqZ+CaOVUEZfwDePYTWZe2e82bVqRkTQ== +"@deuro/eurocoin@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@deuro/eurocoin/-/eurocoin-2.1.0.tgz#20a4fcaf64862ffe11e21c4cc486025d7395f4a1" + integrity sha512-Yx2XGEmNT7kshKmT9cgtryo7LkB76Bk3FmcAuxvWHUmAnQZAEGwBU0Ay1/SKzZWCMzGivsQRn5jx7KPz5t62AA== dependencies: - "@flashbots/ethers-provider-bundle" "^1.0.0" hardhat-abi-exporter "^2.10.0" hardhat-contract-sizer "^2.5.1" prettier "^3.3.3" @@ -645,11 +644,6 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" -"@flashbots/ethers-provider-bundle@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@flashbots/ethers-provider-bundle/-/ethers-provider-bundle-1.0.0.tgz#555c6208c668d1818820143b067d38f2a63a8556" - integrity sha512-KXOSA2RFFq91KN7H6nskbBaV+yd3QKI9jj8r1CEsD00sXBV3uSoQ3wG6u7qkjxp2EfvWy31pynWfZZVoWyNQ3Q== - "@graphql-typed-document-node/core@^3.1.1": version "3.2.0" resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"