2019-12-02 00:42:40 +01:00
|
|
|
/*
|
|
|
|
This file is part of GNU Taler
|
2019-12-16 16:59:09 +01:00
|
|
|
(C) 2019 Taler Systems S.A.
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
|
|
|
terms of the GNU General Public License as published by the Free Software
|
|
|
|
Foundation; either version 3, or (at your option) any later version.
|
|
|
|
|
|
|
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
|
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
|
|
*/
|
|
|
|
|
2021-03-17 17:56:37 +01:00
|
|
|
/**
|
|
|
|
* Imports.
|
|
|
|
*/
|
2019-12-16 16:59:09 +01:00
|
|
|
import {
|
2022-09-05 18:12:30 +02:00
|
|
|
Amounts,
|
|
|
|
BlindedDenominationSignature,
|
|
|
|
codecForMerchantTipResponseV2,
|
|
|
|
codecForTipPickupGetResponse,
|
|
|
|
DenomKeyType,
|
|
|
|
encodeCrock,
|
|
|
|
getRandomBytes,
|
|
|
|
j2s,
|
|
|
|
Logger,
|
|
|
|
parseTipUri,
|
|
|
|
PrepareTipResult,
|
|
|
|
TalerErrorCode,
|
|
|
|
TalerProtocolTimestamp,
|
|
|
|
TipPlanchetDetail,
|
|
|
|
URL,
|
2021-03-17 17:56:37 +01:00
|
|
|
} from "@gnu-taler/taler-util";
|
|
|
|
import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js";
|
2019-12-16 16:59:09 +01:00
|
|
|
import {
|
2020-09-08 15:57:08 +02:00
|
|
|
CoinRecord,
|
|
|
|
CoinSourceType,
|
2022-09-05 18:12:30 +02:00
|
|
|
CoinStatus,
|
|
|
|
DenominationRecord,
|
|
|
|
OperationAttemptResult,
|
|
|
|
OperationAttemptResultType,
|
|
|
|
TipRecord,
|
2021-03-17 17:56:37 +01:00
|
|
|
} from "../db.js";
|
2022-03-23 13:11:36 +01:00
|
|
|
import { makeErrorDetail } from "../errors.js";
|
|
|
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
2021-06-14 16:08:58 +02:00
|
|
|
import {
|
|
|
|
getHttpResponseErrorDetails,
|
2022-09-05 18:12:30 +02:00
|
|
|
readSuccessResponseJsonOrThrow,
|
2021-06-14 16:08:58 +02:00
|
|
|
} from "../util/http.js";
|
2022-05-18 19:41:51 +02:00
|
|
|
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
2022-09-14 20:34:37 +02:00
|
|
|
import { makeCoinAvailable } from "../wallet.js";
|
2022-05-18 19:41:51 +02:00
|
|
|
import { updateExchangeFromUrl } from "./exchanges.js";
|
|
|
|
import {
|
2022-09-05 18:12:30 +02:00
|
|
|
getCandidateWithdrawalDenoms,
|
|
|
|
getExchangeWithdrawalInfo,
|
|
|
|
selectWithdrawalDenominations,
|
|
|
|
updateWithdrawalDenoms,
|
2022-05-18 19:41:51 +02:00
|
|
|
} from "./withdraw.js";
|
2020-08-14 12:23:50 +02:00
|
|
|
|
|
|
|
const logger = new Logger("operations/tip.ts");
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-09-08 14:10:47 +02:00
|
|
|
export async function prepareTip(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
2019-12-16 16:59:09 +01:00
|
|
|
talerTipUri: string,
|
2020-09-08 14:10:47 +02:00
|
|
|
): Promise<PrepareTipResult> {
|
2019-12-02 00:42:40 +01:00
|
|
|
const res = parseTipUri(talerTipUri);
|
|
|
|
if (!res) {
|
|
|
|
throw Error("invalid taler://tip URI");
|
|
|
|
}
|
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
let tipRecord = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.tips])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.tips.indexes.byMerchantTipIdAndBaseUrl.get([
|
|
|
|
res.merchantTipId,
|
|
|
|
res.merchantBaseUrl,
|
|
|
|
]);
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
if (!tipRecord) {
|
2020-11-16 16:17:26 +01:00
|
|
|
const tipStatusUrl = new URL(
|
|
|
|
`tips/${res.merchantTipId}`,
|
|
|
|
res.merchantBaseUrl,
|
|
|
|
);
|
|
|
|
logger.trace("checking tip status from", tipStatusUrl.href);
|
|
|
|
const merchantResp = await ws.http.get(tipStatusUrl.href);
|
|
|
|
const tipPickupStatus = await readSuccessResponseJsonOrThrow(
|
|
|
|
merchantResp,
|
|
|
|
codecForTipPickupGetResponse(),
|
|
|
|
);
|
|
|
|
logger.trace(`status ${j2s(tipPickupStatus)}`);
|
|
|
|
|
|
|
|
const amount = Amounts.parseOrThrow(tipPickupStatus.tip_amount);
|
|
|
|
|
2020-11-16 14:12:37 +01:00
|
|
|
logger.trace("new tip, creating tip record");
|
2020-05-11 14:33:25 +02:00
|
|
|
await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);
|
2022-09-06 22:17:44 +02:00
|
|
|
|
|
|
|
//FIXME: is this needed? withdrawDetails is not used
|
2022-09-13 13:25:41 +02:00
|
|
|
// * if the intention is to update the exchange information in the database
|
2022-09-06 22:17:44 +02:00
|
|
|
// maybe we can use another name. `get` seems like a pure-function
|
2019-12-09 19:59:08 +01:00
|
|
|
const withdrawDetails = await getExchangeWithdrawalInfo(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws,
|
|
|
|
tipPickupStatus.exchange_url,
|
|
|
|
amount,
|
2022-09-13 13:25:41 +02:00
|
|
|
undefined,
|
2019-12-02 00:42:40 +01:00
|
|
|
);
|
|
|
|
|
2020-09-08 14:10:47 +02:00
|
|
|
const walletTipId = encodeCrock(getRandomBytes(32));
|
2020-12-16 17:59:04 +01:00
|
|
|
await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url);
|
2021-01-14 18:00:00 +01:00
|
|
|
const denoms = await getCandidateWithdrawalDenoms(
|
2021-01-06 17:06:19 +01:00
|
|
|
ws,
|
|
|
|
tipPickupStatus.exchange_url,
|
2020-05-11 14:33:25 +02:00
|
|
|
);
|
2021-01-06 17:06:19 +01:00
|
|
|
const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-12-16 17:59:04 +01:00
|
|
|
const secretSeed = encodeCrock(getRandomBytes(64));
|
2021-05-12 13:34:49 +02:00
|
|
|
const denomSelUid = encodeCrock(getRandomBytes(32));
|
2020-12-16 17:59:04 +01:00
|
|
|
|
2022-03-18 15:32:41 +01:00
|
|
|
const newTipRecord: TipRecord = {
|
2020-09-08 14:10:47 +02:00
|
|
|
walletTipId: walletTipId,
|
2019-12-16 12:53:22 +01:00
|
|
|
acceptedTimestamp: undefined,
|
2020-09-08 15:57:08 +02:00
|
|
|
tipAmountRaw: amount,
|
2020-09-08 16:24:23 +02:00
|
|
|
tipExpiration: tipPickupStatus.expiration,
|
|
|
|
exchangeBaseUrl: tipPickupStatus.exchange_url,
|
2019-12-02 00:42:40 +01:00
|
|
|
merchantBaseUrl: res.merchantBaseUrl,
|
2022-03-18 15:32:41 +01:00
|
|
|
createdTimestamp: TalerProtocolTimestamp.now(),
|
2019-12-02 00:42:40 +01:00
|
|
|
merchantTipId: res.merchantTipId,
|
2022-03-28 19:51:17 +02:00
|
|
|
tipAmountEffective: selectedDenoms.totalCoinValue,
|
2022-03-29 21:21:57 +02:00
|
|
|
denomsSel: selectedDenoms,
|
2020-09-08 16:24:23 +02:00
|
|
|
pickedUpTimestamp: undefined,
|
2020-12-16 17:59:04 +01:00
|
|
|
secretSeed,
|
2021-05-12 13:34:49 +02:00
|
|
|
denomSelUid,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.tips])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
await tx.tips.put(newTipRecord);
|
|
|
|
});
|
|
|
|
tipRecord = newTipRecord;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2020-09-08 14:10:47 +02:00
|
|
|
const tipStatus: PrepareTipResult = {
|
2019-12-16 12:53:22 +01:00
|
|
|
accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
|
2020-11-16 16:17:26 +01:00
|
|
|
tipAmountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
|
|
|
|
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
|
2020-11-18 12:44:06 +01:00
|
|
|
merchantBaseUrl: tipRecord.merchantBaseUrl,
|
2020-11-16 16:17:26 +01:00
|
|
|
expirationTimestamp: tipRecord.tipExpiration,
|
2020-09-08 15:57:08 +02:00
|
|
|
tipAmountEffective: Amounts.stringify(tipRecord.tipAmountEffective),
|
2020-09-08 14:10:47 +02:00
|
|
|
walletTipId: tipRecord.walletTipId,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
return tipStatus;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function processTip(
|
2019-12-05 19:38:19 +01:00
|
|
|
ws: InternalWalletState,
|
2020-09-08 15:57:08 +02:00
|
|
|
walletTipId: string,
|
2022-03-29 13:47:32 +02:00
|
|
|
options: {
|
|
|
|
forceNow?: boolean;
|
|
|
|
} = {},
|
2022-09-05 18:12:30 +02:00
|
|
|
): Promise<OperationAttemptResult> {
|
2021-06-09 15:14:17 +02:00
|
|
|
const tipRecord = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.tips])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.tips.get(walletTipId);
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!tipRecord) {
|
2022-09-05 18:12:30 +02:00
|
|
|
return {
|
|
|
|
type: OperationAttemptResultType.Finished,
|
|
|
|
result: undefined,
|
|
|
|
};
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2020-09-08 16:24:23 +02:00
|
|
|
if (tipRecord.pickedUpTimestamp) {
|
2020-08-14 12:23:50 +02:00
|
|
|
logger.warn("tip already picked up");
|
2022-09-05 18:12:30 +02:00
|
|
|
return {
|
|
|
|
type: OperationAttemptResultType.Finished,
|
|
|
|
result: undefined,
|
|
|
|
};
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2020-05-11 14:33:25 +02:00
|
|
|
const denomsForWithdraw = tipRecord.denomsSel;
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-12-15 17:12:22 +01:00
|
|
|
const planchets: DerivedTipPlanchet[] = [];
|
2019-12-02 00:42:40 +01:00
|
|
|
// Planchets in the form that the merchant expects
|
2020-12-15 17:12:22 +01:00
|
|
|
const planchetsDetail: TipPlanchetDetail[] = [];
|
2021-01-06 17:06:19 +01:00
|
|
|
const denomForPlanchet: { [index: number]: DenominationRecord } = [];
|
2020-12-15 17:12:22 +01:00
|
|
|
|
|
|
|
for (const dh of denomsForWithdraw.selectedDenoms) {
|
2021-06-09 15:14:17 +02:00
|
|
|
const denom = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.denominations])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.denominations.get([
|
|
|
|
tipRecord.exchangeBaseUrl,
|
|
|
|
dh.denomPubHash,
|
|
|
|
]);
|
|
|
|
});
|
2020-12-15 17:12:22 +01:00
|
|
|
checkDbInvariant(!!denom, "denomination should be in database");
|
|
|
|
for (let i = 0; i < dh.count; i++) {
|
2021-01-06 17:06:19 +01:00
|
|
|
const deriveReq = {
|
2020-12-17 12:21:03 +01:00
|
|
|
denomPub: denom.denomPub,
|
2020-12-15 17:12:22 +01:00
|
|
|
planchetIndex: planchets.length,
|
|
|
|
secretSeed: tipRecord.secretSeed,
|
2021-01-06 17:06:19 +01:00
|
|
|
};
|
2021-01-13 00:51:30 +01:00
|
|
|
logger.trace(`deriving tip planchet: ${j2s(deriveReq)}`);
|
2021-01-06 17:06:19 +01:00
|
|
|
const p = await ws.cryptoApi.createTipPlanchet(deriveReq);
|
|
|
|
logger.trace(`derive result: ${j2s(p)}`);
|
2020-12-17 12:21:03 +01:00
|
|
|
denomForPlanchet[planchets.length] = denom;
|
2020-12-15 17:12:22 +01:00
|
|
|
planchets.push(p);
|
|
|
|
planchetsDetail.push({
|
|
|
|
coin_ev: p.coinEv,
|
|
|
|
denom_pub_hash: denom.denomPubHash,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-09-08 14:10:47 +02:00
|
|
|
const tipStatusUrl = new URL(
|
2020-11-16 14:12:37 +01:00
|
|
|
`tips/${tipRecord.merchantTipId}/pickup`,
|
2020-09-08 14:10:47 +02:00
|
|
|
tipRecord.merchantBaseUrl,
|
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-09-08 14:10:47 +02:00
|
|
|
const req = { planchets: planchetsDetail };
|
2021-01-06 17:06:19 +01:00
|
|
|
logger.trace(`sending tip request: ${j2s(req)}`);
|
2020-09-08 14:10:47 +02:00
|
|
|
const merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
|
2020-11-26 12:27:31 +01:00
|
|
|
|
2021-01-06 17:06:19 +01:00
|
|
|
logger.trace(`got tip response, status ${merchantResp.status}`);
|
|
|
|
|
2022-09-05 18:12:30 +02:00
|
|
|
// FIXME: Why do we do this?
|
2020-11-26 12:27:31 +01:00
|
|
|
if (
|
2022-09-05 18:12:30 +02:00
|
|
|
(merchantResp.status >= 500 && merchantResp.status <= 599) ||
|
|
|
|
merchantResp.status === 424
|
2020-11-26 12:27:31 +01:00
|
|
|
) {
|
2021-01-06 17:06:19 +01:00
|
|
|
logger.trace(`got transient tip error`);
|
2022-03-29 13:47:32 +02:00
|
|
|
// FIXME: wrap in another error code that indicates a transient error
|
2022-09-05 18:12:30 +02:00
|
|
|
return {
|
|
|
|
type: OperationAttemptResultType.Error,
|
|
|
|
errorDetail: makeErrorDetail(
|
|
|
|
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
|
|
|
|
getHttpResponseErrorDetails(merchantResp),
|
|
|
|
"tip pickup failed (transient)",
|
|
|
|
),
|
|
|
|
};
|
2020-11-26 12:27:31 +01:00
|
|
|
}
|
2021-11-23 23:51:12 +01:00
|
|
|
let blindedSigs: BlindedDenominationSignature[] = [];
|
|
|
|
|
2022-02-21 12:40:51 +01:00
|
|
|
const response = await readSuccessResponseJsonOrThrow(
|
|
|
|
merchantResp,
|
|
|
|
codecForMerchantTipResponseV2(),
|
|
|
|
);
|
|
|
|
blindedSigs = response.blind_sigs.map((x) => x.blind_sig);
|
2021-11-23 23:51:12 +01:00
|
|
|
|
|
|
|
if (blindedSigs.length !== planchets.length) {
|
2019-12-02 00:42:40 +01:00
|
|
|
throw Error("number of tip responses does not match requested planchets");
|
|
|
|
}
|
|
|
|
|
2020-09-08 15:57:08 +02:00
|
|
|
const newCoinRecords: CoinRecord[] = [];
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2021-11-23 23:51:12 +01:00
|
|
|
for (let i = 0; i < blindedSigs.length; i++) {
|
|
|
|
const blindedSig = blindedSigs[i];
|
2020-09-08 15:57:08 +02:00
|
|
|
|
2020-12-15 17:12:22 +01:00
|
|
|
const denom = denomForPlanchet[i];
|
2020-12-17 12:21:03 +01:00
|
|
|
checkLogicInvariant(!!denom);
|
2020-12-15 17:12:22 +01:00
|
|
|
const planchet = planchets[i];
|
2020-12-17 12:21:03 +01:00
|
|
|
checkLogicInvariant(!!planchet);
|
2020-09-08 15:57:08 +02:00
|
|
|
|
2022-02-21 12:40:51 +01:00
|
|
|
if (denom.denomPub.cipher !== DenomKeyType.Rsa) {
|
2021-11-23 23:51:12 +01:00
|
|
|
throw Error("unsupported cipher");
|
|
|
|
}
|
|
|
|
|
2022-02-21 12:40:51 +01:00
|
|
|
if (blindedSig.cipher !== DenomKeyType.Rsa) {
|
2021-11-17 10:23:22 +01:00
|
|
|
throw Error("unsupported cipher");
|
|
|
|
}
|
|
|
|
|
2022-03-23 21:24:23 +01:00
|
|
|
const denomSigRsa = await ws.cryptoApi.rsaUnblind({
|
|
|
|
bk: planchet.blindingKey,
|
|
|
|
blindedSig: blindedSig.blinded_rsa_signature,
|
|
|
|
pk: denom.denomPub.rsa_public_key,
|
|
|
|
});
|
2020-09-08 15:57:08 +02:00
|
|
|
|
2022-03-23 21:24:23 +01:00
|
|
|
const isValid = await ws.cryptoApi.rsaVerify({
|
|
|
|
hm: planchet.coinPub,
|
|
|
|
pk: denom.denomPub.rsa_public_key,
|
|
|
|
sig: denomSigRsa.sig,
|
|
|
|
});
|
2020-09-08 15:57:08 +02:00
|
|
|
|
|
|
|
if (!isValid) {
|
2022-09-05 18:12:30 +02:00
|
|
|
return {
|
|
|
|
type: OperationAttemptResultType.Error,
|
|
|
|
errorDetail: makeErrorDetail(
|
|
|
|
TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID,
|
|
|
|
{},
|
|
|
|
"invalid signature from the exchange (via merchant tip) after unblinding",
|
|
|
|
),
|
|
|
|
};
|
2020-09-08 15:57:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
newCoinRecords.push({
|
|
|
|
blindingKey: planchet.blindingKey,
|
|
|
|
coinPriv: planchet.coinPriv,
|
|
|
|
coinPub: planchet.coinPub,
|
|
|
|
coinSource: {
|
|
|
|
type: CoinSourceType.Tip,
|
|
|
|
coinIndex: i,
|
|
|
|
walletTipId: walletTipId,
|
|
|
|
},
|
2022-09-14 21:27:03 +02:00
|
|
|
currentAmount: DenominationRecord.getValue(denom),
|
2020-12-15 17:12:22 +01:00
|
|
|
denomPubHash: denom.denomPubHash,
|
2022-03-23 21:24:23 +01:00
|
|
|
denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig },
|
2020-09-08 16:24:23 +02:00
|
|
|
exchangeBaseUrl: tipRecord.exchangeBaseUrl,
|
2020-09-08 15:57:08 +02:00
|
|
|
status: CoinStatus.Fresh,
|
2020-12-16 17:59:04 +01:00
|
|
|
coinEvHash: planchet.coinEvHash,
|
2020-09-08 15:57:08 +02:00
|
|
|
});
|
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
2022-09-14 20:34:37 +02:00
|
|
|
.mktx((x) => [x.coins, x.denominations, x.tips])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const tr = await tx.tips.get(walletTipId);
|
2019-12-16 16:59:09 +01:00
|
|
|
if (!tr) {
|
|
|
|
return;
|
|
|
|
}
|
2020-09-08 16:24:23 +02:00
|
|
|
if (tr.pickedUpTimestamp) {
|
2019-12-16 16:59:09 +01:00
|
|
|
return;
|
|
|
|
}
|
2022-03-18 15:32:41 +01:00
|
|
|
tr.pickedUpTimestamp = TalerProtocolTimestamp.now();
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.tips.put(tr);
|
2020-09-08 15:57:08 +02:00
|
|
|
for (const cr of newCoinRecords) {
|
2022-09-14 20:34:37 +02:00
|
|
|
await makeCoinAvailable(ws, tx, cr);
|
2020-05-11 14:33:25 +02:00
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2022-09-05 18:12:30 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
type: OperationAttemptResultType.Finished,
|
|
|
|
result: undefined,
|
|
|
|
};
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function acceptTip(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
tipId: string,
|
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
const found = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.tips])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const tipRecord = await tx.tips.get(tipId);
|
|
|
|
if (!tipRecord) {
|
|
|
|
logger.error("tip not found");
|
|
|
|
return false;
|
|
|
|
}
|
2022-03-18 15:32:41 +01:00
|
|
|
tipRecord.acceptedTimestamp = TalerProtocolTimestamp.now();
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.tips.put(tipRecord);
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
if (found) {
|
|
|
|
await processTip(ws, tipId);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
}
|