diff --git a/package-lock.json b/package-lock.json index 3365b44..6e6c617 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,12 @@ "@fastify/cors": "^8.2.1", "@fastify/env": "^4.2.0", "@fastify/http-proxy": "^9.0.0", + "@fastify/postgres": "^5.2.2", "@fastify/sensible": "^5.0.0", "fastify": "^4.0.0", "fastify-cli": "^5.7.1", - "fastify-plugin": "^4.0.0" + "fastify-plugin": "^4.0.0", + "pg": "^8.11.3" }, "devDependencies": { "@types/jest": "^29.5.0", @@ -672,6 +674,17 @@ "ws": "^8.4.2" } }, + "node_modules/@fastify/postgres": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@fastify/postgres/-/postgres-5.2.2.tgz", + "integrity": "sha512-8TWRqDSiXJp0SZjbHrqwyhl0f55eV4fpYAd9m7G0hGUpyEZJFwcxIDQYjnlRAXcVTq5NloUjFH6DxgmxZ3apbQ==", + "dependencies": { + "fastify-plugin": "^4.0.0" + }, + "peerDependencies": { + "pg": ">=6.0.0" + } + }, "node_modules/@fastify/reply-from": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@fastify/reply-from/-/reply-from-9.0.1.tgz", @@ -1681,6 +1694,14 @@ "dev": true, "license": "MIT" }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -3657,6 +3678,11 @@ "node": ">=6" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/parse-json": { "version": "5.2.0", "dev": true, @@ -3702,6 +3728,97 @@ "dev": true, "license": "MIT" }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pgpass/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/picocolors": { "version": "1.0.0", "dev": true, @@ -3876,6 +3993,41 @@ "node": ">=8" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pretty-format": { "version": "29.5.0", "dev": true, @@ -4696,6 +4848,14 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "dev": true, diff --git a/package.json b/package.json index d03b1fe..693c046 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,12 @@ "@fastify/cors": "^8.2.1", "@fastify/env": "^4.2.0", "@fastify/http-proxy": "^9.0.0", + "@fastify/postgres": "^5.2.2", "@fastify/sensible": "^5.0.0", "fastify": "^4.0.0", "fastify-cli": "^5.7.1", - "fastify-plugin": "^4.0.0" + "fastify-plugin": "^4.0.0", + "pg": "^8.11.3" }, "devDependencies": { "@types/jest": "^29.5.0", diff --git a/src/plugins/env.ts b/src/plugins/env.ts index 80d8c6c..8d57596 100644 --- a/src/plugins/env.ts +++ b/src/plugins/env.ts @@ -11,6 +11,21 @@ const schema = { PROXY_UPSTREAM: { type: "string", }, + NFA_DB_HOST: { + type: "string", + }, + NFA_DB_PORT: { + type: "string", + }, + NFA_DB_LOGIN: { + type: "string", + }, + NFA_DB_PASS: { + type: "string", + }, + NFA_DB_NAME: { + type: "string", + }, }, }; diff --git a/src/routes/nfa/index.ts b/src/routes/nfa/index.ts new file mode 100644 index 0000000..5c58f82 --- /dev/null +++ b/src/routes/nfa/index.ts @@ -0,0 +1,23 @@ +import {FastifyPluginAsync} from 'fastify' +import fastifyPostgres from '@fastify/postgres' +import {NFA_QUERY} from './query' + +const nfa: FastifyPluginAsync = async (fastify, opts): Promise => { + const {NFA_DB_HOST, NFA_DB_PORT, NFA_DB_LOGIN, NFA_DB_PASS, NFA_DB_NAME} = fastify.config + + fastify.register(fastifyPostgres, { + connectionString: `postgres://${NFA_DB_LOGIN}:${NFA_DB_PASS}@${NFA_DB_HOST}:${NFA_DB_PORT}/${NFA_DB_NAME}` + }) + + fastify.get('/', async function (request, reply) { + const client = await fastify.pg.connect() + try { + const { rows } = await client.query(NFA_QUERY, []) + return rows + } finally { + client.release() + } + }) +} + +export default nfa; diff --git a/src/routes/nfa/query.ts b/src/routes/nfa/query.ts new file mode 100644 index 0000000..e30abd1 --- /dev/null +++ b/src/routes/nfa/query.ts @@ -0,0 +1,118 @@ +export const NFA_QUERY = ` + with total_users as (select count(distinct encode(owner, 'hex')) as cnt_users, + count(*) as cnt_orders + from orders + where creation_timestamp >= now() - interval '1' hour) + + +-- SHOW THE LARGEST NUMBER OUT OF TWO +-- if users buy usdc/dai or usdt then retunr next row +------------------------- + +-- most purchased token in the past hour by orders +-- example output: 36% of the orders in the past hour were to buy ETH + + , top_buy_orders as (select 'buy order' as message, + case + when encode(buy_token, 'hex') = 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' or + encode(buy_token, 'hex') = 'c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + then 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + else encode(buy_token, 'hex') end + as token, + cnt_orders as group_col, + count(*) as order_col, + count(*) / cast(cnt_orders as float) as percent, + RANK() OVER (ORDER BY count(*) desc) rank_number + from orders o, + total_users + where creation_timestamp >= now() - interval '1' hour + and encode(buy_token, 'hex') not in ('a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + '6b175474e89094c44da98b954eedeac495271d0f', + 'dac17f958d2ee523a2206206994597c13d831ec7') + group by 1, 2, 3 + order by 4 desc + limit 5 + ) + + +-- most users (x %) prefered x token +-- example output: 22% of all users in the past hour bought ETH + + , top_buy_traders as ( + select + 'buy users' as message, case when encode(buy_token, 'hex')='eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' or + encode(buy_token, 'hex')='c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' then 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + else encode(buy_token, 'hex') end + as token, cnt_users as group_col, count (distinct encode(owner, 'hex')) as order_col, count (distinct encode(owner, 'hex'))/ cast (cnt_users as float) as percent, RANK() OVER (ORDER BY count (distinct encode(owner, 'hex')) desc) rank_number + from orders o, total_users + where creation_timestamp >= now() - interval '1' hour + and encode(buy_token + , 'hex') not in ('a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + , '6b175474e89094c44da98b954eedeac495271d0f' + , 'dac17f958d2ee523a2206206994597c13d831ec7') + group by 1, 2, 3 + order by 4 desc + limit 5 + ) + + +-- most sold token in the past hour + + , top_sell_orders as ( + select + 'sell order' as message, case when encode(sell_token, 'hex')='eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' or + encode(sell_token, 'hex')='c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' then 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + else encode(sell_token, 'hex') end + as token, cnt_orders as group_col, count (*) as order_col, count (*)/ cast (cnt_orders as float) as percent, RANK() OVER (ORDER BY count (*) desc) rank_number + from orders o, total_users + where creation_timestamp >= now() - interval '1' hour + and encode(sell_token + , 'hex') not in ('a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + , '6b175474e89094c44da98b954eedeac495271d0f' + , 'dac17f958d2ee523a2206206994597c13d831ec7') + group by 1, 2, 3 + order by 4 desc + limit 5 + ) + + +-- most users (x %) prefered x token +-- example output: 22% of all users in the past hour bought ETH + + , top_sell_traders as ( + select + 'sell users' as message, case when encode(sell_token, 'hex')='eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' or + encode(sell_token, 'hex')='c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' then 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + else encode(sell_token, 'hex') end + as token, cnt_users as group_col, count (distinct encode(owner, 'hex')) as order_col, count (distinct encode(owner, 'hex'))/ cast (cnt_users as float) as percent, RANK() OVER (ORDER BY count (distinct encode(owner, 'hex')) desc) rank_number + from orders o, total_users + where creation_timestamp >= now() - interval '1' hour + and encode(sell_token + , 'hex') not in ('a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + , '6b175474e89094c44da98b954eedeac495271d0f' + , 'dac17f958d2ee523a2206206994597c13d831ec7') + group by 1, 2, 3 + order by 4 desc + limit 5 + ) + + select message, token, percent, rank_number + from top_buy_orders + + union all + + select message, token, percent, rank_number + from top_buy_traders + + union all + + select message, token, percent, rank_number + from top_sell_orders + + union all + + select message, token, percent, rank_number + from top_sell_traders + + order by 1, 3 desc +` \ No newline at end of file