aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-11-30 00:36:20 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-11-30 00:36:20 +0100
commitaaf7e1338d6cdb1b4e01ad318938b3eaea2f922b (patch)
tree594129ccdf20757aeb86d434dd62c0c1e8259ed5 /src
parent809fa186448dbd924f258f89920b9336f1979bb0 (diff)
wallet robustness WIP
Diffstat (limited to 'src')
-rw-r--r--src/crypto/cryptoApi.ts16
-rw-r--r--src/crypto/cryptoImplementation.ts25
-rw-r--r--src/crypto/primitives/kdf.ts2
-rw-r--r--src/crypto/talerCrypto.ts3
-rw-r--r--src/dbTypes.ts164
-rw-r--r--src/headless/bank.ts31
-rw-r--r--src/headless/clk.ts15
-rw-r--r--src/headless/helpers.ts18
-rw-r--r--src/headless/merchant.ts64
-rw-r--r--src/headless/taler-wallet-cli.ts183
-rw-r--r--src/wallet-test.ts2
-rw-r--r--src/wallet.ts877
-rw-r--r--src/walletTypes.ts49
-rw-r--r--src/webex/messages.ts6
-rw-r--r--src/webex/pages/payback.tsx2
-rw-r--r--src/webex/wxApi.ts10
-rw-r--r--src/webex/wxBackend.ts21
17 files changed, 1067 insertions, 421 deletions
diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts
index 46fe2576e..b5eae9beb 100644
--- a/src/crypto/cryptoApi.ts
+++ b/src/crypto/cryptoApi.ts
@@ -27,7 +27,7 @@ import { AmountJson } from "../amounts";
import {
CoinRecord,
DenominationRecord,
- PreCoinRecord,
+ PlanchetRecord,
RefreshSessionRecord,
ReserveRecord,
TipPlanchet,
@@ -38,7 +38,7 @@ import { CryptoWorker } from "./cryptoWorker";
import { ContractTerms, PaybackRequest } from "../talerTypes";
-import { BenchmarkResult, CoinWithDenom, PayCoinInfo } from "../walletTypes";
+import { BenchmarkResult, CoinWithDenom, PayCoinInfo, PlanchetCreationResult } from "../walletTypes";
import * as timer from "../timer";
@@ -173,6 +173,7 @@ export class CryptoApi {
*/
wake(ws: WorkerState, work: WorkItem): void {
if (this.stopped) {
+ console.log("cryptoApi is stopped");
CryptoApi.enableTracing && console.log("not waking, as cryptoApi is stopped");
return;
}
@@ -299,7 +300,6 @@ export class CryptoApi {
priority: number,
...args: any[]
): Promise<T> {
- CryptoApi.enableTracing && console.log("cryptoApi: doRpc called");
const p: Promise<T> = new Promise<T>((resolve, reject) => {
const rpcId = this.nextRpcId++;
const workItem: WorkItem = {
@@ -332,16 +332,14 @@ export class CryptoApi {
throw Error("assertion failed");
});
- return p.then((r: T) => {
- return r;
- });
+ return p;
}
- createPreCoin(
+ createPlanchet(
denom: DenominationRecord,
reserve: ReserveRecord,
- ): Promise<PreCoinRecord> {
- return this.doRpc<PreCoinRecord>("createPreCoin", 1, denom, reserve);
+ ): Promise<PlanchetCreationResult> {
+ return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, denom, reserve);
}
createTipPlanchet(denom: DenominationRecord): Promise<TipPlanchet> {
diff --git a/src/crypto/cryptoImplementation.ts b/src/crypto/cryptoImplementation.ts
index 9ffdec701..7cddf9031 100644
--- a/src/crypto/cryptoImplementation.ts
+++ b/src/crypto/cryptoImplementation.ts
@@ -28,8 +28,7 @@ import {
CoinRecord,
CoinStatus,
DenominationRecord,
- PreCoinRecord,
- RefreshPreCoinRecord,
+ RefreshPlanchetRecord,
RefreshSessionRecord,
ReserveRecord,
TipPlanchet,
@@ -42,6 +41,7 @@ import {
CoinWithDenom,
PayCoinInfo,
Timestamp,
+ PlanchetCreationResult,
} from "../walletTypes";
import { canonicalJson, getTalerStampSec } from "../helpers";
import { AmountJson } from "../amounts";
@@ -154,10 +154,10 @@ export class CryptoImplementation {
* Create a pre-coin of the given denomination to be withdrawn from then given
* reserve.
*/
- createPreCoin(
+ createPlanchet(
denom: DenominationRecord,
reserve: ReserveRecord,
- ): PreCoinRecord {
+ ): PlanchetCreationResult {
const reservePub = decodeCrock(reserve.reservePub);
const reservePriv = decodeCrock(reserve.reservePriv);
const denomPub = decodeCrock(denom.denomPub);
@@ -179,7 +179,7 @@ export class CryptoImplementation {
const sig = eddsaSign(withdrawRequest, reservePriv);
- const preCoin: PreCoinRecord = {
+ const planchet: PlanchetCreationResult = {
blindingKey: encodeCrock(blindingFactor),
coinEv: encodeCrock(ev),
coinPriv: encodeCrock(coinKeyPair.eddsaPriv),
@@ -188,11 +188,10 @@ export class CryptoImplementation {
denomPub: encodeCrock(denomPub),
denomPubHash: encodeCrock(denomPubHash),
exchangeBaseUrl: reserve.exchangeBaseUrl,
- isFromTip: false,
reservePub: encodeCrock(reservePub),
withdrawSig: encodeCrock(sig),
};
- return preCoin;
+ return planchet;
}
/**
@@ -424,7 +423,7 @@ export class CryptoImplementation {
const transferPubs: string[] = [];
const transferPrivs: string[] = [];
- const preCoinsForGammas: RefreshPreCoinRecord[][] = [];
+ const planchetsForGammas: RefreshPlanchetRecord[][] = [];
for (let i = 0; i < kappa; i++) {
const transferKeyPair = createEcdheKeyPair();
@@ -442,7 +441,7 @@ export class CryptoImplementation {
sessionHc.update(amountToBuffer(valueWithFee));
for (let i = 0; i < kappa; i++) {
- const preCoins: RefreshPreCoinRecord[] = [];
+ const planchets: RefreshPlanchetRecord[] = [];
for (let j = 0; j < newCoinDenoms.length; j++) {
const transferPriv = decodeCrock(transferPrivs[i]);
const oldCoinPub = decodeCrock(meltCoin.coinPub);
@@ -456,16 +455,16 @@ export class CryptoImplementation {
const pubHash = hash(coinPub);
const denomPub = decodeCrock(newCoinDenoms[j].denomPub);
const ev = rsaBlind(pubHash, blindingFactor, denomPub);
- const preCoin: RefreshPreCoinRecord = {
+ const planchet: RefreshPlanchetRecord = {
blindingKey: encodeCrock(blindingFactor),
coinEv: encodeCrock(ev),
privateKey: encodeCrock(coinPriv),
publicKey: encodeCrock(coinPub),
};
- preCoins.push(preCoin);
+ planchets.push(planchet);
sessionHc.update(ev);
}
- preCoinsForGammas.push(preCoins);
+ planchetsForGammas.push(planchets);
}
const sessionHash = sessionHc.finish();
@@ -496,7 +495,7 @@ export class CryptoImplementation {
newDenomHashes: newCoinDenoms.map(d => d.denomPubHash),
newDenoms: newCoinDenoms.map(d => d.denomPub),
norevealIndex: undefined,
- preCoinsForGammas,
+ planchetsForGammas: planchetsForGammas,
transferPrivs,
transferPubs,
valueOutput,
diff --git a/src/crypto/primitives/kdf.ts b/src/crypto/primitives/kdf.ts
index 082963074..e1baed408 100644
--- a/src/crypto/primitives/kdf.ts
+++ b/src/crypto/primitives/kdf.ts
@@ -88,5 +88,5 @@ export function kdf(
output.set(chunk, i * 32);
}
- return output;
+ return output.slice(0, outputLength);
}
diff --git a/src/crypto/talerCrypto.ts b/src/crypto/talerCrypto.ts
index b754b0c57..317b1af55 100644
--- a/src/crypto/talerCrypto.ts
+++ b/src/crypto/talerCrypto.ts
@@ -237,6 +237,9 @@ function rsaFullDomainHash(hm: Uint8Array, rsaPub: RsaPub): bigint.BigInteger {
function rsaPubDecode(rsaPub: Uint8Array): RsaPub {
const modulusLength = (rsaPub[0] << 8) | rsaPub[1];
const exponentLength = (rsaPub[2] << 8) | rsaPub[3];
+ if (4 + exponentLength + modulusLength != rsaPub.length) {
+ throw Error("invalid RSA public key (format wrong)");
+ }
const modulus = rsaPub.slice(4, 4 + modulusLength);
const exponent = rsaPub.slice(
4 + modulusLength,
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index bb4f5dbdf..8dba28edb 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -58,6 +58,13 @@ export enum ReserveRecordStatus {
REGISTERING_BANK = "registering-bank",
/**
+ * We've registered reserve's information with the bank
+ * and are now waiting for the user to confirm the withdraw
+ * with the bank (typically 2nd factor auth).
+ */
+ WAIT_CONFIRM_BANK = "wait-confirm-bank",
+
+ /**
* Querying reserve status with the exchange.
*/
QUERYING_STATUS = "querying-status",
@@ -117,22 +124,26 @@ export interface ReserveRecord {
timestampConfirmed: Timestamp | undefined;
/**
- * Current amount left in the reserve
+ * Amount that's still available for withdrawing
+ * from this reserve.
*/
- currentAmount: AmountJson | null;
+ withdrawRemainingAmount: AmountJson;
/**
- * Amount requested when the reserve was created.
- * When a reserve is re-used (rare!) the current_amount can
- * be higher than the requested_amount
+ * Amount allocated for withdrawing.
+ * The corresponding withdraw operation may or may not
+ * have been completed yet.
*/
- requestedAmount: AmountJson;
+ withdrawAllocatedAmount: AmountJson;
+
+ withdrawCompletedAmount: AmountJson;
/**
- * What's the current amount that sits
- * in precoins?
+ * Amount requested when the reserve was created.
+ * When a reserve is re-used (rare!) the current_amount can
+ * be higher than the requested_amount
*/
- precoinAmount: AmountJson;
+ initiallyRequestedAmount: AmountJson;
/**
* We got some payback to this reserve. We'll cease to automatically
@@ -154,8 +165,19 @@ export interface ReserveRecord {
bankWithdrawStatusUrl?: string;
+ /**
+ * URL that the bank gave us to redirect the customer
+ * to in order to confirm a withdrawal.
+ */
+ bankWithdrawConfirmUrl?: string;
+
reserveStatus: ReserveRecordStatus;
+ /**
+ * Time of the last successful status query.
+ */
+ lastStatusQuery: Timestamp | undefined;
+
lastError?: OperationError;
}
@@ -421,7 +443,16 @@ export interface ExchangeRecord {
/**
* A coin that isn't yet signed by an exchange.
*/
-export interface PreCoinRecord {
+export interface PlanchetRecord {
+ withdrawSessionId: string;
+ /**
+ * Index of the coin in the withdrawal session.
+ */
+ coinIndex: number;
+
+ /**
+ * Public key of the coin.
+ */
coinPub: string;
coinPriv: string;
reservePub: string;
@@ -443,7 +474,7 @@ export interface PreCoinRecord {
/**
* Planchet for a coin during refrehs.
*/
-export interface RefreshPreCoinRecord {
+export interface RefreshPlanchetRecord {
/**
* Public key for the coin.
*/
@@ -486,6 +517,16 @@ export enum CoinStatus {
*/
export interface CoinRecord {
/**
+ * Withdraw session ID, or "" (empty string) if withdrawn via refresh.
+ */
+ withdrawSessionId: string;
+
+ /**
+ * Index of the coin in the withdrawal session.
+ */
+ coinIndex: number;
+
+ /**
* Public key of the coin.
*/
coinPub: string;
@@ -546,11 +587,17 @@ export interface CoinRecord {
status: CoinStatus;
}
+export enum ProposalStatus {
+ PROPOSED = "proposed",
+ ACCEPTED = "accepted",
+ REJECTED = "rejected",
+}
+
/**
- * Proposal record, stored in the wallet's database.
+ * Record for a downloaded order, stored in the wallet's database.
*/
@Checkable.Class()
-export class ProposalDownloadRecord {
+export class ProposalRecord {
/**
* URL where the proposal was downloaded.
*/
@@ -576,10 +623,10 @@ export class ProposalDownloadRecord {
contractTermsHash: string;
/**
- * Serial ID when the offer is stored in the wallet DB.
+ * Unique ID when the order is stored in the wallet DB.
*/
- @Checkable.Optional(Checkable.Number())
- id?: number;
+ @Checkable.String()
+ proposalId: string;
/**
* Timestamp (in ms) of when the record
@@ -594,6 +641,9 @@ export class ProposalDownloadRecord {
@Checkable.String()
noncePriv: string;
+ @Checkable.String()
+ proposalStatus: ProposalStatus;
+
/**
* Session ID we got when downloading the contract.
*/
@@ -604,7 +654,7 @@ export class ProposalDownloadRecord {
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
- static checked: (obj: any) => ProposalDownloadRecord;
+ static checked: (obj: any) => ProposalRecord;
}
/**
@@ -717,9 +767,9 @@ export interface RefreshSessionRecord {
newDenoms: string[];
/**
- * Precoins for each cut-and-choose instance.
+ * Planchets for each cut-and-choose instance.
*/
- preCoinsForGammas: RefreshPreCoinRecord[][];
+ planchetsForGammas: RefreshPlanchetRecord[][];
/**
* The transfer keys, kappa of them.
@@ -933,7 +983,9 @@ export interface CoinsReturnRecord {
wire: any;
}
-export interface WithdrawalRecord {
+export interface WithdrawalSessionRecord {
+ withdrawSessionId: string;
+
/**
* Reserve that we're withdrawing from.
*/
@@ -956,9 +1008,29 @@ export interface WithdrawalRecord {
*/
withdrawalAmount: string;
- numCoinsTotal: number;
+ denoms: string[];
+
+ /**
+ * Coins in this session that are withdrawn are set to true.
+ */
+ withdrawn: boolean[];
+
+ /**
+ * Coins in this session already have a planchet are set to true.
+ */
+ planchetCreated: boolean[];
+}
+
+export interface BankWithdrawUriRecord {
+ /**
+ * The withdraw URI we got from the bank.
+ */
+ talerWithdrawUri: string;
- numCoinsWithdrawn: number;
+ /**
+ * Reserve that was created for the withdraw URI.
+ */
+ reservePub: string;
}
/* tslint:disable:completed-docs */
@@ -967,7 +1039,7 @@ export interface WithdrawalRecord {
* The stores and indices for the wallet database.
*/
export namespace Stores {
- class ExchangeStore extends Store<ExchangeRecord> {
+ class ExchangesStore extends Store<ExchangeRecord> {
constructor() {
super("exchanges", { keyPath: "baseUrl" });
}
@@ -988,16 +1060,18 @@ export namespace Stores {
"denomPubIndex",
"denomPub",
);
+ byWithdrawalWithIdx = new Index<any, CoinRecord>(
+ this,
+ "planchetsByWithdrawalWithIdxIndex",
+ ["withdrawSessionId", "coinIndex"],
+ );
}
- class ProposalsStore extends Store<ProposalDownloadRecord> {
+ class ProposalsStore extends Store<ProposalRecord> {
constructor() {
- super("proposals", {
- autoIncrement: true,
- keyPath: "id",
- });
+ super("proposals", { keyPath: "proposalId" });
}
- urlIndex = new Index<string, ProposalDownloadRecord>(
+ urlIndex = new Index<string, ProposalRecord>(
this,
"urlIndex",
"url",
@@ -1084,28 +1158,39 @@ export namespace Stores {
}
}
- class WithdrawalsStore extends Store<WithdrawalRecord> {
+ class WithdrawalSessionsStore extends Store<WithdrawalSessionRecord> {
constructor() {
- super("withdrawals", { keyPath: "id", autoIncrement: true });
+ super("withdrawals", { keyPath: "withdrawSessionId" });
}
- byReservePub = new Index<string, WithdrawalRecord>(
+ byReservePub = new Index<string, WithdrawalSessionRecord>(
this,
"withdrawalsReservePubIndex",
"reservePub",
);
}
- class PreCoinsStore extends Store<PreCoinRecord> {
+ class BankWithdrawUrisStore extends Store<BankWithdrawUriRecord> {
constructor() {
- super("precoins", {
+ super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
+ }
+ }
+
+ class PlanchetsStore extends Store<PlanchetRecord> {
+ constructor() {
+ super("planchets", {
keyPath: "coinPub",
});
}
- byReservePub = new Index<string, PreCoinRecord>(
+ byReservePub = new Index<string, PlanchetRecord>(
this,
- "precoinsReservePubIndex",
+ "planchetsReservePubIndex",
"reservePub",
);
+ byWithdrawalWithIdx = new Index<any, PlanchetRecord>(
+ this,
+ "planchetsByWithdrawalWithIdxIndex",
+ ["withdrawSessionId", "coinIndex"],
+ );
}
export const coins = new CoinsStore();
@@ -1115,8 +1200,8 @@ export namespace Stores {
export const config = new ConfigStore();
export const currencies = new CurrenciesStore();
export const denominations = new DenominationsStore();
- export const exchanges = new ExchangeStore();
- export const precoins = new PreCoinsStore();
+ export const exchanges = new ExchangesStore();
+ export const planchets = new PlanchetsStore();
export const proposals = new ProposalsStore();
export const refresh = new Store<RefreshSessionRecord>("refresh", {
keyPath: "refreshSessionId",
@@ -1125,7 +1210,8 @@ export namespace Stores {
export const purchases = new PurchasesStore();
export const tips = new TipsStore();
export const senderWires = new SenderWiresStore();
- export const withdrawals = new WithdrawalsStore();
+ export const withdrawalSession = new WithdrawalSessionsStore();
+ export const bankWithdrawUris = new BankWithdrawUrisStore();
}
/* tslint:enable:completed-docs */
diff --git a/src/headless/bank.ts b/src/headless/bank.ts
index f35021003..36f61a71a 100644
--- a/src/headless/bank.ts
+++ b/src/headless/bank.ts
@@ -45,6 +45,37 @@ function makeId(length: number): string {
export class Bank {
constructor(private bankBaseUrl: string) {}
+ async generateWithdrawUri(bankUser: BankUser, amount: string): Promise<string> {
+ const body = {
+ amount,
+ };
+
+ const reqUrl = new URI("api/withdraw-headless-uri")
+ .absoluteTo(this.bankBaseUrl)
+ .href();
+
+ const resp = await Axios({
+ method: "post",
+ url: reqUrl,
+ data: body,
+ responseType: "json",
+ headers: {
+ "X-Taler-Bank-Username": bankUser.username,
+ "X-Taler-Bank-Password": bankUser.password,
+ },
+ });
+
+ if (resp.status != 200) {
+ throw Error("failed to create bank reserve");
+ }
+
+ const withdrawUri = resp.data["taler_withdraw_uri"];
+ if (!withdrawUri) {
+ throw Error("Bank's response did not include withdraw URI");
+ }
+ return withdrawUri;
+ }
+
async createReserve(
bankUser: BankUser,
amount: string,
diff --git a/src/headless/clk.ts b/src/headless/clk.ts
index 4a568dc18..828eb24c0 100644
--- a/src/headless/clk.ts
+++ b/src/headless/clk.ts
@@ -29,6 +29,7 @@ export let STRING: Converter<string> = new Converter<string>();
export interface OptionArgs<T> {
help?: string;
default?: T;
+ onPresentHandler?: (v: T) => void;
}
export interface ArgumentArgs<T> {
@@ -269,9 +270,6 @@ export class CommandGroup<GN extends keyof any, TG> {
}
printHelp(progName: string, parents: CommandGroup<any, any>[]) {
- const chain: CommandGroup<any, any>[] = Array.prototype.concat(parents, [
- this,
- ]);
let usageSpec = "";
for (let p of parents) {
usageSpec += (p.name ?? progName) + " ";
@@ -352,6 +350,7 @@ export class CommandGroup<GN extends keyof any, TG> {
process.exit(-1);
throw Error("not reached");
}
+ foundOptions[d.name] = true;
myArgs[d.name] = true;
} else {
if (r.value === undefined) {
@@ -380,6 +379,7 @@ export class CommandGroup<GN extends keyof any, TG> {
}
if (opt.isFlag) {
myArgs[opt.name] = true;
+ foundOptions[opt.name] = true;
} else {
if (si == optShort.length - 1) {
if (i === unparsedArgs.length - 1) {
@@ -449,6 +449,13 @@ export class CommandGroup<GN extends keyof any, TG> {
}
}
+ for (let option of this.options) {
+ const ph = option.args.onPresentHandler;
+ if (ph && foundOptions[option.name]) {
+ ph(myArgs[option.name]);
+ }
+ }
+
if (parsedArgs[this.argKey].help) {
this.printHelp(progname, parents);
process.exit(-1);
@@ -546,7 +553,7 @@ export class Program<PN extends keyof any, T> {
name: N,
flagspec: string[],
args: OptionArgs<boolean> = {},
- ): Program<N, T & SubRecord<PN, N, boolean>> {
+ ): Program<PN, T & SubRecord<PN, N, boolean>> {
this.mainCommand.flag(name, flagspec, args);
return this as any;
}
diff --git a/src/headless/helpers.ts b/src/headless/helpers.ts
index a38ef1dbe..9faf24daf 100644
--- a/src/headless/helpers.ts
+++ b/src/headless/helpers.ts
@@ -34,35 +34,30 @@ import { Bank } from "./bank";
import fs = require("fs");
import { NodeCryptoWorkerFactory } from "../crypto/nodeProcessWorker";
+import { Logger } from "../logging";
+
+const logger = new Logger("helpers.ts");
-const enableTracing = false;
class ConsoleBadge implements Badge {
startBusy(): void {
- enableTracing && console.log("NOTIFICATION: busy");
}
stopBusy(): void {
- enableTracing && console.log("NOTIFICATION: busy end");
}
showNotification(): void {
- enableTracing && console.log("NOTIFICATION: show");
}
clearNotification(): void {
- enableTracing && console.log("NOTIFICATION: cleared");
}
}
export class NodeHttpLib implements HttpRequestLibrary {
async get(url: string): Promise<import("../http").HttpResponse> {
- enableTracing && console.log("making GET request to", url);
try {
const resp = await Axios({
method: "get",
url: url,
responseType: "json",
});
- enableTracing && console.log("got response", resp.data);
- enableTracing && console.log("resp type", typeof resp.data);
return {
responseJson: resp.data,
status: resp.status,
@@ -76,7 +71,6 @@ export class NodeHttpLib implements HttpRequestLibrary {
url: string,
body: any,
): Promise<import("../http").HttpResponse> {
- enableTracing && console.log("making POST request to", url);
try {
const resp = await Axios({
method: "post",
@@ -84,8 +78,6 @@ export class NodeHttpLib implements HttpRequestLibrary {
responseType: "json",
data: body,
});
- enableTracing && console.log("got response", resp.data);
- enableTracing && console.log("resp type", typeof resp.data);
return {
responseJson: resp.data,
status: resp.status,
@@ -149,7 +141,6 @@ export async function getDefaultNodeWallet(
}
myBackend.afterCommitCallback = async () => {
- console.log("DATABASE COMMITTED");
// Allow caller to stop persisting the wallet.
if (args.persistentStoragePath === undefined) {
return;
@@ -219,7 +210,7 @@ export async function withdrawTestBalance(
const bankUser = await bank.registerRandomUser();
- console.log("bank user", bankUser);
+ logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`)
const exchangePaytoUri = await myWallet.getExchangePaytoUri(
exchangeBaseUrl,
@@ -234,6 +225,5 @@ export async function withdrawTestBalance(
);
await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub });
-
await myWallet.runUntilReserveDepleted(reservePub);
}
diff --git a/src/headless/merchant.ts b/src/headless/merchant.ts
index 889eb2d6a..423e3d09e 100644
--- a/src/headless/merchant.ts
+++ b/src/headless/merchant.ts
@@ -19,9 +19,9 @@
* Used mostly for integration tests.
*/
- /**
- * Imports.
- */
+/**
+ * Imports.
+ */
import axios from "axios";
import { CheckPaymentResponse } from "../talerTypes";
import URI = require("urijs");
@@ -30,10 +30,60 @@ import URI = require("urijs");
* Connection to the *internal* merchant backend.
*/
export class MerchantBackendConnection {
- constructor(
- public merchantBaseUrl: string,
- public apiKey: string,
- ) {}
+ async refund(
+ orderId: string,
+ reason: string,
+ refundAmount: string,
+ ): Promise<void> {
+ const reqUrl = new URI("refund").absoluteTo(this.merchantBaseUrl).href();
+ const refundReq = {
+ order_id: orderId,
+ reason,
+ refund: refundAmount,
+ };
+ const resp = await axios({
+ method: "post",
+ url: reqUrl,
+ data: refundReq,
+ responseType: "json",
+ headers: {
+ Authorization: `ApiKey ${this.apiKey}`,
+ },
+ });
+ if (resp.status != 200) {
+ throw Error("failed to do refund");
+ }
+ console.log("response", resp.data);
+ const refundUri = resp.data.taler_refund_uri;
+ if (!refundUri) {
+ throw Error("no refund URI in response");
+ }
+ return refundUri;
+ }
+
+ constructor(public merchantBaseUrl: string, public apiKey: string) {}
+
+ async authorizeTip(amount: string, justification: string) {
+ const reqUrl = new URI("tip-authorize").absoluteTo(this.merchantBaseUrl).href();
+ const tipReq = {
+ amount,
+ justification,
+ };
+ const resp = await axios({
+ method: "post",
+ url: reqUrl,
+ data: tipReq,
+ responseType: "json",
+ headers: {
+ Authorization: `ApiKey ${this.apiKey}`,
+ },
+ });
+ const tipUri = resp.data.taler_tip_uri;
+ if (!tipUri) {
+ throw Error("response does not contain tip URI");
+ }
+ return tipUri;
+ }
async createOrder(
amount: string,
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 90c04dd97..cb2ff055c 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -26,11 +26,16 @@ import { BridgeIDBFactory, MemoryBackend } from "idb-bridge";
import { Logger } from "../logging";
import * as Amounts from "../amounts";
import { decodeCrock } from "../crypto/talerCrypto";
+import { Bank } from "./bank";
const logger = new Logger("taler-wallet-cli.ts");
const walletDbPath = os.homedir + "/" + ".talerwalletdb.json";
+function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
+
async function doPay(
wallet: Wallet,
payUrl: string,
@@ -78,7 +83,7 @@ async function doPay(
}
if (pay) {
- const payRes = await wallet.confirmPay(result.proposalId!, undefined);
+ const payRes = await wallet.confirmPay(result.proposalId, undefined);
console.log("paid!");
} else {
console.log("not paying");
@@ -93,6 +98,12 @@ function applyVerbose(verbose: boolean) {
}
}
+function printVersion() {
+ const info = require("../../../package.json");
+ console.log(`${info.version}`);
+ process.exit(0);
+}
+
const walletCli = clk
.program("wallet", {
help: "Command line interface for the GNU Taler wallet.",
@@ -101,6 +112,9 @@ const walletCli = clk
help:
"Inhibit running certain operations, useful for debugging and testing.",
})
+ .flag("version", ["-v", "--version"], {
+ onPresentHandler: printVersion,
+ })
.flag("verbose", ["-V", "--verbose"], {
help: "Enable verbose output.",
});
@@ -133,12 +147,21 @@ async function withWallet<T>(
}
walletCli
- .subcommand("", "balance", { help: "Show wallet balance." })
+ .subcommand("balance", "balance", { help: "Show wallet balance." })
+ .flag("json", ["--json"], {
+ help: "Show raw JSON.",
+ })
.action(async args => {
- console.log("balance command called");
await withWallet(args, async wallet => {
const balance = await wallet.getBalances();
- console.log(JSON.stringify(balance, undefined, 2));
+ if (args.balance.json) {
+ console.log(JSON.stringify(balance, undefined, 2));
+ } else {
+ const currencies = Object.keys(balance.byCurrency).sort();
+ for (const c of currencies) {
+ console.log(Amounts.toString(balance.byCurrency[c].available));
+ }
+ }
});
});
@@ -205,15 +228,8 @@ walletCli
process.exit(1);
return;
}
- const { confirmTransferUrl } = await wallet.acceptWithdrawal(
- uri,
- selectedExchange,
- );
- if (confirmTransferUrl) {
- console.log("please confirm the transfer at", confirmTransferUrl);
- }
- } else {
- console.error("unrecognized URI");
+ const res = await wallet.acceptWithdrawal(uri, selectedExchange);
+ await wallet.processReserve(res.reservePub);
}
});
});
@@ -258,13 +274,39 @@ const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
advancedCli
.subcommand("decode", "decode", {
- help: "Decode base32-crockford",
+ help: "Decode base32-crockford.",
})
.action(args => {
- const enc = fs.readFileSync(0, 'utf8');
- fs.writeFileSync(1, decodeCrock(enc.trim()))
+ const enc = fs.readFileSync(0, "utf8");
+ fs.writeFileSync(1, decodeCrock(enc.trim()));
});
+advancedCli
+ .subcommand("payPrepare", "pay-prepare", {
+ help: "Claim an order but don't pay yet.",
+ })
+ .requiredArgument("url", clk.STRING)
+ .action(async args => {
+ await withWallet(args, async wallet => {
+ const res = await wallet.preparePay(args.payPrepare.url);
+ switch (res.status) {
+ case "error":
+ console.log("error:", res.error);
+ break;
+ case "insufficient-balance":
+ console.log("insufficient balance");
+ break;
+ case "paid":
+ console.log("already paid");
+ break;
+ case "payment-possible":
+ console.log("payment possible");
+ break;
+ default:
+ assertUnreachable(res);
+ }
+ });
+ });
advancedCli
.subcommand("refresh", "force-refresh", {
@@ -288,7 +330,9 @@ advancedCli
console.log(`coin ${coin.coinPub}`);
console.log(` status ${coin.status}`);
console.log(` exchange ${coin.exchangeBaseUrl}`);
- console.log(` remaining amount ${Amounts.toString(coin.currentAmount)}`);
+ console.log(
+ ` remaining amount ${Amounts.toString(coin.currentAmount)}`,
+ );
}
});
});
@@ -324,12 +368,11 @@ testCli
return;
}
console.log("taler pay URI:", talerPayUri);
- await withWallet(args, async (wallet) => {
+ await withWallet(args, async wallet => {
await doPay(wallet, talerPayUri, { alwaysYes: true });
});
});
-
testCli
.subcommand("integrationtestCmd", "integrationtest", {
help: "Run integration test with bank, exchange and merchant.",
@@ -377,7 +420,74 @@ testCli
});
testCli
- .subcommand("testMerchantQrcodeCmd", "test-merchant-qrcode")
+ .subcommand("genTipUri", "gen-tip-uri", {
+ help: "Generate a taler://tip URI.",
+ })
+ .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
+ default: "TESTKUDOS:10",
+ })
+ .action(async args => {
+ const merchantBackend = new MerchantBackendConnection(
+ "https://backend.test.taler.net/",
+ "sandbox",
+ );
+ const tipUri = await merchantBackend.authorizeTip("TESTKUDOS:10", "test");
+ console.log(tipUri);
+ });
+
+testCli
+ .subcommand("genRefundUri", "gen-refund-uri", {
+ help: "Generate a taler://refund URI.",
+ })
+ .requiredOption("amount", ["-a", "--amount"], clk.STRING, {
+ default: "TESTKUDOS:5",
+ })
+ .requiredOption("refundAmount", ["-r", "--refund"], clk.STRING, {
+ default: "TESTKUDOS:3",
+ })
+ .requiredOption("summary", ["-s", "--summary"], clk.STRING, {
+ default: "Test Payment (for refund)",
+ })
+ .action(async args => {
+ const cmdArgs = args.genRefundUri;
+ const merchantBackend = new MerchantBackendConnection(
+ "https://backend.test.taler.net/",
+ "sandbox",
+ );
+ const orderResp = await merchantBackend.createOrder(
+ cmdArgs.amount,
+ cmdArgs.summary,
+ "",
+ );
+ console.log("created new order with order ID", orderResp.orderId);
+ const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
+ const talerPayUri = checkPayResp.taler_pay_uri;
+ if (!talerPayUri) {
+ console.error("fatal: no taler pay URI received from backend");
+ process.exit(1);
+ return;
+ }
+ await withWallet(args, async wallet => {
+ await doPay(wallet, talerPayUri, { alwaysYes: true });
+ });
+ const refundUri = await merchantBackend.refund(
+ orderResp.orderId,
+ "test refund",
+ cmdArgs.refundAmount,
+ );
+ console.log(refundUri);
+ });
+
+testCli
+ .subcommand("genPayUri", "gen-pay-uri", {
+ help: "Generate a taler://pay URI.",
+ })
+ .flag("qrcode", ["--qr"], {
+ help: "Show a QR code with the taler://pay URI",
+ })
+ .flag("wait", ["--wait"], {
+ help: "Wait until payment has completed",
+ })
.requiredOption("amount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:1",
})
@@ -385,8 +495,7 @@ testCli
default: "Test Payment",
})
.action(async args => {
- const cmdArgs = args.testMerchantQrcodeCmd;
- applyVerbose(args.wallet.verbose);
+ const cmdArgs = args.genPayUri;
console.log("creating order");
const merchantBackend = new MerchantBackendConnection(
"https://backend.test.taler.net/",
@@ -399,7 +508,6 @@ testCli
);
console.log("created new order with order ID", orderResp.orderId);
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
- const qrcode = qrcodeGenerator(0, "M");
const talerPayUri = checkPayResp.taler_pay_uri;
if (!talerPayUri) {
console.error("fatal: no taler pay URI received from backend");
@@ -407,18 +515,23 @@ testCli
return;
}
console.log("taler pay URI:", talerPayUri);
- qrcode.addData(talerPayUri);
- qrcode.make();
- console.log(qrcode.createASCII());
- console.log("waiting for payment ...");
- while (1) {
- await asyncSleep(500);
- const checkPayResp2 = await merchantBackend.checkPayment(
- orderResp.orderId,
- );
- if (checkPayResp2.paid) {
- console.log("payment successfully received!");
- break;
+ if (cmdArgs.qrcode) {
+ const qrcode = qrcodeGenerator(0, "M");
+ qrcode.addData(talerPayUri);
+ qrcode.make();
+ console.log(qrcode.createASCII());
+ }
+ if (cmdArgs.wait) {
+ console.log("waiting for payment ...");
+ while (1) {
+ await asyncSleep(500);
+ const checkPayResp2 = await merchantBackend.checkPayment(
+ orderResp.orderId,
+ );
+ if (checkPayResp2.paid) {
+ console.log("payment successfully received!");
+ break;
+ }
}
}
});
diff --git a/src/wallet-test.ts b/src/wallet-test.ts
index 86ddb5e73..fef11ae5d 100644
--- a/src/wallet-test.ts
+++ b/src/wallet-test.ts
@@ -47,6 +47,8 @@ function fakeCwd(current: string, value: string, feeDeposit: string): types.Coin
denomSig: "(mock)",
exchangeBaseUrl: "(mock)",
reservePub: "(mock)",
+ coinIndex: -1,
+ withdrawSessionId: "",
status: dbTypes.CoinStatus.Fresh,
},
denom: {
diff --git a/src/wallet.ts b/src/wallet.ts
index f1d7be5e5..8fe8d367d 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- (C) 2015 GNUnet e.V.
+ (C) 2015-2019 GNUnet e.V.
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
@@ -58,19 +58,19 @@ import {
DenominationRecord,
DenominationStatus,
ExchangeRecord,
- PreCoinRecord,
- ProposalDownloadRecord,
+ PlanchetRecord,
+ ProposalRecord,
PurchaseRecord,
- RefreshPreCoinRecord,
+ RefreshPlanchetRecord,
RefreshSessionRecord,
ReserveRecord,
Stores,
TipRecord,
WireFee,
- WithdrawalRecord,
- ExchangeDetails,
+ WithdrawalSessionRecord,
ExchangeUpdateStatus,
ReserveRecordStatus,
+ ProposalStatus,
} from "./dbTypes";
import {
Auditor,
@@ -128,14 +128,15 @@ import {
parseTipUri,
parseRefundUri,
} from "./taleruri";
-import { isFirefox } from "./webex/compat";
import { Logger } from "./logging";
+import { randomBytes } from "./crypto/primitives/nacl-fast";
+import { encodeCrock, getRandomBytes } from "./crypto/talerCrypto";
interface SpeculativePayData {
payCoinInfo: PayCoinInfo;
exchangeUrl: string;
- proposalId: number;
- proposal: ProposalDownloadRecord;
+ orderDownloadId: string;
+ proposal: ProposalRecord;
}
/**
@@ -166,7 +167,7 @@ const builtinCurrencies: CurrencyRecord[] = [
function isWithdrawableDenom(d: DenominationRecord) {
const now = getTimestampNow();
const started = now.t_ms >= d.stampStart.t_ms;
- const stillOkay = d.stampExpireWithdraw.t_ms + (60 * 1000) > now.t_ms;
+ const stillOkay = d.stampExpireWithdraw.t_ms + 60 * 1000 > now.t_ms;
return started && stillOkay;
}
@@ -175,6 +176,10 @@ interface SelectPayCoinsResult {
totalFees: AmountJson;
}
+function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
+
/**
* Get the amount that we lose when refreshing a coin of the given denomination
* with a certain amount left.
@@ -353,6 +358,43 @@ export class OperationFailedAndReportedError extends Error {
const logger = new Logger("wallet.ts");
+interface MemoEntry<T> {
+ p: Promise<T>;
+ t: number;
+ n: number;
+}
+
+class AsyncOpMemo<T> {
+ n = 0;
+ memo: { [k: string]: MemoEntry<T> } = {};
+ put(key: string, p: Promise<T>): Promise<T> {
+ const n = this.n++;
+ this.memo[key] = {
+ p,
+ n,
+ t: new Date().getTime(),
+ };
+ p.finally(() => {
+ const r = this.memo[key];
+ if (r && r.n === n) {
+ delete this.memo[key];
+ }
+ });
+ return p;
+ }
+ find(key: string): Promise<T> | undefined {
+ const res = this.memo[key];
+ const tNow = new Date().getTime();
+ if (res && res.t < tNow - 10 * 1000) {
+ delete this.memo[key];
+ return;
+ } else if (res) {
+ return res.p;
+ }
+ return;
+ }
+}
+
/**
* The platform-independent wallet implementation.
*/
@@ -369,6 +411,8 @@ export class Wallet {
private speculativePayData: SpeculativePayData | undefined;
private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
+ private memoProcessReserve = new AsyncOpMemo<void>();
+
constructor(
db: IDBDatabase,
http: HttpRequestLibrary,
@@ -384,32 +428,51 @@ export class Wallet {
}
/**
- * Process pending operations.
+ * Execute one operation based on the pending operation info record.
*/
- public async runPending(): Promise<void> {
- // FIXME: maybe prioritize pending operations by their urgency?
- const exchangeBaseUrlList = await oneShotIter(
- this.db,
- Stores.exchanges,
- ).map(x => x.baseUrl);
-
- for (let exchangeBaseUrl of exchangeBaseUrlList) {
- await this.updateExchangeFromUrl(exchangeBaseUrl);
- }
-
- const reservesPubList = await oneShotIter(this.db, Stores.reserves).map(
- x => x.reservePub,
- );
-
- for (let reservePub of reservesPubList) {
- await this.processReserve(reservePub);
+ async processOnePendingOperation(
+ pending: PendingOperationInfo,
+ ): Promise<void> {
+ switch (pending.type) {
+ case "bug":
+ return;
+ case "dirty-coin":
+ await this.refresh(pending.coinPub);
+ break;
+ case "exchange-update":
+ await this.updateExchangeFromUrl(pending.exchangeBaseUrl);
+ break;
+ case "planchet":
+ await this.processPlanchet(pending.coinPub);
+ break;
+ case "refresh":
+ await this.processRefreshSession(pending.refreshSessionId);
+ break;
+ case "reserve":
+ await this.processReserve(pending.reservePub);
+ break;
+ case "withdraw":
+ await this.processWithdrawSession(pending.withdrawSessionId);
+ break;
+ case "proposal":
+ // Nothing to do, user needs to accept/reject
+ break;
+ default:
+ assertUnreachable(pending);
}
+ }
- const refreshSessionList = await oneShotIter(this.db, Stores.refresh).map(
- x => x.refreshSessionId,
- );
- for (let rs of refreshSessionList) {
- await this.processRefreshSession(rs);
+ /**
+ * Process pending operations.
+ */
+ public async runPending(): Promise<void> {
+ const pendingOpsResponse = await this.getPendingOperations();
+ for (const p of pendingOpsResponse.pendingOperations) {
+ try {
+ await this.processOnePendingOperation(p);
+ } catch (e) {
+ console.error(e);
+ }
}
}
@@ -427,29 +490,23 @@ export class Wallet {
*/
public async runUntilReserveDepleted(reservePub: string) {
while (true) {
- let reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
- if (!reserve) {
- throw Error("Reserve does not exist.");
- }
- if (reserve.lastError !== undefined) {
- throw Error("Reserve error: " + reserve.lastError.message);
- }
- if (reserve.reserveStatus === ReserveRecordStatus.UNCONFIRMED) {
- throw Error("Reserve is not confirmed.");
- }
- if (reserve.reserveStatus === ReserveRecordStatus.DORMANT) {
- // Check if all withdraws are done!
- const precoins = await oneShotIterIndex(
- this.db,
- Stores.precoins.byReservePub,
- reservePub,
- ).toArray();
- for (const pc of precoins) {
- await this.processPreCoin(pc.coinPub);
+ const r = await this.getPendingOperations();
+ const allPending = r.pendingOperations;
+ const relevantPending = allPending.filter(x => {
+ switch (x.type) {
+ case "planchet":
+ case "reserve":
+ return x.reservePub === reservePub;
+ default:
+ return false;
}
- break;
+ });
+ if (relevantPending.length === 0) {
+ return;
+ }
+ for (const p of relevantPending) {
+ await this.processOnePendingOperation(p);
}
- await this.processReserve(reservePub);
}
}
@@ -478,18 +535,6 @@ export class Wallet {
);
}
- async updateExchanges(): Promise<void> {
- const exchangeUrls = await oneShotIter(this.db, Stores.exchanges).map(
- e => e.baseUrl,
- );
-
- for (const url of exchangeUrls) {
- this.updateExchangeFromUrl(url).catch(e => {
- console.error("updating exchange failed", e);
- });
- }
- }
-
private async getCoinsForReturn(
exchangeBaseUrl: string,
amount: AmountJson,
@@ -554,8 +599,6 @@ export class Wallet {
cds.push({ coin, denom });
}
- console.log("coin return: selecting from possible coins", { cds, amount });
-
const res = selectPayCoins(denoms, cds, amount, amount);
if (res) {
return res.cds;
@@ -711,7 +754,7 @@ export class Wallet {
* pay for a proposal in the wallet's database.
*/
private async recordConfirmPay(
- proposal: ProposalDownloadRecord,
+ proposal: ProposalRecord,
payCoinInfo: PayCoinInfo,
chosenExchange: string,
): Promise<PurchaseRecord> {
@@ -774,7 +817,7 @@ export class Wallet {
};
}
- let proposalId: number;
+ let proposalId: string;
try {
proposalId = await this.downloadProposal(
uriResult.downloadUrl,
@@ -788,7 +831,7 @@ export class Wallet {
}
const proposal = await this.getProposal(proposalId);
if (!proposal) {
- throw Error("could not get proposal");
+ throw Error(`could not get proposal ${proposalId}`);
}
console.log("proposal", proposal);
@@ -868,7 +911,7 @@ export class Wallet {
return {
status: "insufficient-balance",
contractTerms: proposal.contractTerms,
- proposalId: proposal.id!,
+ proposalId: proposal.proposalId,
};
}
@@ -876,7 +919,7 @@ export class Wallet {
if (
!this.speculativePayData ||
(this.speculativePayData &&
- this.speculativePayData.proposalId !== proposalId)
+ this.speculativePayData.orderDownloadId !== proposalId)
) {
const { exchangeUrl, cds, totalAmount } = res;
const payCoinInfo = await this.cryptoApi.signDeposit(
@@ -888,7 +931,7 @@ export class Wallet {
exchangeUrl,
payCoinInfo,
proposal,
- proposalId,
+ orderDownloadId: proposalId,
};
Wallet.enableTracing &&
console.log("created speculative pay data for payment");
@@ -897,7 +940,7 @@ export class Wallet {
return {
status: "payment-possible",
contractTerms: proposal.contractTerms,
- proposalId: proposal.id!,
+ proposalId: proposal.proposalId,
totalFees: res.totalFees,
};
}
@@ -920,14 +963,14 @@ export class Wallet {
* @param sessionId Current session ID, if the proposal is being
* downloaded in the context of a session ID.
*/
- async downloadProposal(url: string, sessionId?: string): Promise<number> {
+ async downloadProposal(url: string, sessionId?: string): Promise<string> {
const oldProposal = await oneShotGetIndexed(
this.db,
Stores.proposals.urlIndex,
url,
);
if (oldProposal) {
- return oldProposal.id!;
+ return oldProposal.proposalId;
}
const { priv, pub } = await this.cryptoApi.createEddsaKeypair();
@@ -946,7 +989,9 @@ export class Wallet {
const contractTermsHash = await this.hashContract(proposal.contract_terms);
- const proposalRecord: ProposalDownloadRecord = {
+ const proposalId = encodeCrock(getRandomBytes(32));
+
+ const proposalRecord: ProposalRecord = {
contractTerms: proposal.contract_terms,
contractTermsHash,
merchantSig: proposal.sig,
@@ -954,14 +999,13 @@ export class Wallet {
timestamp: getTimestampNow(),
url,
downloadSessionId: sessionId,
+ proposalId: proposalId,
+ proposalStatus: ProposalStatus.PROPOSED,
};
-
- const id = await oneShotPut(this.db, Stores.proposals, proposalRecord);
+ await oneShotPut(this.db, Stores.proposals, proposalRecord);
this.notifier.notify();
- if (typeof id !== "number") {
- throw Error("db schema wrong");
- }
- return id;
+
+ return proposalId;
}
async refundFailedPay(proposalId: number) {
@@ -1091,7 +1135,7 @@ export class Wallet {
* Add a contract to the wallet and sign coins, and send them.
*/
async confirmPay(
- proposalId: number,
+ proposalId: string,
sessionIdOverride: string | undefined,
): Promise<ConfirmPayResult> {
Wallet.enableTracing &&
@@ -1175,13 +1219,13 @@ export class Wallet {
* Get the speculative pay data, but only if coins have not changed in between.
*/
async getSpeculativePayData(
- proposalId: number,
+ proposalId: string,
): Promise<SpeculativePayData | undefined> {
const sp = this.speculativePayData;
if (!sp) {
return;
}
- if (sp.proposalId !== proposalId) {
+ if (sp.orderDownloadId !== proposalId) {
return;
}
const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub);
@@ -1209,58 +1253,104 @@ export class Wallet {
return sp;
}
- /**
- * Send reserve details to the bank.
- */
- private async sendReserveInfoToBank(reservePub: string) {
- const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
- if (!reserve) {
- throw Error("reserve not in db");
- }
-
- if (reserve.reserveStatus != ReserveRecordStatus.REGISTERING_BANK) {
- return;
+ private async processReserveBankStatus(reservePub: string): Promise<void> {
+ let reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
+ switch (reserve?.reserveStatus) {
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ case ReserveRecordStatus.REGISTERING_BANK:
+ break;
+ default:
+ return;
}
-
const bankStatusUrl = reserve.bankWithdrawStatusUrl;
if (!bankStatusUrl) {
- throw Error("no bank withdraw status URL available.");
+ return;
}
- const now = getTimestampNow();
- let status;
+ let status: WithdrawOperationStatusResponse;
try {
const statusResp = await this.http.get(bankStatusUrl);
status = WithdrawOperationStatusResponse.checked(statusResp.responseJson);
} catch (e) {
- console.log("bank error response", e);
throw e;
}
+ if (status.selection_done) {
+ if (reserve.reserveStatus === ReserveRecordStatus.REGISTERING_BANK) {
+ await this.registerReserveWithBank(reservePub);
+ return await this.processReserveBankStatus(reservePub);
+ }
+ } else {
+ await this.registerReserveWithBank(reservePub);
+ return await this.processReserveBankStatus(reservePub);
+ }
+
if (status.transfer_done) {
await oneShotMutate(this.db, Stores.reserves, reservePub, r => {
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.REGISTERING_BANK:
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ break;
+ default:
+ return;
+ }
+ const now = getTimestampNow();
r.timestampConfirmed = now;
+ r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
return r;
});
- } else if (reserve.timestampReserveInfoPosted === undefined) {
- try {
- if (!status.selection_done) {
- const bankResp = await this.http.postJson(bankStatusUrl, {
- reserve_pub: reservePub,
- selected_exchange: reserve.exchangeWire,
- });
- }
- } catch (e) {
- console.log("bank error response", e);
- throw e;
- }
+ await this.processReserveImpl(reservePub);
+ } else {
await oneShotMutate(this.db, Stores.reserves, reservePub, r => {
- r.timestampReserveInfoPosted = now;
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ break;
+ default:
+ return;
+ }
+ r.bankWithdrawConfirmUrl = status.confirm_transfer_url;
return r;
});
}
}
+ async registerReserveWithBank(reservePub: string) {
+ let reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
+ switch (reserve?.reserveStatus) {
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ case ReserveRecordStatus.REGISTERING_BANK:
+ break;
+ default:
+ return;
+ }
+ const bankStatusUrl = reserve.bankWithdrawStatusUrl;
+ if (!bankStatusUrl) {
+ return;
+ }
+ console.log("making selection");
+ if (reserve.timestampReserveInfoPosted) {
+ throw Error("bank claims that reserve info selection is not done");
+ }
+ const bankResp = await this.http.postJson(bankStatusUrl, {
+ reserve_pub: reservePub,
+ selected_exchange: reserve.exchangeWire,
+ });
+ console.log("got response", bankResp);
+ await oneShotMutate(this.db, Stores.reserves, reservePub, r => {
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.REGISTERING_BANK:
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ break;
+ default:
+ return;
+ }
+ r.timestampReserveInfoPosted = getTimestampNow();
+ r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK;
+ return r;
+ });
+ return this.processReserveBankStatus(reservePub);
+ }
+
/**
* First fetch information requred to withdraw from the reserve,
* then deplete the reserve, withdrawing coins until it is empty.
@@ -1269,6 +1359,18 @@ export class Wallet {
* state DORMANT.
*/
async processReserve(reservePub: string): Promise<void> {
+ const p = this.memoProcessReserve.find(reservePub);
+ if (p) {
+ return p;
+ } else {
+ return this.memoProcessReserve.put(
+ reservePub,
+ this.processReserveImpl(reservePub),
+ );
+ }
+ }
+
+ private async processReserveImpl(reservePub: string): Promise<void> {
const reserve = await oneShotGet(this.db, Stores.reserves, reservePub);
if (!reserve) {
console.log("not processing reserve: reserve does not exist");
@@ -1282,19 +1384,23 @@ export class Wallet {
// nothing to do
break;
case ReserveRecordStatus.REGISTERING_BANK:
- await this.sendReserveInfoToBank(reservePub);
- return this.processReserve(reservePub);
+ await this.processReserveBankStatus(reservePub);
+ return this.processReserveImpl(reservePub);
case ReserveRecordStatus.QUERYING_STATUS:
await this.updateReserve(reservePub);
- return this.processReserve(reservePub);
+ return this.processReserveImpl(reservePub);
case ReserveRecordStatus.WITHDRAWING:
await this.depleteReserve(reservePub);
break;
case ReserveRecordStatus.DORMANT:
// nothing to do
break;
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ await this.processReserveBankStatus(reservePub);
+ break;
default:
console.warn("unknown reserve record status:", reserve.reserveStatus);
+ assertUnreachable(reserve.reserveStatus);
break;
}
}
@@ -1302,38 +1408,38 @@ export class Wallet {
/**
* Given a planchet, withdraw a coin from the exchange.
*/
- private async processPreCoin(preCoinPub: string): Promise<void> {
- console.log("processPreCoin", preCoinPub);
- const preCoin = await oneShotGet(this.db, Stores.precoins, preCoinPub);
- if (!preCoin) {
- console.log("processPreCoin: preCoinPub not found");
+ private async processPlanchet(coinPub: string): Promise<void> {
+ logger.trace("process planchet", coinPub);
+ const planchet = await oneShotGet(this.db, Stores.planchets, coinPub);
+ if (!planchet) {
+ console.log("processPlanchet: planchet not found");
return;
}
const exchange = await oneShotGet(
this.db,
Stores.exchanges,
- preCoin.exchangeBaseUrl,
+ planchet.exchangeBaseUrl,
);
if (!exchange) {
- console.error("db inconsistent: exchange for precoin not found");
+ console.error("db inconsistent: exchange for planchet not found");
return;
}
const denom = await oneShotGet(this.db, Stores.denominations, [
- preCoin.exchangeBaseUrl,
- preCoin.denomPub,
+ planchet.exchangeBaseUrl,
+ planchet.denomPub,
]);
if (!denom) {
- console.error("db inconsistent: denom for precoin not found");
+ console.error("db inconsistent: denom for planchet not found");
return;
}
const wd: any = {};
- wd.denom_pub_hash = preCoin.denomPubHash;
- wd.reserve_pub = preCoin.reservePub;
- wd.reserve_sig = preCoin.withdrawSig;
- wd.coin_ev = preCoin.coinEv;
+ wd.denom_pub_hash = planchet.denomPubHash;
+ wd.reserve_pub = planchet.reservePub;
+ wd.reserve_sig = planchet.withdrawSig;
+ wd.coin_ev = planchet.coinEv;
const reqUrl = new URI("reserve/withdraw").absoluteTo(exchange.baseUrl);
const resp = await this.http.postJson(reqUrl.href(), wd);
@@ -1341,51 +1447,60 @@ export class Wallet {
const denomSig = await this.cryptoApi.rsaUnblind(
r.ev_sig,
- preCoin.blindingKey,
- preCoin.denomPub,
+ planchet.blindingKey,
+ planchet.denomPub,
);
const coin: CoinRecord = {
- blindingKey: preCoin.blindingKey,
- coinPriv: preCoin.coinPriv,
- coinPub: preCoin.coinPub,
- currentAmount: preCoin.coinValue,
- denomPub: preCoin.denomPub,
- denomPubHash: preCoin.denomPubHash,
+ blindingKey: planchet.blindingKey,
+ coinPriv: planchet.coinPriv,
+ coinPub: planchet.coinPub,
+ currentAmount: planchet.coinValue,
+ denomPub: planchet.denomPub,
+ denomPubHash: planchet.denomPubHash,
denomSig,
- exchangeBaseUrl: preCoin.exchangeBaseUrl,
- reservePub: preCoin.reservePub,
+ exchangeBaseUrl: planchet.exchangeBaseUrl,
+ reservePub: planchet.reservePub,
status: CoinStatus.Fresh,
- };
-
- const mutateReserve = (r: ReserveRecord) => {
- const x = Amounts.sub(
- r.precoinAmount,
- preCoin.coinValue,
- denom.feeWithdraw,
- );
- if (x.saturated) {
- // FIXME!!!!
- console.error("database inconsistent");
- throw TransactionAbort;
- }
- r.precoinAmount = x.amount;
- return r;
+ coinIndex: planchet.coinIndex,
+ withdrawSessionId: planchet.withdrawSessionId,
};
await runWithWriteTransaction(
this.db,
- [Stores.reserves, Stores.precoins, Stores.coins],
+ [Stores.planchets, Stores.coins, Stores.withdrawalSession, Stores.reserves],
async tx => {
- const currentPc = await tx.get(Stores.precoins, coin.coinPub);
+ const currentPc = await tx.get(Stores.planchets, coin.coinPub);
if (!currentPc) {
return;
}
- await tx.mutate(Stores.reserves, preCoin.reservePub, mutateReserve);
- await tx.delete(Stores.precoins, coin.coinPub);
+ const ws = await tx.get(
+ Stores.withdrawalSession,
+ planchet.withdrawSessionId,
+ );
+ if (!ws) {
+ return;
+ }
+ if (ws.withdrawn[planchet.coinIndex]) {
+ // Already withdrawn
+ return;
+ }
+ ws.withdrawn[planchet.coinIndex] = true;
+ await tx.put(Stores.withdrawalSession, ws);
+ const r = await tx.get(Stores.reserves, planchet.reservePub);
+ if (!r) {
+ return;
+ }
+ r.withdrawCompletedAmount = Amounts.add(
+ r.withdrawCompletedAmount,
+ Amounts.add(denom.value, denom.feeWithdraw).amount,
+ ).amount;
+ tx.put(Stores.reserves, r);
+ await tx.delete(Stores.planchets, coin.coinPub);
await tx.add(Stores.coins, coin);
},
);
+ this.notifier.notify();
logger.trace(`withdraw of one coin ${coin.coinPub} finished`);
}
@@ -1409,13 +1524,16 @@ export class Wallet {
reserveStatus = ReserveRecordStatus.UNCONFIRMED;
}
+ const currency = req.amount.currency;
+
const reserveRecord: ReserveRecord = {
created: now,
- currentAmount: null,
+ withdrawAllocatedAmount: Amounts.getZero(currency),
+ withdrawCompletedAmount: Amounts.getZero(currency),
+ withdrawRemainingAmount: Amounts.getZero(currency),
exchangeBaseUrl: canonExchange,
hasPayback: false,
- precoinAmount: Amounts.getZero(req.amount.currency),
- requestedAmount: req.amount,
+ initiallyRequestedAmount: req.amount,
reservePriv: keypair.priv,
reservePub: keypair.pub,
senderWire: req.senderWire,
@@ -1424,6 +1542,7 @@ export class Wallet {
bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
exchangeWire: req.exchangeWire,
reserveStatus,
+ lastStatusQuery: undefined,
};
const senderWire = req.senderWire;
@@ -1463,24 +1582,50 @@ export class Wallet {
const cr: CurrencyRecord = currencyRecord;
- await runWithWriteTransaction(
+ const resp = await runWithWriteTransaction(
this.db,
- [Stores.currencies, Stores.reserves],
+ [Stores.currencies, Stores.reserves, Stores.bankWithdrawUris],
async tx => {
+ // Check if we have already created a reserve for that bankWithdrawStatusUrl
+ if (reserveRecord.bankWithdrawStatusUrl) {
+ const bwi = await tx.get(
+ Stores.bankWithdrawUris,
+ reserveRecord.bankWithdrawStatusUrl,
+ );
+ if (bwi) {
+ const otherReserve = await tx.get(Stores.reserves, bwi.reservePub);
+ if (otherReserve) {
+ logger.trace(
+ "returning existing reserve for bankWithdrawStatusUri",
+ );
+ return {
+ exchange: otherReserve.exchangeBaseUrl,
+ reservePub: otherReserve.reservePub,
+ };
+ }
+ }
+ await tx.put(Stores.bankWithdrawUris, {
+ reservePub: reserveRecord.reservePub,
+ talerWithdrawUri: reserveRecord.bankWithdrawStatusUrl,
+ });
+ }
await tx.put(Stores.currencies, cr);
await tx.put(Stores.reserves, reserveRecord);
+ const r: CreateReserveResponse = {
+ exchange: canonExchange,
+ reservePub: keypair.pub,
+ };
+ return r;
},
);
- this.processReserve(keypair.pub).catch(e => {
+ // Asynchronously process the reserve, but return
+ // to the caller already.
+ this.processReserve(resp.reservePub).catch(e => {
console.error("Processing reserve failed:", e);
});
- const r: CreateReserveResponse = {
- exchange: canonExchange,
- reservePub: keypair.pub,
- };
- return r;
+ return resp;
}
/**
@@ -1526,15 +1671,15 @@ export class Wallet {
}
logger.trace(`depleting reserve ${reservePub}`);
- const withdrawAmount = reserve.currentAmount;
- if (!withdrawAmount) {
- throw Error("BUG: reserveStatus=WITHDRAWING, but currentAmount is empty");
- }
+ const withdrawAmount = reserve.withdrawRemainingAmount;
+
+ logger.trace(`getting denom list`);
const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(
reserve.exchangeBaseUrl,
withdrawAmount,
);
+ logger.trace(`got denom list`);
if (denomsForWithdraw.length === 0) {
const m = `Unable to withdraw from reserve, no denominations are available to withdraw.`;
await this.setReserveError(reserve.reservePub, {
@@ -1542,23 +1687,24 @@ export class Wallet {
message: m,
details: {},
});
+ console.log(m);
throw new OperationFailedAndReportedError(m);
}
- const withdrawalRecord: WithdrawalRecord = {
+ logger.trace("selected denominations");
+
+ const withdrawalSessionId = encodeCrock(randomBytes(32));
+
+ const withdrawalRecord: WithdrawalSessionRecord = {
+ withdrawSessionId: withdrawalSessionId,
reservePub: reserve.reservePub,
withdrawalAmount: Amounts.toString(withdrawAmount),
startTimestamp: getTimestampNow(),
- numCoinsTotal: denomsForWithdraw.length,
- numCoinsWithdrawn: 0,
+ denoms: denomsForWithdraw.map(x => x.denomPub),
+ withdrawn: denomsForWithdraw.map(x => false),
+ planchetCreated: denomsForWithdraw.map(x => false),
};
- const preCoinRecords: PreCoinRecord[] = await Promise.all(
- denomsForWithdraw.map(async denom => {
- return await this.cryptoApi.createPreCoin(denom, reserve);
- }),
- );
-
const totalCoinValue = Amounts.sum(denomsForWithdraw.map(x => x.value))
.amount;
const totalCoinWithdrawFee = Amounts.sum(
@@ -1570,20 +1716,24 @@ export class Wallet {
).amount;
function mutateReserve(r: ReserveRecord): ReserveRecord {
- const currentAmount = r.currentAmount;
- if (!currentAmount) {
- throw Error("can't withdraw when amount is unknown");
+ const remaining = Amounts.sub(
+ r.withdrawRemainingAmount,
+ totalWithdrawAmount,
+ );
+ if (remaining.saturated) {
+ console.error("can't create planchets, saturated");
+ throw TransactionAbort;
}
- r.precoinAmount = Amounts.add(
- r.precoinAmount,
+ const allocated = Amounts.add(
+ r.withdrawAllocatedAmount,
totalWithdrawAmount,
- ).amount;
- const result = Amounts.sub(currentAmount, totalWithdrawAmount);
- if (result.saturated) {
- console.error("can't create precoins, saturated");
+ );
+ if (allocated.saturated) {
+ console.error("can't create planchets, saturated");
throw TransactionAbort;
}
- r.currentAmount = result.amount;
+ r.withdrawRemainingAmount = remaining.amount;
+ r.withdrawAllocatedAmount = allocated.amount;
r.reserveStatus = ReserveRecordStatus.DORMANT;
return r;
@@ -1591,7 +1741,7 @@ export class Wallet {
const success = await runWithWriteTransaction(
this.db,
- [Stores.precoins, Stores.withdrawals, Stores.reserves],
+ [Stores.planchets, Stores.withdrawalSession, Stores.reserves],
async tx => {
const myReserve = await tx.get(Stores.reserves, reservePub);
if (!myReserve) {
@@ -1600,20 +1750,113 @@ export class Wallet {
if (myReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
return false;
}
- for (let pcr of preCoinRecords) {
- await tx.put(Stores.precoins, pcr);
- }
await tx.mutate(Stores.reserves, reserve.reservePub, mutateReserve);
- await tx.put(Stores.withdrawals, withdrawalRecord);
+ await tx.put(Stores.withdrawalSession, withdrawalRecord);
return true;
},
);
if (success) {
- logger.trace(`withdrawing ${preCoinRecords.length} coins`);
- for (let x of preCoinRecords) {
- await this.processPreCoin(x.coinPub);
+ console.log("processing new withdraw session");
+ await this.processWithdrawSession(withdrawalSessionId);
+ } else {
+ console.trace("withdraw session already existed");
+ }
+ }
+
+ private async processWithdrawSession(withdrawalSessionId: string): Promise<void> {
+ logger.trace("processing withdraw session", withdrawalSessionId);
+ const ws = await oneShotGet(
+ this.db,
+ Stores.withdrawalSession,
+ withdrawalSessionId,
+ );
+ if (!ws) {
+ logger.trace("withdraw session doesn't exist");
+ return;
+ }
+
+ const ps = ws.denoms.map((d, i) =>
+ this.processWithdrawCoin(withdrawalSessionId, i),
+ );
+ await Promise.all(ps);
+ this.badge.showNotification();
+ return;
+ }
+
+ private async processWithdrawCoin(
+ withdrawalSessionId: string,
+ coinIndex: number,
+ ) {
+ logger.info("starting withdraw for coin");
+ const ws = await oneShotGet(
+ this.db,
+ Stores.withdrawalSession,
+ withdrawalSessionId,
+ );
+ if (!ws) {
+ console.log("ws doesn't exist");
+ return;
+ }
+
+ const coin = await oneShotGetIndexed(
+ this.db,
+ Stores.coins.byWithdrawalWithIdx,
+ [withdrawalSessionId, coinIndex],
+ );
+
+ if (coin) {
+ console.log("coin already exists");
+ return;
+ }
+
+ const pc = await oneShotGetIndexed(
+ this.db,
+ Stores.planchets.byWithdrawalWithIdx,
+ [withdrawalSessionId, coinIndex],
+ );
+
+ if (pc) {
+ return this.processPlanchet(pc.coinPub);
+ } else {
+ const reserve = await oneShotGet(this.db, Stores.reserves, ws.reservePub);
+ if (!reserve) {
+ return;
}
+ const denom = await oneShotGet(this.db, Stores.denominations, [
+ reserve.exchangeBaseUrl,
+ ws.denoms[coinIndex],
+ ]);
+ if (!denom) {
+ return;
+ }
+ const r = await this.cryptoApi.createPlanchet(denom, reserve);
+ const newPlanchet: PlanchetRecord = {
+ blindingKey: r.blindingKey,
+ coinEv: r.coinEv,
+ coinIndex,
+ coinPriv: r.coinPriv,
+ coinPub: r.coinPub,
+ coinValue: r.coinValue,
+ denomPub: r.denomPub,
+ denomPubHash: r.denomPubHash,
+ exchangeBaseUrl: r.exchangeBaseUrl,
+ isFromTip: false,
+ reservePub: r.reservePub,
+ withdrawSessionId: withdrawalSessionId,
+ withdrawSig: r.withdrawSig,
+ };
+ await runWithWriteTransaction(this.db, [Stores.planchets, Stores.withdrawalSession], async (tx) => {
+ const myWs = await tx.get(Stores.withdrawalSession, withdrawalSessionId);
+ if (!myWs) {
+ return;
+ }
+ if (myWs.planchetCreated[coinIndex]) {
+ return;
+ }
+ await tx.put(Stores.planchets, newPlanchet);
+ });
+ await this.processPlanchet(newPlanchet.coinPub);
}
}
@@ -1644,7 +1887,6 @@ export class Wallet {
resp = await this.http.get(reqUrl.href());
} catch (e) {
if (e.response?.status === 404) {
- console.log("Reserve now known to exchange (yet).");
return;
} else {
const m = e.message;
@@ -1657,15 +1899,40 @@ export class Wallet {
}
}
const reserveInfo = ReserveStatus.checked(resp.responseJson);
+ const balance = Amounts.parseOrThrow(reserveInfo.balance);
await oneShotMutate(this.db, Stores.reserves, reserve.reservePub, r => {
if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
return;
}
- reserve.currentAmount = Amounts.parseOrThrow(reserveInfo.balance);
- reserve.reserveStatus = ReserveRecordStatus.WITHDRAWING;
+
+ // FIXME: check / compare history!
+ if (!r.lastStatusQuery) {
+ // FIXME: check if this matches initial expectations
+ r.withdrawRemainingAmount = balance;
+ } else {
+ const expectedBalance = Amounts.sub(
+ r.withdrawAllocatedAmount,
+ r.withdrawCompletedAmount,
+ );
+ const cmp = Amounts.cmp(balance, expectedBalance.amount);
+ if (cmp == 0) {
+ // Nothing changed.
+ return;
+ }
+ if (cmp > 0) {
+ const extra = Amounts.sub(balance, expectedBalance.amount).amount;
+ r.withdrawRemainingAmount = Amounts.add(
+ r.withdrawRemainingAmount,
+ extra,
+ ).amount;
+ } else {
+ // We're missing some money.
+ }
+ }
+ r.lastStatusQuery = getTimestampNow();
+ r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
return r;
});
- await oneShotPut(this.db, Stores.reserves, reserve);
this.notifier.notify();
}
@@ -1752,15 +2019,21 @@ export class Wallet {
exchangeBaseUrl,
);
if (!exchange) {
+ console.log("exchange not found");
throw Error(`exchange ${exchangeBaseUrl} not found`);
}
const exchangeDetails = exchange.details;
if (!exchangeDetails) {
+ console.log("exchange details not available");
throw Error(`exchange ${exchangeBaseUrl} details not available`);
}
+ console.log("getting possible denoms");
+
const possibleDenoms = await this.getPossibleDenoms(exchange.baseUrl);
+ console.log("got possible denoms");
+
let allValid = false;
let selectedDenoms: DenominationRecord[];
@@ -1769,12 +2042,15 @@ export class Wallet {
allValid = true;
const nextPossibleDenoms = [];
selectedDenoms = getWithdrawDenomList(amount, possibleDenoms);
+ console.log("got withdraw denom list");
for (const denom of selectedDenoms || []) {
if (denom.status === DenominationStatus.Unverified) {
+ console.log("checking validity", denom, exchangeDetails.masterPublicKey);
const valid = await this.cryptoApi.isValidDenom(
denom,
exchangeDetails.masterPublicKey,
);
+ console.log("done checking validity");
if (!valid) {
denom.status = DenominationStatus.VerifiedBad;
allValid = false;
@@ -1789,6 +2065,8 @@ export class Wallet {
}
} while (selectedDenoms.length > 0 && !allValid);
+ console.log("returning denoms");
+
return selectedDenoms;
}
@@ -1958,11 +2236,9 @@ export class Wallet {
exchangeBaseUrl: string,
supportedTargetTypes: string[],
): Promise<string> {
- const exchangeRecord = await oneShotGet(
- this.db,
- Stores.exchanges,
- exchangeBaseUrl,
- );
+ // We do the update here, since the exchange might not even exist
+ // yet in our database.
+ const exchangeRecord = await this.updateExchangeFromUrl(exchangeBaseUrl);
if (!exchangeRecord) {
throw Error(`Exchange '${exchangeBaseUrl}' not found.`);
}
@@ -2347,34 +2623,6 @@ export class Wallet {
);
});
- await tx.iter(Stores.reserves).forEach(r => {
- if (!r.timestampConfirmed) {
- return;
- }
- let amount = Amounts.getZero(r.requestedAmount.currency);
- amount = Amounts.add(amount, r.precoinAmount).amount;
- addTo(balanceStore, "pendingIncoming", amount, r.exchangeBaseUrl);
- addTo(
- balanceStore,
- "pendingIncomingWithdraw",
- amount,
- r.exchangeBaseUrl,
- );
- });
-
- await tx.iter(Stores.reserves).forEach(r => {
- if (!r.hasPayback) {
- return;
- }
- addTo(
- balanceStore,
- "paybackAmount",
- r.currentAmount!,
- r.exchangeBaseUrl,
- );
- return balanceStore;
- });
-
await tx.iter(Stores.purchases).forEach(t => {
if (t.finished) {
return;
@@ -2598,8 +2846,8 @@ export class Wallet {
const privs = Array.from(refreshSession.transferPrivs);
privs.splice(norevealIndex, 1);
- const preCoins = refreshSession.preCoinsForGammas[norevealIndex];
- if (!preCoins) {
+ const planchets = refreshSession.planchetsForGammas[norevealIndex];
+ if (!planchets) {
throw Error("refresh index error");
}
@@ -2612,7 +2860,7 @@ export class Wallet {
throw Error("inconsistent database");
}
- const evs = preCoins.map((x: RefreshPreCoinRecord) => x.coinEv);
+ const evs = planchets.map((x: RefreshPlanchetRecord) => x.coinEv);
const linkSigs: string[] = [];
for (let i = 0; i < refreshSession.newDenoms.length; i++) {
@@ -2621,7 +2869,7 @@ export class Wallet {
refreshSession.newDenomHashes[i],
refreshSession.meltCoinPub,
refreshSession.transferPubs[norevealIndex],
- preCoins[i].coinEv,
+ planchets[i].coinEv,
);
linkSigs.push(linkSig);
}
@@ -2682,7 +2930,7 @@ export class Wallet {
continue;
}
const pc =
- refreshSession.preCoinsForGammas[refreshSession.norevealIndex!][i];
+ refreshSession.planchetsForGammas[refreshSession.norevealIndex!][i];
const denomSig = await this.cryptoApi.rsaUnblind(
respJson.ev_sigs[i].ev_sig,
pc.blindingKey,
@@ -2699,6 +2947,8 @@ export class Wallet {
exchangeBaseUrl: refreshSession.exchangeBaseUrl,
reservePub: undefined,
status: CoinStatus.Fresh,
+ coinIndex: -1,
+ withdrawSessionId: "",
};
coins.push(coin);
@@ -2761,7 +3011,7 @@ export class Wallet {
const withdrawals = await oneShotIter(
this.db,
- Stores.withdrawals,
+ Stores.withdrawalSession,
).toArray();
for (const w of withdrawals) {
history.push({
@@ -2822,7 +3072,7 @@ export class Wallet {
history.push({
detail: {
exchangeBaseUrl: r.exchangeBaseUrl,
- requestedAmount: Amounts.toString(r.requestedAmount),
+ requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
reservePub: r.reservePub,
reserveType,
bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
@@ -2835,7 +3085,7 @@ export class Wallet {
history.push({
detail: {
exchangeBaseUrl: r.exchangeBaseUrl,
- requestedAmount: Amounts.toString(r.requestedAmount),
+ requestedAmount: Amounts.toString(r.initiallyRequestedAmount),
reservePub: r.reservePub,
reserveType,
bankWithdrawStatusUrl: r.bankWithdrawStatusUrl,
@@ -2956,11 +3206,23 @@ export class Wallet {
case ReserveRecordStatus.WITHDRAWING:
case ReserveRecordStatus.UNCONFIRMED:
case ReserveRecordStatus.QUERYING_STATUS:
+ case ReserveRecordStatus.REGISTERING_BANK:
pendingOperations.push({
type: "reserve",
stage: reserve.reserveStatus,
timestampCreated: reserve.created,
reserveType,
+ reservePub: reserve.reservePub,
+ });
+ break;
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ pendingOperations.push({
+ type: "reserve",
+ stage: reserve.reserveStatus,
+ timestampCreated: reserve.created,
+ reserveType,
+ reservePub: reserve.reservePub,
+ bankWithdrawConfirmUrl: reserve.bankWithdrawConfirmUrl,
});
break;
default:
@@ -2992,16 +3254,55 @@ export class Wallet {
oldCoinPub: r.meltCoinPub,
refreshStatus,
refreshOutputSize: r.newDenoms.length,
+ refreshSessionId: r.refreshSessionId,
});
});
- await oneShotIter(this.db, Stores.precoins).forEach(pc => {
+ await oneShotIter(this.db, Stores.planchets).forEach(pc => {
pendingOperations.push({
- type: "withdraw",
- stage: "planchet",
+ type: "planchet",
+ coinPub: pc.coinPub,
reservePub: pc.reservePub,
});
});
+
+ await oneShotIter(this.db, Stores.coins).forEach(coin => {
+ if (coin.status == CoinStatus.Dirty) {
+ pendingOperations.push({
+ type: "dirty-coin",
+ coinPub: coin.coinPub,
+ });
+ }
+ });
+
+ await oneShotIter(this.db, Stores.withdrawalSession).forEach(ws => {
+ const numCoinsWithdrawn = ws.withdrawn.reduce(
+ (a, x) => a + (x ? 1 : 0),
+ 0,
+ );
+ const numCoinsTotal = ws.withdrawn.length;
+ if (numCoinsWithdrawn < numCoinsTotal) {
+ pendingOperations.push({
+ type: "withdraw",
+ numCoinsTotal,
+ numCoinsWithdrawn,
+ reservePub: ws.reservePub,
+ withdrawSessionId: ws.withdrawSessionId,
+ });
+ }
+ });
+
+ await oneShotIter(this.db, Stores.proposals).forEach(proposal => {
+ if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
+ pendingOperations.push({
+ type: "proposal",
+ merchantBaseUrl: proposal.contractTerms.merchant_base_url,
+ proposalId: proposal.proposalId,
+ proposalTimestamp: proposal.timestamp,
+ });
+ }
+ });
+
return {
pendingOperations,
};
@@ -3016,9 +3317,7 @@ export class Wallet {
return denoms;
}
- async getProposal(
- proposalId: number,
- ): Promise<ProposalDownloadRecord | undefined> {
+ async getProposal(proposalId: string): Promise<ProposalRecord | undefined> {
const proposal = await oneShotGet(this.db, Stores.proposals, proposalId);
return proposal;
}
@@ -3053,8 +3352,8 @@ export class Wallet {
return await oneShotIter(this.db, Stores.coins).toArray();
}
- async getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> {
- return await oneShotIter(this.db, Stores.precoins).filter(
+ async getPlanchets(exchangeBaseUrl: string): Promise<PlanchetRecord[]> {
+ return await oneShotIter(this.db, Stores.planchets).filter(
c => c.exchangeBaseUrl === exchangeBaseUrl,
);
}
@@ -3130,9 +3429,13 @@ export class Wallet {
feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
isOffered: true,
masterSig: denomIn.master_sig,
- stampExpireDeposit: extractTalerStampOrThrow(denomIn.stamp_expire_deposit),
+ stampExpireDeposit: extractTalerStampOrThrow(
+ denomIn.stamp_expire_deposit,
+ ),
stampExpireLegal: extractTalerStampOrThrow(denomIn.stamp_expire_legal),
- stampExpireWithdraw: extractTalerStampOrThrow(denomIn.stamp_expire_withdraw),
+ stampExpireWithdraw: extractTalerStampOrThrow(
+ denomIn.stamp_expire_withdraw,
+ ),
stampStart: extractTalerStampOrThrow(denomIn.stamp_start),
status: DenominationStatus.Unverified,
value: Amounts.parseOrThrow(denomIn.value),
@@ -3570,9 +3873,7 @@ export class Wallet {
return feeAcc;
}
-async acceptTip(
- talerTipUri: string,
- ): Promise<void> {
+ async acceptTip(talerTipUri: string): Promise<void> {
const { tipId, merchantOrigin } = await this.getTipStatus(talerTipUri);
let tipRecord = await oneShotGet(this.db, Stores.tips, [
tipId,
@@ -3647,22 +3948,24 @@ async acceptTip(
}
for (let i = 0; i < tipRecord.planchets.length; i++) {
- const planchet = tipRecord.planchets[i];
- const preCoin = {
- blindingKey: planchet.blindingKey,
- coinEv: planchet.coinEv,
- coinPriv: planchet.coinPriv,
- coinPub: planchet.coinPub,
- coinValue: planchet.coinValue,
- denomPub: planchet.denomPub,
- denomPubHash: planchet.denomPubHash,
+ const tipPlanchet = tipRecord.planchets[i];
+ const planchet: PlanchetRecord = {
+ blindingKey: tipPlanchet.blindingKey,
+ coinEv: tipPlanchet.coinEv,
+ coinPriv: tipPlanchet.coinPriv,
+ coinPub: tipPlanchet.coinPub,
+ coinValue: tipPlanchet.coinValue,
+ denomPub: tipPlanchet.denomPub,
+ denomPubHash: tipPlanchet.denomPubHash,
exchangeBaseUrl: tipRecord.exchangeUrl,
isFromTip: true,
reservePub: response.reserve_pub,
withdrawSig: response.reserve_sigs[i].reserve_sig,
+ coinIndex: -1,
+ withdrawSessionId: "",
};
- await oneShotPut(this.db, Stores.precoins, preCoin);
- await this.processPreCoin(preCoin.coinPub);
+ await oneShotPut(this.db, Stores.planchets, planchet);
+ await this.processPlanchet(planchet.coinPub);
}
tipRecord.pickedUp = true;
@@ -3794,6 +4097,19 @@ async acceptTip(
});
}
+ public async handleNotifyReserve() {
+ const reserves = await oneShotIter(this.db, Stores.reserves).toArray();
+ for (const r of reserves) {
+ if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
+ try {
+ this.processReserveBankStatus(r.reservePub);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ }
+
/**
* Remove unreferenced / expired data from the wallet's database
* based on the current system time.
@@ -3805,6 +4121,10 @@ async acceptTip(
// strategy to test it.
}
+ /**
+ * Get information about a withdrawal from
+ * a taler://withdraw URI.
+ */
async getWithdrawalInfo(
talerWithdrawUri: string,
): Promise<DownloadedWithdrawInfo> {
@@ -3843,6 +4163,10 @@ async acceptTip(
senderWire: withdrawInfo.senderWire,
exchangeWire: 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.
+ await this.processReserveBankStatus(reserve.reservePub);
+ console.log("acceptWithdrawal: returning");
return {
reservePub: reserve.reservePub,
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
@@ -3883,13 +4207,6 @@ async acceptTip(
};
}
- /**
- * Reset the retry timeouts for ongoing operations.
- */
- resetRetryTimeouts(): void {
- // FIXME: implement
- }
-
clearNotification(): void {
this.badge.clearNotification();
}
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index b971e300d..45560694e 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -465,14 +465,14 @@ export type PreparePayResult =
export interface PreparePayResultPaymentPossible {
status: "payment-possible";
- proposalId: number;
+ proposalId: string;
contractTerms: ContractTerms;
totalFees: AmountJson;
}
export interface PreparePayResultInsufficientBalance {
status: "insufficient-balance";
- proposalId: number;
+ proposalId: string;
contractTerms: ContractTerms;
}
@@ -523,8 +523,10 @@ export interface WalletDiagnostics {
export interface PendingWithdrawOperation {
type: "withdraw";
- stage: string;
reservePub: string;
+ withdrawSessionId: string;
+ numCoinsWithdrawn: number;
+ numCoinsTotal: number;
}
export interface PendingRefreshOperation {
@@ -561,22 +563,47 @@ export interface PendingReserveOperation {
stage: string;
timestampCreated: Timestamp;
reserveType: string;
+ reservePub: string;
+ bankWithdrawConfirmUrl?: string;
}
export interface PendingRefreshOperation {
type: "refresh";
lastError?: OperationError;
+ refreshSessionId: string;
oldCoinPub: string;
refreshStatus: string;
refreshOutputSize: number;
}
+export interface PendingPlanchetOperation {
+ type: "planchet";
+ coinPub: string;
+ reservePub: string;
+ lastError?: OperationError;
+}
+
+export interface PendingDirtyCoinOperation {
+ type: "dirty-coin";
+ coinPub: string;
+}
+
+export interface PendingProposalOperation {
+ type: "proposal";
+ merchantBaseUrl: string;
+ proposalTimestamp: Timestamp;
+ proposalId: string;
+}
+
export type PendingOperationInfo =
| PendingWithdrawOperation
| PendingReserveOperation
| PendingBugOperation
+ | PendingPlanchetOperation
+ | PendingDirtyCoinOperation
| PendingExchangeUpdateOperation
- | PendingRefreshOperation;
+ | PendingRefreshOperation
+ | PendingProposalOperation;
export interface PendingOperationsResponse {
pendingOperations: PendingOperationInfo[];
@@ -614,3 +641,17 @@ export function getTimestampNow(): Timestamp {
t_ms: new Date().getTime(),
};
}
+
+
+export interface PlanchetCreationResult {
+ coinPub: string;
+ coinPriv: string;
+ reservePub: string;
+ denomPubHash: string;
+ denomPub: string;
+ blindingKey: string;
+ withdrawSig: string;
+ coinEv: string;
+ exchangeBaseUrl: string;
+ coinValue: AmountJson;
+} \ No newline at end of file
diff --git a/src/webex/messages.ts b/src/webex/messages.ts
index 034bf2849..e321e5ac1 100644
--- a/src/webex/messages.ts
+++ b/src/webex/messages.ts
@@ -66,7 +66,7 @@ export interface MessageMap {
response: void;
};
"confirm-pay": {
- request: { proposalId: number; sessionId?: string };
+ request: { proposalId: string; sessionId?: string };
response: walletTypes.ConfirmPayResult;
};
"exchange-info": {
@@ -113,9 +113,9 @@ export interface MessageMap {
request: { reservePub: string };
response: dbTypes.ReserveRecord[];
};
- "get-precoins": {
+ "get-planchets": {
request: { exchangeBaseUrl: string };
- response: dbTypes.PreCoinRecord[];
+ response: dbTypes.PlanchetRecord[];
};
"get-denoms": {
request: { exchangeBaseUrl: string };
diff --git a/src/webex/pages/payback.tsx b/src/webex/pages/payback.tsx
index af14b95d4..806bef17c 100644
--- a/src/webex/pages/payback.tsx
+++ b/src/webex/pages/payback.tsx
@@ -57,7 +57,7 @@ function Payback() {
<div>
{reserves.map(r => (
<div>
- <h2>Reserve for ${renderAmount(r.currentAmount!)}</h2>
+ <h2>Reserve for ${renderAmount(r.withdrawRemainingAmount)}</h2>
<ul>
<li>Exchange: ${r.exchangeBaseUrl}</li>
</ul>
diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts
index a50672131..a8b35ed34 100644
--- a/src/webex/wxApi.ts
+++ b/src/webex/wxApi.ts
@@ -28,7 +28,7 @@ import {
CurrencyRecord,
DenominationRecord,
ExchangeRecord,
- PreCoinRecord,
+ PlanchetRecord,
ReserveRecord,
} from "../dbTypes";
import {
@@ -174,10 +174,10 @@ export function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> {
/**
- * Get all precoins withdrawn from the given exchange.
+ * Get all planchets withdrawn from the given exchange.
*/
-export function getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> {
- return callBackend("get-precoins", { exchangeBaseUrl });
+export function getPlanchets(exchangeBaseUrl: string): Promise<PlanchetRecord[]> {
+ return callBackend("get-planchets", { exchangeBaseUrl });
}
@@ -207,7 +207,7 @@ export function payback(coinPub: string): Promise<void> {
/**
* Pay for a proposal.
*/
-export function confirmPay(proposalId: number, sessionId: string | undefined): Promise<ConfirmPayResult> {
+export function confirmPay(proposalId: string, sessionId: string | undefined): Promise<ConfirmPayResult> {
return callBackend("confirm-pay", { proposalId, sessionId });
}
diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts
index 57c10d94a..78c86a976 100644
--- a/src/webex/wxBackend.ts
+++ b/src/webex/wxBackend.ts
@@ -117,8 +117,8 @@ async function handleMessage(
return needsWallet().confirmReserve(req);
}
case "confirm-pay": {
- if (typeof detail.proposalId !== "number") {
- throw Error("proposalId must be number");
+ if (typeof detail.proposalId !== "string") {
+ throw Error("proposalId must be string");
}
return needsWallet().confirmPay(detail.proposalId, detail.sessionId);
}
@@ -178,11 +178,11 @@ async function handleMessage(
}
return needsWallet().getCoinsForExchange(detail.exchangeBaseUrl);
}
- case "get-precoins": {
+ case "get-planchets": {
if (typeof detail.exchangeBaseUrl !== "string") {
return Promise.reject(Error("exchangBaseUrl missing"));
}
- return needsWallet().getPreCoins(detail.exchangeBaseUrl);
+ return needsWallet().getPlanchets(detail.exchangeBaseUrl);
}
case "get-denoms": {
if (typeof detail.exchangeBaseUrl !== "string") {
@@ -658,8 +658,8 @@ export async function wxMain() {
if (!wallet) {
console.warn("wallet not available while handling header");
}
- if (details.statusCode === 402) {
- console.log(`got 402 from ${details.url}`);
+ if (details.statusCode === 402 || details.statusCode === 202) {
+ console.log(`got 402/202 from ${details.url}`);
for (let header of details.responseHeaders || []) {
if (header.name.toLowerCase() === "taler") {
const talerUri = header.value || "";
@@ -705,6 +705,15 @@ export async function wxMain() {
talerRefundUri: talerUri,
},
);
+ } else if (talerUri.startsWith("taler://notify-reserve/")) {
+ Promise.resolve().then(() => {
+ const w = currentWallet;
+ if (!w) {
+ return;
+ }
+ w.handleNotifyReserve();
+ });
+
} else {
console.warn("Unknown action in taler:// URI, ignoring.");
}