Skip to content
Open
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
23 changes: 22 additions & 1 deletion src/initDbs.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
DatabaseSetup,
JsDesignDocument,
makeJsDesign,
makeMangoIndex,
MangoDesignDocument,
setupDatabase
} from 'edge-server-tools'

import { config } from './config'
import { fixJs } from './util/fixJs'

interface DesignDocumentMap {
[designDocName: string]: MangoDesignDocument | JsDesignDocument
Expand Down Expand Up @@ -73,7 +75,26 @@ const transactionsDatabaseSetup: DatabaseSetup = {
name: 'reports_transactions',
options: { partitioned: true },
documents: {
...transactionIndexes
...transactionIndexes,
'_design/getTxInfo': makeJsDesign(
'payoutHashfixByDate',
({ emit }) => ({
map: function(doc) {
const space = 1099511627776 // 5 bytes of space; 2^40
const prime = 769 // large prime number
let hashfix = 0 // the final hashfix
for (let i = 0; i < doc.payoutAddress.length; i++) {
const byte = doc.payoutAddress.charCodeAt(i)
hashfix = (hashfix * prime + byte) % space
}
emit([hashfix, doc.isoDate], doc._id)
}
}),
{
fixJs,
partitioned: false
}
)
}
}
const progressCacheDatabaseSetup: DatabaseSetup = {
Expand Down
122 changes: 74 additions & 48 deletions src/routes/v1/getTxInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Router from 'express-promise-router'
import { reportsTransactions } from '../../indexApi'
import { asDbTx, DbTx, Status } from '../../types'
import { EdgeTokenId } from '../../util/asEdgeTokenId'
import { asNumberString } from '../../util/asNumberString'
import { HttpError } from '../../util/httpErrors'
import { trial } from '../../util/trail'

Expand All @@ -12,7 +13,7 @@ const asGetTxInfoReq = asObject({
* Prefix of the destination address.
* Minimum 3 character; Maximum 5 characters.
*/
addressPrefix: asString,
addressHashfix: asNumberString,

/**
* ISO date string to start searching for transactions.
Expand All @@ -26,18 +27,24 @@ const asGetTxInfoReq = asObject({
})

interface TxInfo {
providerId: string
orderId: string
isoDate: string
sourceAmount: number // exchangeAmount units
sourceCurrencyCode: string
sourcePluginId?: string
sourceTokenId?: EdgeTokenId
swapInfo: SwapInfo

deposit: AssetInfo
payout: AssetInfo
}

interface AssetInfo {
address: string
pluginId: string
tokenId: EdgeTokenId
amount: number
}

interface SwapInfo {
orderId: string
pluginId: string
status: Status
destinationAddress?: string
destinationAmount: number // exchangeAmount units
destinationPluginId?: string
destinationTokenId?: EdgeTokenId
}

export const getTxInfoRouter = Router()
Expand All @@ -50,62 +57,81 @@ getTxInfoRouter.get('/', async function(req, res) {
}
)

if (query.addressPrefix.length < 3) {
res.status(400).send('addressPrefix must be at least 3 characters')
if (query.addressHashfix < 0 || query.addressHashfix > 2 ** 40) {
res.status(400).send('addressHashfix must be between 0 and 2^40')
return
}

const startDate = new Date(query.startIsoDate)
const endDate = new Date(query.endIsoDate)
const startKey = [query.addressHashfix, startDate.toISOString()]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start and end have the same hash. This doesn't allow for any privacy to ensure we don't exactly know the users addresss.

If this were a standard sha256 hash into hex, we could use the first N chars as the prefix to allow for privacy

Copy link
Collaborator Author

@samholmes samholmes Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hashfix is not collision resistant. It is a "hash prefix" because it takes 5 bytes from the address (5 bytes is 5 chars). The collision space is 5 bytes, or 40 bits (2^40). We can lower the collision space to 3-bytes which would have many more collisions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTS comments:

  1. Use a large enough space size of (I recall 64^6)
  2. Use mango queries to do range queries on the hashfix value

The goal is to be flexible in our design. We want to query different collision space over time rather than be locked to some byte size. To do this we need to do range queries on the hashfix. So the space for the hashfix is a maximum, but the range will determine the actual collision space.

const endKey = [query.addressHashfix, endDate.toISOString()]

const addressKey = query.addressPrefix.slice(
0,
Math.min(query.addressPrefix.length, 5)
const results = await reportsTransactions.view(
'getTxInfo',
'payoutHashfixByDate',
{
start_key: startKey,
end_key: endKey,
inclusive_end: true,
include_docs: true
}
)

const results = await reportsTransactions.find({
selector: {
payoutAddress: {
$gte: addressKey,
$lte: addressKey + '\uffff'
},
isoDate: {
$gte: startDate.toISOString(),
$lte: endDate.toISOString()
}
const rows = results.rows
.map(row => asMaybe(asDbTx)(row.doc))
.filter((item): item is DbTx => item != null)

const txs: TxInfo[] = rows.map(row => {
const swapInfo = getSwapInfo(row)

const deposit: AssetInfo = {
// TODO: Remove empty strings after db migration
address: row.depositAddress ?? '',
pluginId: '',
tokenId: '',
amount: row.depositAmount
}
})

const rows = results.docs
.map(doc => asMaybe(asDbTx)(doc))
.filter((item): item is DbTx => item != null)
const payout: AssetInfo = {
// TODO: Remove empty strings after db migration
address: row.payoutAddress ?? '',
pluginId: '',
tokenId: '',
amount: row.payoutAmount
}

const result: TxInfo = {
swapInfo,
deposit,
payout,
isoDate: row.isoDate
}

const txs: TxInfo[] = rows.map(row => ({
providerId: getProviderId(row),
orderId: row.orderId,
isoDate: row.isoDate,
sourceAmount: row.depositAmount,
sourceCurrencyCode: row.depositCurrency,
status: row.status,
// TODO: Infer the pluginId and tokenId from the document:
// sourcePluginId?: string,
// sourceTokenId?: EdgeTokenId,
destinationAddress: row.payoutAddress,
destinationAmount: row.payoutAmount
// TODO: Infer the pluginId and tokenId from the document:
// destinationPluginId?: string
// destinationTokenId?: EdgeTokenId
}))
return result
})

res.send({
txs
})
})

/*
Returns the providerId from the document id.
Returns the pluginId from the document id.
For example: edge_switchain:<orderId> -> switchain
*/
function getProviderId(row: DbTx): string {
function getPluginId(row: DbTx): string {
return row._id?.split(':')[0].split('_')[1] ?? ''
}

function getSwapInfo(row: DbTx): SwapInfo {
const pluginId = getPluginId(row)
const orderId = row.orderId
const status = row.status

return {
orderId,
pluginId,
status
}
}
10 changes: 10 additions & 0 deletions src/util/asNumberString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { asString, Cleaner } from 'cleaners'

export const asNumberString: Cleaner<number> = (v: unknown): number => {
const str = asString(v)
const n = Number(str)
if (n.toString() !== str) {
throw new TypeError('Expected number string')
}
return n
}
Loading