diff --git a/.github/workflows/data-backend.yml b/.github/workflows/data-backend.yml index ce1555df..db45d377 100644 --- a/.github/workflows/data-backend.yml +++ b/.github/workflows/data-backend.yml @@ -54,11 +54,14 @@ jobs: pnpm install fi - # - name: Prettier Format Check - # run: pnpm format:check + - name: Build indexer-v2-db + run: pnpm -w run build:indexer-v2-db # it's simpler to do this here for moment + + - name: Build + run: pnpm build - name: TypeScript Check run: pnpm ts:check - - name: Build - run: pnpm build + # - name: Prettier Format Check + # run: pnpm format:check diff --git a/.github/workflows/indexer-v2.yml b/.github/workflows/indexer-v2.yml new file mode 100644 index 00000000..eca378a9 --- /dev/null +++ b/.github/workflows/indexer-v2.yml @@ -0,0 +1,64 @@ +name: Indexer v2 App + +on: + push: + paths: + - ".github/workflows/indexer-v2.yml" + - "apps/indexer-v2/**" + - "package.json" + pull_request: + branches: + - main + paths: + - "apps/indexer-v2/**" + +jobs: + check-app: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ./apps/indexer-v2 + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install Dependencies + run: | + if [ -f "pnpm-lock.yaml" ]; then + pnpm install --frozen-lockfile + else + pnpm install + fi + + - name: Build + run: pnpm build:all + + # - name: TypeScript Check + # run: pnpm ts:check + + # - name: Prettier Format Check + # run: pnpm format:check diff --git a/apps/data-backend/package.json b/apps/data-backend/package.json index 742d9543..55298a39 100644 --- a/apps/data-backend/package.json +++ b/apps/data-backend/package.json @@ -7,7 +7,7 @@ "build": "tsc", "build:index": "tsc", "build:all_repo": "pnpm run build:all", - "build:all": "pnpm -w run build:indexer-prisma && pnpm -w run build:prisma-db && pnpm build", + "build:all": "pnpm -w run build:indexer-prisma && pnpm -w run build:prisma-db && pnpm -w run build:indexer-v2-db && pnpm build", "build:prisma": "", "start": "ts-node src/index.ts", "start:dev": "ts-node-dev src/index.ts", @@ -41,6 +41,7 @@ "cors": "^2.8.5", "data-backend": "link:", "dotenv": "^16.4.5", + "drizzle-orm": "^0.37.0", "fastify": "^5.1.0", "fastify-plugin": "^5.0.1", "fastify-socket.io": "^5.1.0", @@ -49,6 +50,7 @@ "graphql": "^16.9.0", "helmet": "^7.2.0", "indexer-prisma": "workspace:*", + "indexer-v2-db": "workspace:*", "jsonwebtoken": "^9.0.2", "nodemailer": "^6.9.16", "pg": "^8.13.1", diff --git a/apps/data-backend/src/routes/indexer/dao.ts b/apps/data-backend/src/routes/indexer/dao.ts new file mode 100644 index 00000000..27a371b0 --- /dev/null +++ b/apps/data-backend/src/routes/indexer/dao.ts @@ -0,0 +1,57 @@ +import type { FastifyInstance, RouteOptions } from 'fastify'; +import { HTTPStatus } from '../../utils/http'; +import { isValidStarknetAddress } from '../../utils/starknet'; +import { eq } from 'drizzle-orm'; +import { db } from 'indexer-v2-db/dist'; +import { daoCreation, daoProposal } from 'indexer-v2-db/dist/schema'; + +interface DaoParams { + dao_address: string; +} + +async function daoServiceRoute(fastify: FastifyInstance, options: RouteOptions) { + // Get tips by sender + fastify.get('/daos', async (request, reply) => { + try { + const daos = await db.select().from(daoCreation).execute(); + + reply.status(HTTPStatus.OK).send({ + data: daos, + }); + } catch (error) { + console.error('Error fetching daos:', error); + reply.status(HTTPStatus.InternalServerError).send({ message: 'Internal server error.' }); + } + }); + + // Get tips by recipient + fastify.get<{ + Params: DaoParams; + }>('/daos/:dao_address/proposals/', async (request, reply) => { + try { + const { dao_address } = request.params; + if (!isValidStarknetAddress(dao_address)) { + reply.status(HTTPStatus.BadRequest).send({ + code: HTTPStatus.BadRequest, + message: 'Invalid dao address', + }); + return; + } + + const daoProposals = await db + .select() + .from(daoProposal) + .where(eq(daoProposal.contractAddress, dao_address)) + .execute(); + + reply.status(HTTPStatus.OK).send({ + data: daoProposals, + }); + } catch (error) { + console.error('Error fetching dao proposals', error); + reply.status(HTTPStatus.InternalServerError).send({ message: 'Internal server error.' }); + } + }); +} + +export default daoServiceRoute; diff --git a/apps/indexer-v2/.env.example b/apps/indexer-v2/.env.example index 40e5c3f2..2f8eed1b 100644 --- a/apps/indexer-v2/.env.example +++ b/apps/indexer-v2/.env.example @@ -1,2 +1,2 @@ -POSTGRES_CONNECTION_STRING=postgres://postgres:postgres@localhost:5432/postgres -DNA_TOKEN_ADDRESS=fdfsfsd \ No newline at end of file +INDEXER_V2_DATABASE_URL=postgres://postgres:postgres@localhost:5435/indexer +DNA_TOKEN_ADDRESS= \ No newline at end of file diff --git a/apps/indexer-v2/README.md b/apps/indexer-v2/README.md index f6d8ba82..d066080d 100644 --- a/apps/indexer-v2/README.md +++ b/apps/indexer-v2/README.md @@ -1,40 +1,32 @@ -# Indexer V2 +# indexer-v2 -## How to run +A new indexer that use the apibara v2 (currently in preview version) +He currently used a new database, see `.env.example` -``` -docker compose up -d -``` +## How generate ABI files -Run indexer - -``` -docker run -it --env-file ./.env dao-indexer run /app/main.ts --tls-accept-invalid-certificates=true --allow-env-from-env POSTGRES_CONNECTION_STRING,AUTH_TOKEN +The more simple to generate ABI files is to use the `abi-wan-kanabi` tool that will convert json contract class file +into typescript file. +```shell +npx abi-wan-kanabi --input ../../onchain/cairo/afk/target/dev/[contract_name].contract_class.json --output indexers/abi/[contract_name].abi.ts ``` -``` -docker run -it --env-file ./.env dao-indexer run /app/main.ts --tls-accept-invalid-certificates=true --allow-env-from-env POSTGRES_CONNECTION_STRING -``` +## How to run -## Install +### Dev -Example: -https://github.com/apibara/typescript-sdk/tree/main/examples/cli +In dev, we can run all indexers or chose one indexer to run. -# Todo +```shell +pnpm dev +pnpm dev --indexer index_name +``` -- Fix script in Typescript: indexer script operation failed - ├╴at /build/source/script/src/script.rs:339:22 - ├╴failed to run indexer event loop - ╰╴error: TypeError: No such file or di +### Prod +In production, we can only run one indexer at time. -- Saved DAO AA Created in db -- Saved DAO Proposal Created in db -- Saved DAO Proposal Voted in db -- Saved DAO Proposal Executed in db -- Saved DAO Proposal Whitelisted in db -- Saved DAO Proposal Ended in db -- Saved DAO Proposal Failed in db -- Saved DAO Proposal Succeeded in db \ No newline at end of file +```shell +pnpm start --indexer index_name +``` \ No newline at end of file diff --git a/apps/indexer-v2/apibara.config.ts b/apps/indexer-v2/apibara.config.ts index 6555e2d4..0746b8ab 100644 --- a/apps/indexer-v2/apibara.config.ts +++ b/apps/indexer-v2/apibara.config.ts @@ -3,6 +3,10 @@ import { defineConfig } from 'apibara/config'; export default defineConfig({ runtimeConfig: { + streamUrl: 'https://starknet-sepolia.preview.apibara.org', + startingCursor: { + orderKey: 500_000, + }, pgLiteDBPath: 'memory://persistence', }, presets: { diff --git a/apps/indexer-v2/indexers/abi/daoAA.abi.ts b/apps/indexer-v2/indexers/abi/daoAA.abi.ts new file mode 100644 index 00000000..cbadba6b --- /dev/null +++ b/apps/indexer-v2/indexers/abi/daoAA.abi.ts @@ -0,0 +1,1087 @@ +export const ABI = [ + { + type: 'impl', + name: 'DaoAA', + interface_name: 'afk::dao::dao_aa::IDaoAA', + }, + { + type: 'struct', + name: 'core::integer::u256', + members: [ + { + name: 'low', + type: 'core::integer::u128', + }, + { + name: 'high', + type: 'core::integer::u128', + }, + ], + }, + { + type: 'enum', + name: 'core::bool', + variants: [ + { + name: 'False', + type: '()', + }, + { + name: 'True', + type: '()', + }, + ], + }, + { + type: 'enum', + name: 'core::option::Option::', + variants: [ + { + name: 'Some', + type: 'core::bool', + }, + { + name: 'None', + type: '()', + }, + ], + }, + { + type: 'enum', + name: 'core::option::Option::', + variants: [ + { + name: 'Some', + type: 'core::starknet::contract_address::ContractAddress', + }, + { + name: 'None', + type: '()', + }, + ], + }, + { + type: 'enum', + name: 'core::option::Option::', + variants: [ + { + name: 'Some', + type: 'core::integer::u256', + }, + { + name: 'None', + type: '()', + }, + ], + }, + { + type: 'enum', + name: 'core::option::Option::', + variants: [ + { + name: 'Some', + type: 'core::integer::u64', + }, + { + name: 'None', + type: '()', + }, + ], + }, + { + type: 'struct', + name: 'afk::interfaces::voting::ConfigParams', + members: [ + { + name: 'is_admin_bypass_available', + type: 'core::option::Option::', + }, + { + name: 'is_only_dao_execution', + type: 'core::option::Option::', + }, + { + name: 'token_contract_address', + type: 'core::option::Option::', + }, + { + name: 'minimal_balance_voting', + type: 'core::option::Option::', + }, + { + name: 'max_balance_per_vote', + type: 'core::option::Option::', + }, + { + name: 'minimal_balance_create_proposal', + type: 'core::option::Option::', + }, + { + name: 'minimum_threshold_percentage', + type: 'core::option::Option::', + }, + ], + }, + { + type: 'struct', + name: 'afk::interfaces::voting::ConfigResponse', + members: [ + { + name: 'is_admin_bypass_available', + type: 'core::bool', + }, + { + name: 'is_only_dao_execution', + type: 'core::bool', + }, + { + name: 'token_contract_address', + type: 'core::starknet::contract_address::ContractAddress', + }, + { + name: 'minimal_balance_voting', + type: 'core::integer::u256', + }, + { + name: 'max_balance_per_vote', + type: 'core::integer::u256', + }, + { + name: 'minimal_balance_create_proposal', + type: 'core::integer::u256', + }, + { + name: 'minimum_threshold_percentage', + type: 'core::integer::u64', + }, + ], + }, + { + type: 'interface', + name: 'afk::dao::dao_aa::IDaoAA', + items: [ + { + type: 'function', + name: 'get_public_key', + inputs: [], + outputs: [ + { + type: 'core::integer::u256', + }, + ], + state_mutability: 'view', + }, + { + type: 'function', + name: 'get_token_contract_address', + inputs: [], + outputs: [ + { + type: 'core::starknet::contract_address::ContractAddress', + }, + ], + state_mutability: 'view', + }, + { + type: 'function', + name: 'update_config', + inputs: [ + { + name: 'config_params', + type: 'afk::interfaces::voting::ConfigParams', + }, + ], + outputs: [], + state_mutability: 'external', + }, + { + type: 'function', + name: 'get_config', + inputs: [], + outputs: [ + { + type: 'afk::interfaces::voting::ConfigResponse', + }, + ], + state_mutability: 'view', + }, + { + type: 'function', + name: 'set_public_key', + inputs: [ + { + name: 'public_key', + type: 'core::integer::u256', + }, + ], + outputs: [], + state_mutability: 'external', + }, + ], + }, + { + type: 'impl', + name: 'DaoAAProposalImpl', + interface_name: 'afk::interfaces::voting::IVoteProposal', + }, + { + type: 'struct', + name: 'core::byte_array::ByteArray', + members: [ + { + name: 'data', + type: 'core::array::Array::', + }, + { + name: 'pending_word', + type: 'core::felt252', + }, + { + name: 'pending_word_len', + type: 'core::integer::u32', + }, + ], + }, + { + type: 'enum', + name: 'afk::interfaces::voting::ProposalType', + variants: [ + { + name: 'SavedAutomatedTransaction', + type: '()', + }, + { + name: 'Execution', + type: '()', + }, + { + name: 'Proposal', + type: '()', + }, + ], + }, + { + type: 'enum', + name: 'afk::interfaces::voting::ProposalAutomatedTransaction', + variants: [ + { + name: 'Transfer', + type: '()', + }, + { + name: 'Mint', + type: '()', + }, + { + name: 'Burn', + type: '()', + }, + { + name: 'Buy', + type: '()', + }, + { + name: 'Sell', + type: '()', + }, + { + name: 'Invest', + type: '()', + }, + { + name: 'Withdraw', + type: '()', + }, + ], + }, + { + type: 'struct', + name: 'afk::interfaces::voting::ProposalParams', + members: [ + { + name: 'content', + type: 'core::byte_array::ByteArray', + }, + { + name: 'proposal_type', + type: 'afk::interfaces::voting::ProposalType', + }, + { + name: 'proposal_automated_transaction', + type: 'afk::interfaces::voting::ProposalAutomatedTransaction', + }, + ], + }, + { + type: 'struct', + name: 'core::array::Span::', + members: [ + { + name: 'snapshot', + type: '@core::array::Array::', + }, + ], + }, + { + type: 'struct', + name: 'core::starknet::account::Call', + members: [ + { + name: 'to', + type: 'core::starknet::contract_address::ContractAddress', + }, + { + name: 'selector', + type: 'core::felt252', + }, + { + name: 'calldata', + type: 'core::array::Span::', + }, + ], + }, + { + type: 'enum', + name: 'afk::interfaces::voting::UserVote', + variants: [ + { + name: 'Yes', + type: '()', + }, + { + name: 'No', + type: '()', + }, + { + name: 'Abstention', + type: '()', + }, + ], + }, + { + type: 'enum', + name: 'core::option::Option::', + variants: [ + { + name: 'Some', + type: 'afk::interfaces::voting::UserVote', + }, + { + name: 'None', + type: '()', + }, + ], + }, + { + type: 'enum', + name: 'afk::interfaces::voting::ProposalStatus', + variants: [ + { + name: 'Pending', + type: '()', + }, + { + name: 'Active', + type: '()', + }, + { + name: 'Passed', + type: '()', + }, + { + name: 'Failed', + type: '()', + }, + { + name: 'Executed', + type: '()', + }, + { + name: 'Canceled', + type: '()', + }, + ], + }, + { + type: 'enum', + name: 'afk::interfaces::voting::ProposalResult', + variants: [ + { + name: 'InProgress', + type: '()', + }, + { + name: 'Passed', + type: '()', + }, + { + name: 'Failed', + type: '()', + }, + { + name: 'Executed', + type: '()', + }, + { + name: 'Canceled', + type: '()', + }, + ], + }, + { + type: 'struct', + name: 'afk::interfaces::voting::Proposal', + members: [ + { + name: 'id', + type: 'core::integer::u256', + }, + { + name: 'created_at', + type: 'core::integer::u64', + }, + { + name: 'end_at', + type: 'core::integer::u64', + }, + { + name: 'is_whitelisted', + type: 'core::bool', + }, + { + name: 'proposal_params', + type: 'afk::interfaces::voting::ProposalParams', + }, + { + name: 'proposal_status', + type: 'afk::interfaces::voting::ProposalStatus', + }, + { + name: 'proposal_result', + type: 'afk::interfaces::voting::ProposalResult', + }, + { + name: 'proposal_result_at', + type: 'core::integer::u64', + }, + { + name: 'owner', + type: 'core::starknet::contract_address::ContractAddress', + }, + { + name: 'proposal_result_by', + type: 'core::starknet::contract_address::ContractAddress', + }, + ], + }, + { + type: 'interface', + name: 'afk::interfaces::voting::IVoteProposal', + items: [ + { + type: 'function', + name: 'create_proposal', + inputs: [ + { + name: 'proposal_params', + type: 'afk::interfaces::voting::ProposalParams', + }, + { + name: 'calldata', + type: 'core::array::Array::', + }, + ], + outputs: [ + { + type: 'core::integer::u256', + }, + ], + state_mutability: 'external', + }, + { + type: 'function', + name: 'cast_vote', + inputs: [ + { + name: 'proposal_id', + type: 'core::integer::u256', + }, + { + name: 'opt_vote_type', + type: 'core::option::Option::', + }, + ], + outputs: [], + state_mutability: 'external', + }, + { + type: 'function', + name: 'get_proposal', + inputs: [ + { + name: 'proposal_id', + type: 'core::integer::u256', + }, + ], + outputs: [ + { + type: 'afk::interfaces::voting::Proposal', + }, + ], + state_mutability: 'view', + }, + { + type: 'function', + name: 'get_user_vote', + inputs: [ + { + name: 'proposal_id', + type: 'core::integer::u256', + }, + { + name: 'user', + type: 'core::starknet::contract_address::ContractAddress', + }, + ], + outputs: [ + { + type: 'afk::interfaces::voting::UserVote', + }, + ], + state_mutability: 'view', + }, + { + type: 'function', + name: 'cancel_proposal', + inputs: [ + { + name: 'proposal_id', + type: 'core::integer::u256', + }, + ], + outputs: [], + state_mutability: 'external', + }, + { + type: 'function', + name: 'process_result', + inputs: [ + { + name: 'proposal_id', + type: 'core::integer::u256', + }, + ], + outputs: [], + state_mutability: 'external', + }, + { + type: 'function', + name: 'is_executable', + inputs: [ + { + name: 'calldata', + type: 'core::starknet::account::Call', + }, + ], + outputs: [ + { + type: 'core::bool', + }, + ], + state_mutability: 'external', + }, + ], + }, + { + type: 'impl', + name: 'ISRC6Impl', + interface_name: 'afk::dao::dao_aa::ISRC6', + }, + { + type: 'interface', + name: 'afk::dao::dao_aa::ISRC6', + items: [ + { + type: 'function', + name: '__execute__', + inputs: [ + { + name: 'calls', + type: 'core::array::Array::', + }, + ], + outputs: [ + { + type: 'core::array::Array::>', + }, + ], + state_mutability: 'external', + }, + { + type: 'function', + name: '__validate__', + inputs: [ + { + name: 'calls', + type: 'core::array::Array::', + }, + ], + outputs: [ + { + type: 'core::felt252', + }, + ], + state_mutability: 'view', + }, + { + type: 'function', + name: 'is_valid_signature', + inputs: [ + { + name: 'hash', + type: 'core::felt252', + }, + { + name: 'signature', + type: 'core::array::Array::', + }, + ], + outputs: [ + { + type: 'core::felt252', + }, + ], + state_mutability: 'view', + }, + ], + }, + { + type: 'impl', + name: 'AccessControlImpl', + interface_name: 'openzeppelin_access::accesscontrol::interface::IAccessControl', + }, + { + type: 'interface', + name: 'openzeppelin_access::accesscontrol::interface::IAccessControl', + items: [ + { + type: 'function', + name: 'has_role', + inputs: [ + { + name: 'role', + type: 'core::felt252', + }, + { + name: 'account', + type: 'core::starknet::contract_address::ContractAddress', + }, + ], + outputs: [ + { + type: 'core::bool', + }, + ], + state_mutability: 'view', + }, + { + type: 'function', + name: 'get_role_admin', + inputs: [ + { + name: 'role', + type: 'core::felt252', + }, + ], + outputs: [ + { + type: 'core::felt252', + }, + ], + state_mutability: 'view', + }, + { + type: 'function', + name: 'grant_role', + inputs: [ + { + name: 'role', + type: 'core::felt252', + }, + { + name: 'account', + type: 'core::starknet::contract_address::ContractAddress', + }, + ], + outputs: [], + state_mutability: 'external', + }, + { + type: 'function', + name: 'revoke_role', + inputs: [ + { + name: 'role', + type: 'core::felt252', + }, + { + name: 'account', + type: 'core::starknet::contract_address::ContractAddress', + }, + ], + outputs: [], + state_mutability: 'external', + }, + { + type: 'function', + name: 'renounce_role', + inputs: [ + { + name: 'role', + type: 'core::felt252', + }, + { + name: 'account', + type: 'core::starknet::contract_address::ContractAddress', + }, + ], + outputs: [], + state_mutability: 'external', + }, + ], + }, + { + type: 'impl', + name: 'SRC5Impl', + interface_name: 'openzeppelin_introspection::interface::ISRC5', + }, + { + type: 'interface', + name: 'openzeppelin_introspection::interface::ISRC5', + items: [ + { + type: 'function', + name: 'supports_interface', + inputs: [ + { + name: 'interface_id', + type: 'core::felt252', + }, + ], + outputs: [ + { + type: 'core::bool', + }, + ], + state_mutability: 'view', + }, + ], + }, + { + type: 'constructor', + name: 'constructor', + inputs: [ + { + name: 'owner', + type: 'core::starknet::contract_address::ContractAddress', + }, + { + name: 'token_contract_address', + type: 'core::starknet::contract_address::ContractAddress', + }, + { + name: 'public_key', + type: 'core::integer::u256', + }, + { + name: 'starknet_address', + type: 'core::felt252', + }, + ], + }, + { + type: 'event', + name: 'afk::dao::dao_aa::DaoAA::AccountCreated', + kind: 'struct', + members: [ + { + name: 'public_key', + type: 'core::integer::u256', + kind: 'key', + }, + ], + }, + { + type: 'event', + name: 'afk::interfaces::voting::ProposalCreated', + kind: 'struct', + members: [ + { + name: 'id', + type: 'core::integer::u256', + kind: 'key', + }, + { + name: 'owner', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'data', + }, + { + name: 'created_at', + type: 'core::integer::u64', + kind: 'data', + }, + { + name: 'end_at', + type: 'core::integer::u64', + kind: 'data', + }, + ], + }, + { + type: 'event', + name: 'afk::interfaces::voting::ProposalVoted', + kind: 'struct', + members: [ + { + name: 'id', + type: 'core::integer::u256', + kind: 'key', + }, + { + name: 'voter', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'data', + }, + { + name: 'vote', + type: 'afk::interfaces::voting::UserVote', + kind: 'data', + }, + { + name: 'votes', + type: 'core::integer::u256', + kind: 'data', + }, + { + name: 'total_votes', + type: 'core::integer::u256', + kind: 'data', + }, + { + name: 'voted_at', + type: 'core::integer::u64', + kind: 'data', + }, + ], + }, + { + type: 'event', + name: 'afk::interfaces::voting::ProposalCanceled', + kind: 'struct', + members: [ + { + name: 'id', + type: 'core::integer::u256', + kind: 'key', + }, + { + name: 'owner', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'data', + }, + { + name: 'is_canceled', + type: 'core::bool', + kind: 'data', + }, + ], + }, + { + type: 'event', + name: 'afk::interfaces::voting::ProposalResolved', + kind: 'struct', + members: [ + { + name: 'id', + type: 'core::integer::u256', + kind: 'key', + }, + { + name: 'owner', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'data', + }, + { + name: 'result', + type: 'afk::interfaces::voting::ProposalResult', + kind: 'data', + }, + ], + }, + { + type: 'event', + name: 'openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleGranted', + kind: 'struct', + members: [ + { + name: 'role', + type: 'core::felt252', + kind: 'data', + }, + { + name: 'account', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'data', + }, + { + name: 'sender', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'data', + }, + ], + }, + { + type: 'event', + name: 'openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleRevoked', + kind: 'struct', + members: [ + { + name: 'role', + type: 'core::felt252', + kind: 'data', + }, + { + name: 'account', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'data', + }, + { + name: 'sender', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'data', + }, + ], + }, + { + type: 'event', + name: 'openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleAdminChanged', + kind: 'struct', + members: [ + { + name: 'role', + type: 'core::felt252', + kind: 'data', + }, + { + name: 'previous_admin_role', + type: 'core::felt252', + kind: 'data', + }, + { + name: 'new_admin_role', + type: 'core::felt252', + kind: 'data', + }, + ], + }, + { + type: 'event', + name: 'openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::Event', + kind: 'enum', + variants: [ + { + name: 'RoleGranted', + type: 'openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleGranted', + kind: 'nested', + }, + { + name: 'RoleRevoked', + type: 'openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleRevoked', + kind: 'nested', + }, + { + name: 'RoleAdminChanged', + type: 'openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::RoleAdminChanged', + kind: 'nested', + }, + ], + }, + { + type: 'event', + name: 'openzeppelin_introspection::src5::SRC5Component::Event', + kind: 'enum', + variants: [], + }, + { + type: 'event', + name: 'openzeppelin_upgrades::upgradeable::UpgradeableComponent::Upgraded', + kind: 'struct', + members: [ + { + name: 'class_hash', + type: 'core::starknet::class_hash::ClassHash', + kind: 'data', + }, + ], + }, + { + type: 'event', + name: 'openzeppelin_upgrades::upgradeable::UpgradeableComponent::Event', + kind: 'enum', + variants: [ + { + name: 'Upgraded', + type: 'openzeppelin_upgrades::upgradeable::UpgradeableComponent::Upgraded', + kind: 'nested', + }, + ], + }, + { + type: 'event', + name: 'afk::dao::dao_aa::DaoAA::Event', + kind: 'enum', + variants: [ + { + name: 'AccountCreated', + type: 'afk::dao::dao_aa::DaoAA::AccountCreated', + kind: 'nested', + }, + { + name: 'ProposalCreated', + type: 'afk::interfaces::voting::ProposalCreated', + kind: 'nested', + }, + { + name: 'ProposalVoted', + type: 'afk::interfaces::voting::ProposalVoted', + kind: 'nested', + }, + { + name: 'ProposalCanceled', + type: 'afk::interfaces::voting::ProposalCanceled', + kind: 'nested', + }, + { + name: 'ProposalResolved', + type: 'afk::interfaces::voting::ProposalResolved', + kind: 'nested', + }, + { + name: 'AccessControlEvent', + type: 'openzeppelin_access::accesscontrol::accesscontrol::AccessControlComponent::Event', + kind: 'flat', + }, + { + name: 'SRC5Event', + type: 'openzeppelin_introspection::src5::SRC5Component::Event', + kind: 'flat', + }, + { + name: 'UpgradeableEvent', + type: 'openzeppelin_upgrades::upgradeable::UpgradeableComponent::Event', + kind: 'flat', + }, + ], + }, +] as const; diff --git a/apps/indexer-v2/indexers/abi/daoFactory.abi.ts b/apps/indexer-v2/indexers/abi/daoFactory.abi.ts new file mode 100644 index 00000000..09bbf567 --- /dev/null +++ b/apps/indexer-v2/indexers/abi/daoFactory.abi.ts @@ -0,0 +1,242 @@ +export const ABI = [ + { + type: 'impl', + name: 'DaoFactoryImpl', + interface_name: 'afk::dao::dao_factory::IDaoFactory', + }, + { + type: 'struct', + name: 'core::integer::u256', + members: [ + { + name: 'low', + type: 'core::integer::u128', + }, + { + name: 'high', + type: 'core::integer::u128', + }, + ], + }, + { + type: 'interface', + name: 'afk::dao::dao_factory::IDaoFactory', + items: [ + { + type: 'function', + name: 'create_dao', + inputs: [ + { + name: 'token_contract_address', + type: 'core::starknet::contract_address::ContractAddress', + }, + { + name: 'public_key', + type: 'core::integer::u256', + }, + { + name: 'starknet_address', + type: 'core::felt252', + }, + ], + outputs: [ + { + type: 'core::starknet::contract_address::ContractAddress', + }, + ], + state_mutability: 'external', + }, + { + type: 'function', + name: 'get_dao_class_hash', + inputs: [], + outputs: [ + { + type: 'core::starknet::class_hash::ClassHash', + }, + ], + state_mutability: 'view', + }, + { + type: 'function', + name: 'update_dao_class_hash', + inputs: [ + { + name: 'new_class_hash', + type: 'core::starknet::class_hash::ClassHash', + }, + ], + outputs: [], + state_mutability: 'external', + }, + ], + }, + { + type: 'impl', + name: 'OwnableImpl', + interface_name: 'openzeppelin_access::ownable::interface::IOwnable', + }, + { + type: 'interface', + name: 'openzeppelin_access::ownable::interface::IOwnable', + items: [ + { + type: 'function', + name: 'owner', + inputs: [], + outputs: [ + { + type: 'core::starknet::contract_address::ContractAddress', + }, + ], + state_mutability: 'view', + }, + { + type: 'function', + name: 'transfer_ownership', + inputs: [ + { + name: 'new_owner', + type: 'core::starknet::contract_address::ContractAddress', + }, + ], + outputs: [], + state_mutability: 'external', + }, + { + type: 'function', + name: 'renounce_ownership', + inputs: [], + outputs: [], + state_mutability: 'external', + }, + ], + }, + { + type: 'constructor', + name: 'constructor', + inputs: [ + { + name: 'class_hash', + type: 'core::starknet::class_hash::ClassHash', + }, + ], + }, + { + type: 'event', + name: 'openzeppelin_access::ownable::ownable::OwnableComponent::OwnershipTransferred', + kind: 'struct', + members: [ + { + name: 'previous_owner', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'key', + }, + { + name: 'new_owner', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'key', + }, + ], + }, + { + type: 'event', + name: 'openzeppelin_access::ownable::ownable::OwnableComponent::OwnershipTransferStarted', + kind: 'struct', + members: [ + { + name: 'previous_owner', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'key', + }, + { + name: 'new_owner', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'key', + }, + ], + }, + { + type: 'event', + name: 'openzeppelin_access::ownable::ownable::OwnableComponent::Event', + kind: 'enum', + variants: [ + { + name: 'OwnershipTransferred', + type: 'openzeppelin_access::ownable::ownable::OwnableComponent::OwnershipTransferred', + kind: 'nested', + }, + { + name: 'OwnershipTransferStarted', + type: 'openzeppelin_access::ownable::ownable::OwnableComponent::OwnershipTransferStarted', + kind: 'nested', + }, + ], + }, + { + type: 'event', + name: 'afk::dao::dao_factory::DaoFactory::ClassHashUpdated', + kind: 'struct', + members: [ + { + name: 'old_class_hash', + type: 'core::starknet::class_hash::ClassHash', + kind: 'data', + }, + { + name: 'new_class_hash', + type: 'core::starknet::class_hash::ClassHash', + kind: 'key', + }, + ], + }, + { + type: 'event', + name: 'afk::dao::dao_factory::DaoFactory::DaoAACreated', + kind: 'struct', + members: [ + { + name: 'contract_address', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'key', + }, + { + name: 'creator', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'data', + }, + { + name: 'token_contract_address', + type: 'core::starknet::contract_address::ContractAddress', + kind: 'data', + }, + { + name: 'starknet_address', + type: 'core::felt252', + kind: 'data', + }, + ], + }, + { + type: 'event', + name: 'afk::dao::dao_factory::DaoFactory::Event', + kind: 'enum', + variants: [ + { + name: 'OwnableEvent', + type: 'openzeppelin_access::ownable::ownable::OwnableComponent::Event', + kind: 'flat', + }, + { + name: 'ClassHashUpdated', + type: 'afk::dao::dao_factory::DaoFactory::ClassHashUpdated', + kind: 'nested', + }, + { + name: 'DaoAACreated', + type: 'afk::dao::dao_factory::DaoFactory::DaoAACreated', + kind: 'nested', + }, + ], + }, +] as const; diff --git a/apps/indexer-v2/indexers/dao-factory.indexer.ts b/apps/indexer-v2/indexers/dao-factory.indexer.ts index 5cfd13dd..76515855 100644 --- a/apps/indexer-v2/indexers/dao-factory.indexer.ts +++ b/apps/indexer-v2/indexers/dao-factory.indexer.ts @@ -1,11 +1,19 @@ import { defineIndexer } from '@apibara/indexer'; import { useLogger } from '@apibara/indexer/plugins'; -import { drizzleStorage, useDrizzleStorage } from '@apibara/plugin-drizzle'; -import { StarknetStream } from '@apibara/starknet'; +import { drizzleStorage } from '@apibara/plugin-drizzle'; +import { decodeEvent, StarknetStream } from '@apibara/starknet'; import { hash } from 'starknet'; import { ApibaraRuntimeConfig } from 'apibara/types'; import { db } from 'indexer-v2-db'; -import { daoCreation } from 'indexer-v2-db/schema'; +import { ABI as daoAAAbi } from './abi/daoAA.abi'; +import { ABI as daoFactoryAbi } from './abi/daoFactory.abi'; +import { + insertDaoCreation, + insertProposal, + updateProposalCancellation, + updateProposalResult, + upsertProposalVote, +} from './db/dao.db'; const DAO_AA_CREATED = hash.getSelectorFromName('DaoAACreated') as `0x${string}`; const PROPOSAL_CREATED = hash.getSelectorFromName('ProposalCreated') as `0x${string}`; @@ -15,9 +23,9 @@ const PROPOSAL_RESOLVED = hash.getSelectorFromName('ProposalResolved') as `0x${s export default function (config: ApibaraRuntimeConfig) { return defineIndexer(StarknetStream)({ - streamUrl: 'https://starknet-sepolia.preview.apibara.org', + streamUrl: config.streamUrl, startingCursor: { - orderKey: 500_000n, + orderKey: BigInt(config.startingCursor.orderKey), }, filter: { events: [ @@ -28,9 +36,8 @@ export default function (config: ApibaraRuntimeConfig) { ], }, plugins: [drizzleStorage({ db })], - async factory({ block: { events }, context }) { + async factory({ block: { events } }) { const logger = useLogger(); - const { db } = useDrizzleStorage(); if (events.length === 0) { return {}; @@ -47,14 +54,20 @@ export default function (config: ApibaraRuntimeConfig) { }); const daoCreationData = events.map((event) => { - const daoAddress = event.keys[0]; - const creator = event.data[0]; - const tokenAddress = event.data[1]; - const starknetAddress = event.data[2]; + const decodedEvent = decodeEvent({ + abi: daoFactoryAbi, + event, + eventName: 'afk::dao::dao_factory::DaoFactory::DaoAACreated', + }); + + const daoAddress = decodedEvent.args.contract_address; + const creator = decodedEvent.args.creator; + const tokenAddress = decodedEvent.args.token_contract_address; + const starknetAddress = decodedEvent.args.starknet_address.toString(); return { number: event.eventIndex, - hash: event.address, + hash: event.transactionHash, contractAddress: daoAddress, creator, tokenAddress, @@ -62,7 +75,7 @@ export default function (config: ApibaraRuntimeConfig) { }; }); - await db.insert(daoCreation).values(daoCreationData).onConflictDoNothing().execute(); + await insertDaoCreation(daoCreationData); return { filter: { @@ -72,7 +85,6 @@ export default function (config: ApibaraRuntimeConfig) { }, async transform({ block }) { const logger = useLogger(); - //const { db } = useDrizzleStorage(); const { events, header } = block; logger.log(`Block number ${header?.blockNumber}`); @@ -82,6 +94,61 @@ export default function (config: ApibaraRuntimeConfig) { for (const event of events) { logger.log(`Event ${event.eventIndex} tx=${event.transactionHash}`); + + if (event.keys[0] === PROPOSAL_CREATED) { + const decodedEvent = decodeEvent({ + abi: daoAAAbi, + event, + eventName: 'afk::interfaces::voting::ProposalCreated', + }); + + await insertProposal({ + contractAddress: decodedEvent.address, + proposalId: decodedEvent.args.id, + creator: decodedEvent.args.owner, + createdAt: Number(decodedEvent.args.created_at), + endAt: Number(decodedEvent.args.end_at), + }); + } else if (event.keys[0] === PROPOSAL_CANCELED) { + const decodedEvent = decodeEvent({ + abi: daoAAAbi, + event, + eventName: 'afk::interfaces::voting::ProposalCanceled', + }); + + await updateProposalCancellation( + decodedEvent.address, + decodedEvent.args.owner, + decodedEvent.args.id, + ); + } else if (event.keys[0] === PROPOSAL_RESOLVED) { + const decodedEvent = decodeEvent({ + abi: daoAAAbi, + event, + eventName: 'afk::interfaces::voting::ProposalResolved', + }); + + await updateProposalResult( + decodedEvent.address, + decodedEvent.args.owner, + decodedEvent.args.id, + decodedEvent.args.result.toString(), + ); + } else if (event.keys[0] === PROPOSAL_VOTED) { + const decodedEvent = decodeEvent({ + abi: daoAAAbi, + event, + eventName: 'afk::interfaces::voting::ProposalVoted', + }); + + await upsertProposalVote({ + contractAddress: decodedEvent.address, + proposalId: decodedEvent.args.id, + voter: decodedEvent.args.voter, + totalVotes: decodedEvent.args.total_votes, + votedAt: Number(decodedEvent.args.voted_at), + }); + } } }, }); diff --git a/apps/indexer-v2/indexers/db/dao.db.ts b/apps/indexer-v2/indexers/db/dao.db.ts new file mode 100644 index 00000000..c6bd7eae --- /dev/null +++ b/apps/indexer-v2/indexers/db/dao.db.ts @@ -0,0 +1,84 @@ +import { db } from 'indexer-v2-db'; +import { daoCreation, daoProposal, daoProposalVote } from 'indexer-v2-db/schema'; +import { and, eq } from 'drizzle-orm'; + +interface DaoCreationData { + number: number; + hash: string; + contractAddress: string; + creator: string; + tokenAddress: string; + starknetAddress: string; +} + +interface ProposalCreationData { + contractAddress: string; + proposalId: bigint; + creator: string; + createdAt: number; + endAt: number; +} + +interface ProposalVoteData { + contractAddress: string; + proposalId: bigint; + voter: string; + totalVotes: bigint; + votedAt: number; +} + +export async function insertDaoCreation(daoCreationData: DaoCreationData[]) { + return db.insert(daoCreation).values(daoCreationData).onConflictDoNothing().execute(); +} + +export async function insertProposal(proposalCreationData: ProposalCreationData) { + return db.insert(daoProposal).values(proposalCreationData).onConflictDoNothing().execute(); +} + +export async function updateProposalCancellation( + contractAddress: string, + creator: string, + proposalId: bigint, +) { + return db + .update(daoProposal) + .set({ isCanceled: true }) + .where( + and( + eq(daoProposal.contractAddress, contractAddress), + eq(daoProposal.creator, creator), + eq(daoProposal.proposalId, proposalId), + ), + ) + .execute(); +} + +export async function updateProposalResult( + contractAddress: string, + creator: string, + proposalId: bigint, + result: string, +) { + return db + .update(daoProposal) + .set({ result }) + .where( + and( + eq(daoProposal.contractAddress, contractAddress), + eq(daoProposal.creator, creator), + eq(daoProposal.proposalId, proposalId), + ), + ) + .execute(); +} + +export async function upsertProposalVote(proposalVoteData: ProposalVoteData) { + return db + .insert(daoProposalVote) + .values(proposalVoteData) + .onConflictDoUpdate({ + target: [daoProposalVote.contractAddress, daoProposalVote.proposalId, daoProposalVote.voter], + set: { totalVotes: proposalVoteData.totalVotes }, + }) + .execute(); +} diff --git a/apps/indexer-v2/package.json b/apps/indexer-v2/package.json index 340fa303..744a4656 100644 --- a/apps/indexer-v2/package.json +++ b/apps/indexer-v2/package.json @@ -4,14 +4,13 @@ "private": true, "scripts": { "build": "apibara build", + "build:db": "pnpm -w run build:indexer-v2-db", + "build:all": "pnpm build:db && pnpm build", "dev": "apibara dev", - "start": "apibara start --indexer dao-factory", + "start": "apibara start", "lint": "biome check .", "lint:fix": "pnpm lint --write", - "test": "vitest", - "drizzle:generate": "drizzle-kit generate", - "drizzle:migrate": "drizzle-kit migrate", - "drizzle:studio": "drizzle-kit studio" + "test": "vitest" }, "dependencies": { "@apibara/evm": "2.0.1-beta.30", @@ -20,6 +19,7 @@ "@apibara/protocol": "2.0.0-beta.40", "@apibara/starknet": "2.0.0-beta.40", "apibara": "2.0.0-beta.40", + "drizzle-orm": "^0.37.0", "indexer-v2": "link:", "indexer-v2-db": "workspace:*", "starknet": "^6.11.0", diff --git a/package.json b/package.json index d372790c..0bd0c7c6 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dev:mobile": "cd apps/mobile && pnpm run start", "build:nostr_sdk": "cd packages/afk_nostr_sdk && pnpm run build", "build:indexer-prisma": "cd packages/indexer-prisma && pnpm run build", + "build:indexer-v2-db": "cd packages/indexer-v2-db && pnpm run build", "build:prisma-db": "cd packages/prisma-db && pnpm run build", "build:backend": "cd apps/data-backend && pnpm run build", "build:backend:all": "pnpm run build:indexer-prisma && cd apps/data-backend && pnpm install && pnpm run build:all", diff --git a/packages/indexer-v2-db/.drizzle/0000_sad_marvel_apes.sql b/packages/indexer-v2-db/.drizzle/0000_sad_marvel_apes.sql new file mode 100644 index 00000000..2426b18b --- /dev/null +++ b/packages/indexer-v2-db/.drizzle/0000_sad_marvel_apes.sql @@ -0,0 +1,31 @@ +CREATE TABLE IF NOT EXISTS "dao_creation" ( + "_id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "number" bigint, + "hash" text, + "creator" text, + "token_address" text, + "contract_address" text, + "starknet_address" text +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "dao_proposal" ( + "contract_address" text, + "proposal_id" bigint, + "creator" text, + "created_at" integer, + "end_at" integer, + "is_canceled" boolean, + "result" text, + CONSTRAINT "dao_proposal_contract_address_proposal_id_pk" PRIMARY KEY("contract_address","proposal_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "dao_proposal_vote" ( + "contract_address" text, + "proposal_id" bigint, + "voter" text, + "vote" text, + "votes" bigint, + "total_votes" bigint, + "voted_at" integer, + CONSTRAINT "dao_proposal_vote_contract_address_proposal_id_voter_pk" PRIMARY KEY("contract_address","proposal_id","voter") +); diff --git a/packages/indexer-v2-db/.drizzle/meta/0000_snapshot.json b/packages/indexer-v2-db/.drizzle/meta/0000_snapshot.json new file mode 100644 index 00000000..7170dde5 --- /dev/null +++ b/packages/indexer-v2-db/.drizzle/meta/0000_snapshot.json @@ -0,0 +1,202 @@ +{ + "id": "1c19d70b-4f1a-451e-9bf2-360ffbb1de6f", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.dao_creation": { + "name": "dao_creation", + "schema": "", + "columns": { + "_id": { + "name": "_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "number": { + "name": "number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator": { + "name": "creator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_address": { + "name": "token_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contract_address": { + "name": "contract_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "starknet_address": { + "name": "starknet_address", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dao_proposal": { + "name": "dao_proposal", + "schema": "", + "columns": { + "contract_address": { + "name": "contract_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposal_id": { + "name": "proposal_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "creator": { + "name": "creator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_at": { + "name": "end_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_canceled": { + "name": "is_canceled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dao_proposal_contract_address_proposal_id_pk": { + "name": "dao_proposal_contract_address_proposal_id_pk", + "columns": [ + "contract_address", + "proposal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dao_proposal_vote": { + "name": "dao_proposal_vote", + "schema": "", + "columns": { + "contract_address": { + "name": "contract_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposal_id": { + "name": "proposal_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "voter": { + "name": "voter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "votes": { + "name": "votes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "total_votes": { + "name": "total_votes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "voted_at": { + "name": "voted_at", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dao_proposal_vote_contract_address_proposal_id_voter_pk": { + "name": "dao_proposal_vote_contract_address_proposal_id_voter_pk", + "columns": [ + "contract_address", + "proposal_id", + "voter" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/indexer-v2-db/.drizzle/meta/_journal.json b/packages/indexer-v2-db/.drizzle/meta/_journal.json new file mode 100644 index 00000000..5385987f --- /dev/null +++ b/packages/indexer-v2-db/.drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1740138676818, + "tag": "0000_sad_marvel_apes", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/indexer-v2-db/.gitignore b/packages/indexer-v2-db/.gitignore index 70a9f998..da8b3d0e 100644 --- a/packages/indexer-v2-db/.gitignore +++ b/packages/indexer-v2-db/.gitignore @@ -7,46 +7,8 @@ yarn-error.log* lerna-debug.log* .pnpm-debug.log* -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - # Dependency directories node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo # Optional npm cache directory .npm @@ -54,24 +16,6 @@ web_modules/ # Optional eslint cache .eslintcache -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - # dotenv environment variable files .env .env.development.local @@ -79,64 +23,4 @@ web_modules/ .env.production.local .env.local -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out -next-env.d.ts - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -# next -.next - -# prisma -.prisma - -# apibara -.apibara - -# drizzle -.drizzle \ No newline at end of file +dist \ No newline at end of file diff --git a/packages/indexer-v2-db/drizzle-prod.config.ts b/packages/indexer-v2-db/drizzle-prod.config.ts new file mode 100644 index 00000000..fb8f73fa --- /dev/null +++ b/packages/indexer-v2-db/drizzle-prod.config.ts @@ -0,0 +1,16 @@ +import type { Config } from 'drizzle-kit'; +import { defineConfig } from 'drizzle-kit'; + +const connectionString = process.env.POSTGRES_CONNECTION_STRING; +if (!connectionString) { + throw new Error('POSTGRES_CONNECTION_STRING is not defined'); +} + +export default defineConfig({ + schema: './src/schema.js', + out: './.drizzle', + dialect: 'postgresql', + dbCredentials: { + url: connectionString, + }, +}) as Config; diff --git a/packages/indexer-v2-db/drizzle.config.ts b/packages/indexer-v2-db/drizzle.config.ts index 96cee791..d14335cf 100644 --- a/packages/indexer-v2-db/drizzle.config.ts +++ b/packages/indexer-v2-db/drizzle.config.ts @@ -1,13 +1,14 @@ import type { Config } from 'drizzle-kit'; +import { defineConfig } from 'drizzle-kit'; const connectionString = process.env.POSTGRES_CONNECTION_STRING ?? 'postgres://postgres:postgres@localhost:5434/indexer'; -export default { +export default defineConfig({ schema: './src/schema.ts', out: './.drizzle', dialect: 'postgresql', dbCredentials: { url: connectionString, }, -} satisfies Config; +}) as Config; diff --git a/packages/indexer-v2-db/package.json b/packages/indexer-v2-db/package.json index 6aaa34cd..348230dd 100644 --- a/packages/indexer-v2-db/package.json +++ b/packages/indexer-v2-db/package.json @@ -2,17 +2,17 @@ "name": "indexer-v2-db", "version": "0.0.1", "private": true, - "main": "dist/index.js", "exports": { ".": "./dist/index.js", "./schema": "./dist/schema.js" }, "scripts": { - "build": "drizzle-kit generate && tsc", + "build": "tsc", "lint": "biome check .", "lint:fix": "pnpm lint --write", "drizzle:generate": "drizzle-kit generate", "drizzle:migrate": "drizzle-kit migrate", + "drizzle:migrate:prod": "drizzle-kit migrate --config=./dist/drizzle-prod.config.js", "drizzle:studio": "drizzle-kit studio" }, "dependencies": { diff --git a/packages/indexer-v2-db/src/schema.ts b/packages/indexer-v2-db/src/schema.ts index d41c031e..b4a28cd0 100644 --- a/packages/indexer-v2-db/src/schema.ts +++ b/packages/indexer-v2-db/src/schema.ts @@ -1,4 +1,4 @@ -import { bigint, pgTable, text, uuid } from 'drizzle-orm/pg-core'; +import { bigint, boolean, integer, pgTable, primaryKey, text, uuid } from 'drizzle-orm/pg-core'; export const daoCreation = pgTable('dao_creation', { _id: uuid('_id').primaryKey().defaultRandom(), @@ -9,3 +9,31 @@ export const daoCreation = pgTable('dao_creation', { contractAddress: text('contract_address'), starknetAddress: text('starknet_address'), }); + +export const daoProposal = pgTable( + 'dao_proposal', + { + contractAddress: text('contract_address'), + proposalId: bigint('proposal_id', { mode: 'bigint' }), + creator: text('creator'), + createdAt: integer('created_at'), + endAt: integer('end_at'), + isCanceled: boolean('is_canceled'), + result: text('result'), + }, + (table) => [primaryKey({ columns: [table.contractAddress, table.proposalId] })], +); + +export const daoProposalVote = pgTable( + 'dao_proposal_vote', + { + contractAddress: text('contract_address'), + proposalId: bigint('proposal_id', { mode: 'bigint' }), + voter: text('voter'), + vote: text('vote'), + votes: bigint('votes', { mode: 'bigint' }), + totalVotes: bigint('total_votes', { mode: 'bigint' }), + votedAt: integer('voted_at'), + }, + (table) => [primaryKey({ columns: [table.contractAddress, table.proposalId, table.voter] })], +); diff --git a/railway.data-backend.Dockerfile b/railway.data-backend.Dockerfile index 21b3fbc8..87970f38 100644 --- a/railway.data-backend.Dockerfile +++ b/railway.data-backend.Dockerfile @@ -113,8 +113,8 @@ RUN npm install -g pnpm # Copy the entire repository into the Docker container COPY . . - - +# when building image on local machine, remove .env files +RUN find . -name ".env" -type f | xargs rm -f # RUN apk add --no-cache openssl # Install all dependencies for the workspace, including common and data-backend diff --git a/railway.indexer-v2.Dockerfile b/railway.indexer-v2.Dockerfile new file mode 100644 index 00000000..cf09f393 --- /dev/null +++ b/railway.indexer-v2.Dockerfile @@ -0,0 +1,57 @@ +# Use a Node.js base image +FROM node:20-alpine AS base + +# Set the working directory inside the container +WORKDIR /build + +ARG INDEXER_DATABASE_URL INDEXER_v2_DATABASE_URL + +# Copy repository into the Docker container +COPY . . +# when building image on local machine, remove .env files +RUN find . -name ".env" -type f | xargs rm -f + +# Install libs + pnpm +# Install dependencies +# Build the project +RUN apk add --no-cache \ + openssl \ + libc6-compat && \ + npm install -g pnpm && \ + pnpm install && \ + pnpm --filter indexer-v2 build:all + +WORKDIR /app + +## Copy the node_modules and built files from the base stage +RUN mv /build/node_modules . && \ + mv /build/packages/common ./node_modules/ && \ + mv /build/packages/indexer-v2-db ./node_modules/ && \ + mkdir -p apps/indexer-v2 && \ + mv /build/apps/indexer-v2/node_modules ./apps/indexer-v2/ && \ + mv /build/apps/indexer-v2/.apibara/build ./apps/indexer-v2/ && \ + mv /build/apps/indexer-v2/package.json ./apps/indexer-v2/ + +# Use a smaller production base image +FROM node:20-alpine AS production + +# Install necessary dependencies in production +RUN apk add --no-cache \ + openssl \ + libc6-compat + +# Set the working directory in the production container +WORKDIR /app + +# Copy all necessary files in a single layer +COPY --from=base /app . + +# Set the environment variable to production +ENV NODE_ENV=production + +# Expose the port your app runs on +EXPOSE 3000 + +# Command to start the application +WORKDIR /app/apps/indexer-v2 +CMD node build/start.mjs start --indexer dao-factory diff --git a/railway.indexer-v2.json b/railway.indexer-v2.json new file mode 100644 index 00000000..6af3dcc6 --- /dev/null +++ b/railway.indexer-v2.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://backboard.railway.app/railway.schema.json", + "build": { + "builder": "DOCKERFILE", + "dockerfilePath": "railway.indexer-v2.Dockerfile" + }, + "deploy": { + "startCommand": "", + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 3 + } +}