diff --git a/.gitignore b/.gitignore deleted file mode 100644 index eb5a316..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 4ae9f3f..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "deno.enablePaths": ["indexers"] -} diff --git a/backend/drizzle/0000_damp_typhoid_mary.sql b/backend/drizzle/0000_damp_typhoid_mary.sql deleted file mode 100644 index 62ed4d3..0000000 --- a/backend/drizzle/0000_damp_typhoid_mary.sql +++ /dev/null @@ -1,44 +0,0 @@ -DO $$ BEGIN - CREATE TYPE "public"."network_type" AS ENUM('mainnet', 'sepolia'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "indexer_locked" ( - "_cursor" bigint, - "created_at" timestamp, - "network" "network_type", - "block_hash" text, - "block_number" bigint, - "block_timestamp" timestamp, - "transaction_hash" text, - "index_in_block" bigint, - "id" text PRIMARY KEY NOT NULL, - "token_address" text, - "from_address" text, - "amount" text -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "indexer_unlocked" ( - "_cursor" bigint, - "created_at" timestamp, - "network" "network_type", - "block_hash" text, - "block_number" bigint, - "block_timestamp" timestamp, - "transaction_hash" text, - "index_in_block" bigint, - "id" text PRIMARY KEY NOT NULL, - "token_address" text, - "from_address" text, - "to_address" text, - "amount" text -); ---> statement-breakpoint -CREATE INDEX IF NOT EXISTS "locked_cursor_idx" ON "indexer_locked" USING btree ("_cursor");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "locked_token_idx" ON "indexer_locked" USING btree ("token_address");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "locked_from_idx" ON "indexer_locked" USING btree ("from_address");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "unlocked_cursor_idx" ON "indexer_unlocked" USING btree ("_cursor");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "unlocked_token_idx" ON "indexer_unlocked" USING btree ("token_address");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "unlocked_from_idx" ON "indexer_unlocked" USING btree ("from_address");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "unlocked_to_idx" ON "indexer_unlocked" USING btree ("to_address"); \ No newline at end of file diff --git a/backend/drizzle/0000_unique_luke_cage.sql b/backend/drizzle/0000_unique_luke_cage.sql new file mode 100644 index 0000000..c49c949 --- /dev/null +++ b/backend/drizzle/0000_unique_luke_cage.sql @@ -0,0 +1,41 @@ +DO $$ BEGIN + CREATE TYPE "public"."network_type" AS ENUM('mainnet', 'sepolia'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + CREATE TYPE "public"."ramp_type" AS ENUM('Revolut'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "liquidity" ( + "owner" text, + "offchain_id" text, + "locked" boolean DEFAULT false, + "amount" bigint, + "_cursor" "int8range" NOT NULL, + CONSTRAINT "liquidity_key" PRIMARY KEY("owner","offchain_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "liquidity_request" ( + "owner" text, + "offchain_id" text, + "requestor" text, + "requestor_offchain_id" text, + "amount" bigint NOT NULL, + "expires_at" timestamp NOT NULL, + "_cursor" bigint +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "registration" ( + "address" text PRIMARY KEY NOT NULL, + "revolut" text[] DEFAULT ARRAY[]::text[] NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "liquidity_request" ADD CONSTRAINT "liquidity_key" FOREIGN KEY ("owner","offchain_id") REFERENCES "public"."liquidity"("owner","offchain_id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/backend/drizzle/meta/0000_snapshot.json b/backend/drizzle/meta/0000_snapshot.json index 1e7715b..824aed0 100644 --- a/backend/drizzle/meta/0000_snapshot.json +++ b/backend/drizzle/meta/0000_snapshot.json @@ -1,284 +1,145 @@ { - "id": "d5a50aa7-acac-4a5f-8d1f-e537c7e92d09", + "id": "3b693785-62e8-42c6-98ca-b6e244bc78c0", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", "tables": { - "public.indexer_locked": { - "name": "indexer_locked", + "public.liquidity": { + "name": "liquidity", "schema": "", "columns": { - "_cursor": { - "name": "_cursor", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "network": { - "name": "network", - "type": "network_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": false - }, - "block_hash": { - "name": "block_hash", + "owner": { + "name": "owner", "type": "text", "primaryKey": false, "notNull": false }, - "block_number": { - "name": "block_number", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "block_timestamp": { - "name": "block_timestamp", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "transaction_hash": { - "name": "transaction_hash", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "index_in_block": { - "name": "index_in_block", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "token_address": { - "name": "token_address", + "offchain_id": { + "name": "offchain_id", "type": "text", "primaryKey": false, "notNull": false }, - "from_address": { - "name": "from_address", - "type": "text", + "locked": { + "name": "locked", + "type": "boolean", "primaryKey": false, - "notNull": false + "notNull": false, + "default": false }, "amount": { "name": "amount", - "type": "text", + "type": "bigint", "primaryKey": false, "notNull": false + }, + "_cursor": { + "name": "_cursor", + "type": "int8range", + "primaryKey": false, + "notNull": true } }, - "indexes": { - "locked_cursor_idx": { - "name": "locked_cursor_idx", - "columns": [ - { - "expression": "_cursor", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "locked_token_idx": { - "name": "locked_token_idx", - "columns": [ - { - "expression": "token_address", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "locked_from_idx": { - "name": "locked_from_idx", + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "liquidity_key": { + "name": "liquidity_key", "columns": [ - { - "expression": "from_address", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} + "owner", + "offchain_id" + ] } }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, "uniqueConstraints": {} }, - "public.indexer_unlocked": { - "name": "indexer_unlocked", + "public.liquidity_request": { + "name": "liquidity_request", "schema": "", "columns": { - "_cursor": { - "name": "_cursor", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "network": { - "name": "network", - "type": "network_type", - "typeSchema": "public", - "primaryKey": false, - "notNull": false - }, - "block_hash": { - "name": "block_hash", + "owner": { + "name": "owner", "type": "text", "primaryKey": false, "notNull": false }, - "block_number": { - "name": "block_number", - "type": "bigint", + "offchain_id": { + "name": "offchain_id", + "type": "text", "primaryKey": false, "notNull": false }, - "block_timestamp": { - "name": "block_timestamp", - "type": "timestamp", + "requestor": { + "name": "requestor", + "type": "text", "primaryKey": false, "notNull": false }, - "transaction_hash": { - "name": "transaction_hash", + "requestor_offchain_id": { + "name": "requestor_offchain_id", "type": "text", "primaryKey": false, "notNull": false }, - "index_in_block": { - "name": "index_in_block", + "amount": { + "name": "amount", "type": "bigint", "primaryKey": false, - "notNull": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, "notNull": true }, - "token_address": { - "name": "token_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "from_address": { - "name": "from_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "to_address": { - "name": "to_address", - "type": "text", + "expires_at": { + "name": "expires_at", + "type": "timestamp", "primaryKey": false, - "notNull": false + "notNull": true }, - "amount": { - "name": "amount", - "type": "text", + "_cursor": { + "name": "_cursor", + "type": "bigint", "primaryKey": false, "notNull": false } }, - "indexes": { - "unlocked_cursor_idx": { - "name": "unlocked_cursor_idx", - "columns": [ - { - "expression": "_cursor", - "isExpression": false, - "asc": true, - "nulls": "last" - } + "indexes": {}, + "foreignKeys": { + "liquidity_key": { + "name": "liquidity_key", + "tableFrom": "liquidity_request", + "tableTo": "liquidity", + "columnsFrom": [ + "owner", + "offchain_id" ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "unlocked_token_idx": { - "name": "unlocked_token_idx", - "columns": [ - { - "expression": "token_address", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "unlocked_from_idx": { - "name": "unlocked_from_idx", - "columns": [ - { - "expression": "from_address", - "isExpression": false, - "asc": true, - "nulls": "last" - } + "columnsTo": [ + "owner", + "offchain_id" ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.registration": { + "name": "registration", + "schema": "", + "columns": { + "address": { + "name": "address", + "type": "text", + "primaryKey": true, + "notNull": true }, - "unlocked_to_idx": { - "name": "unlocked_to_idx", - "columns": [ - { - "expression": "to_address", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} + "revolut": { + "name": "revolut", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" } }, + "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {} @@ -292,6 +153,13 @@ "mainnet", "sepolia" ] + }, + "public.ramp_type": { + "name": "ramp_type", + "schema": "public", + "values": [ + "Revolut" + ] } }, "schemas": {}, diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index 6a1742e..8291619 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1725556971568, - "tag": "0000_damp_typhoid_mary", + "when": 1727794402421, + "tag": "0000_unique_luke_cage", "breakpoints": true } ] diff --git a/backend/package.json b/backend/package.json index b97c223..3427fe5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,7 +18,8 @@ "drizzle-orm": "^0.33.0", "fastify": "^4.28.1", "fastify-plugin": "^4.5.1", - "postgres": "^3.4.4" + "postgres": "^3.4.4", + "postgres-range": "^1.1.4" }, "devDependencies": { "@testcontainers/postgresql": "^10.13.0", diff --git a/backend/src/db/int8range.ts b/backend/src/db/int8range.ts new file mode 100644 index 0000000..4f174ca --- /dev/null +++ b/backend/src/db/int8range.ts @@ -0,0 +1,49 @@ +import { customType } from 'drizzle-orm/pg-core' +import type { Range } from 'postgres-range' +import { parse as rangeParse, serialize as rangeSerialize } from 'postgres-range' + +type Comparable = string | number + +type RangeBound = + | T + | { + value: T + inclusive: boolean + } + +class Int8Range { + constructor(public readonly range: Range) {} + + get start(): RangeBound | null { + return this.range.lower != null + ? { + value: this.range.lower, + inclusive: this.range.isLowerBoundClosed(), + } + : null + } + + get end(): RangeBound | null { + return this.range.upper != null + ? { + value: this.range.upper, + inclusive: this.range.isUpperBoundClosed(), + } + : null + } +} + +export const int8range = customType<{ + data: Int8Range +}>({ + dataType: () => 'int8range', + fromDriver: (value: unknown): Int8Range => { + if (typeof value !== 'string') { + throw new Error('Expected string') + } + + const parsed = rangeParse(value, (val) => Number.parseInt(val, 10)) + return new Int8Range(parsed) + }, + toDriver: (value: Int8Range): string => rangeSerialize(value.range), +}) diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 746a960..2ce247e 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -1,61 +1,54 @@ -import { bigint, index, pgEnum, pgTable, text, timestamp } from 'drizzle-orm/pg-core' +import { sql } from 'drizzle-orm' +import { bigint, boolean, foreignKey, pgEnum, pgTable, primaryKey, text, timestamp } from 'drizzle-orm/pg-core' + +import { int8range } from './int8range' export const networkEnum = pgEnum('network_type', ['mainnet', 'sepolia']) -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const indexerCommonSchema = { - cursor: bigint('_cursor', { mode: 'number' }), - createdAt: timestamp('created_at', { mode: 'date', withTimezone: false }), +export const rampEnum = pgEnum('ramp_type', ['Revolut']) - network: networkEnum('network'), - blockHash: text('block_hash'), - blockNumber: bigint('block_number', { mode: 'number' }), - blockTimestamp: timestamp('block_timestamp', { - mode: 'date', - withTimezone: false, - }), - transactionHash: text('transaction_hash'), - indexInBlock: bigint('index_in_block', { mode: 'number' }), -} +export const registration = pgTable('registration', { + address: text('address').primaryKey(), + revolut: text('revolut') + .array() + .notNull() + .default(sql`ARRAY[]::text[]`), +}) -export const locked = pgTable( - 'indexer_locked', +export const liquidity = pgTable( + 'liquidity', { - ...indexerCommonSchema, - - id: text('id').primaryKey(), - - token: text('token_address'), - from: text('from_address'), - amount: text('amount'), + owner: text('owner'), + offchainId: text('offchain_id'), + locked: boolean('locked').default(false), + amount: bigint('amount', { mode: 'bigint' }), + cursor: int8range('_cursor').notNull(), }, (table) => { return { - cursorIdx: index('locked_cursor_idx').on(table.cursor), - tokenIdx: index('locked_token_idx').on(table.token), - fromIdx: index('locked_from_idx').on(table.from), + liquidityKey: primaryKey({ name: 'liquidity_key', columns: [table.owner, table.offchainId] }), } }, ) -export const unlocked = pgTable( - 'indexer_unlocked', +export const liquidityRequest = pgTable( + 'liquidity_request', { - ...indexerCommonSchema, - - id: text('id').primaryKey(), - - token: text('token_address'), - from: text('from_address'), - to: text('to_address'), - amount: text('amount'), + owner: text('owner'), + offchainId: text('offchain_id'), + requestor: text('requestor'), + requestorOffchainId: text('requestor_offchain_id'), + amount: bigint('amount', { mode: 'bigint' }).notNull(), + expiresAt: timestamp('expires_at').notNull(), + cursor: bigint('_cursor', { mode: 'number' }), }, (table) => { return { - cursorIdx: index('unlocked_cursor_idx').on(table.cursor), - tokenIdx: index('unlocked_token_idx').on(table.token), - fromIdx: index('unlocked_from_idx').on(table.from), - toIdx: index('unlocked_to_idx').on(table.to), + liquidityKey: foreignKey({ + columns: [table.owner, table.offchainId], + foreignColumns: [liquidity.owner, liquidity.offchainId], + name: 'liquidity_key', + }), } }, ) diff --git a/backend/yarn.lock b/backend/yarn.lock index b07aae1..1a3e632 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -3417,6 +3417,11 @@ postcss@^8.4.43: picocolors "^1.0.1" source-map-js "^1.2.0" +postgres-range@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" + integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== + postgres@^3.4.4: version "3.4.4" resolved "https://registry.yarnpkg.com/postgres/-/postgres-3.4.4.tgz#adbe08dc1fff0dea3559aa4f83ded70a289a6cb8" @@ -3854,8 +3859,16 @@ streamx@^2.15.0, streamx@^2.18.0: optionalDependencies: bare-events "^2.2.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: - name string-width-cjs +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3915,7 +3928,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== diff --git a/indexers/.env.example b/indexers/.env.example deleted file mode 100644 index 8f6f437..0000000 --- a/indexers/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -AUTH_TOKEN=dna_your_apibara_token -POSTGRES_CONNECTION_STRING=postgresql://admin:password@postgres:5432/zkramp diff --git a/indexers/.gitignore b/indexers/.gitignore index 9b1ee42..722d5e7 100644 --- a/indexers/.gitignore +++ b/indexers/.gitignore @@ -1,175 +1 @@ -# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore - -# Logs - -logs -_.log -npm-debug.log_ -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Caches - -.cache - -# 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 - -# 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 -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) - -.parcel-cache - -# Next.js build output - -.next -out - -# Nuxt.js build / generate output - -.nuxt -dist - -# Gatsby files - -# 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.* - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store +.vscode diff --git a/indexers/Dockerfile b/indexers/Dockerfile new file mode 100644 index 0000000..e5fa391 --- /dev/null +++ b/indexers/Dockerfile @@ -0,0 +1,14 @@ +# Notice that the base image is build from scratch (not an OS like Ubuntu), +# so the binary is in a location that depends on the build. +# For this reason we stick to a specific version and architecture. +# +# When updating the image you also need to update the entrypoint below. +# +# - docker image pull quay.io/apibara/sink-postgres:0.7.0-x86_64 +# - docker image inspect quay.io/apibara/sink-postgres:0.7.0-x86_64 | jq '.[].Config.Entrypoint' +FROM quay.io/apibara/sink-postgres:0.7.0-x86_64 + +WORKDIR /app +COPY ./src/* /app + +ENTRYPOINT ["/nix/store/rh1g8pb7wfnyr527jfmkkc5lm3sa1f0l-apibara-sink-postgres-0.7.0/bin/apibara-sink-postgres"] diff --git a/indexers/deno.json b/indexers/deno.json deleted file mode 100644 index df54125..0000000 --- a/indexers/deno.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "lint": { - "include": ["src/"], - "rules": { - "tags": ["recommended"], - "include": ["ban-untagged-todo"], - "exclude": ["no-unused-vars"] - } - }, - "fmt": { - "useTabs": false, - "lineWidth": 120, - "indentWidth": 2, - "semiColons": false, - "singleQuote": true, - "proseWrap": "preserve", - "include": ["src/"] - } -} diff --git a/indexers/docker-compose.yml b/indexers/docker-compose.yml deleted file mode 100644 index 60a4d35..0000000 --- a/indexers/docker-compose.yml +++ /dev/null @@ -1,30 +0,0 @@ -version: '3.8' - -services: - zkramp-locked-indexer: - environment: - - AUTH_TOKEN=${AUTH_TOKEN} - - POSTGRES_CONNECTION_STRING=${POSTGRES_CONNECTION_STRING} - image: quay.io/apibara/sink-postgres:latest - command: 'run ./indexer/escrow-locked.indexer.ts --connection-string ${POSTGRES_CONNECTION_STRING} -A ${AUTH_TOKEN}' - volumes: - - ./src:/indexer - networks: - - indexer - restart: on-failure - - zkramp-unlocked-indexer: - environment: - - AUTH_TOKEN=${AUTH_TOKEN} - - POSTGRES_CONNECTION_STRING=${POSTGRES_CONNECTION_STRING} - image: quay.io/apibara/sink-postgres:latest - command: 'run ./indexer/escrow-unlocked.indexer.ts --connection-string ${POSTGRES_CONNECTION_STRING} -A ${AUTH_TOKEN}' - volumes: - - ./src:/indexer - networks: - - indexer - restart: on-failure - -networks: - indexer: - driver: bridge diff --git a/indexers/src/escrow-locked.indexer.ts b/indexers/src/escrow-locked.indexer.ts deleted file mode 100644 index 6daef89..0000000 --- a/indexers/src/escrow-locked.indexer.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { apibara, starknet } from './utils/deps.ts' -import { ESCROW_ADDRESS, STARTING_BLOCK } from './utils/constants.ts' -import { getCommonValues } from './utils/helpers.ts' - -const filter = { - header: { - weak: true, - }, - events: [ - { - fromAddress: ESCROW_ADDRESS, - keys: [starknet.hash.getSelectorFromName('Locked')], - includeReceipt: false, - }, - ], -} - -export const config = { - streamUrl: 'https://mainnet.starknet.a5a.ch', - startingBlock: STARTING_BLOCK, - network: 'starknet', - finality: 'DATA_STATUS_ACCEPTED', - filter, - sinkType: 'postgres', - sinkOptions: { - tableName: 'indexer_locked', - }, -} - -export default function transform({ header, events }: apibara.Block) { - return (events ?? []) - .map(({ event, transaction }) => { - if (!event.data || !event.keys) return null - - const eventId = `${transaction.meta.hash}_${event.index ?? 0}` - - const tokenAddress = event.keys[1] - const [fromAddress, amountLow, amountHigh] = event.data - - const amount = starknet.uint256.uint256ToBN({ - low: amountLow, - high: amountHigh, - }) - - return { - ...getCommonValues(header!, event, transaction), - - id: eventId, - - token_address: tokenAddress, - from_address: fromAddress, - amount: amount.toString(), - } - }) - .filter(Boolean) -} diff --git a/indexers/src/escrow-unlocked.indexer.ts b/indexers/src/escrow-unlocked.indexer.ts deleted file mode 100644 index 7fa55e7..0000000 --- a/indexers/src/escrow-unlocked.indexer.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { apibara, starknet } from './utils/deps.ts' -import { ESCROW_ADDRESS, STARTING_BLOCK } from './utils/constants.ts' -import { getCommonValues } from './utils/helpers.ts' - -const filter = { - header: { - weak: true, - }, - events: [ - { - fromAddress: ESCROW_ADDRESS, - keys: [starknet.hash.getSelectorFromName('UnLocked')], - includeReceipt: false, - }, - ], -} - -export const config = { - streamUrl: 'https://mainnet.starknet.a5a.ch', - startingBlock: STARTING_BLOCK, - network: 'starknet', - finality: 'DATA_STATUS_ACCEPTED', - filter, - sinkType: 'postgres', - sinkOptions: { - tableName: 'indexer_unlocked', - }, -} - -export default function transform({ header, events }: apibara.Block) { - return (events ?? []) - .map(({ event, transaction }) => { - if (!event.data || !event.keys) return null - - const eventId = `${transaction.meta.hash}_${event.index ?? 0}` - - const tokenAddress = event.keys[1] - const [fromAddress, toAddress, amountLow, amountHigh] = event.data - - const amount = starknet.uint256.uint256ToBN({ - low: amountLow, - high: amountHigh, - }) - - return { - ...getCommonValues(header!, event, transaction), - - id: eventId, - - token_address: tokenAddress, - from_address: fromAddress, - to_address: toAddress, - amount: amount.toString(), - } - }) - .filter(Boolean) -} diff --git a/indexers/src/liquidity-addition.ts b/indexers/src/liquidity-addition.ts new file mode 100644 index 0000000..e619965 --- /dev/null +++ b/indexers/src/liquidity-addition.ts @@ -0,0 +1,95 @@ +import { apibara, starknet } from './utils/deps.ts' +import { LIQUIDITY_VAR_NAME, REVOLUT_ADDRESSES, SN_CHAIN_ID, STARTING_BLOCK, STREAM_URLS } from './utils/constants.ts' +import { getLiquidityKeyMapStorageLocation } from './utils/helpers.ts' +import { LiquidityKey } from './utils/types.ts'; + +const filter = { + header: { + weak: true, + }, + events: [ + { + fromAddress: REVOLUT_ADDRESSES[SN_CHAIN_ID], + keys: [starknet.hash.getSelectorFromName('LiquidityAdded')], + includeReceipt: false, + }, + ], + stateUpdate: { + storageDiffs: [{ contractAddress: REVOLUT_ADDRESSES[SN_CHAIN_ID] }], + }, +} + +const streamUrl = STREAM_URLS[SN_CHAIN_ID] +const startingBlock = STARTING_BLOCK + +export const config = { + streamUrl, + startingBlock, + network: 'starknet', + finality: 'DATA_STATUS_PENDING', + filter, + sinkType: 'postgres', + sinkOptions: { + tableName: 'liquidity', + entityMode: true, + }, +} + +function getLiquidity(storageMap: Map, liquidityKey: LiquidityKey): bigint { + const liquidityAmountLocation = getLiquidityKeyMapStorageLocation(LIQUIDITY_VAR_NAME, liquidityKey) + + const addressBalanceLow = storageMap.get(liquidityAmountLocation) + const addressBalanceHigh = storageMap.get(liquidityAmountLocation + 1n) + + return starknet.uint256.uint256ToBN({ + low: addressBalanceLow ?? 0n, + high: addressBalanceHigh ?? 0n, + }) +} + +export default function transform({ events, stateUpdate }: apibara.Block) { + // Step 1: map state updates. + const storageMap = new Map() + const storageDiffs = stateUpdate?.stateDiff?.storageDiffs ?? [] + + for (const storageDiff of storageDiffs) { + for (const storageEntry of storageDiff.storageEntries ?? []) { + if (!storageEntry.key || !storageEntry.value) { + continue + } + + const key = BigInt(storageEntry.key) + const value = BigInt(storageEntry.value) + + storageMap.set(key, value) + } + } + + // Step 2: aggregate everyting + return (events ?? []) + .map(({ event }) => { + if (!event.data || !event.keys) return null + + const [, owner, offchainIdPlateform, offchainIdValue] = event.keys + + const offchainId = `${offchainIdPlateform}@${offchainIdValue}` + + const liquidityKey = { + offchainId: { + plateform: +offchainIdPlateform, + id: offchainIdValue + }, + owner + } + + const amount = getLiquidity(storageMap, liquidityKey) + + return { + owner, + offchainId, + locked: false, + amount, + } + }) + .filter(Boolean) +} diff --git a/indexers/src/utils/constants.ts b/indexers/src/utils/constants.ts index 4f17e8b..4500bc6 100644 --- a/indexers/src/utils/constants.ts +++ b/indexers/src/utils/constants.ts @@ -1,2 +1,32 @@ -export const ESCROW_ADDRESS = '0x0' -export const STARTING_BLOCK = 0 +import { starknet } from './deps.ts' + +// Order should match the order in the contract +export enum Plateform { + Revolut = 0, +} + +export const STORAGE_ADDRESS_BOUND = 2n ** 251n + +type SupportedChainId = Exclude + +type AddressesMap = Record + +export const REVOLUT_ADDRESSES: AddressesMap = { + [starknet.constants.StarknetChainId.SN_MAIN]: '0x0', + [starknet.constants.StarknetChainId.SN_SEPOLIA]: '0x0', +} + +const DEFAULT_NETWORK_NAME = starknet.constants.NetworkName.SN_SEPOLIA + +export const SN_CHAIN_ID = + (starknet.constants.StarknetChainId[(Deno.env.get('SN_NETWORK') ?? '') as starknet.constants.NetworkName] ?? + starknet.constants.StarknetChainId[DEFAULT_NETWORK_NAME]) as SupportedChainId + +export const STREAM_URLS = { + [starknet.constants.StarknetChainId.SN_MAIN]: 'https://mainnet.starknet.a5a.ch', + [starknet.constants.StarknetChainId.SN_SEPOLIA]: 'https://sepolia.starknet.a5a.ch', +} + +export const STARTING_BLOCK = Number(Deno.env.get('STARTING_BLOCK')) ?? 0 + +export const LIQUIDITY_VAR_NAME = 'liquidity' diff --git a/indexers/src/utils/deps.ts b/indexers/src/utils/deps.ts index c987452..1f0db3f 100644 --- a/indexers/src/utils/deps.ts +++ b/indexers/src/utils/deps.ts @@ -1,4 +1,4 @@ -export * as starknet from 'https://esm.sh/starknet@5.14' +export * as starknet from 'https://esm.sh/starknet@5.25' export * as viem from 'https://esm.sh/viem@1.4' export * as apibara from 'https://esm.sh/@apibara/indexer@0.3/starknet' diff --git a/indexers/src/utils/helpers.ts b/indexers/src/utils/helpers.ts index 6f2371a..b846ab7 100644 --- a/indexers/src/utils/helpers.ts +++ b/indexers/src/utils/helpers.ts @@ -1,22 +1,13 @@ -import { apibara } from './deps.ts' +import { STORAGE_ADDRESS_BOUND } from './constants.ts' +import { starknet } from './deps.ts' +import { LiquidityKey } from './types.ts' -export const getCommonValues = ( - header: apibara.BlockHeader, - event: apibara.Event, - transaction: apibara.Transaction, -) => { - const { blockNumber, blockHash, timestamp } = header +export function getLiquidityKeyMapStorageLocation(varName: string, liquidityKey: LiquidityKey) { + const hashedVarName = starknet.hash.getSelectorFromName(varName) + const serializedLiquidityKey = [liquidityKey.owner, liquidityKey.offchainId.plateform, liquidityKey.offchainId.id] - const transactionHash = transaction.meta.hash - const IndexInBlock = (transaction.meta.transactionIndex ?? 0) * 1_000 + (event.index ?? 0) + const location = BigInt([serializedLiquidityKey.length, ...serializedLiquidityKey] + .reduce((x, y) => starknet.ec.starkCurve.pedersen(x, y), hashedVarName)) - return { - created_at: new Date().toISOString(), - network: 'mainnet', - block_hash: blockHash, - block_number: +(blockNumber ?? 0), - block_timestamp: timestamp, - transaction_hash: transactionHash, - index_in_block: IndexInBlock, - } + return location >= STORAGE_ADDRESS_BOUND ? location - STORAGE_ADDRESS_BOUND : location } diff --git a/indexers/src/utils/types.ts b/indexers/src/utils/types.ts new file mode 100644 index 0000000..8c13195 --- /dev/null +++ b/indexers/src/utils/types.ts @@ -0,0 +1,11 @@ +import { Plateform } from './constants.ts'; + +export interface OffchainId { + plateform: Plateform + id: string +} + +export interface LiquidityKey { + owner: string + offchainId: OffchainId +}