UNPKG

@ocap/indexdb-memory

Version:

OCAP indexdb adapter that uses memory as backend, just for test purpose

590 lines (588 loc) 22.2 kB
import { name } from "../package.mjs"; import { BaseIndexDB, formatDelegationAfterRead, formatNextPagination, formatPagination, formatSearchResult, parseDateTime } from "@ocap/indexdb"; import debugFactory from "debug"; import { toChecksumAddress } from "@arcblock/did/lib/type"; import { BN } from "@ocap/util"; import { DEFAULT_TOKEN_DECIMAL } from "@ocap/util/lib/constant"; import omit from "lodash/omit.js"; //#region src/db/base.ts const debug = debugFactory(name); const MAX_REQUEST_FACTORY_ADDRESS_SIZE = 100; const SEARCH_LIMIT_PER_TABLE = 5; /** Score a match: 3=exact, 2=prefix, 1=contains */ function scoreMatch(fieldValue, keyword) { const lower = fieldValue.toLowerCase(); if (lower === keyword) return 3; if (lower.startsWith(keyword)) return 2; return 1; } /** Helper to safely get collection from IIndexTable */ function getCollection(table) { return table.collection; } function applyTimeFilter(query, timeFilter, defaultField, validFields) { let { startDateTime, endDateTime, field = defaultField } = timeFilter; startDateTime = parseDateTime(startDateTime); endDateTime = parseDateTime(endDateTime); if (!validFields.includes(field)) field = defaultField; if (startDateTime && endDateTime) query.where((x) => x[field] > startDateTime && x[field] <= endDateTime); else if (startDateTime) query.where((x) => x[field] > startDateTime); else if (endDateTime) query.where((x) => x[field] <= endDateTime); } function applyArrayFilter(query, items, fieldName, isArrayField) { if (!items.length) return; if (isArrayField) query.where((x) => x[fieldName].some((f) => items.includes(f))); else query.where((x) => items.includes(x[fieldName])); } function buildPagingResult(total, pagination, itemCount) { return { cursor: String(pagination.cursor + itemCount), next: itemCount >= pagination.size, total }; } var LocalBaseIndexDB = class extends BaseIndexDB { listTransactions(params = {}) { const { addressFilter = {}, paging = {}, timeFilter = {}, typeFilter = {}, assetFilter = {}, factoryFilter = {}, tokenFilter = {}, accountFilter = {}, validityFilter = {}, txFilter = {}, rollupFilter = {}, stakeFilter = {}, delegationFilter = {}, tokenFactoryFilter = {}, includeItxData = false } = params; const query = getCollection(this.tx).chain(); const { sender, receiver, direction = "UNION" } = addressFilter; const { types = [] } = typeFilter; const { factories = [] } = factoryFilter; const { assets = [] } = assetFilter; const { tokens = [] } = tokenFilter; const { accounts = [] } = accountFilter; const { txs = [] } = txFilter; const { rollups = [] } = rollupFilter; const { stakes = [] } = stakeFilter; const { delegations = [] } = delegationFilter; const { tokenFactories = [] } = tokenFactoryFilter; const pagination = formatPagination({ paging, defaultSortField: "time" }); debug("listTransactions", { sender, receiver, pagination, types, factories, assets, tokens, accounts, rollups, stakes, delegations, includeItxData }); if (sender && receiver) { if (direction === "MUTUAL") query.where((x) => x.sender === sender && x.receiver === receiver || x.sender === receiver && x.receiver === sender); else if (direction === "ONE_WAY") query.where((x) => x.sender === sender && x.receiver === receiver); else if (direction === "UNION") query.where((x) => x.sender === sender || x.receiver === receiver); } else if (sender) query.where((x) => x.sender === sender); else if (receiver) query.where((x) => x.receiver === receiver); applyArrayFilter(query, types, "type", false); applyArrayFilter(query, factories, "factories", true); applyArrayFilter(query, assets, "assets", true); applyArrayFilter(query, tokens, "tokens", true); applyArrayFilter(query, accounts, "accounts", true); applyArrayFilter(query, tokenFactories, "tokenFactories", true); applyArrayFilter(query, txs, "hash", false); applyArrayFilter(query, rollups, "rollups", true); applyArrayFilter(query, stakes, "stakes", true); applyArrayFilter(query, delegations, "delegations", true); applyTimeFilter(query, timeFilter, "time", ["time"]); const { validity } = validityFilter; if (validity === "VALID") query.where((x) => x.valid === true); else if (validity === "INVALID") query.where((x) => x.valid === false); let transactions = query.simplesort(pagination.order.field, pagination.order.type === "desc").offset(pagination.cursor).limit(pagination.size).data(); if (!includeItxData) transactions = transactions.map((tx) => { if (tx.tx && tx.tx?.itxJson) return { ...tx, tx: { ...tx.tx, itxJson: omit(tx.tx.itxJson, ["data"]) } }; return tx; }); const total = query.count(); return { transactions, paging: buildPagingResult(total, pagination, transactions.length) }; } async getRelatedAddresses(address) { let account = await this.account.get(address); if (!account) return []; const related = [address]; while (account?.migratedFrom && related.length <= 8) { const migratedFrom = account.migratedFrom; related.push(migratedFrom); account = await this.account.get(migratedFrom); } return related.filter(Boolean); } async listAssets(params = {}) { const { ownerAddress, factoryAddress, paging, timeFilter = {} } = params; const pagination = formatPagination({ paging, defaultSortField: "renaissanceTime" }); const query = getCollection(this.asset).chain(); if (ownerAddress) { const possibleOwners = await this.getRelatedAddresses(ownerAddress); if (possibleOwners.length) query.where((x) => possibleOwners.includes(x.owner)); else return { assets: [], account: null, paging: { cursor: "0", next: false, total: 0 } }; } if (factoryAddress) query.where((x) => x.parent === factoryAddress); applyTimeFilter(query, timeFilter, "renaissanceTime", [ "genesisTime", "renaissanceTime", "consumedTime" ]); debug("listAssets", { ownerAddress, factoryAddress, timeFilter, paging, pagination }); const assets = query.simplesort(pagination.order.field, pagination.order.type === "desc").offset(pagination.cursor).limit(pagination.size).data(); const total = query.count(); return { assets, account: ownerAddress ? await this.account.get(ownerAddress) : null, paging: formatNextPagination(total, pagination) }; } async listTopAccounts(params = {}) { const { paging, tokenAddress } = params; const query = getCollection(this.account).chain(); const pagination = formatPagination({ paging, defaultSortField: tokenAddress ? "balance" : "renaissanceTime", supportedSortFields: [ "genesisTime", "renaissanceTime", "balance", "moniker" ] }); if (pagination.order.field === "balance" && !tokenAddress) throw new Error("tokenAddress is required when order.field is balance"); debug("listTopAccounts", { tokenAddress, paging, pagination }); if (tokenAddress) query.where((x) => { const tokens = x.tokens; if (!tokens) return false; const owned = tokens.find((t) => t.address === tokenAddress); return owned ? owned.balance > "0" : false; }); if (pagination.order.field === "balance") { const descending = pagination.order.type === "desc"; if (tokenAddress) query.sort((a, b) => { const tokensA = a.tokens; const tokensB = b.tokens; const tokenA = tokensA?.find((t) => t.address === tokenAddress); const tokenB = tokensB?.find((t) => t.address === tokenAddress); const balanceA = new BN(tokenA ? tokenA.balance : "0"); const balanceB = new BN(tokenB ? tokenB.balance : "0"); if (balanceB.gt(balanceA)) return descending ? 1 : -1; if (balanceB.eq(balanceA)) return 0; return descending ? -1 : 1; }); else query.sort((a, b) => { const balanceA = new BN(a.balance || "0"); const balanceB = new BN(b.balance || "0"); if (balanceB.gt(balanceA)) return descending ? 1 : -1; if (balanceB.eq(balanceA)) return 0; return descending ? -1 : 1; }); } else query.simplesort(pagination.order.field, pagination.order.type === "desc"); const accounts = query.offset(pagination.cursor).limit(pagination.size).data(); const total = query.count(); accounts.forEach((account) => { const acc = account; if (Array.isArray(acc.tokens) && acc.tokens.length) acc.tokens = acc.tokens.map((token) => { token.decimal = typeof token.decimal === "undefined" ? DEFAULT_TOKEN_DECIMAL : token.decimal; return token; }); }); return { accounts, paging: formatNextPagination(total, pagination) }; } async listTokens(params = {}) { const { issuerAddress, paging } = params; const conditions = {}; if (issuerAddress) conditions.issuer = issuerAddress; const pagination = formatPagination({ paging, defaultSortField: "renaissanceTime", supportedSortFields: [ "genesisTime", "renaissanceTime", "issuer", "symbol", "totalSupply" ] }); debug("listTokens", { issuerAddress, paging, conditions, pagination }); let tokens = getCollection(this.token).chain().find(conditions).simplesort(pagination.order.field, pagination.order.type === "desc").offset(pagination.cursor).limit(pagination.size).data(); tokens = tokens.map((token) => { const t = token; t.decimal = typeof t.decimal === "undefined" ? DEFAULT_TOKEN_DECIMAL : t.decimal; return t; }); const total = this.token.count(Object.keys(conditions).length ? conditions : void 0); return { tokens, paging: formatNextPagination(total, pagination) }; } async listTokenFactories(params = {}) { const { owner, reserveAddress, tokenAddress, paging } = params; const conditions = {}; if (owner) conditions.owner = owner; if (reserveAddress) conditions.reserveAddress = reserveAddress; if (tokenAddress) conditions.tokenAddress = tokenAddress; const pagination = formatPagination({ paging, defaultSortField: "renaissanceTime", supportedSortFields: [ "genesisTime", "renaissanceTime", "reserveBalance", "currentSupply" ] }); debug("listTokenFactories", { owner, reserveAddress, tokenAddress, paging, conditions, pagination }); return { tokenFactories: getCollection(this.tokenFactory).chain().find(conditions).simplesort(pagination.order.field, pagination.order.type === "desc").offset(pagination.cursor).limit(pagination.size).data(), paging: formatNextPagination(this.tokenFactory.count(Object.keys(conditions).length ? conditions : void 0), pagination) }; } async listFactories(params = {}) { const { ownerAddress, addressList, paging } = params; const conditions = {}; if (ownerAddress) conditions.owner = ownerAddress; if (Array.isArray(addressList) && addressList.length > 0) { if (addressList.length > MAX_REQUEST_FACTORY_ADDRESS_SIZE) throw new Error(`The length of 'addressList' cannot exceed the length of ${MAX_REQUEST_FACTORY_ADDRESS_SIZE}`); conditions.address = { $in: addressList }; } const pagination = formatPagination({ paging, defaultSortField: "renaissanceTime", supportedSortFields: [ "genesisTime", "renaissanceTime", "owner", "numMinted", "limit", "name" ] }); debug("listFactories", { ownerAddress, paging, conditions, pagination }); return { factories: getCollection(this.factory).chain().find(conditions).simplesort(pagination.order.field, pagination.order.type === "desc").offset(pagination.cursor).limit(pagination.size).data(), paging: formatNextPagination(this.factory.count(Object.keys(conditions).length ? conditions : void 0), pagination) }; } listStakes(params = {}) { const { addressFilter = {}, paging = {}, timeFilter = {}, assetFilter = {} } = params; const query = getCollection(this.stake).chain(); const { sender, receiver } = addressFilter; const { assets = [] } = assetFilter; const pagination = formatPagination({ paging, defaultSortField: "renaissanceTime", supportedSortFields: ["genesisTime", "renaissanceTime"] }); if (sender && receiver) query.where((x) => x.sender === sender && x.receiver === receiver); else if (sender) query.where((x) => x.sender === sender); else if (receiver) query.where((x) => x.receiver === receiver); applyArrayFilter(query, assets, "assets", true); applyTimeFilter(query, timeFilter, "renaissanceTime", ["genesisTime", "renaissanceTime"]); const stakes = query.simplesort(pagination.order.field, pagination.order.type === "desc").offset(pagination.cursor).limit(pagination.size).data(); return { stakes, paging: buildPagingResult(query.count(), pagination, stakes.length) }; } async listRollups(params = {}) { const { paging, tokenAddress = "", erc20TokenAddress = "", foreignTokenAddress = "" } = params; const pagination = formatPagination({ paging, defaultSortField: "renaissanceTime", supportedSortFields: ["genesisTime", "renaissanceTime"] }); const query = getCollection(this.rollup).chain(); if (tokenAddress) query.where((x) => x.tokenAddress === tokenAddress); const foreignTokenAddr = foreignTokenAddress || erc20TokenAddress; if (foreignTokenAddr) { const checksumAddr = toChecksumAddress(foreignTokenAddr); const lowerAddr = foreignTokenAddr.toLowerCase(); query.where((x) => { const contractAddr = x.foreignToken?.contractAddress; return contractAddr === checksumAddr || contractAddr?.toLowerCase() === lowerAddr; }); } debug("listRollups", { paging, pagination }); return { rollups: query.simplesort(pagination.order.field, pagination.order.type === "desc").offset(pagination.cursor).limit(pagination.size).data(), paging: formatNextPagination(query.count(), pagination) }; } listRollupBlocks(params = {}) { const { paging = {}, rollupAddress = "", tokenAddress = "", proposer = "", height = "", timeFilter = {}, txFilter = {}, validatorFilter = {} } = params; const query = getCollection(this.rollupBlock).chain(); const { txs = [] } = txFilter; const { validators = [] } = validatorFilter; const pagination = formatPagination({ paging, defaultSortField: "genesisTime", supportedSortFields: ["genesisTime", "renaissanceTime"] }); if (rollupAddress) query.where((x) => x.rollup === rollupAddress); if (Number(height) > 0) query.where((x) => Number(x.height) === Number(height)); if (proposer) query.where((x) => x.proposer === proposer); if (tokenAddress) query.where((x) => x.tokenInfo.address === tokenAddress); applyArrayFilter(query, txs, "txs", true); applyArrayFilter(query, validators, "validators", true); applyTimeFilter(query, timeFilter, "genesisTime", ["genesisTime", "renaissanceTime"]); const blocks = query.simplesort(pagination.order.field, pagination.order.type === "desc").offset(pagination.cursor).limit(pagination.size).data(); return { blocks, paging: buildPagingResult(query.count(), pagination, blocks.length) }; } listRollupValidators(params = {}) { const { paging = {}, rollupAddress = "" } = params; const query = getCollection(this.rollupValidator).chain(); const pagination = formatPagination({ paging, defaultSortField: "genesisTime", supportedSortFields: [ "joinTime", "leaveTime", "genesisTime", "renaissanceTime" ] }); if (rollupAddress) query.where((x) => x.rollup === rollupAddress); const validators = query.simplesort(pagination.order.field, pagination.order.type === "desc").offset(pagination.cursor).limit(pagination.size).data(); return { validators, paging: buildPagingResult(query.count(), pagination, validators.length) }; } listDelegations(params = {}) { const { from, to, paging = {}, timeFilter = {} } = params; const query = getCollection(this.delegation).chain(); const pagination = formatPagination({ paging, defaultSortField: "renaissanceTime", supportedSortFields: ["genesisTime", "renaissanceTime"] }); if (from && to) query.where((x) => x.from === from && x.to === to); else if (from) query.where((x) => x.from === from); else if (to) query.where((x) => x.to === to); applyTimeFilter(query, timeFilter, "renaissanceTime", ["genesisTime", "renaissanceTime"]); const delegations = query.simplesort(pagination.order.field, pagination.order.type === "desc").offset(pagination.cursor).limit(pagination.size).data(); const total = query.count(); return { delegations: delegations.map((d) => formatDelegationAfterRead(d)), paging: buildPagingResult(total, pagination, delegations.length) }; } /** * Search entities by semantic fields (moniker, name, symbol, description) */ search(keyword) { const trimmed = keyword?.trim(); if (!trimmed) return []; const lowerKw = trimmed.toLowerCase(); const typePriority = { token: 0, account: 1, factory: 2, asset: 3, tokenFactory: 4, stake: 5 }; const results = []; const tokenMatches = getCollection(this.token).chain().where((x) => { const name$1 = x.name; const symbol = x.symbol; const description = x.description; return (name$1 ? name$1.toLowerCase().includes(lowerKw) : false) || (symbol ? symbol.toLowerCase().includes(lowerKw) : false) || (description ? description.toLowerCase().includes(lowerKw) : false); }).limit(SEARCH_LIMIT_PER_TABLE).data(); for (const row of tokenMatches) { const nameScore = row.name ? scoreMatch(row.name, lowerKw) : 0; const symbolScore = row.symbol ? scoreMatch(row.symbol, lowerKw) : 0; const descScore = row.description ? scoreMatch(row.description, lowerKw) : 0; const score = Math.max(nameScore, symbolScore, descScore); results.push({ ...formatSearchResult("token", row), score, priority: typePriority.token }); } const accountMatches = getCollection(this.account).chain().where((x) => { const moniker = x.moniker; return moniker ? moniker.toLowerCase().includes(lowerKw) : false; }).limit(SEARCH_LIMIT_PER_TABLE).data(); for (const row of accountMatches) { const score = scoreMatch(row.moniker, lowerKw); results.push({ ...formatSearchResult("account", row), score, priority: typePriority.account }); } const factoryMatches = getCollection(this.factory).chain().where((x) => { const name$1 = x.name; const description = x.description; return (name$1 ? name$1.toLowerCase().includes(lowerKw) : false) || (description ? description.toLowerCase().includes(lowerKw) : false); }).limit(SEARCH_LIMIT_PER_TABLE).data(); for (const row of factoryMatches) { const nameScore = row.name ? scoreMatch(row.name, lowerKw) : 0; const descScore = row.description ? scoreMatch(row.description, lowerKw) : 0; const score = Math.max(nameScore, descScore); results.push({ ...formatSearchResult("factory", row), score, priority: typePriority.factory }); } const assetMatches = getCollection(this.asset).chain().where((x) => { const moniker = x.moniker; const tags = x.tags; return (moniker ? moniker.toLowerCase().includes(lowerKw) : false) || Array.isArray(tags) && tags.some((t) => t.toLowerCase().includes(lowerKw)); }).limit(SEARCH_LIMIT_PER_TABLE).data(); for (const row of assetMatches) { const monikerScore = row.moniker ? scoreMatch(row.moniker, lowerKw) : 0; const tags = row.tags; const tagScore = Array.isArray(tags) ? Math.max(0, ...tags.map((t) => scoreMatch(t, lowerKw))) : 0; const score = Math.max(monikerScore, tagScore); results.push({ ...formatSearchResult("asset", row), score, priority: typePriority.asset }); } const tokenFactoryMatches = getCollection(this.tokenFactory).chain().where((x) => { const name$1 = x.name; const moniker = x.moniker; return (name$1 ? name$1.toLowerCase().includes(lowerKw) : false) || (moniker ? moniker.toLowerCase().includes(lowerKw) : false); }).limit(SEARCH_LIMIT_PER_TABLE).data(); for (const row of tokenFactoryMatches) { const nameScore = row.name ? scoreMatch(row.name, lowerKw) : 0; const monikerScore = row.moniker ? scoreMatch(row.moniker, lowerKw) : 0; const score = Math.max(nameScore, monikerScore); results.push({ ...formatSearchResult("tokenFactory", row), score, priority: typePriority.tokenFactory }); } const stakeMatches = getCollection(this.stake).chain().where((x) => { const message = x.message; return message ? message.toLowerCase().includes(lowerKw) : false; }).limit(SEARCH_LIMIT_PER_TABLE).data(); for (const row of stakeMatches) { const score = scoreMatch(row.message, lowerKw); results.push({ ...formatSearchResult("stake", row), score, priority: typePriority.stake }); } if (/^(did:abt:)?(z[1-9A-HJ-NP-Za-km-z]|0x[0-9a-fA-F])/.test(trimmed)) { const didPrefix = trimmed.replace(/^did:abt:/i, ""); if (didPrefix.length >= 4) { const seen = new Set(results.map((r) => `${r.type}:${r.id}`)); const collections = [ { collection: this.account, type: "account" }, { collection: this.token, type: "token" }, { collection: this.asset, type: "asset" }, { collection: this.factory, type: "factory" }, { collection: this.tokenFactory, type: "tokenFactory" }, { collection: this.stake, type: "stake" } ]; for (const { collection, type } of collections) { const matches = getCollection(collection).chain().where((x) => { const addr = x.address; return addr ? addr.startsWith(didPrefix) : false; }).limit(SEARCH_LIMIT_PER_TABLE).data(); for (const row of matches) { const addr = row.address; const key = `${type}:${addr}`; if (!seen.has(key)) { seen.add(key); results.push({ ...formatSearchResult(type, row, { id: addr }), score: 2, priority: typePriority[type] ?? 99 }); } } } } } results.sort((a, b) => b.score - a.score || a.priority - b.priority); return results.map(({ score, priority, ...result }) => result); } }; var base_default = LocalBaseIndexDB; //#endregion export { base_default as default };