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

View File

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

View File

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

View File

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

View File

@ -22,10 +22,22 @@ const fork = require("child_process").fork;
const nodeWorkerEntry = path.join(__dirname, "nodeWorkerEntry.js"); const nodeWorkerEntry = path.join(__dirname, "nodeWorkerEntry.js");
/**
* Worker implementation that uses node subprocesses.
*/
export class Worker { 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); onmessage: undefined | ((m: any) => void);
/**
* Function to be called when we receive an error from the worker thread.
*/
onerror: undefined | ((m: any) => void); onerror: undefined | ((m: any) => void);
constructor(scriptFilename: string) { constructor(scriptFilename: string) {
this.child = fork(nodeWorkerEntry); this.child = fork(nodeWorkerEntry);
this.onerror = undefined; this.onerror = undefined;
@ -55,6 +67,9 @@ export class Worker {
this.child.send({scriptFilename, cwd: process.cwd()}); 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 { addEventListener(event: "message" | "error", fn: (x: any) => void): void {
switch (event) { switch (event) {
case "message": case "message":
@ -66,10 +81,16 @@ export class Worker {
} }
} }
/**
* Send a message to the worker thread.
*/
postMessage (msg: any) { postMessage (msg: any) {
this.child.send(JSON.stringify({data: msg})); this.child.send(JSON.stringify({data: msg}));
} }
/**
* Forcibly terminate the worker thread.
*/
terminate () { terminate () {
this.child.kill("SIGINT"); 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 { export function deepEquals(x: any, y: any): boolean {
if (x === y) { if (x === y) {
return true; 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[] { export function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] {
return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []); return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []);
} }

View File

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

View File

@ -53,6 +53,9 @@ export class Store<T> {
* Definition of an index. * Definition of an index.
*/ */
export class Index<S extends IDBValidKey, T> { export class Index<S extends IDBValidKey, T> {
/**
* Name of the store that this index is associated with.
*/
storeName: string; storeName: string;
constructor(s: Store<T>, public indexName: string, public keyPath: string | 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. * Query result that consists of at most one value.
*/ */
export interface QueryValue<T> { export interface QueryValue<T> {
/**
* Apply a function to a query value.
*/
map<S>(f: (x: T) => S): QueryValue<S>; 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>; cond<R>(f: (x: T) => boolean, onTrue: (r: QueryRoot) => R, onFalse: (r: QueryRoot) => R): Promise<void>;
} }
abstract class BaseQueryValue<T> implements QueryValue<T> { abstract class BaseQueryValue<T> implements QueryValue<T> {
root: QueryRoot;
constructor(root: QueryRoot) { constructor(public root: QueryRoot) {
this.root = root;
} }
map<S>(f: (x: T) => S): QueryValue<S> { 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> { class FirstQueryValue<T> extends BaseQueryValue<T> {
gotValue = false; private gotValue = false;
s: QueryStreamBase<T>; private s: QueryStreamBase<T>;
constructor(stream: QueryStreamBase<T>) { constructor(stream: QueryStreamBase<T>) {
super(stream.root); super(stream.root);
this.s = stream; this.s = stream;
@ -183,13 +197,8 @@ class FirstQueryValue<T> extends BaseQueryValue<T> {
} }
class MapQueryValue<T, S> extends BaseQueryValue<S> { class MapQueryValue<T, S> extends BaseQueryValue<S> {
mapFn: (x: T) => S; constructor(private v: BaseQueryValue<T>, private mapFn: (x: T) => S) {
v: BaseQueryValue<T>;
constructor(v: BaseQueryValue<T>, mapFn: (x: T) => S) {
super(v.root); super(v.root);
this.v = v;
this.mapFn = mapFn;
} }
subscribeOne(f: SubscribeOneFn): void { subscribeOne(f: SubscribeOneFn): void {
@ -226,11 +235,7 @@ abstract class QueryStreamBase<T> implements QueryStream<T>, PromiseLike<void> {
abstract subscribe(f: (isDone: boolean, abstract subscribe(f: (isDone: boolean,
value: any, value: any,
tx: IDBTransaction) => void): void; tx: IDBTransaction) => void): void;
constructor(public root: QueryRoot) {
root: QueryRoot;
constructor(root: QueryRoot) {
this.root = root;
} }
first(): QueryValue<T> { first(): QueryValue<T> {
@ -313,13 +318,8 @@ type SubscribeOneFn = (value: any, tx: IDBTransaction) => void;
type FlatMapFn<T> = (v: T) => T[]; type FlatMapFn<T> = (v: T) => T[];
class QueryStreamFilter<T> extends QueryStreamBase<T> { class QueryStreamFilter<T> extends QueryStreamBase<T> {
s: QueryStreamBase<T>; constructor(public s: QueryStreamBase<T>, public filterFn: FilterFn) {
filterFn: FilterFn;
constructor(s: QueryStreamBase<T>, filterFn: FilterFn) {
super(s.root); super(s.root);
this.s = s;
this.filterFn = filterFn;
} }
subscribe(f: SubscribeFn) { subscribe(f: SubscribeFn) {
@ -337,13 +337,8 @@ class QueryStreamFilter<T> extends QueryStreamBase<T> {
class QueryStreamFlatMap<T, S> extends QueryStreamBase<S> { class QueryStreamFlatMap<T, S> extends QueryStreamBase<S> {
s: QueryStreamBase<T>; constructor(public s: QueryStreamBase<T>, public flatMapFn: (v: T) => S[]) {
flatMapFn: (v: T) => S[];
constructor(s: QueryStreamBase<T>, flatMapFn: (v: T) => S[]) {
super(s.root); super(s.root);
this.s = s;
this.flatMapFn = flatMapFn;
} }
subscribe(f: SubscribeFn) { subscribe(f: SubscribeFn) {
@ -362,13 +357,8 @@ class QueryStreamFlatMap<T, S> extends QueryStreamBase<S> {
class QueryStreamMap<S, T> extends QueryStreamBase<T> { class QueryStreamMap<S, T> extends QueryStreamBase<T> {
s: QueryStreamBase<S>; constructor(public s: QueryStreamBase<S>, public mapFn: (v: S) => T) {
mapFn: (v: S) => T;
constructor(s: QueryStreamBase<S>, mapFn: (v: S) => T) {
super(s.root); super(s.root);
this.s = s;
this.mapFn = mapFn;
} }
subscribe(f: SubscribeFn) { subscribe(f: SubscribeFn) {
@ -385,18 +375,9 @@ class QueryStreamMap<S, T> extends QueryStreamBase<T> {
class QueryStreamIndexJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> { class QueryStreamIndexJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
s: QueryStreamBase<T>; constructor(public s: QueryStreamBase<T>, public storeName: string, public indexName: string,
storeName: string; public key: any) {
key: any;
indexName: string;
constructor(s: QueryStreamBase<T>, storeName: string, indexName: string,
key: any) {
super(s.root); super(s.root);
this.s = s;
this.storeName = storeName;
this.key = key;
this.indexName = indexName;
} }
subscribe(f: SubscribeFn) { 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>> { class QueryStreamIndexJoinLeft<T, S> extends QueryStreamBase<JoinLeftResult<T, S>> {
s: QueryStreamBase<T>; constructor(public s: QueryStreamBase<T>, public storeName: string, public indexName: string,
storeName: string; public key: any) {
key: any;
indexName: string;
constructor(s: QueryStreamBase<T>, storeName: string, indexName: string,
key: any) {
super(s.root); super(s.root);
this.s = s;
this.storeName = storeName;
this.key = key;
this.indexName = indexName;
} }
subscribe(f: SubscribeFn) { 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>> { class QueryStreamKeyJoin<T, S> extends QueryStreamBase<JoinResult<T, S>> {
s: QueryStreamBase<T>; constructor(public s: QueryStreamBase<T>, public storeName: string,
storeName: string; public key: any) {
key: any;
constructor(s: QueryStreamBase<T>, storeName: string,
key: any) {
super(s.root); super(s.root);
this.s = s;
this.storeName = storeName;
this.key = key;
} }
subscribe(f: SubscribeFn) { subscribe(f: SubscribeFn) {

View File

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

View File

@ -69,7 +69,7 @@ test("coin selection 1", (t) => {
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"), 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) { if (!res) {
t.fail(); t.fail();
return; return;
@ -86,7 +86,7 @@ test("coin selection 2", (t) => {
// Merchant covers the fee, this one shouldn't be used // Merchant covers the fee, this one shouldn't be used
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"), 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) { if (!res) {
t.fail(); t.fail();
return; return;
@ -103,7 +103,7 @@ test("coin selection 3", (t) => {
// this coin should be selected instead of previous one with fee // this coin should be selected instead of previous one with fee
fakeCwd("EUR:1.0", "EUR:1.0", "EUR:0.0"), 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) { if (!res) {
t.fail(); t.fail();
return; 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"),
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) { if (!res) {
t.fail(); t.fail();
return; 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"),
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.true(!res);
t.pass(); 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"),
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.true(!res);
t.pass(); t.pass();
}); });

View File

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

View File

@ -14,6 +14,14 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
/**
* Interface to the wallet through WebExtension messaging.
*/
/**
* Imports.
*/
import { import {
AmountJson, AmountJson,
CoinRecord, CoinRecord,
@ -25,12 +33,11 @@ import {
ReserveRecord, ReserveRecord,
} from "./types"; } from "./types";
/** /**
* Interface to the wallet through WebExtension messaging. * Query the wallet for the coins that would be used to withdraw
* @author Florian Dold * from a given reserve.
*/ */
export function getReserveCreationInfo(baseUrl: string, export function getReserveCreationInfo(baseUrl: string,
amount: AmountJson): Promise<ReserveCreationInfo> { amount: AmountJson): Promise<ReserveCreationInfo> {
const m = { type: "reserve-creation-info", detail: { baseUrl, amount } }; 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) => { return new Promise<any>((resolve, reject) => {
chrome.runtime.sendMessage({ type, detail }, (resp) => { chrome.runtime.sendMessage({ type, detail }, (resp) => {
if (resp && resp.error) { 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[]> { export async function getExchanges(): Promise<ExchangeRecord[]> {
return await callBackend("get-exchanges"); return await callBackend("get-exchanges");
} }
/**
* Get all currencies the exchange knows about.
*/
export async function getCurrencies(): Promise<CurrencyRecord[]> { export async function getCurrencies(): Promise<CurrencyRecord[]> {
return await callBackend("get-currencies"); return await callBackend("get-currencies");
} }
/**
* Get information about a specific currency.
*/
export async function getCurrency(name: string): Promise<CurrencyRecord|null> { export async function getCurrency(name: string): Promise<CurrencyRecord|null> {
return await callBackend("currency-info", {name}); return await callBackend("currency-info", {name});
} }
/**
* Get information about a specific exchange.
*/
export async function getExchangeInfo(baseUrl: string): Promise<ExchangeRecord> { export async function getExchangeInfo(baseUrl: string): Promise<ExchangeRecord> {
return await callBackend("exchange-info", {baseUrl}); 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> { export async function updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
return await callBackend("update-currency", { currencyRecord }); return await callBackend("update-currency", { currencyRecord });
} }
/**
* Get all reserves the wallet has at an exchange.
*/
export async function getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> { export async function getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> {
return await callBackend("get-reserves", { exchangeBaseUrl }); return await callBackend("get-reserves", { exchangeBaseUrl });
} }
/**
* Get all reserves for which a payback is available.
*/
export async function getPaybackReserves(): Promise<ReserveRecord[]> { export async function getPaybackReserves(): Promise<ReserveRecord[]> {
return await callBackend("get-payback-reserves"); return await callBackend("get-payback-reserves");
} }
/**
* Withdraw the payback that is available for a reserve.
*/
export async function withdrawPaybackReserve(reservePub: string): Promise<ReserveRecord[]> { export async function withdrawPaybackReserve(reservePub: string): Promise<ReserveRecord[]> {
return await callBackend("withdraw-payback-reserve", { reservePub }); return await callBackend("withdraw-payback-reserve", { reservePub });
} }
/**
* Get all coins withdrawn from the given exchange.
*/
export async function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> { export async function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> {
return await callBackend("get-coins", { exchangeBaseUrl }); return await callBackend("get-coins", { exchangeBaseUrl });
} }
/**
* Get all precoins withdrawn from the given exchange.
*/
export async function getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> { export async function getPreCoins(exchangeBaseUrl: string): Promise<PreCoinRecord[]> {
return await callBackend("get-precoins", { exchangeBaseUrl }); return await callBackend("get-precoins", { exchangeBaseUrl });
} }
/**
* Get all denoms offered by the given exchange.
*/
export async function getDenoms(exchangeBaseUrl: string): Promise<DenominationRecord[]> { export async function getDenoms(exchangeBaseUrl: string): Promise<DenominationRecord[]> {
return await callBackend("get-denoms", { exchangeBaseUrl }); return await callBackend("get-denoms", { exchangeBaseUrl });
} }
/**
* Start refreshing a coin.
*/
export async function refresh(coinPub: string): Promise<void> { export async function refresh(coinPub: string): Promise<void> {
return await callBackend("refresh-coin", { coinPub }); 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> { export async function payback(coinPub: string): Promise<void> {
return await callBackend("payback-coin", { coinPub }); 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 { class ChromeNotifier implements Notifier {
ports: Port[] = []; private ports: Port[] = [];
constructor() { constructor() {
chrome.runtime.onConnect.addListener((port) => { chrome.runtime.onConnect.addListener((port) => {
@ -483,6 +484,11 @@ function clearRateLimitCache() {
rateLimitCache = {}; rateLimitCache = {};
} }
/**
* Main function to run for the WebExtension backend.
*
* Sets up all event handlers and other machinery.
*/
export async function wxMain() { export async function wxMain() {
window.onerror = (m, source, lineno, colno, error) => { window.onerror = (m, source, lineno, colno, error) => {
logging.record("error", m + error, undefined, source || "(unknown)", lineno || 0, colno || 0); logging.record("error", m + error, undefined, source || "(unknown)", lineno || 0, colno || 0);

View File

@ -27,7 +27,35 @@
"array-type": [true, "array-simple"], "array-type": [true, "array-simple"],
"class-name": false, "class-name": false,
"no-bitwise": 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": [] "rulesDirectory": []
} }