download, store and check signatures for wire fees
This commit is contained in:
parent
68e44e0e80
commit
82b5754e15
119
src/checkable.ts
119
src/checkable.ts
@ -40,9 +40,17 @@ export namespace Checkable {
|
||||
interface Prop {
|
||||
propertyKey: any;
|
||||
checker: any;
|
||||
type: any;
|
||||
type?: any;
|
||||
elementChecker?: any;
|
||||
elementProp?: any;
|
||||
keyProp?: any;
|
||||
valueProp?: any;
|
||||
optional?: boolean;
|
||||
extraAllowed?: boolean;
|
||||
}
|
||||
|
||||
interface CheckableInfo {
|
||||
props: Prop[];
|
||||
}
|
||||
|
||||
export let SchemaError = (function SchemaError(message: string) {
|
||||
@ -54,7 +62,24 @@ export namespace Checkable {
|
||||
|
||||
SchemaError.prototype = new Error;
|
||||
|
||||
let chkSym = Symbol("checkable");
|
||||
/**
|
||||
* Classes that are checkable are annotated with this
|
||||
* checkable info symbol, which contains the information necessary
|
||||
* to check if they're valid.
|
||||
*/
|
||||
let checkableInfoSym = Symbol("checkableInfo");
|
||||
|
||||
/**
|
||||
* Get the current property list for a checkable type.
|
||||
*/
|
||||
function getCheckableInfo(target: any): CheckableInfo {
|
||||
let chk = target[checkableInfoSym] as CheckableInfo|undefined;
|
||||
if (!chk) {
|
||||
chk = { props: [] };
|
||||
target[checkableInfoSym] = chk;
|
||||
}
|
||||
return chk;
|
||||
}
|
||||
|
||||
|
||||
function checkNumber(target: any, prop: Prop, path: Path): any {
|
||||
@ -104,6 +129,17 @@ export namespace Checkable {
|
||||
return target;
|
||||
}
|
||||
|
||||
function checkMap(target: any, prop: Prop, path: Path): any {
|
||||
if (typeof target !== "object") {
|
||||
throw new SchemaError(`expected object for ${path}, got ${typeof target} instead`);
|
||||
}
|
||||
for (let key in target) {
|
||||
prop.keyProp.checker(key, prop.keyProp, path.concat([key]));
|
||||
let value = target[key];
|
||||
prop.valueProp.checker(value, prop.valueProp, path.concat([key]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function checkOptional(target: any, prop: Prop, path: Path): any {
|
||||
console.assert(prop.propertyKey);
|
||||
@ -124,7 +160,7 @@ export namespace Checkable {
|
||||
throw new SchemaError(
|
||||
`expected object for ${path.join(".")}, got ${typeof v} instead`);
|
||||
}
|
||||
let props = type.prototype[chkSym].props;
|
||||
let props = type.prototype[checkableInfoSym].props;
|
||||
let remainingPropNames = new Set(Object.getOwnPropertyNames(v));
|
||||
let obj = new type();
|
||||
for (let prop of props) {
|
||||
@ -132,7 +168,7 @@ export namespace Checkable {
|
||||
if (prop.optional) {
|
||||
continue;
|
||||
}
|
||||
throw new SchemaError("Property missing: " + prop.propertyKey);
|
||||
throw new SchemaError(`Property ${prop.propertyKey} missing on ${path}`);
|
||||
}
|
||||
if (!remainingPropNames.delete(prop.propertyKey)) {
|
||||
throw new SchemaError("assertion failed");
|
||||
@ -143,7 +179,7 @@ export namespace Checkable {
|
||||
path.concat([prop.propertyKey]));
|
||||
}
|
||||
|
||||
if (remainingPropNames.size != 0) {
|
||||
if (!prop.extraAllowed && remainingPropNames.size != 0) {
|
||||
throw new SchemaError("superfluous properties " + JSON.stringify(Array.from(
|
||||
remainingPropNames.values())));
|
||||
}
|
||||
@ -162,6 +198,18 @@ export namespace Checkable {
|
||||
return target;
|
||||
}
|
||||
|
||||
export function ClassWithExtra(target: any) {
|
||||
target.checked = (v: any) => {
|
||||
return checkValue(v, {
|
||||
propertyKey: "(root)",
|
||||
type: target,
|
||||
extraAllowed: true,
|
||||
checker: checkValue
|
||||
}, ["(root)"]);
|
||||
};
|
||||
return target;
|
||||
}
|
||||
|
||||
|
||||
export function ClassWithValidator(target: any) {
|
||||
target.checked = (v: any) => {
|
||||
@ -187,7 +235,7 @@ export namespace Checkable {
|
||||
throw Error("Type does not exist yet (wrong order of definitions?)");
|
||||
}
|
||||
function deco(target: Object, propertyKey: string | symbol): void {
|
||||
let chk = mkChk(target);
|
||||
let chk = getCheckableInfo(target);
|
||||
chk.props.push({
|
||||
propertyKey: propertyKey,
|
||||
checker: checkValue,
|
||||
@ -202,13 +250,13 @@ export namespace Checkable {
|
||||
export function List(type: any) {
|
||||
let stub = {};
|
||||
type(stub, "(list-element)");
|
||||
let elementProp = mkChk(stub).props[0];
|
||||
let elementProp = getCheckableInfo(stub).props[0];
|
||||
let elementChecker = elementProp.checker;
|
||||
if (!elementChecker) {
|
||||
throw Error("assertion failed");
|
||||
}
|
||||
function deco(target: Object, propertyKey: string | symbol): void {
|
||||
let chk = mkChk(target);
|
||||
let chk = getCheckableInfo(target);
|
||||
chk.props.push({
|
||||
elementChecker,
|
||||
elementProp,
|
||||
@ -221,16 +269,43 @@ export namespace Checkable {
|
||||
}
|
||||
|
||||
|
||||
export function Map(keyType: any, valueType: any) {
|
||||
let keyStub = {};
|
||||
keyType(keyStub, "(map-key)");
|
||||
let keyProp = getCheckableInfo(keyStub).props[0];
|
||||
if (!keyProp) {
|
||||
throw Error("assertion failed");
|
||||
}
|
||||
let valueStub = {};
|
||||
valueType(valueStub, "(map-value)");
|
||||
let valueProp = getCheckableInfo(valueStub).props[0];
|
||||
if (!valueProp) {
|
||||
throw Error("assertion failed");
|
||||
}
|
||||
function deco(target: Object, propertyKey: string | symbol): void {
|
||||
let chk = getCheckableInfo(target);
|
||||
chk.props.push({
|
||||
keyProp,
|
||||
valueProp,
|
||||
propertyKey: propertyKey,
|
||||
checker: checkMap,
|
||||
});
|
||||
}
|
||||
|
||||
return deco;
|
||||
}
|
||||
|
||||
|
||||
export function Optional(type: any) {
|
||||
let stub = {};
|
||||
type(stub, "(optional-element)");
|
||||
let elementProp = mkChk(stub).props[0];
|
||||
let elementProp = getCheckableInfo(stub).props[0];
|
||||
let elementChecker = elementProp.checker;
|
||||
if (!elementChecker) {
|
||||
throw Error("assertion failed");
|
||||
}
|
||||
function deco(target: Object, propertyKey: string | symbol): void {
|
||||
let chk = mkChk(target);
|
||||
let chk = getCheckableInfo(target);
|
||||
chk.props.push({
|
||||
elementChecker,
|
||||
elementProp,
|
||||
@ -245,14 +320,13 @@ export namespace Checkable {
|
||||
|
||||
|
||||
export function Number(target: Object, propertyKey: string | symbol): void {
|
||||
let chk = mkChk(target);
|
||||
let chk = getCheckableInfo(target);
|
||||
chk.props.push({ propertyKey: propertyKey, checker: checkNumber });
|
||||
}
|
||||
|
||||
|
||||
export function AnyObject(target: Object,
|
||||
propertyKey: string | symbol): void {
|
||||
let chk = mkChk(target);
|
||||
export function AnyObject(target: Object, propertyKey: string | symbol): void {
|
||||
let chk = getCheckableInfo(target);
|
||||
chk.props.push({
|
||||
propertyKey: propertyKey,
|
||||
checker: checkAnyObject
|
||||
@ -260,9 +334,8 @@ export namespace Checkable {
|
||||
}
|
||||
|
||||
|
||||
export function Any(target: Object,
|
||||
propertyKey: string | symbol): void {
|
||||
let chk = mkChk(target);
|
||||
export function Any(target: Object, propertyKey: string | symbol): void {
|
||||
let chk = getCheckableInfo(target);
|
||||
chk.props.push({
|
||||
propertyKey: propertyKey,
|
||||
checker: checkAny,
|
||||
@ -272,22 +345,14 @@ export namespace Checkable {
|
||||
|
||||
|
||||
export function String(target: Object, propertyKey: string | symbol): void {
|
||||
let chk = mkChk(target);
|
||||
let chk = getCheckableInfo(target);
|
||||
chk.props.push({ propertyKey: propertyKey, checker: checkString });
|
||||
}
|
||||
|
||||
export function Boolean(target: Object, propertyKey: string | symbol): void {
|
||||
let chk = mkChk(target);
|
||||
let chk = getCheckableInfo(target);
|
||||
chk.props.push({ propertyKey: propertyKey, checker: checkBoolean });
|
||||
}
|
||||
|
||||
|
||||
function mkChk(target: any) {
|
||||
let chk = target[chkSym];
|
||||
if (!chk) {
|
||||
chk = { props: [] };
|
||||
target[chkSym] = chk;
|
||||
}
|
||||
return chk;
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ import {
|
||||
import {OfferRecord} from "./wallet";
|
||||
import {CoinWithDenom} from "./wallet";
|
||||
import {PayCoinInfo} from "./types";
|
||||
import {RefreshSessionRecord} from "./types";
|
||||
import {RefreshSessionRecord, WireFee} from "./types";
|
||||
|
||||
|
||||
interface WorkerState {
|
||||
@ -235,6 +235,10 @@ export class CryptoApi {
|
||||
return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub);
|
||||
}
|
||||
|
||||
isValidWireFee(type: string, wf: WireFee, masterPub: string): Promise<boolean> {
|
||||
return this.doRpc<boolean>("isValidWireFee", 2, type, wf, masterPub);
|
||||
}
|
||||
|
||||
isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string) {
|
||||
return this.doRpc<PayCoinInfo>("isValidPaymentSignature", 1, sig, contractHash, merchantPub);
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ import create = chrome.alarms.create;
|
||||
import {OfferRecord} from "./wallet";
|
||||
import {CoinWithDenom} from "./wallet";
|
||||
import {CoinPaySig, CoinRecord} from "./types";
|
||||
import {DenominationRecord, Amounts} from "./types";
|
||||
import {DenominationRecord, Amounts, WireFee} from "./types";
|
||||
import {Amount} from "./emscriptif";
|
||||
import {HashContext} from "./emscriptif";
|
||||
import {RefreshMeltCoinAffirmationPS} from "./emscriptif";
|
||||
@ -110,6 +110,25 @@ namespace RpcFunctions {
|
||||
nativePub);
|
||||
}
|
||||
|
||||
export function isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean {
|
||||
let p = new native.MasterWireFeePS({
|
||||
h_wire_method: native.ByteArray.fromStringWithNull(type).hash(),
|
||||
start_date: native.AbsoluteTimeNbo.fromStamp(wf.startStamp),
|
||||
end_date: native.AbsoluteTimeNbo.fromStamp(wf.endStamp),
|
||||
wire_fee: (new native.Amount(wf.wireFee)).toNbo(),
|
||||
closing_fee: (new native.Amount(wf.closingFee)).toNbo(),
|
||||
});
|
||||
|
||||
let nativeSig = new native.EddsaSignature();
|
||||
nativeSig.loadCrock(wf.sig);
|
||||
let nativePub = native.EddsaPublicKey.fromCrock(masterPub);
|
||||
|
||||
return native.eddsaVerify(native.SignaturePurpose.MASTER_WIRE_FEES,
|
||||
p.toPurpose(),
|
||||
nativeSig,
|
||||
nativePub);
|
||||
}
|
||||
|
||||
|
||||
export function isValidDenom(denom: DenominationRecord,
|
||||
masterPub: string): boolean {
|
||||
|
@ -207,6 +207,7 @@ export enum SignaturePurpose {
|
||||
WALLET_COIN_MELT = 1202,
|
||||
TEST = 4242,
|
||||
MERCHANT_PAYMENT_OK = 1104,
|
||||
MASTER_WIRE_FEES = 1028,
|
||||
}
|
||||
|
||||
|
||||
@ -993,6 +994,35 @@ export class RefreshMeltCoinAffirmationPS extends SignatureStruct {
|
||||
}
|
||||
|
||||
|
||||
interface MasterWireFeePS_Args {
|
||||
h_wire_method: HashCode;
|
||||
start_date: AbsoluteTimeNbo;
|
||||
end_date: AbsoluteTimeNbo;
|
||||
wire_fee: AmountNbo;
|
||||
closing_fee: AmountNbo;
|
||||
}
|
||||
|
||||
export class MasterWireFeePS extends SignatureStruct {
|
||||
constructor(w: MasterWireFeePS_Args) {
|
||||
super(w);
|
||||
}
|
||||
|
||||
purpose() {
|
||||
return SignaturePurpose.MASTER_WIRE_FEES;
|
||||
}
|
||||
|
||||
fieldTypes() {
|
||||
return [
|
||||
["h_wire_method", HashCode],
|
||||
["start_date", AbsoluteTimeNbo],
|
||||
["end_date", AbsoluteTimeNbo],
|
||||
["wire_fee", AmountNbo],
|
||||
["closing_fee", AmountNbo],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class AbsoluteTimeNbo extends PackedArenaObject {
|
||||
static fromTalerString(s: string): AbsoluteTimeNbo {
|
||||
let x = new AbsoluteTimeNbo();
|
||||
@ -1008,6 +1038,15 @@ export class AbsoluteTimeNbo extends PackedArenaObject {
|
||||
return x;
|
||||
}
|
||||
|
||||
static fromStamp(stamp: number): AbsoluteTimeNbo {
|
||||
let x = new AbsoluteTimeNbo();
|
||||
x.alloc();
|
||||
// XXX: This only works up to 54 bit numbers.
|
||||
set64(x.nativePtr, stamp);
|
||||
return x;
|
||||
}
|
||||
|
||||
|
||||
size() {
|
||||
return 8;
|
||||
}
|
||||
|
17
src/types.ts
17
src/types.ts
@ -474,6 +474,9 @@ export class Contract {
|
||||
@Checkable.String
|
||||
H_wire: string;
|
||||
|
||||
@Checkable.String
|
||||
wire_method: string;
|
||||
|
||||
@Checkable.Optional(Checkable.String)
|
||||
summary?: string;
|
||||
|
||||
@ -535,6 +538,20 @@ export class Contract {
|
||||
}
|
||||
|
||||
|
||||
export interface WireFee {
|
||||
wireFee: AmountJson;
|
||||
closingFee: AmountJson;
|
||||
startStamp: number;
|
||||
endStamp: number;
|
||||
sig: string;
|
||||
}
|
||||
|
||||
export interface ExchangeWireFeesRecord {
|
||||
exchangeBaseUrl: string;
|
||||
feesForType: { [type: string]: WireFee[] };
|
||||
}
|
||||
|
||||
|
||||
export type PayCoinInfo = Array<{ updatedCoin: CoinRecord, sig: CoinPaySig }>;
|
||||
|
||||
|
||||
|
122
src/wallet.ts
122
src/wallet.ts
@ -42,6 +42,8 @@ import {
|
||||
AuditorRecord,
|
||||
WalletBalance,
|
||||
WalletBalanceEntry,
|
||||
WireFee,
|
||||
ExchangeWireFeesRecord,
|
||||
WireInfo, DenominationRecord, DenominationStatus, denominationRecordFromKeys,
|
||||
CoinStatus,
|
||||
} from "./types";
|
||||
@ -113,6 +115,41 @@ export class KeysJson {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Checkable.Class
|
||||
class WireFeesJson {
|
||||
@Checkable.Value(AmountJson)
|
||||
wire_fee: AmountJson;
|
||||
|
||||
@Checkable.Value(AmountJson)
|
||||
closing_fee: AmountJson;
|
||||
|
||||
@Checkable.String
|
||||
sig: string;
|
||||
|
||||
@Checkable.String
|
||||
start_date: string;
|
||||
|
||||
@Checkable.String
|
||||
end_date: string;
|
||||
|
||||
static checked: (obj: any) => WireFeesJson;
|
||||
}
|
||||
|
||||
|
||||
@Checkable.ClassWithExtra
|
||||
class WireDetailJson {
|
||||
@Checkable.String
|
||||
type: string;
|
||||
|
||||
@Checkable.List(Checkable.Value(WireFeesJson))
|
||||
fees: WireFeesJson[];
|
||||
|
||||
static checked: (obj: any) => WireDetailJson;
|
||||
}
|
||||
|
||||
|
||||
@Checkable.Class
|
||||
export class CreateReserveRequest {
|
||||
/**
|
||||
@ -223,6 +260,7 @@ export interface ConfigRecord {
|
||||
}
|
||||
|
||||
|
||||
|
||||
const builtinCurrencies: CurrencyRecord[] = [
|
||||
{
|
||||
name: "KUDOS",
|
||||
@ -417,7 +455,13 @@ export namespace Stores {
|
||||
}
|
||||
}
|
||||
|
||||
class ExchangeWireFeesStore extends Store<ExchangeWireFeesRecord> {
|
||||
constructor() {
|
||||
super("exchangeWireFees", {keyPath: "exchangeBaseUrl"});
|
||||
}
|
||||
}
|
||||
export const exchanges: ExchangeStore = new ExchangeStore();
|
||||
export const exchangeWireFees: ExchangeWireFeesStore = new ExchangeWireFeesStore();
|
||||
export const nonces: NonceStore = new NonceStore();
|
||||
export const transactions: TransactionsStore = new TransactionsStore();
|
||||
export const reserves: Store<ReserveRecord> = new Store<ReserveRecord>("reserves", {keyPath: "reserve_pub"});
|
||||
@ -1254,13 +1298,27 @@ export class Wallet {
|
||||
*/
|
||||
async updateExchangeFromUrl(baseUrl: string): Promise<ExchangeRecord> {
|
||||
baseUrl = canonicalizeBaseUrl(baseUrl);
|
||||
let reqUrl = new URI("keys").absoluteTo(baseUrl);
|
||||
let resp = await this.http.get(reqUrl.href());
|
||||
if (resp.status != 200) {
|
||||
let keysUrl = new URI("keys").absoluteTo(baseUrl);
|
||||
let wireUrl = new URI("wire").absoluteTo(baseUrl);
|
||||
let keysResp = await this.http.get(keysUrl.href());
|
||||
if (keysResp.status != 200) {
|
||||
throw Error("/keys request failed");
|
||||
}
|
||||
let exchangeKeysJson = KeysJson.checked(JSON.parse(resp.responseText));
|
||||
return this.updateExchangeFromJson(baseUrl, exchangeKeysJson);
|
||||
let wireResp = await this.http.get(wireUrl.href());
|
||||
if (wireResp.status != 200) {
|
||||
throw Error("/wire request failed");
|
||||
}
|
||||
let exchangeKeysJson = KeysJson.checked(JSON.parse(keysResp.responseText));
|
||||
let wireRespJson = JSON.parse(wireResp.responseText);
|
||||
if (typeof wireRespJson !== "object") {
|
||||
throw Error("/wire response is not an object");
|
||||
}
|
||||
console.log("exchange wire", wireRespJson);
|
||||
let wireMethodDetails: WireDetailJson[] = [];
|
||||
for (let methodName in wireRespJson) {
|
||||
wireMethodDetails.push(WireDetailJson.checked(wireRespJson[methodName]));
|
||||
}
|
||||
return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, wireMethodDetails);
|
||||
}
|
||||
|
||||
|
||||
@ -1289,7 +1347,10 @@ export class Wallet {
|
||||
|
||||
|
||||
private async updateExchangeFromJson(baseUrl: string,
|
||||
exchangeKeysJson: KeysJson): Promise<ExchangeRecord> {
|
||||
exchangeKeysJson: KeysJson,
|
||||
wireMethodDetails: WireDetailJson[]): Promise<ExchangeRecord> {
|
||||
|
||||
// FIXME: all this should probably be commited atomically
|
||||
const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date);
|
||||
if (updateTimeSec === null) {
|
||||
throw Error("invalid update time");
|
||||
@ -1325,6 +1386,55 @@ export class Wallet {
|
||||
.put(Stores.exchanges, updatedExchangeInfo)
|
||||
.finish();
|
||||
|
||||
let oldWireFees = await this.q().get(Stores.exchangeWireFees, baseUrl);
|
||||
if (!oldWireFees) {
|
||||
oldWireFees = {
|
||||
exchangeBaseUrl: baseUrl,
|
||||
feesForType: {},
|
||||
};
|
||||
}
|
||||
|
||||
for (let detail of wireMethodDetails) {
|
||||
let latestFeeStamp = 0;
|
||||
let fees = oldWireFees.feesForType[detail.type] || [];
|
||||
oldWireFees.feesForType[detail.type] = fees;
|
||||
for (let oldFee of fees) {
|
||||
if (oldFee.endStamp > latestFeeStamp) {
|
||||
latestFeeStamp = oldFee.endStamp;
|
||||
}
|
||||
}
|
||||
for (let fee of detail.fees) {
|
||||
let start = getTalerStampSec(fee.start_date);
|
||||
if (start == null) {
|
||||
console.error("invalid start stamp in fee", fee);
|
||||
continue;
|
||||
}
|
||||
if (start < latestFeeStamp) {
|
||||
continue;
|
||||
}
|
||||
let end = getTalerStampSec(fee.end_date);
|
||||
if (end == null) {
|
||||
console.error("invalid end stamp in fee", fee);
|
||||
continue;
|
||||
}
|
||||
let wf: WireFee = {
|
||||
wireFee: fee.wire_fee,
|
||||
closingFee: fee.closing_fee,
|
||||
sig: fee.sig,
|
||||
startStamp: start,
|
||||
endStamp: end,
|
||||
}
|
||||
let valid: boolean = await this.cryptoApi.isValidWireFee(detail.type, wf, exchangeInfo.masterPublicKey);
|
||||
if (!valid) {
|
||||
console.error("fee signature invalid", fee);
|
||||
continue;
|
||||
}
|
||||
fees.push(wf);
|
||||
}
|
||||
}
|
||||
|
||||
await this.q().put(Stores.exchangeWireFees, oldWireFees);
|
||||
|
||||
return updatedExchangeInfo;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user