check contract hash, fix unicode bug
This commit is contained in:
parent
29909a27f5
commit
274204c21e
@ -76,8 +76,8 @@ namespace TalerNotify {
|
|||||||
console.log("it's execute");
|
console.log("it's execute");
|
||||||
document.documentElement.style.visibility = "hidden";
|
document.documentElement.style.visibility = "hidden";
|
||||||
taler.internalExecutePayment(resp.contractHash,
|
taler.internalExecutePayment(resp.contractHash,
|
||||||
resp.payUrl,
|
resp.payUrl,
|
||||||
resp.offerUrl);
|
resp.offerUrl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -163,38 +163,62 @@ namespace TalerNotify {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const walletMsg = {
|
if (!offer.H_contract) {
|
||||||
type: "check-repurchase",
|
console.error("H_contract field missing");
|
||||||
detail: {
|
return;
|
||||||
contract: offer.contract
|
}
|
||||||
},
|
|
||||||
|
let walletHashContractMsg = {
|
||||||
|
type: "hash-contract",
|
||||||
|
detail: {contract: offer.contract}
|
||||||
};
|
};
|
||||||
|
|
||||||
chrome.runtime.sendMessage(walletMsg, (resp: any) => {
|
chrome.runtime.sendMessage(walletHashContractMsg, (resp: any) => {
|
||||||
if (resp.error) {
|
|
||||||
console.error("wallet backend error", resp);
|
if (!resp.hash) {
|
||||||
|
console.log("error", resp);
|
||||||
|
throw Error("hashing failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.hash != offer.H_contract) {
|
||||||
|
console.error("merchant-supplied contract hash is wrong");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (resp.isRepurchase) {
|
|
||||||
console.log("doing repurchase");
|
|
||||||
console.assert(resp.existingFulfillmentUrl);
|
|
||||||
console.assert(resp.existingContractHash);
|
|
||||||
window.location.href = subst(resp.existingFulfillmentUrl,
|
|
||||||
resp.existingContractHash);
|
|
||||||
|
|
||||||
} else {
|
const walletMsg = {
|
||||||
const uri = URI(chrome.extension.getURL("pages/confirm-contract.html"));
|
type: "check-repurchase",
|
||||||
const params = {
|
detail: {
|
||||||
offer: JSON.stringify(offer),
|
contract: offer.contract
|
||||||
merchantPageUrl: document.location.href,
|
},
|
||||||
};
|
};
|
||||||
const target = uri.query(params).href();
|
|
||||||
if (msg.replace_navigation === true) {
|
chrome.runtime.sendMessage(walletMsg, (resp: any) => {
|
||||||
document.location.replace(target);
|
if (resp.error) {
|
||||||
} else {
|
console.error("wallet backend error", resp);
|
||||||
document.location.href = target;
|
return;
|
||||||
}
|
}
|
||||||
}
|
if (resp.isRepurchase) {
|
||||||
|
console.log("doing repurchase");
|
||||||
|
console.assert(resp.existingFulfillmentUrl);
|
||||||
|
console.assert(resp.existingContractHash);
|
||||||
|
window.location.href = subst(resp.existingFulfillmentUrl,
|
||||||
|
resp.existingContractHash);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const uri = URI(chrome.extension.getURL(
|
||||||
|
"pages/confirm-contract.html"));
|
||||||
|
const params = {
|
||||||
|
offer: JSON.stringify(offer),
|
||||||
|
merchantPageUrl: document.location.href,
|
||||||
|
};
|
||||||
|
const target = uri.query(params).href();
|
||||||
|
if (msg.replace_navigation === true) {
|
||||||
|
document.location.replace(target);
|
||||||
|
} else {
|
||||||
|
document.location.href = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
2
lib/emscripten/emsc.d.ts
vendored
2
lib/emscripten/emsc.d.ts
vendored
@ -33,6 +33,8 @@ export interface EmscFunGen {
|
|||||||
export declare namespace Module {
|
export declare namespace Module {
|
||||||
var cwrap: EmscFunGen;
|
var cwrap: EmscFunGen;
|
||||||
|
|
||||||
|
function stringToUTF8(s: string, addr: number, maxLength: number): void
|
||||||
|
|
||||||
function _free(ptr: number): void;
|
function _free(ptr: number): void;
|
||||||
|
|
||||||
function _malloc(n: number): number;
|
function _malloc(n: number): number;
|
||||||
|
@ -176,6 +176,10 @@ export class CryptoApi {
|
|||||||
return this.doRpc("createPreCoin", 1, denom, reserve);
|
return this.doRpc("createPreCoin", 1, denom, reserve);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hashString(str: string): Promise<string> {
|
||||||
|
return this.doRpc("hashString", 1, str);
|
||||||
|
}
|
||||||
|
|
||||||
hashRsaPub(rsaPub: string): Promise<string> {
|
hashRsaPub(rsaPub: string): Promise<string> {
|
||||||
return this.doRpc("hashRsaPub", 2, rsaPub);
|
return this.doRpc("hashRsaPub", 2, rsaPub);
|
||||||
}
|
}
|
||||||
|
@ -139,6 +139,11 @@ namespace RpcFunctions {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hashString(str: string): string {
|
||||||
|
const b = native.ByteArray.fromString(str);
|
||||||
|
return b.hash().toCrock();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function hashRsaPub(rsaPub: string): string {
|
export function hashRsaPub(rsaPub: string): string {
|
||||||
return native.RsaPublicKey.fromCrock(rsaPub)
|
return native.RsaPublicKey.fromCrock(rsaPub)
|
||||||
|
@ -36,8 +36,9 @@ const GNUNET_SYSERR = -1;
|
|||||||
|
|
||||||
let Module = EmscWrapper.Module;
|
let Module = EmscWrapper.Module;
|
||||||
|
|
||||||
let getEmsc: EmscWrapper.EmscFunGen = (...args: any[]) => Module.cwrap.apply(null,
|
let getEmsc: EmscWrapper.EmscFunGen = (...args: any[]) => Module.cwrap.apply(
|
||||||
args);
|
null,
|
||||||
|
args);
|
||||||
|
|
||||||
var emsc = {
|
var emsc = {
|
||||||
free: (ptr: number) => Module._free(ptr),
|
free: (ptr: number) => Module._free(ptr),
|
||||||
@ -395,6 +396,30 @@ export class Amount extends ArenaObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the UTF-8 characters in a JavaScript string.
|
||||||
|
*/
|
||||||
|
function countBytes(str: string): number {
|
||||||
|
var s = str.length;
|
||||||
|
// JavaScript strings are UTF-16 arrays
|
||||||
|
for (let i = str.length - 1; i >= 0; i--) {
|
||||||
|
var code = str.charCodeAt(i);
|
||||||
|
if (code > 0x7f && code <= 0x7ff) {
|
||||||
|
// We need an extra byte in utf-8 here
|
||||||
|
s++;
|
||||||
|
} else if (code > 0x7ff && code <= 0xffff) {
|
||||||
|
// We need two extra bytes in utf-8 here
|
||||||
|
s += 2;
|
||||||
|
}
|
||||||
|
// Skip over the other surrogate
|
||||||
|
if (code >= 0xDC00 && code <= 0xDFFF) {
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Managed reference to a contiguous block of memory in the Emscripten heap.
|
* Managed reference to a contiguous block of memory in the Emscripten heap.
|
||||||
* Should contain only data, not pointers.
|
* Should contain only data, not pointers.
|
||||||
@ -632,17 +657,20 @@ export class ByteArray extends PackedArenaObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromString(s: string, a?: Arena): ByteArray {
|
static fromString(s: string, a?: Arena): ByteArray {
|
||||||
let hstr = emscAlloc.malloc(s.length + 1);
|
// UTF-8 bytes, including 0-terminator
|
||||||
Module.writeStringToMemory(s, hstr);
|
let terminatedByteLength = countBytes(s) + 1;
|
||||||
return new ByteArray(s.length, hstr, a);
|
let hstr = emscAlloc.malloc(terminatedByteLength);
|
||||||
|
Module.stringToUTF8(s, hstr, terminatedByteLength);
|
||||||
|
return new ByteArray(terminatedByteLength, hstr, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromCrock(s: string, a?: Arena): ByteArray {
|
static fromCrock(s: string, a?: Arena): ByteArray {
|
||||||
let hstr = emscAlloc.malloc(s.length + 1);
|
let byteLength = countBytes(s) + 1;
|
||||||
Module.writeStringToMemory(s, hstr);
|
let hstr = emscAlloc.malloc(byteLength);
|
||||||
let decodedLen = Math.floor((s.length * 5) / 8);
|
Module.stringToUTF8(s, hstr, byteLength);
|
||||||
|
let decodedLen = Math.floor((byteLength * 5) / 8);
|
||||||
let ba = new ByteArray(decodedLen, undefined, a);
|
let ba = new ByteArray(decodedLen, undefined, a);
|
||||||
let res = emsc.string_to_data(hstr, s.length, ba.nativePtr, decodedLen);
|
let res = emsc.string_to_data(hstr, byteLength, ba.nativePtr, decodedLen);
|
||||||
emsc.free(hstr);
|
emsc.free(hstr);
|
||||||
if (res != GNUNET_OK) {
|
if (res != GNUNET_OK) {
|
||||||
throw Error("decoding failed");
|
throw Error("decoding failed");
|
||||||
|
@ -45,12 +45,22 @@ import {ExchangeHandle} from "./types";
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
export interface CoinWithDenom {
|
export interface CoinWithDenom {
|
||||||
coin: Coin;
|
coin: Coin;
|
||||||
denom: Denomination;
|
denom: Denomination;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ReserveRecord {
|
||||||
|
reserve_pub: string;
|
||||||
|
reserve_priv: string,
|
||||||
|
exchange_base_url: string,
|
||||||
|
created: number,
|
||||||
|
last_query: number|null,
|
||||||
|
current_amount: null,
|
||||||
|
requested_amount: AmountJson,
|
||||||
|
confirmed: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Checkable.Class
|
@Checkable.Class
|
||||||
export class KeysJson {
|
export class KeysJson {
|
||||||
@ -124,6 +134,13 @@ export class Offer {
|
|||||||
static checked: (obj: any) => Offer;
|
static checked: (obj: any) => Offer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HistoryRecord {
|
||||||
|
type: string;
|
||||||
|
timestamp: number;
|
||||||
|
subjectId?: string;
|
||||||
|
detail: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
interface ExchangeCoins {
|
interface ExchangeCoins {
|
||||||
[exchangeUrl: string]: CoinWithDenom[];
|
[exchangeUrl: string]: CoinWithDenom[];
|
||||||
@ -145,6 +162,32 @@ export interface Badge {
|
|||||||
stopBusy(): void;
|
stopBusy(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canonicalJson(obj: any): string {
|
||||||
|
// Check for cycles, etc.
|
||||||
|
JSON.stringify(obj);
|
||||||
|
if (typeof obj == "string" || typeof obj == "number" || obj === null) {
|
||||||
|
return JSON.stringify(obj)
|
||||||
|
}
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
let objs: string[] = obj.map((e) => canonicalJson(e));
|
||||||
|
return `[${objs.join(',')}]`;
|
||||||
|
}
|
||||||
|
let keys: string[] = [];
|
||||||
|
for (let key in obj) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
keys.sort();
|
||||||
|
let s = "{";
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
let key = keys[i];
|
||||||
|
s += JSON.stringify(key) + ":" + canonicalJson(obj[key]);
|
||||||
|
if (i != keys.length - 1) {
|
||||||
|
s += ",";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function deepEquals(x: any, y: any): boolean {
|
function deepEquals(x: any, y: any): boolean {
|
||||||
if (x === y) {
|
if (x === y) {
|
||||||
@ -467,6 +510,7 @@ export class Wallet {
|
|||||||
let historyEntry = {
|
let historyEntry = {
|
||||||
type: "pay",
|
type: "pay",
|
||||||
timestamp: (new Date).getTime(),
|
timestamp: (new Date).getTime(),
|
||||||
|
subjectId: `contract-${offer.H_contract}`,
|
||||||
detail: {
|
detail: {
|
||||||
merchantName: offer.contract.merchant.name,
|
merchantName: offer.contract.merchant.name,
|
||||||
amount: offer.contract.amount,
|
amount: offer.contract.amount,
|
||||||
@ -485,6 +529,11 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async putHistory(historyEntry: HistoryRecord): Promise<void> {
|
||||||
|
await Query(this.db).put("history", historyEntry).finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a contract to the wallet and sign coins,
|
* Add a contract to the wallet and sign coins,
|
||||||
* but do not send them yet.
|
* but do not send them yet.
|
||||||
@ -574,7 +623,7 @@ export class Wallet {
|
|||||||
* First fetch information requred to withdraw from the reserve,
|
* First fetch information requred to withdraw from the reserve,
|
||||||
* then deplete the reserve, withdrawing coins until it is empty.
|
* then deplete the reserve, withdrawing coins until it is empty.
|
||||||
*/
|
*/
|
||||||
private async processReserve(reserveRecord: any,
|
private async processReserve(reserveRecord: ReserveRecord,
|
||||||
retryDelayMs: number = 250): Promise<void> {
|
retryDelayMs: number = 250): Promise<void> {
|
||||||
const opId = "reserve-" + reserveRecord.reserve_pub;
|
const opId = "reserve-" + reserveRecord.reserve_pub;
|
||||||
this.startOperation(opId);
|
this.startOperation(opId);
|
||||||
@ -586,9 +635,11 @@ export class Wallet {
|
|||||||
await this.depleteReserve(reserve, exchange);
|
await this.depleteReserve(reserve, exchange);
|
||||||
let depleted = {
|
let depleted = {
|
||||||
type: "depleted-reserve",
|
type: "depleted-reserve",
|
||||||
|
subjectId: `reserve-progress-${reserveRecord.reserve_pub}`,
|
||||||
timestamp: (new Date).getTime(),
|
timestamp: (new Date).getTime(),
|
||||||
detail: {
|
detail: {
|
||||||
reservePub: reserveRecord.reserve_pub,
|
reservePub: reserveRecord.reserve_pub,
|
||||||
|
currentAmount: reserveRecord.current_amount,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
await Query(this.db).put("history", depleted).finish();
|
await Query(this.db).put("history", depleted).finish();
|
||||||
@ -630,7 +681,7 @@ export class Wallet {
|
|||||||
const now = (new Date).getTime();
|
const now = (new Date).getTime();
|
||||||
const canonExchange = canonicalizeBaseUrl(req.exchange);
|
const canonExchange = canonicalizeBaseUrl(req.exchange);
|
||||||
|
|
||||||
const reserveRecord = {
|
const reserveRecord: ReserveRecord = {
|
||||||
reserve_pub: keypair.pub,
|
reserve_pub: keypair.pub,
|
||||||
reserve_priv: keypair.priv,
|
reserve_priv: keypair.priv,
|
||||||
exchange_base_url: canonExchange,
|
exchange_base_url: canonExchange,
|
||||||
@ -644,6 +695,7 @@ export class Wallet {
|
|||||||
const historyEntry = {
|
const historyEntry = {
|
||||||
type: "create-reserve",
|
type: "create-reserve",
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
|
subjectId: `reserve-progress-${reserveRecord.reserve_pub}`,
|
||||||
detail: {
|
detail: {
|
||||||
requestedAmount: req.amount,
|
requestedAmount: req.amount,
|
||||||
reservePub: reserveRecord.reserve_pub,
|
reservePub: reserveRecord.reserve_pub,
|
||||||
@ -674,26 +726,28 @@ export class Wallet {
|
|||||||
*/
|
*/
|
||||||
async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
|
async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
|
||||||
const now = (new Date).getTime();
|
const now = (new Date).getTime();
|
||||||
|
let reserve: ReserveRecord = await Query(this.db)
|
||||||
|
.get("reserves", req.reservePub);
|
||||||
const historyEntry = {
|
const historyEntry = {
|
||||||
type: "confirm-reserve",
|
type: "confirm-reserve",
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
|
subjectId: `reserve-progress-${reserve.reserve_pub}`,
|
||||||
detail: {
|
detail: {
|
||||||
reservePub: req.reservePub,
|
reservePub: req.reservePub,
|
||||||
|
requestedAmount: reserve.requested_amount,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let r = await Query(this.db)
|
if (!reserve) {
|
||||||
.get("reserves", req.reservePub);
|
|
||||||
if (!r) {
|
|
||||||
console.error("Unable to confirm reserve, not found in DB");
|
console.error("Unable to confirm reserve, not found in DB");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
r.confirmed = true;
|
reserve.confirmed = true;
|
||||||
await Query(this.db)
|
await Query(this.db)
|
||||||
.put("reserves", r)
|
.put("reserves", reserve)
|
||||||
.put("history", historyEntry)
|
.put("history", historyEntry)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
this.processReserve(r);
|
this.processReserve(reserve);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -801,8 +855,10 @@ export class Wallet {
|
|||||||
let historyEntry = {
|
let historyEntry = {
|
||||||
type: "reserve-update",
|
type: "reserve-update",
|
||||||
timestamp: (new Date).getTime(),
|
timestamp: (new Date).getTime(),
|
||||||
|
subjectId: `reserve-progress-${reserve.reserve_pub}`,
|
||||||
detail: {
|
detail: {
|
||||||
reservePub,
|
reservePub,
|
||||||
|
requestedAmount: reserve.requested_amount,
|
||||||
oldAmount,
|
oldAmount,
|
||||||
newAmount
|
newAmount
|
||||||
}
|
}
|
||||||
@ -1040,6 +1096,10 @@ export class Wallet {
|
|||||||
return {history};
|
return {history};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async hashContract(contract: any): Promise<string> {
|
||||||
|
return this.cryptoApi.hashString(canonicalJson(contract));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if there's an equivalent contract we've already purchased.
|
* Check if there's an equivalent contract we've already purchased.
|
||||||
*/
|
*/
|
||||||
|
@ -151,6 +151,20 @@ function makeHandlers(db: IDBDatabase,
|
|||||||
}
|
}
|
||||||
return wallet.updateExchangeFromUrl(detail.baseUrl);
|
return wallet.updateExchangeFromUrl(detail.baseUrl);
|
||||||
},
|
},
|
||||||
|
["hash-contract"]: function(detail) {
|
||||||
|
if (!detail.contract) {
|
||||||
|
return Promise.resolve({error: "contract missing"});
|
||||||
|
}
|
||||||
|
return wallet.hashContract(detail.contract).then((hash) => {
|
||||||
|
return {hash};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
["put-history-entry"]: function(detail: any) {
|
||||||
|
if (!detail.historyEntry) {
|
||||||
|
return Promise.resolve({error: "historyEntry missing"});
|
||||||
|
}
|
||||||
|
return wallet.putHistory(detail.historyEntry);
|
||||||
|
},
|
||||||
["reserve-creation-info"]: function(detail, sender) {
|
["reserve-creation-info"]: function(detail, sender) {
|
||||||
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
|
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
|
||||||
return Promise.resolve({error: "bad url"});
|
return Promise.resolve({error: "bad url"});
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
|
import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
|
||||||
import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent;
|
import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent;
|
||||||
import {Wallet} from "../lib/wallet/wallet";
|
import {HistoryRecord} from "../lib/wallet/wallet";
|
||||||
import {AmountJson} from "../lib/wallet/types";
|
import {AmountJson} from "../lib/wallet/types";
|
||||||
|
|
||||||
declare var m: any;
|
declare var m: any;
|
||||||
@ -173,7 +173,7 @@ function retryPayment(url: string, contractHash: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function formatHistoryItem(historyItem: any) {
|
function formatHistoryItem(historyItem: HistoryRecord) {
|
||||||
const d = historyItem.detail;
|
const d = historyItem.detail;
|
||||||
const t = historyItem.timestamp;
|
const t = historyItem.timestamp;
|
||||||
console.log("hist item", historyItem);
|
console.log("hist item", historyItem);
|
||||||
@ -215,7 +215,7 @@ namespace WalletHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Controller {
|
class Controller {
|
||||||
myHistory: any;
|
myHistory: any[];
|
||||||
gotError = false;
|
gotError = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -241,14 +241,24 @@ namespace WalletHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function view(ctrl: Controller) {
|
export function view(ctrl: Controller) {
|
||||||
let history = ctrl.myHistory;
|
let history: HistoryRecord[] = ctrl.myHistory;
|
||||||
if (ctrl.gotError) {
|
if (ctrl.gotError) {
|
||||||
return i18n`Error: could not retrieve event history`;
|
return i18n`Error: could not retrieve event history`;
|
||||||
}
|
}
|
||||||
if (!history) {
|
if (!history) {
|
||||||
throw Error("Could not retrieve history");
|
throw Error("Could not retrieve history");
|
||||||
}
|
}
|
||||||
let listing = _.map(history, formatHistoryItem);
|
|
||||||
|
let subjectMemo: {[s: string]: boolean} = {};
|
||||||
|
let listing: any[] = [];
|
||||||
|
for (let record of history.reverse()) {
|
||||||
|
//if (record.subjectId && subjectMemo[record.subjectId]) {
|
||||||
|
// return;
|
||||||
|
//}
|
||||||
|
subjectMemo[record.subjectId as string] = true;
|
||||||
|
listing.push(formatHistoryItem(record));
|
||||||
|
}
|
||||||
|
|
||||||
if (listing.length > 0) {
|
if (listing.length > 0) {
|
||||||
return m("div.container", listing);
|
return m("div.container", listing);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user