Skip to content

Commit

Permalink
[Backend] login + pagination +fix (#98)
Browse files Browse the repository at this point in the history
* remove NODE_URL from env

* add padding on deployed address

* remove historical balance

* remove historical balance test

* add `before` support for the txs pagination

* fix node url on sepolia

* update account constructor args

* fix address computation

* implement basic login logic

* fix login

* add multiple balances var name support

* fix balances tracking

* improve tx history response spec

* remove duplicated fields in tx history

* add `startCursor` in tx history response

* keep tx history pageInfo order consistant

* add `transferId` to tx history response

* remove useless log
  • Loading branch information
0xChqrles authored Jun 28, 2024
1 parent 7e3b603 commit 6ef0c75
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 210 deletions.
4 changes: 3 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ DATABASE_URL=""

PORT=""

NODE_URL=""
NODE_API_KEY=""

SN_NETWORK=""

DEPLOYER_ADDRESS=""
DEPLOYER_PK=""
14 changes: 11 additions & 3 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import twilio from 'twilio'

import { fastifyDrizzle } from '@/db/plugin'

import { DEFAULT_NETWORK_NAME, NODE_URL } from './constants/contracts'
import { declareRoutes } from './routes'

export type AppConfiguration = {
Expand All @@ -30,24 +31,31 @@ export async function buildApp(config: AppConfiguration) {
throw new Error('Deployer address not set')
}

if (!process.env.NODE_URL) {
throw new Error('Starknet node url not set')
if (!process.env.NODE_API_KEY) {
throw new Error('Starknet node api key not set')
}

if (!process.env.SN_NETWORK) {
console.log(`Starknet network not set, falling back to default network (${DEFAULT_NETWORK_NAME})`)
}

if (!process.env.DEPLOYER_PK) {
throw new Error('Deployer private key not set')
}

if (!process.env.TWILIO_ACCOUNT_SSID) {
throw new Error('Twilio account ssid not set')
}

if (!process.env.TWILIO_AUTH_TOKEN) {
throw new Error('Twilio auth token not set')
}

if (!process.env.TWILIO_SERVICE_ID) {
throw new Error('Twilio service id not set')
}

const deployer = new Account({ nodeUrl: process.env.NODE_URL }, process.env.DEPLOYER_ADDRESS, process.env.DEPLOYER_PK)
const deployer = new Account({ nodeUrl: NODE_URL }, process.env.DEPLOYER_ADDRESS, process.env.DEPLOYER_PK)
const twilio_services = twilio(process.env.TWILIO_ACCOUNT_SSID, process.env.TWILIO_AUTH_TOKEN).verify.v2.services(
process.env.TWILIO_SERVICE_ID,
)
Expand Down
10 changes: 9 additions & 1 deletion backend/src/constants/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,16 @@ export const VAULT_FACTORY_ADDRESSES: AddressesMap = {
[constants.StarknetChainId.SN_SEPOLIA]: '0x33498f0d9e6ebef71b3d8dfa56501388cfe5ce96cba81503cd8572be92bd77c',
}

const DEFAULT_NETWORK_NAME = constants.NetworkName.SN_SEPOLIA
export const DEFAULT_NETWORK_NAME = constants.NetworkName.SN_SEPOLIA

// eslint-disable-next-line import/no-unused-modules
export const SN_CHAIN_ID = (constants.StarknetChainId[(process.env.SN_NETWORK ?? '') as constants.NetworkName] ??
constants.StarknetChainId[DEFAULT_NETWORK_NAME]) as SupportedChainId

const NODE_URLS = {
[constants.StarknetChainId.SN_MAIN]: (apiKey: string) => `https://rpc.nethermind.io/mainnet-juno/?apikey=${apiKey}`,
[constants.StarknetChainId.SN_SEPOLIA]: (apiKey: string) =>
`https://rpc.nethermind.io/sepolia-juno/?apikey=${apiKey}`,
}

export const NODE_URL = NODE_URLS[SN_CHAIN_ID](process.env.NODE_API_KEY!)
56 changes: 0 additions & 56 deletions backend/src/routes/getHistoricalBalance.ts

This file was deleted.

82 changes: 59 additions & 23 deletions backend/src/routes/getTransactionHistory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { and, desc, eq, lt, or, sql } from 'drizzle-orm'
import { and, asc, desc, eq, gt, lt, or, sql } from 'drizzle-orm'
import { PgSelectQueryBuilderBase } from 'drizzle-orm/pg-core'
import type { FastifyInstance } from 'fastify'

import { usdcTransfer } from '@/db/schema'
Expand All @@ -8,32 +9,53 @@ import { addressRegex } from '.'

const MAX_PAGE_SIZE = 20

function getCursorQuery(cursor?: string): Parameters<typeof and> {
interface GetCursorQueryResult {
where: Parameters<typeof and>
order: Parameters<PgSelectQueryBuilderBase<any, any, any, any>['orderBy']>
}

function getCursorQuery(cursor: string | undefined, isReversed: boolean): GetCursorQueryResult {
const [indexInBlock, timestamp] = fromCursorHash(cursor)

return [
or(
and(
timestamp ? eq(usdcTransfer.blockTimestamp, new Date(Number(timestamp) * 1000)) : undefined,
indexInBlock ? lt(usdcTransfer.indexInBlock, +indexInBlock) : undefined,
const sortingExpression = isReversed ? asc : desc
const compareExpression = isReversed ? gt : lt

return {
where: [
or(
and(
timestamp ? eq(usdcTransfer.blockTimestamp, new Date(Number(timestamp) * 1000)) : undefined,
indexInBlock ? compareExpression(usdcTransfer.indexInBlock, +indexInBlock) : undefined,
),
timestamp ? compareExpression(usdcTransfer.blockTimestamp, new Date(Number(timestamp) * 1000)) : undefined,
),
timestamp ? lt(usdcTransfer.blockTimestamp, new Date(Number(timestamp) * 1000)) : undefined,
),
]
],
order: [sortingExpression(usdcTransfer.blockTimestamp), sortingExpression(usdcTransfer.indexInBlock)],
}
}

interface CursorableTransaction {
index_in_block: number | null
transaction_timestamp: Date | null
}

function getCursor(tx: CursorableTransaction): string {
return toCursorHash((tx.index_in_block ?? 0).toString(), (tx.transaction_timestamp!.getTime() / 1000).toString())
}

interface TransactionHistoryQuery {
address?: string
first?: string
after?: string
before?: string
}

export function getTransactionHistory(fastify: FastifyInstance) {
fastify.get(
'/transaction_history',

async (request, reply) => {
const { address, first: firstStr, after } = request.query as TransactionHistoryQuery
const { address, first: firstStr, after, before } = request.query as TransactionHistoryQuery
const first = Number(firstStr ?? MAX_PAGE_SIZE)

if (!address) {
Expand All @@ -44,53 +66,67 @@ export function getTransactionHistory(fastify: FastifyInstance) {
return reply.status(400).send({ error: `First cannot exceed ${MAX_PAGE_SIZE}.` })
}

if (after && before) {
return reply.status(400).send({ error: 'After and before are clashing' })
}

// Validate address format
if (!addressRegex.test(address)) {
return reply.status(400).send({ error: 'Invalid address format.' })
}

const afterQuery = getCursorQuery(after)
const isReversed = !!before

const paginationQuery = getCursorQuery(after ?? before, isReversed)

try {
const txs = await fastify.db
.select({
transaction_timestamp: usdcTransfer.blockTimestamp,
amount: usdcTransfer.amount,
index_in_block: usdcTransfer.indexInBlock,
senderBalance: usdcTransfer.senderBalance,
recipientBalance: usdcTransfer.recipientBalance,
transferId: usdcTransfer.transferId,
from: {
nickname: sql`"from_user"."nickname"`,
contract_address: sql`"from_user"."contract_address"`,
phone_number: sql`"from_user"."phone_number"`,
balance: usdcTransfer.senderBalance,
},
to: {
nickname: sql`"to_user"."nickname"`,
contract_address: sql`"to_user"."contract_address"`,
phone_number: sql`"to_user"."phone_number"`,
balance: usdcTransfer.recipientBalance,
},
})
.from(usdcTransfer)
.leftJoin(sql`registration AS "from_user"`, eq(usdcTransfer.fromAddress, sql`"from_user"."contract_address"`))
.leftJoin(sql`registration AS "to_user"`, eq(usdcTransfer.toAddress, sql`"to_user"."contract_address"`))
.where(and(...afterQuery, or(eq(usdcTransfer.fromAddress, address), eq(usdcTransfer.toAddress, address))))
.where(
and(
...paginationQuery.where,
or(eq(usdcTransfer.fromAddress, address), eq(usdcTransfer.toAddress, address)),
),
)
.limit(Number(first) + 1)
.orderBy(desc(usdcTransfer.blockTimestamp), desc(usdcTransfer.indexInBlock))
.orderBy(...paginationQuery.order)
.execute()

// get pagination infos
const firstTx = txs[0] ?? null
const lastTx = txs.length ? txs[Math.min(txs.length - 1, first - 1)] : null

const endCursor = lastTx
? toCursorHash(
(lastTx.index_in_block ?? 0).toString(),
(lastTx.transaction_timestamp!.getTime() / 1000).toString(),
)
: null
const firstCursor = firstTx ? getCursor(firstTx) : null
const lastCursor = lastTx ? getCursor(lastTx) : null

const hasNext = txs.length > first

return reply.status(200).send({ items: txs.slice(0, first), endCursor, hasNext })
return reply.status(200).send({
items: txs.slice(0, first),
startCursor: isReversed ? lastCursor : firstCursor,
endCursor: isReversed ? firstCursor : lastCursor,
hasNext,
})
} catch (error) {
console.error(error)
return reply.status(500).send({ error: 'Internal server error' })
Expand Down
2 changes: 0 additions & 2 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { getClaimRoute } from './claim'
import { getExecuteFromOutsideRoute } from './executeFromOutside'
import { getGenerateClaimLinkRoute } from './generateClaimLink'
import { getCurrentExpenseRoute } from './getCurrentExpense'
import { getHistoricalBalanceRoute } from './getHistoricalBalance'
import { getLimitRoute } from './getLimit'
import { getOtp } from './getOtp'
import { getTransactionHistory } from './getTransactionHistory'
Expand All @@ -23,7 +22,6 @@ export function declareRoutes(fastify: FastifyInstance, deployer: Account, twili
getTransactionHistory(fastify)
getOtp(fastify, twilio_services.verifications)
verifyOtp(fastify, deployer, twilio_services.verificationChecks)
getHistoricalBalanceRoute(fastify)
getGenerateClaimLinkRoute(fastify)
getClaimRoute(fastify)
getLimitRoute(fastify)
Expand Down
19 changes: 14 additions & 5 deletions backend/src/routes/verifyOtp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm/pg-core/expressions'
import type { FastifyInstance } from 'fastify'
import { type Account, uint256 } from 'starknet'
import { type Account, addAddressPadding, uint256 } from 'starknet'
import { VerificationCheckListInstance } from 'twilio/lib/rest/verify/v2/service/verificationCheck'

import { Entrypoint, SN_CHAIN_ID, VAULT_FACTORY_ADDRESSES } from '@/constants/contracts'
Expand Down Expand Up @@ -62,21 +62,30 @@ export function verifyOtp(
})
}

// check if user is already registered
const user = (
await fastify.db.select().from(registration).where(eq(registration.phone_number, phone_number))
)[0]

// user is already registered
if (user.is_confirmed) {
return reply.code(200).send({
contract_address: user.contract_address,
})
}

// public key, approver, limit
const { transaction_hash } = await deployer.execute({
contractAddress: VAULT_FACTORY_ADDRESSES[SN_CHAIN_ID],
calldata: [
hashPhoneNumber(phone_number),
uint256.bnToUint256(public_key_x),
uint256.bnToUint256(public_key_y),
0,
1000000000,
0,
],
entrypoint: Entrypoint.DEPLOY_ACCOUNT,
})

const contractAddress = computeAddress(phone_number)
const contractAddress = addAddressPadding(computeAddress(phone_number))

fastify.log.info(
'Deploying account: ',
Expand Down
4 changes: 2 additions & 2 deletions backend/src/utils/address.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { hash } from 'starknet'

import { BLANK_ACCOUNT_CLASS_HASH, VAULT_FACTORY_ADDRESS } from '@/constants/contracts'
import { BLANK_ACCOUNT_CLASS_HASH, SN_CHAIN_ID, VAULT_FACTORY_ADDRESSES } from '@/constants/contracts'

import { hashPhoneNumber } from './phoneNumber'

Expand All @@ -9,6 +9,6 @@ export function computeAddress(phoneNumber: string) {
hashPhoneNumber(phoneNumber),
BLANK_ACCOUNT_CLASS_HASH,
[],
VAULT_FACTORY_ADDRESS,
VAULT_FACTORY_ADDRESSES[SN_CHAIN_ID],
)
}
Loading

0 comments on commit 6ef0c75

Please sign in to comment.