wallet robustness WIP

This commit is contained in:
Florian Dold 2019-11-30 00:36:20 +01:00
parent 809fa18644
commit aaf7e1338d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
20 changed files with 1075 additions and 431 deletions

View File

@ -60,8 +60,6 @@ const paths = {
dist: [
"dist/*-bundle.js",
"dist/*-bundle.js.map",
"emscripten/taler-emscripten-lib.js",
"emscripten/taler-emscripten-lib.wasm",
"img/icon.png",
"img/logo.png",
"src/webex/**/*.{js,css,html}",
@ -149,7 +147,7 @@ function dist_prod() {
}
function compile_prod(callback) {
let config = require("./webpack.config.js")({ prod: true });
let config = require("./webpack.config.js")({ mode: "production" });
webpack(config, function(err, stats) {
if (err) {
throw new gutil.PluginError("webpack", err);

View File

@ -64,7 +64,7 @@
"@types/urijs": "^1.19.3",
"axios": "^0.19.0",
"big-integer": "^1.6.48",
"idb-bridge": "^0.0.14",
"idb-bridge": "^0.0.15",
"qrcode-generator": "^1.4.3",
"source-map-support": "^0.5.12",
"urijs": "^1.18.10"

View File

@ -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> {

View File

@ -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,

View File

@ -88,5 +88,5 @@ export function kdf(
output.set(chunk, i * 32);
}
return output;
return output.slice(0, outputLength);
}

View File

@ -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,

View File

@ -57,6 +57,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.
*/
@ -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 allocated for withdrawing.
* The corresponding withdraw operation may or may not
* have been completed yet.
*/
withdrawAllocatedAmount: AmountJson;
withdrawCompletedAmount: 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
*/
requestedAmount: AmountJson;
/**
* What's the current amount that sits
* in precoins?
*/
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.
*/
@ -485,6 +516,16 @@ export enum CoinStatus {
* of the wallet database.
*/
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.
*/
@ -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[];
numCoinsWithdrawn: number;
/**
* 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;
/**
* 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 */

View File

@ -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,

View File

@ -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;
}

View File

@ -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);
}

View File

@ -19,7 +19,7 @@
* Used mostly for integration tests.
*/
/**
/**
* Imports.
*/
import axios from "axios";
@ -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,

View File

@ -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();
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,9 +515,13 @@ testCli
return;
}
console.log("taler pay URI:", talerPayUri);
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);
@ -421,6 +533,7 @@ testCli
break;
}
}
}
});
testCli

View File

@ -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: {

File diff suppressed because it is too large Load Diff

View File

@ -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;
}

View File

@ -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 };

View File

@ -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>

View File

@ -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 });
}

View File

@ -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.");
}

View File

@ -3417,10 +3417,10 @@ iconv-lite@0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
dependencies:
safer-buffer ">= 2.1.2 < 3"
idb-bridge@^0.0.14:
version "0.0.14"
resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.14.tgz#5fd50cd68b574df0eb6b1a960cef0cb984a21ded"
integrity sha512-jc9ZYGhhIrW6nh/pWyycGWzCmsLTFQ0iMY61lN+y9YcIOCxREpAkZxdfmhwNL7H0RvsYp7iJv0GH7ujs7HPC+g==
idb-bridge@^0.0.15:
version "0.0.15"
resolved "https://registry.yarnpkg.com/idb-bridge/-/idb-bridge-0.0.15.tgz#3fddc91b9aab775fae273d02b272205c6090d270"
integrity sha512-xuZM/i4vCm/NkqyrKNJDEuBaZK7M2kyj+1F4hDGqtEJZSmQMSV3v9A6Ie3fR12VXDKIbMr7uV22eWjIKwSosOA==
ieee754@^1.1.4:
version "1.1.13"