Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions pages/api/payments/paymentId/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Decimal } from '@prisma/client/runtime/library'
import { generatePaymentId } from 'services/transactionService'
import { parseAddress, parseCreatePaymentIdPOSTRequest } from 'utils/validators'
import { RESPONSE_MESSAGES } from 'constants/index'

export default async (req: any, res: any): Promise<void> => {
if (req.method === 'POST') {
try {
const values = parseCreatePaymentIdPOSTRequest(req.body)
const address = parseAddress(values.address)
const amount = values.amount as Decimal | undefined

const paymentId = await generatePaymentId(address, amount)

res.status(200).json({ paymentId })
} catch (error: any) {
switch (error.message) {
case RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.message:
res.status(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.statusCode).json(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400)
break
default:
res.status(500).json({ statusCode: 500, message: error.message })
}
}
} else {
res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED.statusCode)
.json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE `ClientPayment` (
`paymentId` VARCHAR(191) NOT NULL,
`status` ENUM('PENDING', 'ADDED_TO_MEMPOOL', 'CONFIRMED') NOT NULL,
`addressString` VARCHAR(191) NOT NULL,
`amount` DECIMAL(65, 30) NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,

PRIMARY KEY (`paymentId`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `ClientPayment` ADD CONSTRAINT `ClientPayment_addressString_fkey` FOREIGN KEY (`addressString`) REFERENCES `Address`(`address`) ON DELETE RESTRICT ON UPDATE CASCADE;
17 changes: 17 additions & 0 deletions prisma-local/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ model Address {
syncing Boolean @default(false)
paybuttons AddressesOnButtons[]
transactions Transaction[]
clientPayments ClientPayment[]

@@index([networkId], map: "Address_networkId_fkey")
}
Expand Down Expand Up @@ -271,3 +272,19 @@ model Invoice {
@@unique([invoiceNumber, userId], map: "Invoice_invoiceNumber_userId_unique_constraint")
@@unique([transactionId, userId], map: "Invoice_transactionId_userId_unique_constraint")
}

enum ClientPaymentStatus {
PENDING
ADDED_TO_MEMPOOL
CONFIRMED
}

model ClientPayment {
paymentId String @id
status ClientPaymentStatus
addressString String
amount Decimal?
address Address @relation(fields: [addressString], references: [address])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
31 changes: 29 additions & 2 deletions services/chronikService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import {
upsertTransaction,
getSimplifiedTransactions,
getSimplifiedTrasaction,
connectAllTransactionsToPrices
connectAllTransactionsToPrices,
updatePaymentStatus,
getClientPayment
} from './transactionService'
import { Address, Prisma } from '@prisma/client'
import { Address, Prisma, ClientPaymentStatus } from '@prisma/client'
import xecaddr from 'xecaddrjs'
import { getAddressPrefix, satoshisToUnit } from 'utils/index'
import { fetchAddressesArray, fetchAllAddressesForNetworkId, getEarliestUnconfirmedTxTimestampForAddress, getLatestConfirmedTxTimestampForAddress, setSyncing, setSyncingBatch, updateLastSynced, updateManyLastSynced } from './addressService'
Expand Down Expand Up @@ -551,10 +553,23 @@ export class ChronikBlockchainClient {
}
}

private async updateClientPaymentStatusToConfirmed (addressesWithTransactions: AddressWithTransaction[]): Promise<void> {
for (const addressWithTransaction of addressesWithTransactions) {
const parsedOpReturn = parseOpReturnData(addressWithTransaction.transaction.opReturn ?? '')
const paymentId = parsedOpReturn.paymentId
const newClientPaymentStatus = 'CONFIRMED' as ClientPaymentStatus

await updatePaymentStatus(paymentId, newClientPaymentStatus)
}
}

private async processWsMessage (msg: WsMsgClient): Promise<void> {
// delete unconfirmed transaction from our database
// if they were cancelled and not confirmed
if (msg.type === 'Tx') {
const transaction = await this.chronik.tx(msg.txid)
const addressesWithTransactions = await this.getAddressesForTransaction(transaction)

if (msg.msgType === 'TX_REMOVED_FROM_MEMPOOL') {
console.log(`${this.CHRONIK_MSG_PREFIX}: [${msg.msgType}] ${msg.txid}`)
const transactionsToDelete = await fetchUnconfirmedTransactions(msg.txid)
Expand All @@ -569,6 +584,7 @@ export class ChronikBlockchainClient {
} else if (msg.msgType === 'TX_CONFIRMED') {
console.log(`${this.CHRONIK_MSG_PREFIX}: [${msg.msgType}] ${msg.txid}`)
this.confirmedTxsHashesFromLastBlock = [...this.confirmedTxsHashesFromLastBlock, msg.txid]
await this.updateClientPaymentStatusToConfirmed(addressesWithTransactions)
} else if (msg.msgType === 'TX_ADDED_TO_MEMPOOL') {
if (this.isAlreadyBeingProcessed(msg.txid, false)) {
return
Expand All @@ -588,6 +604,17 @@ export class ChronikBlockchainClient {
if (created) { // only execute trigger for newly added txs
await executeAddressTriggers(broadcastTxData, tx.address.networkId)
}
const parsedOpReturn = parseOpReturnData(tx.opReturn)
const paymentId = parsedOpReturn.paymentId
const newClientPaymentStatus = 'ADDED_TO_MEMPOOL' as ClientPaymentStatus
const clientPayment = await getClientPayment(paymentId)
if (clientPayment.amount !== null) {
if (clientPayment.amount === tx.amount) {
await updatePaymentStatus(paymentId, newClientPaymentStatus)
}
} else {
await updatePaymentStatus(paymentId, newClientPaymentStatus)
}
}
}
this.mempoolTxsBeingProcessed -= 1
Expand Down
55 changes: 53 additions & 2 deletions services/transactionService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import prisma from 'prisma-local/clientInstance'
import { Prisma, Transaction } from '@prisma/client'
import { RESPONSE_MESSAGES, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES, UPSERT_TRANSACTION_PRICES_ON_DB_TIMEOUT, SupportedQuotesType, NETWORK_IDS, PRICES_CONNECTION_BATCH_SIZE, PRICES_CONNECTION_TIMEOUT } from 'constants/index'
import { Prisma, Transaction, ClientPaymentStatus } from '@prisma/client'
import { RESPONSE_MESSAGES, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES, UPSERT_TRANSACTION_PRICES_ON_DB_TIMEOUT, SupportedQuotesType, NETWORK_IDS, NETWORK_IDS_FROM_SLUGS, PRICES_CONNECTION_BATCH_SIZE, PRICES_CONNECTION_TIMEOUT } from 'constants/index'
import { fetchAddressBySubstring, fetchAddressById, fetchAddressesByPaybuttonId, addressExists } from 'services/addressService'
import { AllPrices, QuoteValues, fetchPricesForNetworkAndTimestamp, flattenTimestamp } from 'services/priceService'
import _ from 'lodash'
Expand All @@ -9,6 +9,8 @@ import { SimplifiedTransaction } from 'ws-service/types'
import { OpReturnData, parseAddress } from 'utils/validators'
import { generatePaymentFromTxWithInvoices } from 'redis/paymentCache'
import { ButtonDisplayData, Payment } from 'redis/types'
import { v4 as uuidv4 } from 'uuid'
import { multiBlockchainClient } from 'services/chronikService'

export function getTransactionValue (transaction: TransactionWithPrices | TransactionsWithPaybuttonsAndPrices | SimplifiedTransaction): QuoteValues {
const ret: QuoteValues = {
Expand Down Expand Up @@ -974,3 +976,52 @@ export const fetchDistinctPaymentYearsByUser = async (userId: string): Promise<n

return years.map(y => y.year)
}

export const generatePaymentId = async (address: string, amount?: Prisma.Decimal): Promise<string> => {
const rawUUID = uuidv4()
const cleanUUID = rawUUID.replace(/-/g, '')
const status = 'PENDING' as ClientPaymentStatus
const prefix = address.split(':')[0].toLowerCase()
const networkId = NETWORK_IDS_FROM_SLUGS[prefix]
const isAddressRegistered = await addressExists(address)

const clientPayment = await prisma.clientPayment.create({
data: {
address: {
connectOrCreate: {
where: { address },
create: {
address,
networkId
}
}
},
paymentId: cleanUUID,
status,
amount
},
include: {
address: true
}
})

if (!isAddressRegistered) {
void multiBlockchainClient.syncAndSubscribeAddresses([clientPayment.address])
}

return clientPayment.paymentId
}

export const updatePaymentStatus = async (paymentId: string, status: ClientPaymentStatus): Promise<void> => {
await prisma.clientPayment.update({
where: { paymentId },
data: { status }
})
}

export const getClientPayment = async (paymentId: string): Promise<Prisma.ClientPaymentGetPayload<{ include: { address: true } }>> => {
return await prisma.clientPayment.findUniqueOrThrow({
where: { paymentId },
include: { address: true }
})
}
22 changes: 22 additions & 0 deletions utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,3 +566,25 @@ export interface CreateInvoicePOSTParameters {
customerName: string
customerAddress: string
}
export interface CreatePaymentIdPOSTParameters {
address?: string
amount?: string
}
export interface CreatePaymentIdInput {
address: string
amount?: string
}

export const parseCreatePaymentIdPOSTRequest = function (params: CreatePaymentIdPOSTParameters): CreatePaymentIdInput {
if (
params.address === undefined ||
params.address === ''
) {
throw new Error(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.message)
}

return {
address: params.address,
amount: params.amount === '' ? undefined : params.amount
}
}