documentation and tslint settings to check for docs

This commit is contained in:
Florian Dold 2017-05-28 16:27:34 +02:00
parent 08bd3dc0e8
commit e7fa87bcc0
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
14 changed files with 946 additions and 136 deletions

View File

@ -30,33 +30,37 @@ function rAF(cb: (ts: number) => void) {
}
/**
* Badge for Chrome that renders a Taler logo with a rotating ring if some
* background activity is happening.
*/
export class ChromeBadge implements Badge {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
/**
* True if animation running. The animation
* might still be running even if we're not busy anymore,
* just to transition to the "normal" state in a animated way.
*/
animationRunning: boolean = false;
private animationRunning: boolean = false;
/**
* Is the wallet still busy? Note that we do not stop the
* animation immediately when the wallet goes idle, but
* instead slowly close the gap.
*/
isBusy: boolean = false;
private isBusy: boolean = false;
/**
* Current rotation angle, ranges from 0 to rotationAngleMax.
*/
rotationAngle: number = 0;
private rotationAngle: number = 0;
/**
* While animating, how wide is the current gap in the circle?
* Ranges from 0 to openMax.
*/
gapWidth: number = 0;
private gapWidth: number = 0;
/**
* Maximum value for our rotationAngle, corresponds to 2 Pi.
@ -207,14 +211,6 @@ export class ChromeBadge implements Badge {
rAF(step);
}
setText(s: string) {
chrome.browserAction.setBadgeText({text: s});
}
setColor(c: string) {
chrome.browserAction.setBadgeBackgroundColor({color: c});
}
startBusy() {
if (this.isBusy) {
return;

View File

@ -37,7 +37,7 @@ export interface StateHolder<T> {
* but has multiple state holders.
*/
export abstract class ImplicitStateComponent<PropType> extends React.Component<PropType, any> {
_implicit = {needsUpdate: false, didMount: false};
private _implicit = {needsUpdate: false, didMount: false};
componentDidMount() {
this._implicit.didMount = true;
if (this._implicit.needsUpdate) {

View File

@ -108,6 +108,10 @@ namespace RpcFunctions {
return preCoin;
}
/**
* Create and sign a message to request payback for a coin.
*/
export function createPaybackRequest(coin: CoinRecord): PaybackRequest {
const p = new native.PaybackRequestPS({
coin_blind: native.RsaBlindingKeySecret.fromCrock(coin.blindingKey),
@ -127,6 +131,9 @@ namespace RpcFunctions {
}
/**
* Check if a payment signature is valid.
*/
export function isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string): boolean {
const p = new native.PaymentSignaturePS({
contract_hash: native.HashCode.fromCrock(contractHash),
@ -140,6 +147,9 @@ namespace RpcFunctions {
nativePub);
}
/**
* Check if a wire fee is correctly signed.
*/
export function isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean {
const p = new native.MasterWireFeePS({
closing_fee: (new native.Amount(wf.closingFee)).toNbo(),
@ -160,6 +170,9 @@ namespace RpcFunctions {
}
/**
* Check if the signature of a denomination is valid.
*/
export function isValidDenom(denom: DenominationRecord,
masterPub: string): boolean {
const p = new native.DenominationKeyValidityPS({
@ -189,6 +202,9 @@ namespace RpcFunctions {
}
/**
* Create a new EdDSA key pair.
*/
export function createEddsaKeypair(): {priv: string, pub: string} {
const priv = native.EddsaPrivateKey.create();
const pub = priv.getPublicKey();
@ -196,6 +212,9 @@ namespace RpcFunctions {
}
/**
* Unblind a blindly signed value.
*/
export function rsaUnblind(sig: string, bk: string, pk: string): string {
const denomSig = native.rsaUnblind(native.RsaSignature.fromCrock(sig),
native.RsaBlindingKeySecret.fromCrock(bk),
@ -278,6 +297,9 @@ namespace RpcFunctions {
}
/**
* Create a new refresh session.
*/
export function createRefreshSession(exchangeBaseUrl: string,
kappa: number,
meltCoin: CoinRecord,
@ -398,6 +420,9 @@ namespace RpcFunctions {
return b.hash().toCrock();
}
/**
* Hash a denomination public key.
*/
export function hashDenomPub(denomPub: string): string {
return native.RsaPublicKey.fromCrock(denomPub).encode().hash().toCrock();
}

View File

@ -259,7 +259,7 @@ interface Arena {
* Arena that must be manually destroyed.
*/
class SimpleArena implements Arena {
heap: ArenaObject[];
protected heap: ArenaObject[];
constructor() {
this.heap = [];
@ -774,7 +774,7 @@ export class EccSignaturePurpose extends PackedArenaObject {
return this.payloadSize + 8;
}
payloadSize: number;
private payloadSize: number;
constructor(purpose: SignaturePurpose,
payload: PackedArenaObject,

View File

@ -22,10 +22,22 @@ const fork = require("child_process").fork;
const nodeWorkerEntry = path.join(__dirname, "nodeWorkerEntry.js");
/**
* Worker implementation that uses node subprocesses.
*/
export class Worker {
child: any;
private child: any;
/**
* Function to be called when we receive a message from the worker thread.
*/
onmessage: undefined | ((m: any) => void);
/**
* Function to be called when we receive an error from the worker thread.
*/
onerror: undefined | ((m: any) => void);
constructor(scriptFilename: string) {
this.child = fork(nodeWorkerEntry);
this.onerror = undefined;
@ -55,6 +67,9 @@ export class Worker {
this.child.send({scriptFilename, cwd: process.cwd()});
}
/**
* Add an event listener for either an "error" or "message" event.
*/
addEventListener(event: "message" | "error", fn: (x: any) => void): void {
switch (event) {
case "message":
@ -66,10 +81,16 @@ export class Worker {
}
}
/**
* Send a message to the worker thread.
*/
postMessage (msg: any) {
this.child.send(JSON.stringify({data: msg}));
}
/**
* Forcibly terminate the worker thread.
*/
terminate () {
this.child.kill("SIGINT");
}

View File

@ -83,6 +83,10 @@ export function canonicalJson(obj: any): string {
}
/**
* Check for deep equality of two objects.
* Only arrays, objects and primitives are supported.
*/
export function deepEquals(x: any, y: any): boolean {
if (x === y) {
return true;
@ -98,6 +102,10 @@ export function deepEquals(x: any, y: any): boolean {
}
/**
* Map from a collection to a list or results and then
* concatenate the results.
*/
export function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] {
return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []);
}

View File

@ -15,9 +15,7 @@
*/
/**
* Configurable logging.
*
* @author Florian Dold
* Configurable logging. Allows to log persistently to a database.
*/
import {
@ -26,8 +24,14 @@ import {
openPromise,
} from "./query";
/**
* Supported log levels.
*/
export type Level = "error" | "debug" | "info" | "warn";
// Right now, our debug/info/warn/debug loggers just use the console based
// loggers. This might change in the future.
function makeInfo() {
return console.info.bind(console, "%o");
}
@ -44,6 +48,9 @@ function makeDebug() {
return console.log.bind(console, "%o");
}
/**
* Log a message using the configurable logger.
*/
export async function log(msg: string, level: Level = "info"): Promise<void> {
const ci = getCallInfo(2);
return record(level, msg, undefined, ci.file, ci.line, ci.column);
@ -122,17 +129,50 @@ function parseStackLine(stackLine: string): Frame {
let db: IDBDatabase|undefined;
/**
* A structured log entry as stored in the database.
*/
export interface LogEntry {
/**
* Soure code column where the error occured.
*/
col?: number;
/**
* Additional detail for the log statement.
*/
detail?: string;
/**
* Id of the log entry, used as primary
* key for the database.
*/
id?: number;
/**
* Log level, see [[Level}}.
*/
level: string;
/**
* Line where the log was created from.
*/
line?: number;
/**
* The actual log message.
*/
msg: string;
/**
* The source file where the log enctry
* was created from.
*/
source?: string;
/**
* Time when the log entry was created.
*/
timestamp: number;
}
/**
* Get all logs. Only use for debugging, since this returns all logs ever made
* at once without pagination.
*/
export async function getLogs(): Promise<LogEntry[]> {
if (!db) {
db = await openLoggingDb();
@ -147,6 +187,9 @@ export async function getLogs(): Promise<LogEntry[]> {
*/
let barrier: any;
/**
* Record an exeption in the log.
*/
export async function recordException(msg: string, e: any): Promise<void> {
let stack: string|undefined;
let frame: Frame|undefined;
@ -165,6 +208,9 @@ export async function recordException(msg: string, e: any): Promise<void> {
return record("error", e.toString(), stack, frame.file, frame.line, frame.column);
}
/**
* Record a log entry in the database.
*/
export async function record(level: Level,
msg: string,
detail?: string,
@ -215,6 +261,10 @@ const loggingDbVersion = 1;
const logsStore: Store<LogEntry> = new Store<LogEntry>("logs");
/**
* Get a handle to the IndexedDB used to store
* logs.
*/
export function openLoggingDb(): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => {
const req = indexedDB.open("taler-logging", loggingDbVersion);
@ -238,7 +288,22 @@ export function openLoggingDb(): Promise<IDBDatabase> {
});
}
/**
* Log a message at severity info.
*/
export const info = makeInfo();
/**
* Log a message at severity debug.
*/
export const debug = makeDebug();
/**
* Log a message at severity warn.
*/
export const warn = makeWarn();
/**
* Log a message at severity error.
*/
export const error = makeError();

View File

@ -53,6 +53,9 @@ export class Store<T> {
* Definition of an index.
*/
export class Index<S extends IDBValidKey, T> {
/**
* Name of the store that this index is associated with.
*/
storeName: string;
constructor(s: Store<T>, public indexName: string, public keyPath: string | string[]) {
@ -127,16 +130,26 @@ export interface QueryStream<T> {
* Query result that consists of at most one value.
*/
export interface QueryValue<T> {
/**
* Apply a function to a query value.
*/
map<S>(f: (x: T) => S): QueryValue<S>;
/**
* Conditionally execute either of two queries based
* on a property of this query value.
*
* Useful to properly implement complex queries within a transaction (as
* opposed to just computing the conditional and then executing either
* branch). This is necessary since IndexedDB does not allow long-lived
* transactions.
*/
cond<R>(f: (x: T) => boolean, onTrue: (r: QueryRoot) => R, onFalse: (r: QueryRoot) => R): Promise<void>;
}
abstract class BaseQueryValue<T> implements QueryValue<T> {
root: QueryRoot;
constructor(root: QueryRoot) {
this.root = root;
constructor(public root: QueryRoot) {
}
map<S>(f: (x: T) => S): QueryValue<S> {
@ -160,8 +173,9 @@ abstract class BaseQueryValue<T> implements QueryValue<T> {
}
class FirstQueryValue<T> extends BaseQueryValue<T> {
gotValue = false;
s: QueryStreamBase<T>;
private gotValue = false;
private s: QueryStreamBase<T>;
constructor(stream: QueryStreamBase<T>) {
super(stream.root);
this.s = stream;
@ -183,13 +197,8 @@ class FirstQueryValue<T> extends BaseQueryValue<T> {
}
class MapQueryValue<T, S> extends BaseQueryValue<S> {
mapFn: (x: T) => S;
v: BaseQueryValue<T>;
constructor(v: BaseQueryValue<T>, mapFn: (x: T) => S) {
constructor(private v: BaseQueryValue<T>, private mapFn: (x: T) => S) {
super(v.root);
this.v = v;
this.mapFn = mapFn;
}
subscribeOne(f: SubscribeOneFn): void {
@ -226,11 +235,7 @@ abstract class QueryStreamBase<T> implements QueryStream<T>, PromiseLike<void> {
abstract subscribe(f: (isDone: boolean,
value: any,
tx: IDBTransaction) => void): void;
root: QueryRoot;
constructor(root: QueryRoot) {
this.root = root;
constructor(public root: QueryRoot) {
}
first(): QueryValue<T> {
@ -313,13 +318,8 @@ type SubscribeOneFn = (value: any, tx: IDBTransaction) => void;
type FlatMapFn<T> = (v: T) => T[];
class QueryStreamFilter<T> extends QueryStreamBase<T> {
s: QueryStreamBase<T>;
filterFn: FilterFn;
constructor(s: QueryStreamBase<T>, filterFn: FilterFn) {
constructor(public s: QueryStreamBase<T>, public filterFn: FilterFn) {
super(s.root);
this.s = s;
this.filterFn = filterFn;
}
subscribe(f: SubscribeFn) {
@ -337,13 +337,8 @@ class QueryStreamFilter<T> extends QueryStreamBase<T> {
class QueryStreamFlatMap<T, S> extends QueryStreamBase<S> {
s: QueryStreamBase<T>;
flatMapFn: (v: T) => S[];
constructor(s: QueryStreamBase<T>, flatMapFn: (v: T) => S[]) {
constructor(public s: QueryStreamBase<T>, public flatMapFn: (v: T) => S[]) {
super(s.root);
this.s = s;
this.flatMapFn = flatMapFn;
}
subscribe(f: SubscribeFn) {
@ -362,13 +357,8 @@ class QueryStreamFlatMap<T, S> extends QueryStreamBase<S> {
class QueryStreamMap<S, T> extends QueryStreamBase<T> {
s: QueryStreamBase<S>;
mapFn: (v: S) => T;
constructor(s: QueryStreamBase<S>, mapFn: (v: S) => T) {
constructor(public s: QueryStreamBase<S>, public mapFn: (v: S) => T) {
super(s.root);
this.s = s;
this.mapFn = mapFn;
}
subscribe(f: SubscribeFn) {
@ -385,18 +375,9 @@ class QueryStreamMap<S, T> extends QueryStreamBase<T> {
class QueryStreamIndexJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
s: QueryStreamBase<T>;
storeName: string;
key: any;
indexName: string;
constructor(s: QueryStreamBase<T>, storeName: string, indexName: string,
key: any) {
constructor(public s: QueryStreamBase<T>, public storeName: string, public indexName: string,
public key: any) {
super(s.root);
this.s = s;
this.storeName = storeName;
this.key = key;
this.indexName = indexName;
}
subscribe(f: SubscribeFn) {
@ -420,18 +401,9 @@ class QueryStreamIndexJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
class QueryStreamIndexJoinLeft<T, S> extends QueryStreamBase<JoinLeftResult<T, S>> {
s: QueryStreamBase<T>;
storeName: string;
key: any;
indexName: string;
constructor(s: QueryStreamBase<T>, storeName: string, indexName: string,
key: any) {
constructor(public s: QueryStreamBase<T>, public storeName: string, public indexName: string,
public key: any) {
super(s.root);
this.s = s;
this.storeName = storeName;
this.key = key;
this.indexName = indexName;
}
subscribe(f: SubscribeFn) {
@ -461,16 +433,9 @@ class QueryStreamIndexJoinLeft<T, S> extends QueryStreamBase<JoinLeftResult<T, S
class QueryStreamKeyJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
s: QueryStreamBase<T>;
storeName: string;
key: any;
constructor(s: QueryStreamBase<T>, storeName: string,
key: any) {
constructor(public s: QueryStreamBase<T>, public storeName: string,
public key: any) {
super(s.root);
this.s = s;
this.storeName = storeName;
this.key = key;
}
subscribe(f: SubscribeFn) {

View File

@ -28,32 +28,77 @@
*/
import { Checkable } from "./checkable";
/**
* Non-negative financial amount. Fractional values are expressed as multiples
* of 1e-8.
*/
@Checkable.Class()
export class AmountJson {
/**
* Value, must be an integer.
*/
@Checkable.Number
value: number;
readonly value: number;
/**
* Fraction, must be an integer. Represent 1/1e8 of a unit.
*/
@Checkable.Number
fraction: number;
readonly fraction: number;
/**
* Currency of the amount.
*/
@Checkable.String
currency: string;
readonly currency: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => AmountJson;
}
/**
* Amount with a sign.
*/
export interface SignedAmountJson {
/**
* The absolute amount.
*/
amount: AmountJson;
/**
* Sign.
*/
isNegative: boolean;
}
/**
* A reserve record as stored in the wallet's database.
*/
export interface ReserveRecord {
/**
* The reserve public key.
*/
reserve_pub: string;
/**
* The reserve private key.
*/
reserve_priv: string;
/**
* The exchange base URL.
*/
exchange_base_url: string;
/**
* Time when the reserve was created.
*/
created: number;
/**
* Time when the reserve was last queried,
* or 'null' if it was never queried.
*/
last_query: number | null;
/**
* Current amount left in the reserve
@ -65,17 +110,16 @@ export interface ReserveRecord {
* be higher than the requested_amount
*/
requested_amount: AmountJson;
/**
* What's the current amount that sits
* in precoins?
*/
precoin_amount: AmountJson;
/**
* The bank conformed that the reserve will eventually
* be filled with money.
*/
confirmed: boolean;
/**
* We got some payback to this reserve. We'll cease to automatically
* withdraw money from it.
@ -106,6 +150,9 @@ export interface CurrencyRecord {
}
/**
* Response for the create reserve request to the wallet.
*/
@Checkable.Class()
export class CreateReserveResponse {
/**
@ -115,52 +162,114 @@ export class CreateReserveResponse {
@Checkable.String
exchange: string;
/**
* Reserve public key of the newly created reserve.
*/
@Checkable.String
reservePub: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => CreateReserveResponse;
}
/**
* Status of a denomination.
*/
export enum DenominationStatus {
/**
* Verification was delayed.
*/
Unverified,
/**
* Verified as valid.
*/
VerifiedGood,
/**
* Verified as invalid.
*/
VerifiedBad,
}
/**
* Denomination record as stored in the wallet's database.
*/
@Checkable.Class()
export class DenominationRecord {
/**
* Value of one coin of the denomination.
*/
@Checkable.Value(AmountJson)
value: AmountJson;
/**
* The denomination public key.
*/
@Checkable.String
denomPub: string;
/**
* Hash of the denomination public key.
* Stored in the database for faster lookups.
*/
@Checkable.String
denomPubHash: string;
/**
* Fee for withdrawing.
*/
@Checkable.Value(AmountJson)
feeWithdraw: AmountJson;
/**
* Fee for depositing.
*/
@Checkable.Value(AmountJson)
feeDeposit: AmountJson;
/**
* Fee for refreshing.
*/
@Checkable.Value(AmountJson)
feeRefresh: AmountJson;
/**
* Fee for refunding.
*/
@Checkable.Value(AmountJson)
feeRefund: AmountJson;
/**
* Validity start date of the denomination.
*/
@Checkable.String
stampStart: string;
/**
* Date after which the currency can't be withdrawn anymore.
*/
@Checkable.String
stampExpireWithdraw: string;
/**
* Date after the denomination officially doesn't exist anymore.
*/
@Checkable.String
stampExpireLegal: string;
/**
* Data after which coins of this denomination can't be deposited anymore.
*/
@Checkable.String
stampExpireDeposit: string;
/**
* Signature by the exchange's master key over the denomination
* information.
*/
@Checkable.String
masterSig: string;
@ -178,9 +287,16 @@ export class DenominationRecord {
@Checkable.Boolean
isOffered: boolean;
/**
* Base URL of the exchange.
*/
@Checkable.String
exchangeBaseUrl: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => Denomination;
}
@ -189,59 +305,124 @@ export class DenominationRecord {
*/
@Checkable.Class()
export class Denomination {
/**
* Value of one coin of the denomination.
*/
@Checkable.Value(AmountJson)
value: AmountJson;
/**
* Public signing key of the denomination.
*/
@Checkable.String
denom_pub: string;
/**
* Fee for withdrawing.
*/
@Checkable.Value(AmountJson)
fee_withdraw: AmountJson;
/**
* Fee for depositing.
*/
@Checkable.Value(AmountJson)
fee_deposit: AmountJson;
/**
* Fee for refreshing.
*/
@Checkable.Value(AmountJson)
fee_refresh: AmountJson;
/**
* Fee for refunding.
*/
@Checkable.Value(AmountJson)
fee_refund: AmountJson;
/**
* Start date from which withdraw is allowed.
*/
@Checkable.String
stamp_start: string;
/**
* End date for withdrawing.
*/
@Checkable.String
stamp_expire_withdraw: string;
/**
* Expiration date after which the exchange can forget about
* the currency.
*/
@Checkable.String
stamp_expire_legal: string;
/**
* Date after which the coins of this denomination can't be
* deposited anymore.
*/
@Checkable.String
stamp_expire_deposit: string;
/**
* Signature over the denomination information by the exchange's master
* signing key.
*/
@Checkable.String
master_sig: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => Denomination;
}
/**
* Auditor information.
*/
export interface Auditor {
// official name
/**
* Official name.
*/
name: string;
// Auditor's public key
/**
* Auditor's public key.
*/
auditor_pub: string;
// Base URL of the auditor
/**
* Base URL of the auditor.
*/
url: string;
}
/**
* Exchange record as stored in the wallet's database.
*/
export interface ExchangeRecord {
/**
* Base url of the exchange.
*/
baseUrl: string;
/**
* Master public key of the exchange.
*/
masterPublicKey: string;
/**
* Auditors (partially) auditing the exchange.
*/
auditors: Auditor[];
/**
* Currency that the exchange offers.
*/
currency: string;
/**
@ -282,14 +463,36 @@ export interface PreCoinRecord {
coinValue: AmountJson;
}
/**
* Planchet for a coin during refrehs.
*/
export interface RefreshPreCoinRecord {
/**
* Public key for the coin.
*/
publicKey: string;
/**
* Private key for the coin.
*/
privateKey: string;
/**
* Blinded public key.
*/
coinEv: string;
/**
* Blinding key used.
*/
blindingKey: string;
}
/**
* Request that we send to the exchange to get a payback.
*/
export interface PaybackRequest {
/**
* Denomination public key of the coin we want to get
* paid back.
*/
denom_pub: string;
/**
@ -297,13 +500,26 @@ export interface PaybackRequest {
*/
denom_sig: string;
/**
* Coin public key of the coin we want to refund.
*/
coin_pub: string;
/**
* Blinding key that was used during withdraw,
* used to prove that we were actually withdrawing the coin.
*/
coin_blind_key_secret: string;
/**
* Signature made by the coin, authorizing the payback.
*/
coin_sig: string;
}
/**
* Response that we get from the exchange for a payback request.
*/
@Checkable.Class()
export class PaybackConfirmation {
/**
@ -344,6 +560,10 @@ export class PaybackConfirmation {
@Checkable.String
exchange_pub: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => PaybackConfirmation;
}
@ -378,15 +598,19 @@ export interface RefreshSessionRecord {
*/
newDenoms: string[];
/**
* Precoins for each cut-and-choose instance.
*/
preCoinsForGammas: RefreshPreCoinRecord[][];
/**
* The transfer keys, kappa of them.
*/
transferPubs: string[];
/**
* Private keys for the transfer public keys.
*/
transferPrivs: string[];
/**
@ -399,23 +623,73 @@ export interface RefreshSessionRecord {
*/
hash: string;
/**
* Base URL for the exchange we're doing the refresh with.
*/
exchangeBaseUrl: string;
/**
* Is this session finished?
*/
finished: boolean;
}
/**
* Deposit permission for a single coin.
*/
export interface CoinPaySig {
/**
* Signature by the coin.
*/
coin_sig: string;
/**
* Public key of the coin being spend.
*/
coin_pub: string;
/**
* Signature made by the denomination public key.
*/
ub_sig: string;
/**
* The denomination public key associated with this coin.
*/
denom_pub: string;
/**
* The amount that is subtracted from this coin with this payment.
*/
f: AmountJson;
}
/**
* Status of a coin.
*/
export enum CoinStatus {
Fresh, TransactionPending, Dirty, Refreshed, PaybackPending, PaybackDone,
/**
* Withdrawn and never shown to anybody.
*/
Fresh,
/**
* Currently planned to be sent to a merchant for a transaction.
*/
TransactionPending,
/**
* Used for a completed transaction and now dirty.
*/
Dirty,
/**
* A coin that was refreshed.
*/
Refreshed,
/**
* Coin marked to be paid back, but payback not finished.
*/
PaybackPending,
/**
* Coin fully paid back.
*/
PaybackDone,
}
@ -462,6 +736,10 @@ export interface CoinRecord {
*/
suspended?: boolean;
/**
* Blinding key used when withdrawing the coin.
* Potentionally sed again during payback.
*/
blindingKey: string;
/**
@ -477,29 +755,70 @@ export interface CoinRecord {
}
/**
* Information about an exchange as stored inside a
* merchant's contract terms.
*/
@Checkable.Class()
export class ExchangeHandle {
/**
* Master public signing key of the exchange.
*/
@Checkable.String
master_pub: string;
/**
* Base URL of the exchange.
*/
@Checkable.String
url: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => ExchangeHandle;
}
export interface WalletBalance {
[currency: string]: WalletBalanceEntry;
}
/**
* Mapping from currency names to detailed balance
* information for that particular currency.
*/
export interface WalletBalance {
/**
* Mapping from currency name to defailed balance info.
*/
[currency: string]: WalletBalanceEntry;
};
/**
* Detailed wallet balance for a particular currency.
*/
export interface WalletBalanceEntry {
/**
* Directly available amount.
*/
available: AmountJson;
/**
* Amount that we're waiting for (refresh, withdrawal).
*/
pendingIncoming: AmountJson;
/**
* Amount that's marked for a pending payment.
*/
pendingPayment: AmountJson;
/**
* Amount that was paid back and we could withdraw again.
*/
paybackAmount: AmountJson;
}
/**
* Information about a merchant.
*/
interface Merchant {
/**
* label for a location with the business address of the merchant
@ -524,108 +843,235 @@ interface Merchant {
instance?: string;
}
/**
* Contract terms from a merchant.
*/
@Checkable.Class({validate: true})
export class Contract {
validate() {
private validate() {
if (this.exchanges.length === 0) {
throw Error("no exchanges in contract");
}
}
/**
* Hash of the merchant's wire details.
*/
@Checkable.String
H_wire: string;
/**
* Wire method the merchant wants to use.
*/
@Checkable.String
wire_method: string;
/**
* Human-readable short summary of the contract.
*/
@Checkable.Optional(Checkable.String)
summary?: string;
/**
* Nonce used to ensure freshness.
*/
@Checkable.Optional(Checkable.String)
nonce?: string;
/**
* Total amount payable.
*/
@Checkable.Value(AmountJson)
amount: AmountJson;
/**
* Auditors accepted by the merchant.
*/
@Checkable.List(Checkable.AnyObject)
auditors: any[];
/**
* Deadline to pay for the contract.
*/
@Checkable.Optional(Checkable.String)
pay_deadline: string;
/**
* Delivery locations.
*/
@Checkable.Any
locations: any;
/**
* Maximum deposit fee covered by the merchant.
*/
@Checkable.Value(AmountJson)
max_fee: AmountJson;
/**
* Information about the merchant.
*/
@Checkable.Any
merchant: any;
/**
* Public key of the merchant.
*/
@Checkable.String
merchant_pub: string;
/**
* List of accepted exchanges.
*/
@Checkable.List(Checkable.Value(ExchangeHandle))
exchanges: ExchangeHandle[];
/**
* Products that are sold in this contract.
*/
@Checkable.List(Checkable.AnyObject)
products: any[];
/**
* Deadline for refunds.
*/
@Checkable.String
refund_deadline: string;
/**
* Time when the contract was generated by the merchant.
*/
@Checkable.String
timestamp: string;
/**
* Order id to uniquely identify the purchase within
* one merchant instance.
*/
@Checkable.String
order_id: string;
/**
* URL to post the payment to.
*/
@Checkable.String
pay_url: string;
/**
* Fulfillment URL to view the product or
* delivery status.
*/
@Checkable.String
fulfillment_url: string;
/**
* Share of the wire fee that must be settled with one payment.
*/
@Checkable.Optional(Checkable.Number)
wire_fee_amortization?: number;
/**
* Maximum wire fee that the merchant agrees to pay for.
*/
@Checkable.Optional(Checkable.Value(AmountJson))
max_wire_fee?: AmountJson;
/**
* Extra data, interpreted by the mechant only.
*/
@Checkable.Any
extra: any;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => Contract;
}
/**
* Wire fee for one wire method as stored in the
* wallet's database.
*/
export interface WireFee {
/**
* Fee for wire transfers.
*/
wireFee: AmountJson;
/**
* Fees to close and refund a reserve.
*/
closingFee: AmountJson;
/**
* Start date of the fee.
*/
startStamp: number;
/**
* End date of the fee.
*/
endStamp: number;
/**
* Signature made by the exchange master key.
*/
sig: string;
}
/**
* Wire fees for an exchange.
*/
export interface ExchangeWireFeesRecord {
/**
* Base URL of the exchange.
*/
exchangeBaseUrl: string;
feesForType: { [type: string]: WireFee[] };
/**
* Mapping from wire method type to the wire fee.
*/
feesForType: { [wireMethod: string]: WireFee[] };
}
/**
* Coins used for a payment, with signatures authorizing the payment and the
* coins with remaining value updated to accomodate for a payment.
*/
export type PayCoinInfo = Array<{ updatedCoin: CoinRecord, sig: CoinPaySig }>;
/**
* Amount helpers.
*/
export namespace Amounts {
/**
* Number of fractional units that one value unit represents.
*/
export const fractionalBase = 1e8;
/**
* Result of a possibly overflowing operation.
*/
export interface Result {
/**
* Resulting, possibly saturated amount.
*/
amount: AmountJson;
// Was there an over-/underflow?
/**
* Was there an over-/underflow?
*/
saturated: boolean;
}
/**
* Get the largest amount that is safely representable.
*/
export function getMaxAmount(currency: string): AmountJson {
return {
currency,
@ -634,6 +1080,9 @@ export namespace Amounts {
};
}
/**
* Get an amount that represents zero units of a currency.
*/
export function getZero(currency: string): AmountJson {
return {
currency,
@ -642,6 +1091,13 @@ export namespace Amounts {
};
}
/**
* Add two amounts. Return the result and whether
* the addition overflowed. The overflow is always handled
* by saturating and never by wrapping.
*
* Throws when currencies don't match.
*/
export function add(first: AmountJson, ...rest: AmountJson[]): Result {
const currency = first.currency;
let value = first.value + Math.floor(first.fraction / fractionalBase);
@ -663,7 +1119,13 @@ export namespace Amounts {
return { amount: { currency, value, fraction }, saturated: false };
}
/**
* Subtract two amounts. Return the result and whether
* the subtraction overflowed. The overflow is always handled
* by saturating and never by wrapping.
*
* Throws when currencies don't match.
*/
export function sub(a: AmountJson, ...rest: AmountJson[]): Result {
const currency = a.currency;
let value = a.value;
@ -691,6 +1153,10 @@ export namespace Amounts {
return { amount: { currency, value, fraction }, saturated: false };
}
/**
* Compare two amounts. Returns 0 when equal, -1 when a < b
* and +1 when a > b. Throws when currencies don't match.
*/
export function cmp(a: AmountJson, b: AmountJson): number {
if (a.currency !== b.currency) {
throw Error(`Mismatched currency: ${a.currency} and ${b.currency}`);
@ -715,6 +1181,9 @@ export namespace Amounts {
}
}
/**
* Create a copy of an amount.
*/
export function copy(a: AmountJson): AmountJson {
return {
currency: a.currency,
@ -723,6 +1192,9 @@ export namespace Amounts {
};
}
/**
* Divide an amount. Throws on division by zero.
*/
export function divide(a: AmountJson, n: number): AmountJson {
if (n === 0) {
throw Error(`Division by 0`);
@ -738,7 +1210,10 @@ export namespace Amounts {
};
}
export function isNonZero(a: AmountJson) {
/**
* Check if an amount is non-zero.
*/
export function isNonZero(a: AmountJson): boolean {
return a.value > 0 || a.fraction > 0;
}
@ -759,7 +1234,13 @@ export namespace Amounts {
}
/**
* Listener for notifications from the wallet.
*/
export interface Notifier {
/**
* Called when a new notification arrives.
*/
notify(): void;
}

View File

@ -69,7 +69,7 @@ test("coin selection 1", (t) => {
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"),
];
const res = wallet.selectCoins(cds, a("EUR:2.0"), a("EUR:0.1"));
const res = wallet.selectPayCoins(cds, a("EUR:2.0"), a("EUR:0.1"));
if (!res) {
t.fail();
return;
@ -86,7 +86,7 @@ test("coin selection 2", (t) => {
// Merchant covers the fee, this one shouldn't be used
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"),
];
const res = wallet.selectCoins(cds, a("EUR:2.0"), a("EUR:0.5"));
const res = wallet.selectPayCoins(cds, a("EUR:2.0"), a("EUR:0.5"));
if (!res) {
t.fail();
return;
@ -103,7 +103,7 @@ test("coin selection 3", (t) => {
// this coin should be selected instead of previous one with fee
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"),
];
const res = wallet.selectCoins(cds, a("EUR:2.0"), a("EUR:0.5"));
const res = wallet.selectPayCoins(cds, a("EUR:2.0"), a("EUR:0.5"));
if (!res) {
t.fail();
return;
@ -119,7 +119,7 @@ test("coin selection 4", (t) => {
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
];
const res = wallet.selectCoins(cds, a("EUR:2.0"), a("EUR:0.2"));
const res = wallet.selectPayCoins(cds, a("EUR:2.0"), a("EUR:0.2"));
if (!res) {
t.fail();
return;
@ -135,7 +135,7 @@ test("coin selection 5", (t) => {
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
];
const res = wallet.selectCoins(cds, a("EUR:4.0"), a("EUR:0.2"));
const res = wallet.selectPayCoins(cds, a("EUR:4.0"), a("EUR:0.2"));
t.true(!res);
t.pass();
});
@ -146,7 +146,7 @@ test("coin selection 6", (t) => {
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.5"),
];
const res = wallet.selectCoins(cds, a("EUR:2.0"), a("EUR:0.2"));
const res = wallet.selectPayCoins(cds, a("EUR:2.0"), a("EUR:0.2"));
t.true(!res);
t.pass();
});

View File

@ -81,7 +81,14 @@ import URI = require("urijs");
* Named tuple of coin and denomination.
*/
export interface CoinWithDenom {
/**
* A coin. Must have the same denomination public key as the associated
* denomination.
*/
coin: CoinRecord;
/**
* An associated denomination.
*/
denom: DenominationRecord;
}
@ -92,6 +99,9 @@ export interface CoinWithDenom {
*/
@Checkable.Class()
export class Payback {
/**
* The hash of the denomination public key for which the payback is offered.
*/
@Checkable.String
h_denom_pub: string;
}
@ -102,67 +112,123 @@ export class Payback {
*/
@Checkable.Class({extra: true})
export class KeysJson {
/**
* List of offered denominations.
*/
@Checkable.List(Checkable.Value(Denomination))
denoms: Denomination[];
/**
* The exchange's master public key.
*/
@Checkable.String
master_public_key: string;
/**
* The list of auditors (partially) auditing the exchange.
*/
@Checkable.Any
auditors: any[];
/**
* Timestamp when this response was issued.
*/
@Checkable.String
list_issue_date: string;
/**
* List of paybacks for compromised denominations.
*/
@Checkable.List(Checkable.Value(Payback))
payback?: Payback[];
/**
* Short-lived signing keys used to sign online
* responses.
*/
@Checkable.Any
signkeys: any;
@Checkable.String
eddsa_pub: string;
@Checkable.String
eddsa_sig: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => KeysJson;
}
/**
* Wire fees as anounced by the exchange.
*/
@Checkable.Class()
class WireFeesJson {
/**
* Cost of a wire transfer.
*/
@Checkable.Value(AmountJson)
wire_fee: AmountJson;
/**
* Cost of clising a reserve.
*/
@Checkable.Value(AmountJson)
closing_fee: AmountJson;
/**
* Signature made with the exchange's master key.
*/
@Checkable.String
sig: string;
/**
* Date from which the fee applies.
*/
@Checkable.String
start_date: string;
/**
* Data after which the fee doesn't apply anymore.
*/
@Checkable.String
end_date: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => WireFeesJson;
}
/**
* Information about wire transfer methods supported
* by the exchange.
*/
@Checkable.Class({extra: true})
class WireDetailJson {
/**
* Name of the wire transfer method.
*/
@Checkable.String
type: string;
/**
* Fees associated with the wire transfer method.
*/
@Checkable.List(Checkable.Value(WireFeesJson))
fees: WireFeesJson[];
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => WireDetailJson;
}
/**
* Request to mark a reserve as confirmed.
*/
@Checkable.Class()
export class CreateReserveRequest {
/**
@ -177,10 +243,17 @@ export class CreateReserveRequest {
@Checkable.String
exchange: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => CreateReserveRequest;
}
/**
* Request to mark a reserve as confirmed.
*/
@Checkable.Class()
export class ConfirmReserveRequest {
/**
@ -190,21 +263,40 @@ export class ConfirmReserveRequest {
@Checkable.String
reservePub: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => ConfirmReserveRequest;
}
/**
* Offer record, stored in the wallet's database.
*/
@Checkable.Class()
export class OfferRecord {
/**
* The contract that was offered by the merchant.
*/
@Checkable.Value(Contract)
contract: Contract;
/**
* Signature by the merchant over the contract details.
*/
@Checkable.String
merchant_sig: string;
/**
* Hash of the contract terms.
*/
@Checkable.String
H_contract: string;
/**
* Time when the offer was made.
*/
@Checkable.Number
offer_time: number;
@ -214,14 +306,41 @@ export class OfferRecord {
@Checkable.Optional(Checkable.Number)
id?: number;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => OfferRecord;
}
/**
* Activity history record.
*/
export interface HistoryRecord {
/**
* Type of the history event.
*/
type: string;
/**
* Time when the activity was recorded.
*/
timestamp: number;
/**
* Subject of the entry. Used to group multiple history records together.
* Only the latest history record with the same subjectId will be shown.
*/
subjectId?: string;
/**
* Details used when rendering the history record.
*/
detail: any;
/**
* Level of detail of the history entry.
*/
level: HistoryLevel;
}
@ -246,6 +365,11 @@ interface TransactionRecord {
finished: boolean;
}
/**
* Level of detail at which a history
* entry should be shown.
*/
export enum HistoryLevel {
Trace = 1,
Developer = 2,
@ -254,19 +378,34 @@ export enum HistoryLevel {
}
/**
* Badge that shows activity for the wallet.
*/
export interface Badge {
setText(s: string): void;
setColor(c: string): void;
/**
* Start indicating background activity.
*/
startBusy(): void;
/**
* Stop indicating background activity.
*/
stopBusy(): void;
}
/**
* Nonce record as stored in the wallet's database.
*/
export interface NonceRecord {
priv: string;
pub: string;
}
/**
* Configuration key/value entries to configure
* the wallet.
*/
export interface ConfigRecord {
key: string;
value: any;
@ -328,10 +467,17 @@ function isWithdrawableDenom(d: DenominationRecord) {
}
/**
* Result of selecting coins, contains the exchange, and selected
* coins with their denomination.
*/
export type CoinSelectionResult = {exchangeUrl: string, cds: CoinWithDenom[]}|undefined;
export function selectCoins(cds: CoinWithDenom[], paymentAmount: AmountJson,
depositFeeLimit: AmountJson): CoinWithDenom[]|undefined {
/**
* Select coins for a payment under the merchant's constraints.
*/
export function selectPayCoins(cds: CoinWithDenom[], paymentAmount: AmountJson,
depositFeeLimit: AmountJson): CoinWithDenom[]|undefined {
if (cds.length === 0) {
return undefined;
}
@ -406,7 +552,11 @@ function getWithdrawDenomList(amountAvailable: AmountJson,
return ds;
}
/* tslint:disable:completed-docs */
/**
* The stores and indices for the wallet database.
*/
export namespace Stores {
class ExchangeStore extends Store<ExchangeRecord> {
constructor() {
@ -489,6 +639,7 @@ export namespace Stores {
super("exchangeWireFees", {keyPath: "exchangeBaseUrl"});
}
}
export const exchanges = new ExchangeStore();
export const exchangeWireFees = new ExchangeWireFeesStore();
export const nonces = new NonceStore();
@ -504,6 +655,8 @@ export namespace Stores {
export const config = new ConfigStore();
}
/* tslint:enable:completed-docs */
interface CoinsForPaymentArgs {
allowedAuditors: Auditor[];
@ -517,13 +670,15 @@ interface CoinsForPaymentArgs {
}
/**
* The platform-independent wallet implementation.
*/
export class Wallet {
private db: IDBDatabase;
private http: HttpRequestLibrary;
private badge: Badge;
private notifier: Notifier;
public cryptoApi: CryptoApi;
private cryptoApi: CryptoApi;
private processPreCoinConcurrent = 0;
private processPreCoinThrottle: {[url: string]: number} = {};
@ -748,7 +903,7 @@ export class Wallet {
}
}
const res = selectCoins(cds, remainingAmount, depositFeeLimit);
const res = selectPayCoins(cds, remainingAmount, depositFeeLimit);
if (res) {
return {
cds: res,

View File

@ -14,6 +14,14 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Interface to the wallet through WebExtension messaging.
*/
/**
* Imports.
*/
import {
AmountJson,
CoinRecord,
@ -25,12 +33,11 @@ import {
ReserveRecord,
} from "./types";
/**
* Interface to the wallet through WebExtension messaging.
* @author Florian Dold
* Query the wallet for the coins that would be used to withdraw
* from a given reserve.
*/
export function getReserveCreationInfo(baseUrl: string,
amount: AmountJson): Promise<ReserveCreationInfo> {
const m = { type: "reserve-creation-info", detail: { baseUrl, amount } };
@ -48,7 +55,8 @@ export function getReserveCreationInfo(baseUrl: string,
});
}
export async function callBackend(type: string, detail?: any): Promise<any> {
async function callBackend(type: string, detail?: any): Promise<any> {
return new Promise<any>((resolve, reject) => {
chrome.runtime.sendMessage({ type, detail }, (resp) => {
if (resp && resp.error) {
@ -60,55 +68,107 @@ export async function callBackend(type: string, detail?: any): Promise<any> {
});
}
/**
* Get all exchanges the wallet knows about.
*/
export async function getExchanges(): Promise<ExchangeRecord[]> {
return await callBackend("get-exchanges");
}
/**
* Get all currencies the exchange knows about.
*/
export async function getCurrencies(): Promise<CurrencyRecord[]> {
return await callBackend("get-currencies");
}
/**
* Get information about a specific currency.
*/
export async function getCurrency(name: string): Promise<CurrencyRecord|null> {
return await callBackend("currency-info", {name});
}
/**
* Get information about a specific exchange.
*/
export async function getExchangeInfo(baseUrl: string): Promise<ExchangeRecord> {
return await callBackend("exchange-info", {baseUrl});
}
/**
* Replace an existing currency record with the one given. The currency to
* replace is specified inside the currency record.
*/
export async function updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
return await callBackend("update-currency", { currencyRecord });
}
/**
* Get all reserves the wallet has at an exchange.
*/
export async function getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
return await callBackend("get-reserves", { exchangeBaseUrl });
}
/**
* Get all reserves for which a payback is available.
*/
export async function getPaybackReserves(): Promise<ReserveRecord[]> {
return await callBackend("get-payback-reserves");
}
/**
* Withdraw the payback that is available for a reserve.
*/
export async function withdrawPaybackReserve(reservePub: string): Promise<ReserveRecord[]> {
return await callBackend("withdraw-payback-reserve", { reservePub });
}
/**
* Get all coins withdrawn from the given exchange.
*/
export async function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> {
return await callBackend("get-coins", { exchangeBaseUrl });
}
/**
* Get all precoins withdrawn from the given exchange.
*/
export async function getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> {
return await callBackend("get-precoins", { exchangeBaseUrl });
}
/**
* Get all denoms offered by the given exchange.
*/
export async function getDenoms(exchangeBaseUrl: string): Promise<DenominationRecord[]> {
return await callBackend("get-denoms", { exchangeBaseUrl });
}
/**
* Start refreshing a coin.
*/
export async function refresh(coinPub: string): Promise<void> {
return await callBackend("refresh-coin", { coinPub });
}
/**
* Request payback for a coin. Only works for non-refreshed coins.
*/
export async function payback(coinPub: string): Promise<void> {
return await callBackend("payback-coin", { coinPub });
}

View File

@ -340,8 +340,9 @@ async function dispatch(handlers: any, req: any, sender: any, sendResponse: any)
}
}
class ChromeNotifier implements Notifier {
ports: Port[] = [];
private ports: Port[] = [];
constructor() {
chrome.runtime.onConnect.addListener((port) => {
@ -483,6 +484,11 @@ function clearRateLimitCache() {
rateLimitCache = {};
}
/**
* Main function to run for the WebExtension backend.
*
* Sets up all event handlers and other machinery.
*/
export async function wxMain() {
window.onerror = (m, source, lineno, colno, error) => {
logging.record("error", m + error, undefined, source || "(unknown)", lineno || 0, colno || 0);

View File

@ -27,7 +27,35 @@
"array-type": [true, "array-simple"],
"class-name": false,
"no-bitwise": false,
"file-header": [true, "GNU General Public License"]
"file-header": [true, "GNU General Public License"],
"completed-docs": [true, {
"methods": {
"privacies": ["public"],
"locations": "all"
},
"properties": {
"privacies": ["public"],
"locations": ["all"]
},
"functions": {
"visibilities": ["exported"]
},
"interfaces": {
"visibilities": ["exported"]
},
"types": {
"visibilities": ["exported"]
},
"enums": {
"visibilities": ["exported"]
},
"classes": {
"visibilities": ["exported"]
},
"namespaces": {
"visibilities": ["exported"]
}
}]
},
"rulesDirectory": []
}