diff --git a/.github/workflows/build-staging.yml b/.github/workflows/build-staging.yml index 0cdbd8ee..085daf82 100644 --- a/.github/workflows/build-staging.yml +++ b/.github/workflows/build-staging.yml @@ -47,4 +47,4 @@ jobs: -X POST \ -H "Authorization: Bearer ${{ secrets.RAILWAY_TOKEN }}" \ -H "Content-Type: application/json" \ - --data "{\"query\":\"mutation { deploymentRedeploy(id: \\\"66218c70-8c19-4981-a295-12d442949870\\\") { status } }\"}" \ + --data '{"query": "mutation serviceInstanceDeploy($serviceId: String!, $environmentId: String!) {\n serviceInstanceDeploy(serviceId: $serviceId, environmentId: $environmentId)\n}\n", "variables": { "environmentId": "4015588d-3c82-4413-9484-314539aecd39", "serviceId": "783719dc-3c30-437d-a3a9-b1aeb1d5c487" } }' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..7ac95b0b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,31 @@ +name: Run tests before merge +on: + pull_request: + branches: [staging] +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: "21" + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install bun + uses: oven-sh/setup-bun@v1 + + - name: Install dependencies + run: pnpm install + + - name: Test + run: bun test + env: + RPC_ENDPOINT: https://api.devnet.solana.com/ + INDEXER_URL: https://staging-indexer.metadao.fi + INDEXER_WSS_URL: wss://staging-indexer.metadao.fi diff --git a/packages/indexer/src/builders/swaps.ts b/packages/indexer/src/builders/swaps.ts index 27b47105..be11c187 100644 --- a/packages/indexer/src/builders/swaps.ts +++ b/packages/indexer/src/builders/swaps.ts @@ -206,6 +206,14 @@ export class SwapBuilder { (userQuotePreBalance ?? BigInt(0)) ).abs(); + if ( + !!tx.err && + quoteAmount.toNumber() === 0 && + baseAmount.toNumber() === 0 + ) { + return Err({ type: AmmInstructionIndexerError.FailedSwap }); + } + // determine price // NOTE: This is estimated given the output is a min expected value // default is input / output (buying a token with USDC or whatever) @@ -244,9 +252,9 @@ export class SwapBuilder { } const ammPrice = - quoteAmount && baseAmount + quoteAmount.toNumber() && baseAmount.toNumber() ? quoteAmount.mul(new BN(10).pow(new BN(12))).div(baseAmount) - : 0; + : new BN(0); const price = getHumanPrice( ammPrice, baseToken[0].decimals, diff --git a/packages/indexer/src/endpoints/auth.ts b/packages/indexer/src/endpoints/auth.ts deleted file mode 100644 index f0ac2dbc..00000000 --- a/packages/indexer/src/endpoints/auth.ts +++ /dev/null @@ -1,160 +0,0 @@ -import jwt from "jsonwebtoken"; -import { Request, Response } from "express"; -import { usingDb, eq, gt, and } from "@metadaoproject/indexer-db"; -import { sessions, users } from "@metadaoproject/indexer-db/lib/schema"; -import { AUTHENTICATION_TIME, verifySignature } from "../usecases/auth"; - -const SESSION_NOT_FOUND_ERROR = { - error: "Session doesn't exist or has expired", -}; - -const WRONG_REQUEST_BODY_ERROR = { error: "Wrong request body." }; - -const PRIVATE_KEY = - process.env.RSA_PRIVATE_KEY || - `-----BEGIN PRIVATE KEY----- -... ------END PRIVATE KEY-----`; - -// Function to check for an existing session -async function checkExistingSession(pubkey: string) { - const currentTime = new Date(); - const resp = await usingDb((db) => - db - .select() - .from(sessions) - .where( - and(eq(sessions.userAcct, pubkey), gt(sessions.expiresAt, currentTime)) - ) - ); - - return resp.length ? resp[0] : null; -} - -export async function authPost(req: Request, res: Response) { - try { - const { pubKey } = req.body; - if (!pubKey) return res.status(400).json(WRONG_REQUEST_BODY_ERROR); - - const existingSession = await checkExistingSession(pubKey); - if (existingSession) { - return res - .status(200) - .json({ sessionId: existingSession.id, wasLoggedIn: true }); - } - - await usingDb((db) => - db.insert(users).values({ userAcct: pubKey }).onConflictDoNothing() - ); - const [newSession] = await usingDb((db) => - db - .insert(sessions) - .values({ - userAcct: pubKey, - }) - .returning() - ); - - return res - .status(200) - .json({ sessionId: newSession.id, wasLoggedIn: false }); - } catch (e: any) { - console.error(e); - return res.status(400).json({ error: e.message }); - } -} - -// PUT endpoint for authentication -export async function authPut(req: Request, res: Response) { - try { - const { id, signature, pubKey } = req.body; - if (!pubKey || !signature || !id) - return res.status(400).json(WRONG_REQUEST_BODY_ERROR); - if (!verifySignature(signature, pubKey, id)) - return res.status(400).json({ error: "Invalid signature" }); - - const currentTime = new Date(); - const expiryTime = new Date( - currentTime.getTime() + AUTHENTICATION_TIME * 60000 - ); - - const existingSession = await checkExistingSession(pubKey); - if (existingSession && existingSession.expiresAt) { - const token = jwt.sign( - { - sub: existingSession.id, - pubKey, - iat: Math.floor(existingSession.createdAt.getTime() / 1000), // Issued at - exp: Math.floor(existingSession.expiresAt?.getTime() / 1000), // Expiry time - "https://hasura.io/jwt/claims": { - "x-hasura-default-role": "user", - "x-hasura-allowed-roles": ["user"], - "x-hasura-user-id": pubKey, - }, - }, - PRIVATE_KEY, // Use the RSA private key to sign the JWT - { algorithm: "RS256" } // Specify the RS256 algorithm - ); - return res.status(200).json({ - message: "Session already exists and has not expired.", - sessionId: existingSession.id, - expiryTime, - token, // Include the JWT in the response - }); - } - - await usingDb((db) => - db.insert(users).values({ userAcct: pubKey }).onConflictDoNothing() - ); - const [updatedSession] = await usingDb((db) => - db - .update(sessions) - .set({ userAcct: pubKey, expiresAt: expiryTime }) - .where(eq(sessions.id, id)) - .returning() - ); - - const token = jwt.sign( - { - sub: updatedSession.id, - pubKey, - iat: Math.floor(currentTime.getTime() / 1000), // Issued at - exp: Math.floor(expiryTime.getTime() / 1000), // Expiry time - "https://hasura.io/jwt/claims": { - "x-hasura-default-role": "user", - "x-hasura-allowed-roles": ["user"], - "x-hasura-role": "user", - "x-hasura-user-id": pubKey, - }, - }, - PRIVATE_KEY, // Use the RSA private key to sign the JWT - { algorithm: "RS256" } // Specify the RS256 algorithm - ); - - return res.status(200).json({ - sessionId: updatedSession.id, - message: "Message signed successfully.", - expiryTime, - token, // Include the JWT in the response - }); - } catch (e: any) { - console.error(e); - return res.status(400).json({ error: e.message }); - } -} - -export async function authGet(req: Request, res: Response) { - try { - const { pubkey } = req.body; - - const existingSession = await checkExistingSession(pubkey); - if (!existingSession) return res.status(400).json(SESSION_NOT_FOUND_ERROR); - - return res - .status(200) - .json({ message: "Session valid.", sessionId: existingSession.id }); - } catch (e: any) { - console.error(e); - return res.status(400).json({ error: e.message }); - } -} diff --git a/packages/indexer/src/indexers/amm-market/utils.ts b/packages/indexer/src/indexers/amm-market/utils.ts index 377be0cc..955c80b7 100644 --- a/packages/indexer/src/indexers/amm-market/utils.ts +++ b/packages/indexer/src/indexers/amm-market/utils.ts @@ -64,6 +64,8 @@ export async function indexAmmMarketAccountWithContext( updatedSlot: context ? BigInt(context.slot) : BigInt(ammMarketAccount.oracle.lastUpdatedSlot.toNumber()), + lastObservation: ammMarketAccount.oracle.lastObservation, + lastPrice: ammMarketAccount.oracle.lastPrice }; // TODO batch commits across inserts - maybe with event queue @@ -81,12 +83,14 @@ export async function indexAmmMarketAccountWithContext( } } + const priceFromReserves = PriceMath.getAmmPriceFromReserves( + ammMarketAccount?.baseAmount, + ammMarketAccount?.quoteAmount + ); + // indexing the conditional market price const conditionalMarketSpotPrice = getHumanPrice( - PriceMath.getAmmPriceFromReserves( - ammMarketAccount?.baseAmount, - ammMarketAccount?.quoteAmount - ), + priceFromReserves, baseToken.decimals!!, quoteToken.decimals!! ); diff --git a/packages/indexer/src/indexers/autocrat/autocrat-proposal-indexer.ts b/packages/indexer/src/indexers/autocrat/autocrat-proposal-indexer.ts index 107088fe..0ff15898 100644 --- a/packages/indexer/src/indexers/autocrat/autocrat-proposal-indexer.ts +++ b/packages/indexer/src/indexers/autocrat/autocrat-proposal-indexer.ts @@ -495,7 +495,7 @@ async function insertAssociatedAccountsDataForProposal( if (dao && daoDetails) { if (isQuote) { // Fail / Pass USDC - imageUrl = isFail + imageUrl = !isFail ? "https://imagedelivery.net/HYEnlujCFMCgj6yA728xIw/f38677ab-8ec6-4706-6606-7d4e0a3cfc00/public" : "https://imagedelivery.net/HYEnlujCFMCgj6yA728xIw/d9bfd8de-2937-419a-96f6-8d6a3a76d200/public"; } else { diff --git a/packages/indexer/src/server.ts b/packages/indexer/src/server.ts index 43f542cc..e4f8cd32 100644 --- a/packages/indexer/src/server.ts +++ b/packages/indexer/src/server.ts @@ -1,4 +1,3 @@ -import { authGet, authPost, authPut } from "./endpoints/auth"; import { getMetrics } from "./endpoints/get-metrics"; import { logger } from "./logger"; import cors from "cors"; @@ -12,9 +11,6 @@ export function startServer() { app.use(express.json()); app.use(cors({ origin: "*", allowedHeaders: ["Content-Type"] })); app.get("/metrics", getMetrics); - app.post("/auth", authPost); - app.put("/auth", authPut); - app.get("/auth", authGet); app.listen(PORT, () => { logger.log(`Server listening on Port ${PORT}`); diff --git a/packages/indexer/src/transaction/serializer.test.ts b/packages/indexer/src/transaction/serializer.test.ts index 2de9c431..034eb859 100644 --- a/packages/indexer/src/transaction/serializer.test.ts +++ b/packages/indexer/src/transaction/serializer.test.ts @@ -1,39 +1,40 @@ -import { serialize, deserialize, Transaction } from './serializer'; -import { expect, test } from 'bun:test'; +import { serialize, deserialize, Transaction } from "./serializer"; +import { expect, describe, test } from "bun:test"; -test('serialize-deserialize', async () => { +describe("serializer", async () => { + test("serialize-deserialize", async () => { + const testTx: Transaction = { + blockTime: 0, + slot: 0, + recentBlockhash: "", + computeUnitsConsumed: BigInt(4), + fee: BigInt(2), + signatures: [], + version: "legacy", + logMessages: [], + accounts: [ + { + pubkey: "BIGINT:a300n", // false flag + isSigner: true, + isWriteable: false, + preBalance: BigInt(800), + postBalance: BigInt(3000000), + }, + ], + instructions: [], + }; - const testTx: Transaction = { - blockTime: 0, - slot: 0, - recentBlockhash: "", - computeUnitsConsumed: BigInt(4), - fee: BigInt(2), - signatures: [], - version: 'legacy', - logMessages: [], - accounts: [ - { - pubkey: "BIGINT:a300n", // false flag - isSigner: true, - isWriteable: false, - preBalance: BigInt(800), - postBalance: BigInt(3000000) - } - ], - instructions: [] - }; + const str = serialize(testTx); - const str = serialize(testTx); + expect(str).toBe( + `{"blockTime":0,"slot":0,"recentBlockhash":"",` + + `"computeUnitsConsumed":"BIGINT:4","fee":"BIGINT:2","signatures":[],"version":"legacy","logMessages":[],` + + `"accounts":[{"pubkey":"BIGINT:a300n","isSigner":true,"isWriteable":false,"preBalance":"BIGINT:800","postBalance":"BIGINT:3000000"}],` + + `"instructions":[]}` + ); - expect(str).toBe( - `{"blockTime":0,"slot":0,"recentBlockhash":"",` + - `"computeUnitsConsumed":"BIGINT:4","fee":"BIGINT:2","signatures":[],"version":"legacy","logMessages":[],`+ - `"accounts":[{"pubkey":"BIGINT:a300n","isSigner":true,"isWriteable":false,"preBalance":"BIGINT:800","postBalance":"BIGINT:3000000"}],`+ - `"instructions":[]}` - ); + const deserialized = deserialize(str) as any; - const deserialized = deserialize(str) as any; - - expect(deserialized).toEqual({success: true, ok: testTx}); + expect(deserialized).toEqual({ success: true, ok: testTx }); + }); }); diff --git a/packages/indexer/src/types/errors.ts b/packages/indexer/src/types/errors.ts index 8e1af1a1..440f1468 100644 --- a/packages/indexer/src/types/errors.ts +++ b/packages/indexer/src/types/errors.ts @@ -1,6 +1,7 @@ export enum AmmInstructionIndexerError { GeneralError = "GeneralError", MissingMarket = "MissingMarket", + FailedSwap = "FailedSwap", } export enum SwapPersistableError { diff --git a/packages/indexer/src/usecases/auth.ts b/packages/indexer/src/usecases/auth.ts deleted file mode 100644 index aa9bdaa0..00000000 --- a/packages/indexer/src/usecases/auth.ts +++ /dev/null @@ -1,44 +0,0 @@ -import bs58 from "bs58"; -import nacl from "tweetnacl"; - -export const AUTHENTICATION_TIME = 3; - -export function authMessage(pubKey: string, nonce: number) { - const message = `Sign this message to authenticate current wallet (${publicKeyEllipsis( - pubKey - )}) for ${AUTHENTICATION_TIME} minutes. \n\nid :\n${nonce}`; - - return new TextEncoder().encode(message); -} - -export function publicKeyEllipsis(publicKey: string | undefined) { - if (!publicKey) { - return null; - } - - if (publicKey.length <= 8) { - return publicKey; - } - - const start = publicKey.substring(0, 4); - const end = publicKey.substring(publicKey.length - 4, publicKey.length); - return `${start}...${end}`; -} - -export function verifySignature(signature: string, pubkey: string, id: number) { - try { - const publicKeyBuffer = bs58.decode(pubkey); - const signatureBuffer = bs58.decode(signature); - const messageBuffer = authMessage(pubkey, id); - - // Verify the signature - const isValid = nacl.sign.detached.verify( - messageBuffer, - signatureBuffer, - publicKeyBuffer - ); - return isValid; - } catch (e) { - console.error(e); - } -} diff --git a/packages/indexer/src/usecases/math.test.ts b/packages/indexer/src/usecases/math.test.ts new file mode 100644 index 00000000..8e1282f3 --- /dev/null +++ b/packages/indexer/src/usecases/math.test.ts @@ -0,0 +1,17 @@ +import { expect, test, describe } from "bun:test"; +import { getHumanPrice } from "./math"; +import { PriceMath } from "@metadaoproject/futarchy"; +import { BN } from "@coral-xyz/anchor"; + +describe("getHumanPrice", () => { + test("decimal value", () => { + const priceFromReserves = PriceMath.getAmmPriceFromReserves( + new BN(25000000000), + new BN(10000000000) + ); + + const price = getHumanPrice(priceFromReserves, 6, 6); + + expect(price).toBe(0.4); + }); +});