@ocap/indexdb-memory
Version:
OCAP indexdb adapter that uses memory as backend, just for test purpose
590 lines (588 loc) • 22.2 kB
JavaScript
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 };