From 85a095fa7d4d31e1e84e5e096fa28c59f3cd1918 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 16 Jul 2020 22:52:56 +0530 Subject: [PATCH] manual withdrawal --- src/android/index.ts | 6 +-- src/db.ts | 2 +- src/headless/helpers.ts | 9 +--- src/headless/taler-wallet-cli.ts | 24 ++++------ src/operations/pending.ts | 14 ------ src/operations/reserves.ts | 78 +++++++++++++++++--------------- src/operations/transactions.ts | 19 +------- src/types/dbTypes.ts | 26 +++++------ src/types/walletTypes.ts | 16 ++++++- src/wallet.ts | 58 ++++++++++++------------ src/webex/messages.ts | 12 ----- src/webex/wxApi.ts | 19 -------- src/webex/wxBackend.ts | 19 -------- 13 files changed, 113 insertions(+), 189 deletions(-) diff --git a/src/android/index.ts b/src/android/index.ts index fcdbdaa6e..63d88d70b 100644 --- a/src/android/index.ts +++ b/src/android/index.ts @@ -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": { diff --git a/src/db.ts b/src/db.ts index 072b7844e..ff9ee5a42 100644 --- a/src/db.ts +++ b/src/db.ts @@ -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 diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts index 47a0844bd..aa6fa9751 100644 --- a/src/headless/helpers.ts +++ b/src/headless/helpers.ts @@ -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 { - 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; } diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index c9264caea..5637732b3 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -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}`, diff --git a/src/operations/pending.ts b/src/operations/pending.ts index cf9b306d6..e610d42ef 100644 --- a/src/operations/pending.ts +++ b/src/operations/pending.ts @@ -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: diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts index 965831704..7dd97decb 100644 --- a/src/operations/reserves.ts +++ b/src/operations/reserves.ts @@ -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 { - 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 { + 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}`, + }), + ); +} diff --git a/src/operations/transactions.ts b/src/operations/transactions.ts index bcf7a9e68..bdb68a3bb 100644 --- a/src/operations/transactions.ts +++ b/src/operations/transactions.ts @@ -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({ diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts index 55f16f40b..4f7b89b67 100644 --- a/src/types/dbTypes.ts +++ b/src/types/dbTypes.ts @@ -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. */ diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts index 74f2428dd..63b20095e 100644 --- a/src/types/walletTypes.ts +++ b/src/types/walletTypes.ts @@ -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 => makeCodecForObject() .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 diff --git a/src/wallet.ts b/src/wallet.ts index 737704fd6..5412a0fd2 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -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 { - 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 { + async acceptManualWithdrawal( + exchangeBaseUrl: string, + amount: AmountJson, + ): Promise { 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 { - 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 { diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 5cf2fefdb..fd9fe0347 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -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; diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index 47e73ca4c..de37b3639 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -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 { - return callBackend("confirm-reserve", { reservePub }); -} - /** * Check upgrade information */ @@ -186,17 +178,6 @@ export function checkUpgrade(): Promise { return callBackend("check-upgrade", {}); } -/** - * Create a reserve. - */ -export function createReserve(args: { - amount: AmountJson; - exchange: string; - senderWire?: string; -}): Promise { - return callBackend("create-reserve", args); -} - /** * Reset database */ diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 540c79771..126756165 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -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");