This commit is contained in:
Florian Dold 2019-12-03 14:40:05 +01:00
parent 8683c93613
commit 829acdd3d9
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
5 changed files with 229 additions and 254 deletions

View File

@ -157,6 +157,7 @@ export function installAndroidWalletListener() {
case "withdrawTestkudos": {
const wallet = await wp.promise;
await withdrawTestBalance(wallet);
result = {};
break;
}
case "getHistory": {
@ -164,6 +165,12 @@ export function installAndroidWalletListener() {
result = await wallet.getHistory();
break;
}
case "retryPendingNow": {
const wallet = await wp.promise;
await wallet.runPending(true);
result = {};
break;
}
case "preparePay": {
const wallet = await wp.promise;
result = await wallet.preparePay(msg.args.url);
@ -197,9 +204,6 @@ export function installAndroidWalletListener() {
break;
}
case "reset": {
const wallet = await wp.promise;
wallet.stop();
wp = openPromise<Wallet>();
const oldArgs = walletArgs;
walletArgs = { ...oldArgs };
if (oldArgs && oldArgs.persistentStoragePath) {
@ -211,6 +215,9 @@ export function installAndroidWalletListener() {
// Prevent further storage!
walletArgs.persistentStoragePath = undefined;
}
const wallet = await wp.promise;
wallet.stop();
wp = openPromise<Wallet>();
maybeWallet = undefined;
const w = await getDefaultNodeWallet(walletArgs);
maybeWallet = w;
@ -218,6 +225,7 @@ export function installAndroidWalletListener() {
console.error("Error during wallet retry loop", e);
});
wp.resolve(w);
result = {};
break;
}
default:

View File

@ -22,6 +22,8 @@ import {
PayReq,
Proposal,
ContractTerms,
MerchantRefundPermission,
RefundRequest,
} from "../talerTypes";
import {
Timestamp,
@ -39,6 +41,7 @@ import {
runWithWriteTransaction,
oneShotPut,
oneShotGetIndexed,
oneShotMutate,
} from "../util/query";
import {
Stores,
@ -59,9 +62,8 @@ import {
} from "../util/helpers";
import { Logger } from "../util/logging";
import { InternalWalletState } from "./state";
import { parsePayUri } from "../util/taleruri";
import { parsePayUri, parseRefundUri } from "../util/taleruri";
import { getTotalRefreshCost, refresh } from "./refresh";
import { acceptRefundResponse } from "./refund";
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
export interface SpeculativePayData {
@ -856,3 +858,212 @@ export async function confirmPay(
return submitPay(ws, proposalId, sessionId);
}
export async function getFullRefundFees(
ws: InternalWalletState,
refundPermissions: MerchantRefundPermission[],
): Promise<AmountJson> {
if (refundPermissions.length === 0) {
throw Error("no refunds given");
}
const coin0 = await oneShotGet(
ws.db,
Stores.coins,
refundPermissions[0].coin_pub,
);
if (!coin0) {
throw Error("coin not found");
}
let feeAcc = Amounts.getZero(
Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
);
const denoms = await oneShotIterIndex(
ws.db,
Stores.denominations.exchangeBaseUrlIndex,
coin0.exchangeBaseUrl,
).toArray();
for (const rp of refundPermissions) {
const coin = await oneShotGet(ws.db, Stores.coins, rp.coin_pub);
if (!coin) {
throw Error("coin not found");
}
const denom = await oneShotGet(ws.db, Stores.denominations, [
coin0.exchangeBaseUrl,
coin.denomPub,
]);
if (!denom) {
throw Error(`denom not found (${coin.denomPub})`);
}
// FIXME: this assumes that the refund already happened.
// When it hasn't, the refresh cost is inaccurate. To fix this,
// we need introduce a flag to tell if a coin was refunded or
// refreshed normally (and what about incremental refunds?)
const refundAmount = Amounts.parseOrThrow(rp.refund_amount);
const refundFee = Amounts.parseOrThrow(rp.refund_fee);
const refreshCost = getTotalRefreshCost(
denoms,
denom,
Amounts.sub(refundAmount, refundFee).amount,
);
feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount;
}
return feeAcc;
}
async function submitRefunds(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
if (!purchase) {
console.error(
"not submitting refunds, payment not found:",
);
return;
}
const pendingKeys = Object.keys(purchase.refundsPending);
if (pendingKeys.length === 0) {
return;
}
for (const pk of pendingKeys) {
const perm = purchase.refundsPending[pk];
const req: RefundRequest = {
coin_pub: perm.coin_pub,
h_contract_terms: purchase.contractTermsHash,
merchant_pub: purchase.contractTerms.merchant_pub,
merchant_sig: perm.merchant_sig,
refund_amount: perm.refund_amount,
refund_fee: perm.refund_fee,
rtransaction_id: perm.rtransaction_id,
};
console.log("sending refund permission", perm);
// FIXME: not correct once we support multiple exchanges per payment
const exchangeUrl = purchase.payReq.coins[0].exchange_url;
const reqUrl = new URL("refund", exchangeUrl);
const resp = await ws.http.postJson(reqUrl.href, req);
if (resp.status !== 200) {
console.error("refund failed", resp);
continue;
}
// Transactionally mark successful refunds as done
const transformPurchase = (
t: PurchaseRecord | undefined,
): PurchaseRecord | undefined => {
if (!t) {
console.warn("purchase not found, not updating refund");
return;
}
if (t.refundsPending[pk]) {
t.refundsDone[pk] = t.refundsPending[pk];
delete t.refundsPending[pk];
}
return t;
};
const transformCoin = (
c: CoinRecord | undefined,
): CoinRecord | undefined => {
if (!c) {
console.warn("coin not found, can't apply refund");
return;
}
const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
const refundFee = Amounts.parseOrThrow(perm.refund_fee);
c.status = CoinStatus.Dirty;
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
return c;
};
await runWithWriteTransaction(
ws.db,
[Stores.purchases, Stores.coins],
async tx => {
await tx.mutate(Stores.purchases, proposalId, transformPurchase);
await tx.mutate(Stores.coins, perm.coin_pub, transformCoin);
},
);
refresh(ws, perm.coin_pub);
}
ws.badge.showNotification();
ws.notifier.notify();
}
export async function acceptRefundResponse(
ws: InternalWalletState,
refundResponse: MerchantRefundResponse,
): Promise<string> {
const refundPermissions = refundResponse.refund_permissions;
if (!refundPermissions.length) {
console.warn("got empty refund list");
throw Error("empty refund");
}
/**
* Add refund to purchase if not already added.
*/
function f(t: PurchaseRecord | undefined): PurchaseRecord | undefined {
if (!t) {
console.error("purchase not found, not adding refunds");
return;
}
t.timestamp_refund = getTimestampNow();
for (const perm of refundPermissions) {
if (
!t.refundsPending[perm.merchant_sig] &&
!t.refundsDone[perm.merchant_sig]
) {
t.refundsPending[perm.merchant_sig] = perm;
}
}
return t;
}
const hc = refundResponse.h_contract_terms;
// Add the refund permissions to the purchase within a DB transaction
await oneShotMutate(ws.db, Stores.purchases, hc, f);
ws.notifier.notify();
await submitRefunds(ws, hc);
return hc;
}
/**
* Accept a refund, return the contract hash for the contract
* that was involved in the refund.
*/
export async function applyRefund(
ws: InternalWalletState,
talerRefundUri: string,
): Promise<string> {
const parseResult = parseRefundUri(talerRefundUri);
if (!parseResult) {
throw Error("invalid refund URI");
}
const refundUrl = parseResult.refundUrl;
logger.trace("processing refund");
let resp;
try {
resp = await ws.http.get(refundUrl);
} catch (e) {
console.error("error downloading refund permission", e);
throw e;
}
const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
return acceptRefundResponse(ws, refundResponse);
}

View File

@ -1,244 +0,0 @@
/*
This file is part of GNU Taler
(C) 2019 GNUnet e.V.
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/>
*/
import {
MerchantRefundResponse,
RefundRequest,
MerchantRefundPermission,
} from "../talerTypes";
import { PurchaseRecord, Stores, CoinRecord, CoinStatus } from "../dbTypes";
import { getTimestampNow } from "../walletTypes";
import {
oneShotMutate,
oneShotGet,
runWithWriteTransaction,
oneShotIterIndex,
} from "../util/query";
import { InternalWalletState } from "./state";
import { parseRefundUri } from "../util/taleruri";
import { Logger } from "../util/logging";
import { AmountJson } from "../util/amounts";
import * as Amounts from "../util/amounts";
import { getTotalRefreshCost, refresh } from "./refresh";
const logger = new Logger("refund.ts");
export async function getFullRefundFees(
ws: InternalWalletState,
refundPermissions: MerchantRefundPermission[],
): Promise<AmountJson> {
if (refundPermissions.length === 0) {
throw Error("no refunds given");
}
const coin0 = await oneShotGet(
ws.db,
Stores.coins,
refundPermissions[0].coin_pub,
);
if (!coin0) {
throw Error("coin not found");
}
let feeAcc = Amounts.getZero(
Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
);
const denoms = await oneShotIterIndex(
ws.db,
Stores.denominations.exchangeBaseUrlIndex,
coin0.exchangeBaseUrl,
).toArray();
for (const rp of refundPermissions) {
const coin = await oneShotGet(ws.db, Stores.coins, rp.coin_pub);
if (!coin) {
throw Error("coin not found");
}
const denom = await oneShotGet(ws.db, Stores.denominations, [
coin0.exchangeBaseUrl,
coin.denomPub,
]);
if (!denom) {
throw Error(`denom not found (${coin.denomPub})`);
}
// FIXME: this assumes that the refund already happened.
// When it hasn't, the refresh cost is inaccurate. To fix this,
// we need introduce a flag to tell if a coin was refunded or
// refreshed normally (and what about incremental refunds?)
const refundAmount = Amounts.parseOrThrow(rp.refund_amount);
const refundFee = Amounts.parseOrThrow(rp.refund_fee);
const refreshCost = getTotalRefreshCost(
denoms,
denom,
Amounts.sub(refundAmount, refundFee).amount,
);
feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount;
}
return feeAcc;
}
async function submitRefunds(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
if (!purchase) {
console.error(
"not submitting refunds, payment not found:",
);
return;
}
const pendingKeys = Object.keys(purchase.refundsPending);
if (pendingKeys.length === 0) {
return;
}
for (const pk of pendingKeys) {
const perm = purchase.refundsPending[pk];
const req: RefundRequest = {
coin_pub: perm.coin_pub,
h_contract_terms: purchase.contractTermsHash,
merchant_pub: purchase.contractTerms.merchant_pub,
merchant_sig: perm.merchant_sig,
refund_amount: perm.refund_amount,
refund_fee: perm.refund_fee,
rtransaction_id: perm.rtransaction_id,
};
console.log("sending refund permission", perm);
// FIXME: not correct once we support multiple exchanges per payment
const exchangeUrl = purchase.payReq.coins[0].exchange_url;
const reqUrl = new URL("refund", exchangeUrl);
const resp = await ws.http.postJson(reqUrl.href, req);
if (resp.status !== 200) {
console.error("refund failed", resp);
continue;
}
// Transactionally mark successful refunds as done
const transformPurchase = (
t: PurchaseRecord | undefined,
): PurchaseRecord | undefined => {
if (!t) {
console.warn("purchase not found, not updating refund");
return;
}
if (t.refundsPending[pk]) {
t.refundsDone[pk] = t.refundsPending[pk];
delete t.refundsPending[pk];
}
return t;
};
const transformCoin = (
c: CoinRecord | undefined,
): CoinRecord | undefined => {
if (!c) {
console.warn("coin not found, can't apply refund");
return;
}
const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
const refundFee = Amounts.parseOrThrow(perm.refund_fee);
c.status = CoinStatus.Dirty;
c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
return c;
};
await runWithWriteTransaction(
ws.db,
[Stores.purchases, Stores.coins],
async tx => {
await tx.mutate(Stores.purchases, proposalId, transformPurchase);
await tx.mutate(Stores.coins, perm.coin_pub, transformCoin);
},
);
refresh(ws, perm.coin_pub);
}
ws.badge.showNotification();
ws.notifier.notify();
}
export async function acceptRefundResponse(
ws: InternalWalletState,
refundResponse: MerchantRefundResponse,
): Promise<string> {
const refundPermissions = refundResponse.refund_permissions;
if (!refundPermissions.length) {
console.warn("got empty refund list");
throw Error("empty refund");
}
/**
* Add refund to purchase if not already added.
*/
function f(t: PurchaseRecord | undefined): PurchaseRecord | undefined {
if (!t) {
console.error("purchase not found, not adding refunds");
return;
}
t.timestamp_refund = getTimestampNow();
for (const perm of refundPermissions) {
if (
!t.refundsPending[perm.merchant_sig] &&
!t.refundsDone[perm.merchant_sig]
) {
t.refundsPending[perm.merchant_sig] = perm;
}
}
return t;
}
const hc = refundResponse.h_contract_terms;
// Add the refund permissions to the purchase within a DB transaction
await oneShotMutate(ws.db, Stores.purchases, hc, f);
ws.notifier.notify();
await submitRefunds(ws, hc);
return hc;
}
/**
* Accept a refund, return the contract hash for the contract
* that was involved in the refund.
*/
export async function applyRefund(
ws: InternalWalletState,
talerRefundUri: string,
): Promise<string> {
const parseResult = parseRefundUri(talerRefundUri);
if (!parseResult) {
throw Error("invalid refund URI");
}
const refundUrl = parseResult.refundUrl;
logger.trace("processing refund");
let resp;
try {
resp = await ws.http.get(refundUrl);
} catch (e) {
console.error("error downloading refund permission", e);
throw e;
}
const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
return acceptRefundResponse(ws, refundResponse);
}

View File

@ -47,6 +47,8 @@ import {
preparePay,
confirmPay,
processDownloadProposal,
applyRefund,
getFullRefundFees,
} from "./wallet-impl/pay";
import {
@ -88,8 +90,6 @@ import { Logger } from "./util/logging";
import { assertUnreachable } from "./util/assertUnreachable";
import { applyRefund, getFullRefundFees } from "./wallet-impl/refund";
import {
updateExchangeFromUrl,
getExchangeTrust,
@ -209,6 +209,7 @@ export class Wallet {
*/
async processOnePendingOperation(
pending: PendingOperationInfo,
forceNow: boolean = false,
): Promise<void> {
switch (pending.type) {
case "bug":
@ -247,11 +248,11 @@ export class Wallet {
/**
* Process pending operations.
*/
public async runPending(): Promise<void> {
public async runPending(forceNow: boolean = false): Promise<void> {
const pendingOpsResponse = await this.getPendingOperations();
for (const p of pendingOpsResponse.pendingOperations) {
try {
await this.processOnePendingOperation(p);
await this.processOnePendingOperation(p, forceNow);
} catch (e) {
console.error(e);
}

View File

@ -74,7 +74,6 @@
"src/wallet-impl/payback.ts",
"src/wallet-impl/pending.ts",
"src/wallet-impl/refresh.ts",
"src/wallet-impl/refund.ts",
"src/wallet-impl/reserves.ts",
"src/wallet-impl/return.ts",
"src/wallet-impl/state.ts",