manual withdrawal

This commit is contained in:
Florian Dold 2020-07-16 22:52:56 +05:30
parent dd3a31f33d
commit 85a095fa7d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
13 changed files with 113 additions and 189 deletions

View File

@ -37,6 +37,7 @@ import {
WALLET_EXCHANGE_PROTOCOL_VERSION,
WALLET_MERCHANT_PROTOCOL_VERSION,
} from "../operations/versions";
import { Amounts } from "../util/amounts";
// @ts-ignore: special built-in module
//import akono = require("akono");
@ -234,10 +235,9 @@ class AndroidWalletMessageHandler {
const wallet = await this.wp.promise;
return await wallet.confirmPay(args.proposalId, args.sessionId);
}
case "createManualReserve": {
case "acceptManualWithdrawal": {
const wallet = await this.wp.promise;
const res = await wallet.createReserve(args);
await wallet.confirmReserve({ reservePub: res.reservePub });
const res = await wallet.acceptManualWithdrawal(args.exchangeBaseUrl, Amounts.parseOrThrow(args.amount));
return res;
}
case "startTunnel": {

View File

@ -7,7 +7,7 @@ import { openDatabase, Database, Store, Index } from "./util/query";
* with each major change. When incrementing the major version,
* the wallet should import data from the previous version.
*/
const TALER_DB_NAME = "taler-walletdb-v5";
const TALER_DB_NAME = "taler-walletdb-v6";
/**
* Current database minor version, should be incremented

View File

@ -26,7 +26,6 @@ import { Wallet } from "../wallet";
import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge";
import { openTalerDatabase } from "../db";
import { HttpRequestLibrary } from "../util/http";
import * as amounts from "../util/amounts";
import { Bank } from "./bank";
import fs from "fs";
import { NodeThreadCryptoWorkerFactory } from "../crypto/workers/nodeThreadWorker";
@ -36,6 +35,7 @@ import { NodeHttpLib } from "./NodeHttpLib";
import { Logger } from "../util/logging";
import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker";
import { WithdrawalSourceType } from "../types/dbTypes";
import { Amounts } from "../util/amounts";
const logger = new Logger("helpers.ts");
@ -142,11 +142,7 @@ export async function withdrawTestBalance(
bankBaseUrl = "https://bank.test.taler.net/",
exchangeBaseUrl = "https://exchange.test.taler.net/",
): Promise<void> {
const reserveResponse = await myWallet.createReserve({
amount: amounts.parseOrThrow(amount),
exchange: exchangeBaseUrl,
exchangeWire: "payto://unknown",
});
const reserveResponse = await myWallet.acceptManualWithdrawal(exchangeBaseUrl, Amounts.parseOrThrow(amount));
const reservePub = reserveResponse.reservePub;
@ -176,6 +172,5 @@ export async function withdrawTestBalance(
});
await bank.createReserve(bankUser, amount, reservePub, exchangePaytoUri);
await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub });
await donePromise;
}

View File

@ -367,9 +367,7 @@ exchangesCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.updateExchangeFromUrl(
args.exchangesAddCmd.url,
);
await wallet.updateExchangeFromUrl(args.exchangesAddCmd.url);
});
});
@ -387,12 +385,12 @@ exchangesCli
await withWallet(args, async (wallet) => {
await wallet.acceptExchangeTermsOfService(
args.exchangesAcceptTosCmd.url,
args.exchangesAcceptTosCmd.etag
args.exchangesAcceptTosCmd.etag,
);
});
});
exchangesCli
exchangesCli
.subcommand("exchangesTosCmd", "tos", {
help: "Show terms of service.",
})
@ -401,9 +399,7 @@ exchangesCli
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
const tosResult = await wallet.getExchangeTos(
args.exchangesTosCmd.url,
);
const tosResult = await wallet.getExchangeTos(args.exchangesTosCmd.url);
console.log(JSON.stringify(tosResult, undefined, 2));
});
});
@ -458,14 +454,10 @@ advancedCli
console.log("exchange has no accounts");
return;
}
const reserve = await wallet.createReserve({
amount: Amounts.parseOrThrow(args.withdrawManually.amount),
exchangeWire: acct.payto_uri,
exchange: exchange.baseUrl,
});
await wallet.confirmReserve({
reservePub: reserve.reservePub,
});
const reserve = await wallet.acceptManualWithdrawal(
exchange.baseUrl,
Amounts.parseOrThrow(args.withdrawManually.amount),
);
const completePaytoUri = addPaytoQueryParams(acct.payto_uri, {
amount: args.withdrawManually.amount,
message: `Taler top-up ${reserve.reservePub}`,

View File

@ -160,20 +160,6 @@ async function gatherReservePending(
case ReserveRecordStatus.DORMANT:
// nothing to report as pending
break;
case ReserveRecordStatus.UNCONFIRMED:
if (onlyDue) {
break;
}
resp.pendingOperations.push({
type: PendingOperationType.Reserve,
givesLifeness: false,
stage: reserve.reserveStatus,
timestampCreated: reserve.timestampCreated,
reserveType,
reservePub: reserve.reservePub,
retryInfo: reserve.retryInfo,
});
break;
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
case ReserveRecordStatus.WITHDRAWING:
case ReserveRecordStatus.QUERYING_STATUS:

View File

@ -17,7 +17,6 @@
import {
CreateReserveRequest,
CreateReserveResponse,
ConfirmReserveRequest,
OperationError,
AcceptWithdrawalResponse,
} from "../types/walletTypes";
@ -66,6 +65,8 @@ import {
reconcileReserveHistory,
summarizeReserveHistory,
} from "../util/reserveHistoryUtil";
import { TransactionHandle } from "../util/query";
import { addPaytoQueryParams } from "../util/payto";
const logger = new Logger("reserves.ts");
@ -99,14 +100,18 @@ export async function createReserve(
if (req.bankWithdrawStatusUrl) {
reserveStatus = ReserveRecordStatus.REGISTERING_BANK;
} else {
reserveStatus = ReserveRecordStatus.UNCONFIRMED;
reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
}
let bankInfo: ReserveBankInfo | undefined;
if (req.bankWithdrawStatusUrl) {
if (!req.exchangePaytoUri) {
throw Error("Exchange payto URI must be specified for a bank-integrated withdrawal");
}
bankInfo = {
statusUrl: req.bankWithdrawStatusUrl,
exchangePaytoUri: req.exchangePaytoUri,
};
}
@ -129,10 +134,9 @@ export async function createReserve(
reservePriv: keypair.priv,
reservePub: keypair.pub,
senderWire: req.senderWire,
timestampConfirmed: undefined,
timestampBankConfirmed: undefined,
timestampReserveInfoPosted: undefined,
bankInfo,
exchangeWire: req.exchangeWire,
reserveStatus,
lastSuccessfulStatusQuery: undefined,
retryInfo: initRetryInfo(),
@ -314,7 +318,7 @@ async function registerReserveWithBank(
// FIXME: parse bank response
await ws.http.postJson(bankStatusUrl, {
reserve_pub: reservePub,
selected_exchange: reserve.exchangeWire,
selected_exchange: bankInfo.exchangePaytoUri,
});
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
switch (r.reserveStatus) {
@ -395,7 +399,7 @@ async function processReserveBankStatusImpl(
return;
}
const now = getTimestampNow();
r.timestampConfirmed = now;
r.timestampBankConfirmed = now;
r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
r.retryInfo = initRetryInfo();
return r;
@ -461,10 +465,6 @@ async function updateReserve(
throw Error("reserve not in db");
}
if (reserve.timestampConfirmed === undefined) {
throw Error("reserve not confirmed yet");
}
if (reserve.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
return;
}
@ -590,9 +590,6 @@ async function processReserveImpl(
`Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
);
switch (reserve.reserveStatus) {
case ReserveRecordStatus.UNCONFIRMED:
// nothing to do
break;
case ReserveRecordStatus.REGISTERING_BANK:
await processReserveBankStatus(ws, reservePub);
return await processReserveImpl(ws, reservePub, true);
@ -615,28 +612,6 @@ async function processReserveImpl(
}
}
export async function confirmReserve(
ws: InternalWalletState,
req: ConfirmReserveRequest,
): Promise<void> {
const now = getTimestampNow();
await ws.db.mutate(Stores.reserves, req.reservePub, (reserve) => {
if (reserve.reserveStatus !== ReserveRecordStatus.UNCONFIRMED) {
return;
}
reserve.timestampConfirmed = now;
reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
reserve.retryInfo = initRetryInfo();
return reserve;
});
ws.notify({ type: NotificationType.ReserveUpdated });
processReserve(ws, req.reservePub, true).catch((e) => {
console.log("processing reserve (after confirmReserve) failed:", e);
});
}
/**
* Withdraw coins from a reserve until it is empty.
*
@ -818,7 +793,7 @@ export async function createTalerWithdrawReserve(
bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl,
exchange: selectedExchange,
senderWire: withdrawInfo.senderWire,
exchangeWire: exchangeWire,
exchangePaytoUri: exchangeWire,
});
// We do this here, as the reserve should be registered before we return,
// so that we can redirect the user to the bank's status page.
@ -829,3 +804,34 @@ export async function createTalerWithdrawReserve(
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
};
}
/**
* Get payto URIs needed to fund a reserve.
*/
export async function getFundingPaytoUris(
tx: TransactionHandle,
reservePub: string,
): Promise<string[]> {
const r = await tx.get(Stores.reserves, reservePub);
if (!r) {
logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
return [];
}
const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
if (!exchange) {
logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`);
return [];
}
const plainPaytoUris =
exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
if (!plainPaytoUris) {
logger.error(`exchange ${r.exchangeBaseUrl} has no wire info`);
return [];
}
return plainPaytoUris.map((x) =>
addPaytoQueryParams(x, {
amount: Amounts.stringify(r.instructedAmount),
message: `Taler Withdrawal ${r.reservePub}`,
}),
);
}

View File

@ -35,9 +35,7 @@ import {
WithdrawalType,
WithdrawalDetails,
} from "../types/transactions";
import { WithdrawalDetailsResponse } from "../types/walletTypes";
import { Logger } from "../util/logging";
import { addPaytoQueryParams } from "../util/payto";
import { getFundingPaytoUris } from "./reserves";
/**
* Create an event ID from the type and the primary key for the event.
@ -257,22 +255,9 @@ export async function getTransactions(
bankConfirmationUrl: r.bankInfo.confirmUrl,
}
} else {
const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
if (!exchange) {
// FIXME: report somehow
return;
}
const plainPaytoUris = exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
if (!plainPaytoUris) {
// FIXME: report somehow
return;
}
withdrawalDetails = {
type: WithdrawalType.ManualTransfer,
exchangePaytoUris: plainPaytoUris.map((x) => addPaytoQueryParams(x, {
amount: Amounts.stringify(r.instructedAmount),
message: `Taler Withdrawal ${r.reservePub}`,
})),
exchangePaytoUris: await getFundingPaytoUris(tx, r.reservePub),
};
}
transactions.push({

View File

@ -48,11 +48,6 @@ import { Timestamp, Duration, getTimestampNow } from "../util/time";
import { PayCoinSelection, PayCostInfo } from "../operations/pay";
export enum ReserveRecordStatus {
/**
* Waiting for manual confirmation.
*/
UNCONFIRMED = "unconfirmed",
/**
* Reserve must be registered with the bank.
*/
@ -219,8 +214,18 @@ export interface ReserveHistoryRecord {
}
export interface ReserveBankInfo {
/**
* Status URL that the wallet will use to query the status
* of the Taler withdrawal operation on the bank's side.
*/
statusUrl: string;
confirmUrl?: string;
/**
* Exchange payto URI that the bank will use to fund the reserve.
*/
exchangePaytoUri: string;
}
/**
@ -262,12 +267,11 @@ export interface ReserveRecord {
timestampReserveInfoPosted: Timestamp | undefined;
/**
* Time when the reserve was confirmed, either manually by the user
* or by the bank.
* Time when the reserve was confirmed by the bank.
*
* Set to undefined if not confirmed yet.
*/
timestampConfirmed: Timestamp | undefined;
timestampBankConfirmed: Timestamp | undefined;
/**
* Wire information (as payto URI) for the bank account that
@ -275,12 +279,6 @@ export interface ReserveRecord {
*/
senderWire?: string;
/**
* Wire information (as payto URI) for the exchange, specifically
* the account that was transferred to when creating the reserve.
*/
exchangeWire: string;
/**
* Amount that was sent by the user to fund the reserve.
*/

View File

@ -246,7 +246,7 @@ export interface CreateReserveRequest {
* Payto URI that identifies the exchange's account that the funds
* for this reserve go into.
*/
exchangeWire: string;
exchangePaytoUri?: string;
/**
* Wire details (as a payto URI) for the bank account that sent the funds to
@ -264,7 +264,7 @@ export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> =>
makeCodecForObject<CreateReserveRequest>()
.property("amount", codecForAmountJson())
.property("exchange", codecForString)
.property("exchangeWire", codecForString)
.property("exchangePaytoUri", codecForString)
.property("senderWire", makeCodecOptional(codecForString))
.property("bankWithdrawStatusUrl", makeCodecOptional(codecForString))
.build("CreateReserveRequest");
@ -491,6 +491,18 @@ export interface ExchangeListItem {
paytoUris: string[];
}
export interface AcceptManualWithdrawalResult {
/**
* Payto URIs that can be used to fund the withdrawal.
*/
exchangePaytoUris: string[];
/**
* Public key of the newly created reserve.
*/
reservePub: string;
}
export interface ManualWithdrawalDetails {
/**
* Did the user accept the current version of the exchange's

View File

@ -56,9 +56,6 @@ import { MerchantRefundDetails, CoinDumpJson } from "./types/talerTypes";
import {
BenchmarkResult,
ConfirmPayResult,
ConfirmReserveRequest,
CreateReserveRequest,
CreateReserveResponse,
ReturnCoinsRequest,
SenderWireInfos,
TipStatus,
@ -72,6 +69,7 @@ import {
ExchangesListRespose,
ManualWithdrawalDetails,
GetExchangeTosResult,
AcceptManualWithdrawalResult,
} from "./types/walletTypes";
import { Logger } from "./util/logging";
@ -87,10 +85,11 @@ import {
processReserve,
createTalerWithdrawReserve,
forceQueryReserve,
getFundingPaytoUris,
} from "./operations/reserves";
import { InternalWalletState } from "./operations/state";
import { createReserve, confirmReserve } from "./operations/reserves";
import { createReserve } from "./operations/reserves";
import { processRefreshGroup, createRefreshGroup } from "./operations/refresh";
import { processWithdrawGroup } from "./operations/withdraw";
import { getHistory } from "./operations/history";
@ -171,8 +170,14 @@ export class Wallet {
exchangeBaseUrl: string,
amount: AmountJson,
): Promise<ManualWithdrawalDetails> {
const wi = await getExchangeWithdrawalInfo(this.ws, exchangeBaseUrl, amount);
const paytoUris = wi.exchangeInfo.wireInfo?.accounts.map((x) => x.payto_uri);
const wi = await getExchangeWithdrawalInfo(
this.ws,
exchangeBaseUrl,
amount,
);
const paytoUris = wi.exchangeInfo.wireInfo?.accounts.map(
(x) => x.payto_uri,
);
if (!paytoUris) {
throw Error("exchange is in invalid state");
}
@ -437,28 +442,23 @@ export class Wallet {
* Adds the corresponding exchange as a trusted exchange if it is neither
* audited nor trusted already.
*/
async createReserve(
req: CreateReserveRequest,
): Promise<CreateReserveResponse> {
async acceptManualWithdrawal(
exchangeBaseUrl: string,
amount: AmountJson,
): Promise<AcceptManualWithdrawalResult> {
try {
return createReserve(this.ws, req);
} finally {
this.latch.trigger();
}
}
/**
* Mark an existing reserve as confirmed. The wallet will start trying
* to withdraw from that reserve. This may not immediately succeed,
* since the exchange might not know about the reserve yet, even though the
* bank confirmed its creation.
*
* A confirmed reserve should be shown to the user in the UI, while
* an unconfirmed reserve should be hidden.
*/
async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
try {
return confirmReserve(this.ws, req);
const resp = await createReserve(this.ws, {
amount,
exchange: exchangeBaseUrl,
});
const exchangePaytoUris = await this.db.runWithReadTransaction(
[Stores.exchanges, Stores.reserves],
(tx) => getFundingPaytoUris(tx, resp.reservePub),
);
return {
reservePub: resp.reservePub,
exchangePaytoUris,
};
} finally {
this.latch.trigger();
}
@ -511,7 +511,7 @@ export class Wallet {
acceptedEtag: exchange.termsOfServiceAcceptedEtag,
currentEtag,
tos,
}
};
}
/**
@ -602,7 +602,7 @@ export class Wallet {
return {
exchangeBaseUrl: x.baseUrl,
currency: details.currency,
paytoUris: x.wireInfo.accounts.map(x => x.payto_uri),
paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri),
};
});
return {

View File

@ -21,7 +21,6 @@
// Messages are already documented in wxApi.
/* tslint:disable:completed-docs */
import { AmountJson } from "../util/amounts";
import * as dbTypes from "../types/dbTypes";
import * as walletTypes from "../types/walletTypes";
@ -54,17 +53,6 @@ export interface MessageMap {
request: {};
response: void;
};
"create-reserve": {
request: {
amount: AmountJson;
exchange: string;
};
response: void;
};
"confirm-reserve": {
request: { reservePub: string };
response: void;
};
"confirm-pay": {
request: { proposalId: string; sessionId?: string };
response: walletTypes.ConfirmPayResult;

View File

@ -32,7 +32,6 @@ import {
import {
BenchmarkResult,
ConfirmPayResult,
ExchangeWithdrawDetails,
SenderWireInfos,
TipStatus,
WalletBalance,
@ -172,13 +171,6 @@ export function confirmPay(
return callBackend("confirm-pay", { proposalId, sessionId });
}
/**
* Mark a reserve as confirmed.
*/
export function confirmReserve(reservePub: string): Promise<void> {
return callBackend("confirm-reserve", { reservePub });
}
/**
* Check upgrade information
*/
@ -186,17 +178,6 @@ export function checkUpgrade(): Promise<UpgradeResponse> {
return callBackend("check-upgrade", {});
}
/**
* Create a reserve.
*/
export function createReserve(args: {
amount: AmountJson;
exchange: string;
senderWire?: string;
}): Promise<any> {
return callBackend("create-reserve", args);
}
/**
* Reset database
*/

View File

@ -32,10 +32,7 @@ import {
import {
ReturnCoinsRequest,
WalletDiagnostics,
codecForCreateReserveRequest,
codecForConfirmReserveRequest,
} from "../types/walletTypes";
import { codecForAmountJson } from "../util/amounts";
import { BrowserHttpLib } from "../util/http";
import { OpenedPromise, openPromise } from "../util/promiseUtils";
import { classifyTalerUri, TalerUriType } from "../util/taleruri";
@ -111,22 +108,6 @@ async function handleMessage(
}
return Promise.resolve({});
}
case "create-reserve": {
const d = {
amount: detail.amount,
exchange: detail.exchange,
senderWire: detail.senderWire,
};
const req = codecForCreateReserveRequest().decode(d);
return needsWallet().createReserve(req);
}
case "confirm-reserve": {
const d = {
reservePub: detail.reservePub,
};
const req = codecForConfirmReserveRequest().decode(d);
return needsWallet().confirmReserve(req);
}
case "confirm-pay": {
if (typeof detail.proposalId !== "string") {
throw Error("proposalId must be string");