wallet-core: put signing keys in separate object store
This commit is contained in:
parent
bd88dcebbc
commit
a41d1ee53e
@ -26,6 +26,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
IDBCursorDirection,
|
IDBCursorDirection,
|
||||||
IDBCursorWithValue,
|
IDBCursorWithValue,
|
||||||
|
IDBDatabase,
|
||||||
IDBKeyRange,
|
IDBKeyRange,
|
||||||
IDBValidKey,
|
IDBValidKey,
|
||||||
} from "./idbtypes.js";
|
} from "./idbtypes.js";
|
||||||
@ -439,6 +440,45 @@ test("update with non-existent index values", async (t) => {
|
|||||||
t.pass();
|
t.pass();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("delete from unique index", async (t) => {
|
||||||
|
const backend = new MemoryBackend();
|
||||||
|
backend.enableTracing = true;
|
||||||
|
const idb = new BridgeIDBFactory(backend);
|
||||||
|
const request = idb.open("mydb");
|
||||||
|
request.onupgradeneeded = () => {
|
||||||
|
const db = request.result as IDBDatabase;
|
||||||
|
const store = db.createObjectStore("bla", { keyPath: "x" });
|
||||||
|
store.createIndex("by_yz", ["y", "z"], {
|
||||||
|
unique: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const db: BridgeIDBDatabase = await promiseFromRequest(request);
|
||||||
|
|
||||||
|
t.is(db.name, "mydb");
|
||||||
|
|
||||||
|
{
|
||||||
|
const tx = db.transaction("bla", "readwrite");
|
||||||
|
const store = tx.objectStore("bla");
|
||||||
|
store.put({ x: 0, y: "a", z: 42 });
|
||||||
|
const index = store.index("by_yz");
|
||||||
|
const indRes = await promiseFromRequest(index.get(["a", 42]));
|
||||||
|
t.is(indRes.x, 0);
|
||||||
|
const res = await promiseFromRequest(store.get(0));
|
||||||
|
t.is(res.z, 42);
|
||||||
|
await promiseFromTransaction(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const tx = db.transaction("bla", "readwrite");
|
||||||
|
const store = tx.objectStore("bla");
|
||||||
|
store.put({ x: 0, y: "a", z: 42, extra: 123 });
|
||||||
|
await promiseFromTransaction(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
t.pass();
|
||||||
|
});
|
||||||
|
|
||||||
test("range queries", async (t) => {
|
test("range queries", async (t) => {
|
||||||
const backend = new MemoryBackend();
|
const backend = new MemoryBackend();
|
||||||
backend.enableTracing = true;
|
backend.enableTracing = true;
|
||||||
|
@ -51,6 +51,8 @@ import {
|
|||||||
TransactionIdStr,
|
TransactionIdStr,
|
||||||
CoinRefreshRequest,
|
CoinRefreshRequest,
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
|
EddsaPublicKeyString,
|
||||||
|
EddsaSignatureString,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { RetryInfo, RetryTags } from "./util/retries.js";
|
import { RetryInfo, RetryTags } from "./util/retries.js";
|
||||||
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
|
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
|
||||||
@ -409,11 +411,26 @@ export namespace DenominationRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExchangeSignkeysRecord {
|
||||||
|
stampStart: TalerProtocolTimestamp;
|
||||||
|
stampExpire: TalerProtocolTimestamp;
|
||||||
|
stampEnd: TalerProtocolTimestamp;
|
||||||
|
signkeyPub: EddsaPublicKeyString;
|
||||||
|
masterSig: EddsaSignatureString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchange details that thiis signkeys record belongs to.
|
||||||
|
*/
|
||||||
|
exchangeDetailsRowId: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exchange details for a particular
|
* Exchange details for a particular
|
||||||
* (exchangeBaseUrl, masterPublicKey, currency) tuple.
|
* (exchangeBaseUrl, masterPublicKey, currency) tuple.
|
||||||
*/
|
*/
|
||||||
export interface ExchangeDetailsRecord {
|
export interface ExchangeDetailsRecord {
|
||||||
|
rowId?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master public key of the exchange.
|
* Master public key of the exchange.
|
||||||
*/
|
*/
|
||||||
@ -445,14 +462,6 @@ export interface ExchangeDetailsRecord {
|
|||||||
*/
|
*/
|
||||||
globalFees: ExchangeGlobalFees[];
|
globalFees: ExchangeGlobalFees[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Signing keys we got from the exchange, can also contain
|
|
||||||
* older signing keys that are not returned by /keys anymore.
|
|
||||||
*
|
|
||||||
* FIXME: Should this be put into a separate object store?
|
|
||||||
*/
|
|
||||||
signingKeys: ExchangeSignKeyJson[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Etag of the current ToS of the exchange.
|
* Etag of the current ToS of the exchange.
|
||||||
*/
|
*/
|
||||||
@ -1892,9 +1901,29 @@ export const WalletStoresV1 = {
|
|||||||
exchangeDetails: describeStore(
|
exchangeDetails: describeStore(
|
||||||
"exchangeDetails",
|
"exchangeDetails",
|
||||||
describeContents<ExchangeDetailsRecord>({
|
describeContents<ExchangeDetailsRecord>({
|
||||||
keyPath: ["exchangeBaseUrl", "currency", "masterPublicKey"],
|
keyPath: "rowId",
|
||||||
|
autoIncrement: true,
|
||||||
}),
|
}),
|
||||||
{},
|
{
|
||||||
|
byPointer: describeIndex(
|
||||||
|
"byDetailsPointer",
|
||||||
|
["exchangeBaseUrl", "currency", "masterPublicKey"],
|
||||||
|
{
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
exchangeSignkeys: describeStore(
|
||||||
|
"exchangeSignKeys",
|
||||||
|
describeContents<ExchangeSignkeysRecord>({
|
||||||
|
keyPath: ["exchangeDetailsRowId", "signkeyPub"],
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
byExchangeDetailsRowId: describeIndex("byExchangeDetailsRowId", [
|
||||||
|
"exchangeDetailsRowId",
|
||||||
|
]),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
refreshGroups: describeStore(
|
refreshGroups: describeStore(
|
||||||
"refreshGroups",
|
"refreshGroups",
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
BackupDenomination,
|
BackupDenomination,
|
||||||
BackupExchange,
|
BackupExchange,
|
||||||
BackupExchangeDetails,
|
BackupExchangeDetails,
|
||||||
|
BackupExchangeSignKey,
|
||||||
BackupExchangeWireFee,
|
BackupExchangeWireFee,
|
||||||
BackupOperationStatus,
|
BackupOperationStatus,
|
||||||
BackupPayInfo,
|
BackupPayInfo,
|
||||||
@ -74,6 +75,7 @@ import {
|
|||||||
} from "../../db.js";
|
} from "../../db.js";
|
||||||
import { InternalWalletState } from "../../internal-wallet-state.js";
|
import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||||
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
||||||
|
import { checkDbInvariant } from "../../util/invariants.js";
|
||||||
import { getWalletBackupState, provideBackupState } from "./state.js";
|
import { getWalletBackupState, provideBackupState } from "./state.js";
|
||||||
|
|
||||||
const logger = new Logger("backup/export.ts");
|
const logger = new Logger("backup/export.ts");
|
||||||
@ -87,6 +89,7 @@ export async function exportBackup(
|
|||||||
x.config,
|
x.config,
|
||||||
x.exchanges,
|
x.exchanges,
|
||||||
x.exchangeDetails,
|
x.exchangeDetails,
|
||||||
|
x.exchangeSignkeys,
|
||||||
x.coins,
|
x.coins,
|
||||||
x.contractTerms,
|
x.contractTerms,
|
||||||
x.denominations,
|
x.denominations,
|
||||||
@ -324,6 +327,18 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
checkDbInvariant(ex.rowId != null);
|
||||||
|
const exchangeSk =
|
||||||
|
await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll(
|
||||||
|
ex.rowId,
|
||||||
|
);
|
||||||
|
let signingKeys: BackupExchangeSignKey[] = exchangeSk.map((x) => ({
|
||||||
|
key: x.signkeyPub,
|
||||||
|
master_sig: x.masterSig,
|
||||||
|
stamp_end: x.stampEnd,
|
||||||
|
stamp_expire: x.stampExpire,
|
||||||
|
stamp_start: x.stampStart,
|
||||||
|
}));
|
||||||
|
|
||||||
backupExchangeDetails.push({
|
backupExchangeDetails.push({
|
||||||
base_url: ex.exchangeBaseUrl,
|
base_url: ex.exchangeBaseUrl,
|
||||||
@ -341,13 +356,7 @@ export async function exportBackup(
|
|||||||
currency: ex.currency,
|
currency: ex.currency,
|
||||||
protocol_version: ex.protocolVersionRange,
|
protocol_version: ex.protocolVersionRange,
|
||||||
wire_fees: wireFees,
|
wire_fees: wireFees,
|
||||||
signing_keys: ex.signingKeys.map((x) => ({
|
signing_keys: signingKeys,
|
||||||
key: x.key,
|
|
||||||
master_sig: x.master_sig,
|
|
||||||
stamp_end: x.stamp_end,
|
|
||||||
stamp_expire: x.stamp_expire,
|
|
||||||
stamp_start: x.stamp_start,
|
|
||||||
})),
|
|
||||||
global_fees: ex.globalFees.map((x) => ({
|
global_fees: ex.globalFees.map((x) => ({
|
||||||
accountFee: Amounts.stringify(x.accountFee),
|
accountFee: Amounts.stringify(x.accountFee),
|
||||||
historyFee: Amounts.stringify(x.historyFee),
|
historyFee: Amounts.stringify(x.historyFee),
|
||||||
|
@ -62,7 +62,12 @@ import { InternalWalletState } from "../../internal-wallet-state.js";
|
|||||||
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
||||||
import { checkLogicInvariant } from "../../util/invariants.js";
|
import { checkLogicInvariant } from "../../util/invariants.js";
|
||||||
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
||||||
import { makeCoinAvailable, makeTombstoneId, makeTransactionId, TombstoneTag } from "../common.js";
|
import {
|
||||||
|
makeCoinAvailable,
|
||||||
|
makeTombstoneId,
|
||||||
|
makeTransactionId,
|
||||||
|
TombstoneTag,
|
||||||
|
} from "../common.js";
|
||||||
import { getExchangeDetails } from "../exchanges.js";
|
import { getExchangeDetails } from "../exchanges.js";
|
||||||
import { extractContractData } from "../pay-merchant.js";
|
import { extractContractData } from "../pay-merchant.js";
|
||||||
import { provideBackupState } from "./state.js";
|
import { provideBackupState } from "./state.js";
|
||||||
@ -360,7 +365,8 @@ export async function importBackup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const backupExchangeDetails of backupBlob.exchange_details) {
|
for (const backupExchangeDetails of backupBlob.exchange_details) {
|
||||||
const existingExchangeDetails = await tx.exchangeDetails.get([
|
const existingExchangeDetails =
|
||||||
|
await tx.exchangeDetails.indexes.byPointer.get([
|
||||||
backupExchangeDetails.base_url,
|
backupExchangeDetails.base_url,
|
||||||
backupExchangeDetails.currency,
|
backupExchangeDetails.currency,
|
||||||
backupExchangeDetails.master_public_key,
|
backupExchangeDetails.master_public_key,
|
||||||
@ -422,13 +428,6 @@ export async function importBackup(
|
|||||||
purseTimeout: x.purseTimeout,
|
purseTimeout: x.purseTimeout,
|
||||||
startDate: x.startDate,
|
startDate: x.startDate,
|
||||||
})),
|
})),
|
||||||
signingKeys: backupExchangeDetails.signing_keys.map((x) => ({
|
|
||||||
key: x.key,
|
|
||||||
master_sig: x.master_sig,
|
|
||||||
stamp_end: x.stamp_end,
|
|
||||||
stamp_expire: x.stamp_expire,
|
|
||||||
stamp_start: x.stamp_start,
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -789,7 +788,10 @@ export async function importBackup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const backupTip of backupBlob.tips) {
|
for (const backupTip of backupBlob.tips) {
|
||||||
const ts = makeTombstoneId(TombstoneTag.DeleteTip, backupTip.wallet_tip_id);
|
const ts = makeTombstoneId(
|
||||||
|
TombstoneTag.DeleteTip,
|
||||||
|
backupTip.wallet_tip_id,
|
||||||
|
);
|
||||||
if (tombstoneSet.has(ts)) {
|
if (tombstoneSet.has(ts)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,7 @@ import {
|
|||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
readSuccessResponseTextOrThrow,
|
readSuccessResponseTextOrThrow,
|
||||||
} from "../util/http.js";
|
} from "../util/http.js";
|
||||||
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
import {
|
import {
|
||||||
DbAccess,
|
DbAccess,
|
||||||
GetReadOnlyAccess,
|
GetReadOnlyAccess,
|
||||||
@ -168,7 +169,11 @@ export async function getExchangeDetails(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { currency, masterPublicKey } = dp;
|
const { currency, masterPublicKey } = dp;
|
||||||
return await tx.exchangeDetails.get([r.baseUrl, currency, masterPublicKey]);
|
return await tx.exchangeDetails.indexes.byPointer.get([
|
||||||
|
r.baseUrl,
|
||||||
|
currency,
|
||||||
|
masterPublicKey,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
|
getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
|
||||||
@ -568,10 +573,14 @@ export async function updateExchangeFromUrlHandler(
|
|||||||
|
|
||||||
const now = AbsoluteTime.now();
|
const now = AbsoluteTime.now();
|
||||||
baseUrl = canonicalizeBaseUrl(baseUrl);
|
baseUrl = canonicalizeBaseUrl(baseUrl);
|
||||||
|
let isNewExchange = true;
|
||||||
const { exchange, exchangeDetails } = await ws.db
|
const { exchange, exchangeDetails } = await ws.db
|
||||||
.mktx((x) => [x.exchanges, x.exchangeDetails])
|
.mktx((x) => [x.exchanges, x.exchangeDetails])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
|
let oldExch = await tx.exchanges.get(baseUrl);
|
||||||
|
if (oldExch) {
|
||||||
|
isNewExchange = false;
|
||||||
|
}
|
||||||
return provideExchangeRecordInTx(ws, tx, baseUrl, now);
|
return provideExchangeRecordInTx(ws, tx, baseUrl, now);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -637,10 +646,13 @@ export async function updateExchangeFromUrlHandler(
|
|||||||
|
|
||||||
logger.trace("updating exchange info in database");
|
logger.trace("updating exchange info in database");
|
||||||
|
|
||||||
|
let detailsPointerChanged = false;
|
||||||
|
|
||||||
const updated = await ws.db
|
const updated = await ws.db
|
||||||
.mktx((x) => [
|
.mktx((x) => [
|
||||||
x.exchanges,
|
x.exchanges,
|
||||||
x.exchangeDetails,
|
x.exchangeDetails,
|
||||||
|
x.exchangeSignkeys,
|
||||||
x.denominations,
|
x.denominations,
|
||||||
x.coins,
|
x.coins,
|
||||||
x.refreshGroups,
|
x.refreshGroups,
|
||||||
@ -652,42 +664,63 @@ export async function updateExchangeFromUrlHandler(
|
|||||||
logger.warn(`exchange ${baseUrl} no longer present`);
|
logger.warn(`exchange ${baseUrl} no longer present`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let details = await getExchangeDetails(tx, r.baseUrl);
|
let existingDetails = await getExchangeDetails(tx, r.baseUrl);
|
||||||
if (details) {
|
let acceptedTosEtag = undefined;
|
||||||
|
if (!existingDetails) {
|
||||||
|
detailsPointerChanged = true;
|
||||||
|
}
|
||||||
|
if (existingDetails) {
|
||||||
|
acceptedTosEtag = existingDetails.tosAccepted?.etag;
|
||||||
|
if (existingDetails.masterPublicKey !== keysInfo.masterPublicKey) {
|
||||||
|
detailsPointerChanged = true;
|
||||||
|
}
|
||||||
|
if (existingDetails.currency !== keysInfo.currency) {
|
||||||
|
detailsPointerChanged = true;
|
||||||
|
}
|
||||||
// FIXME: We need to do some consistency checks!
|
// FIXME: We need to do some consistency checks!
|
||||||
}
|
}
|
||||||
// FIXME: validate signing keys and merge with old set
|
let existingTosAccepted = existingDetails?.tosAccepted;
|
||||||
details = {
|
const newDetails = {
|
||||||
|
rowId: existingDetails?.rowId,
|
||||||
auditors: keysInfo.auditors,
|
auditors: keysInfo.auditors,
|
||||||
currency: keysInfo.currency,
|
currency: keysInfo.currency,
|
||||||
masterPublicKey: keysInfo.masterPublicKey,
|
masterPublicKey: keysInfo.masterPublicKey,
|
||||||
protocolVersionRange: keysInfo.protocolVersion,
|
protocolVersionRange: keysInfo.protocolVersion,
|
||||||
signingKeys: keysInfo.signingKeys,
|
|
||||||
reserveClosingDelay: keysInfo.reserveClosingDelay,
|
reserveClosingDelay: keysInfo.reserveClosingDelay,
|
||||||
globalFees,
|
globalFees,
|
||||||
exchangeBaseUrl: r.baseUrl,
|
exchangeBaseUrl: r.baseUrl,
|
||||||
wireInfo,
|
wireInfo,
|
||||||
tosCurrentEtag: tosDownload.tosContentType,
|
tosCurrentEtag: tosDownload.tosEtag,
|
||||||
tosAccepted: tosHasBeenAccepted
|
tosAccepted: existingTosAccepted,
|
||||||
? {
|
|
||||||
etag: tosDownload.tosEtag,
|
|
||||||
timestamp: TalerProtocolTimestamp.now(),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
// FIXME: only update if pointer got updated
|
|
||||||
r.lastUpdate = TalerProtocolTimestamp.now();
|
r.lastUpdate = TalerProtocolTimestamp.now();
|
||||||
r.nextUpdate = keysInfo.expiry;
|
r.nextUpdate = keysInfo.expiry;
|
||||||
// New denominations might be available.
|
// New denominations might be available.
|
||||||
r.nextRefreshCheck = TalerProtocolTimestamp.now();
|
r.nextRefreshCheck = TalerProtocolTimestamp.now();
|
||||||
|
if (detailsPointerChanged) {
|
||||||
r.detailsPointer = {
|
r.detailsPointer = {
|
||||||
currency: details.currency,
|
currency: newDetails.currency,
|
||||||
masterPublicKey: details.masterPublicKey,
|
masterPublicKey: newDetails.masterPublicKey,
|
||||||
// FIXME: only change if pointer really changed
|
|
||||||
updateClock: TalerProtocolTimestamp.now(),
|
updateClock: TalerProtocolTimestamp.now(),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
await tx.exchanges.put(r);
|
await tx.exchanges.put(r);
|
||||||
await tx.exchangeDetails.put(details);
|
logger.info(`existing details ${j2s(existingDetails)}`);
|
||||||
|
logger.info(`inserting new details ${j2s(newDetails)}`);
|
||||||
|
const drRowId = await tx.exchangeDetails.put(newDetails);
|
||||||
|
checkDbInvariant(typeof drRowId.key === "number");
|
||||||
|
|
||||||
|
for (const sk of keysInfo.signingKeys) {
|
||||||
|
// FIXME: validate signing keys before inserting them
|
||||||
|
await tx.exchangeSignKeys.put({
|
||||||
|
exchangeDetailsRowId: drRowId.key,
|
||||||
|
masterSig: sk.master_sig,
|
||||||
|
signkeyPub: sk.key,
|
||||||
|
stampEnd: sk.stamp_end,
|
||||||
|
stampExpire: sk.stamp_expire,
|
||||||
|
stampStart: sk.stamp_start,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("updating denominations in database");
|
logger.info("updating denominations in database");
|
||||||
const currentDenomSet = new Set<string>(
|
const currentDenomSet = new Set<string>(
|
||||||
@ -773,7 +806,7 @@ export async function updateExchangeFromUrlHandler(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
exchange: r,
|
exchange: r,
|
||||||
exchangeDetails: details,
|
exchangeDetails: newDetails,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -791,9 +824,12 @@ export async function updateExchangeFromUrlHandler(
|
|||||||
|
|
||||||
logger.trace("done updating exchange info in database");
|
logger.trace("done updating exchange info in database");
|
||||||
|
|
||||||
|
if (isNewExchange) {
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.ExchangeAdded,
|
type: NotificationType.ExchangeAdded,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: OperationAttemptResultType.Finished,
|
type: OperationAttemptResultType.Finished,
|
||||||
result: {
|
result: {
|
||||||
|
@ -494,6 +494,7 @@ function runTx<Arg, Res>(
|
|||||||
msg = "Transaction aborted (no DB error)";
|
msg = "Transaction aborted (no DB error)";
|
||||||
}
|
}
|
||||||
logger.error(msg);
|
logger.error(msg);
|
||||||
|
logger.error(`${stack.stack}`);
|
||||||
reject(new TransactionAbortedError(msg));
|
reject(new TransactionAbortedError(msg));
|
||||||
};
|
};
|
||||||
const resP = Promise.resolve().then(() => f(arg, tx));
|
const resP = Promise.resolve().then(() => f(arg, tx));
|
||||||
|
Loading…
Reference in New Issue
Block a user