Skip to content

Commit

Permalink
feat: download report for Chrysalis.
Browse files Browse the repository at this point in the history
Signed-off-by: Eugene Panteleymonchuk <[email protected]>
  • Loading branch information
panteleymonchuk committed Feb 9, 2024
1 parent e2a6662 commit 1487a2b
Show file tree
Hide file tree
Showing 10 changed files with 440 additions and 7 deletions.
8 changes: 8 additions & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ export const routes: IRoute[] = [
folder: "chrysalis/transactionhistory",
func: "get",
},
{
path: "/transactionhistory/download/:network/:address",
method: "post",
folder: "chrysalis/transactionhistory/download",
func: "post",
dataBody: true,
dataResponse: true,
},
{
path: "/chrysalis/did/:network/:did/document",
method: "get",
Expand Down
147 changes: 147 additions & 0 deletions api/src/routes/chrysalis/transactionhistory/download/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { UnitsHelper } from "@iota/iota.js";
import JSZip from "jszip";
import moment from "moment";
import { ServiceFactory } from "../../../../factories/serviceFactory";
import logger from "../../../../logger";
import { IDataResponse } from "../../../../models/api/IDataResponse";
import { ITransactionHistoryDownloadBody } from "../../../../models/api/stardust/chronicle/ITransactionHistoryDownloadBody";
import { ITransactionHistoryRequest } from "../../../../models/api/stardust/chronicle/ITransactionHistoryRequest";
import { IConfiguration } from "../../../../models/configuration/IConfiguration";
import { CHRYSALIS } from "../../../../models/db/protocolVersion";
import { NetworkService } from "../../../../services/networkService";
import { ChrysalisTangleHelper } from "../../../../utils/chrysalis/chrysalisTangleHelper";
import { ValidationHelper } from "../../../../utils/validationHelper";

interface IParsedRow {
messageId: string;
transactionId: string;
referencedByMilestoneIndex: string;
milestoneTimestampReferenced: string;
timestampFormatted: string;
ledgerInclusionState: string;
conflictReason: string;
inputsCount: string;
outputsCount: string;
addressBalanceChange: string;
addressBalanceChangeFormatted: string;
}

/**
* Download the transaction history from chronicle stardust.
* @param _ The configuration.
* @param request The request.
* @param body The request body
* @returns The response.
*/
export async function post(
_: IConfiguration,
request: ITransactionHistoryRequest,
body: ITransactionHistoryDownloadBody,
): Promise<IDataResponse | null> {
const networkService = ServiceFactory.get<NetworkService>("network");
ValidationHelper.oneOf(request.network, networkService.networkNames(), "network");

const networkConfig = networkService.get(request.network);

if (networkConfig.protocolVersion !== CHRYSALIS || !networkConfig.permaNodeEndpoint || !request.address) {
return null;
}

const transactionHistoryDownload = await ChrysalisTangleHelper.transactionHistoryDownload(networkConfig, request.address);

const parsed = parseResponse(transactionHistoryDownload);

let csvContent = `${["Timestamp", "TransactionId", "Balance changes"].join(",")}\n`;

const filtered = parsed.body.filter((row) => {
return moment(row.milestoneTimestampReferenced).isAfter(body.targetDate);
});

for (const i of filtered) {
const row = [i.timestampFormatted, i.transactionId, i.addressBalanceChangeFormatted].join(",");
csvContent += `${row}\n`;
}

const jsZip = new JSZip();
let response: IDataResponse = null;

try {
jsZip.file("history.csv", csvContent);
const content = await jsZip.generateAsync({ type: "nodebuffer" });

response = {
data: content,
contentType: "application/octet-stream",
};
} catch (e) {
logger.error(`Failed to zip transaction history for download. Cause: ${e}`);
}

return response;
}

/**
* Split response into lines, format each line
* @param response The response from endpoint to parse.
* @returns Object with headers and body.
*/
function parseResponse(response: string) {
const lines = response.split("\n");
let isHeadersSet = false;
let headers: IParsedRow; // Headers: "MessageID", "TransactionID", "ReferencedByMilestoneIndex", "MilestoneTimestampReferenced", "LedgerInclusionState", "ConflictReason", "InputsCount", "OutputsCount", "AddressBalanceChange"
const body: IParsedRow[] = [];

for (const line of lines) {
const row = parseRow(line);

if (row) {
if (isHeadersSet) {
body.push(row);
} else {
headers = row;
isHeadersSet = true;
}
}
}

return { headers, body };
}

/**
* @param row The row to parse.
* @returns Object with parsed and formatted values.
*/
function parseRow(row: string): IParsedRow {
const cols = row.split(",");
if (!cols || cols.length < 9) {
return null;
}

const [
messageId,
transactionId,
referencedByMilestoneIndex,
milestoneTimestampReferenced,
ledgerInclusionState,
conflictReason,
inputsCount,
outputsCount,
addressBalanceChange,
] = cols;

const timestamp = milestoneTimestampReferenced.replaceAll("\"", "");

return {
messageId,
transactionId,
referencedByMilestoneIndex,
milestoneTimestampReferenced: timestamp,
timestampFormatted: moment(timestamp).format("YYYY-MM-DD HH:mm:ss"),
ledgerInclusionState,
conflictReason,
inputsCount,
outputsCount,
addressBalanceChange,
addressBalanceChangeFormatted: UnitsHelper.formatBest(Number(addressBalanceChange)),
};
}
17 changes: 17 additions & 0 deletions api/src/utils/chrysalis/chrysalisTangleHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,23 @@ export class ChrysalisTangleHelper {
} catch {}
}

/**
* Download the transaction history as csv
* @param network The network to find the items on.
* @param address Address for exporting
*/
public static async transactionHistoryDownload(network: INetwork, address: string): Promise<string> {
try {
const csvResp = await fetch(`${network.permaNodeEndpoint}/api/core/v1/addresses/${address}/tx-history`, {
method: "GET",
headers: {
Accept: "text/csv",
},
});
return await csvResp.text();
} catch {}
}

/**
* Get the milestone details.
* @param network The network to find the items on.
Expand Down
115 changes: 115 additions & 0 deletions client/src/app/components/chrysalis/DownloadModal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
@import "../../../scss/fonts";
@import "../../../scss/mixins";
@import "../../../scss/media-queries";
@import "../../../scss/variables";
@import "../../../scss/themes";

.download-modal {
display: inline-block;

button {
border: none;
background-color: transparent;

&:focus {
box-shadow: none;
}
}

.modal--icon {
span {
color: #b0bfd9;
font-size: 20px;
}
}

.modal--bg {
position: fixed;
z-index: 2000;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: rgba(19, 31, 55, 0.75);
}

.modal--content {
position: fixed;
z-index: 3000;
top: 50%;
left: 50%;
width: 100%;
max-width: 660px;
max-height: 100%;
padding: 32px;
transform: translate(-50%, -50%);
border: 1px solid #e8eefb;
border-radius: 6px;
background-color: var(--body-background);
box-shadow: 0 4px 8px rgba(19, 31, 55, 0.04);

@include tablet-down {
width: 100%;
overflow-y: auto;
}

.modal--header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 22px;
border-bottom: 1px solid #e8eefb;
letter-spacing: 0.02em;

.modal--title {
@include font-size(20px);

color: var(--body-color);
font-family: $metropolis;
font-weight: 600;
}

button {
color: var(--body-color);
}
}

.modal--body {
margin-top: 24px;

.input-container {
width: 100%;

.date-label {
color: var(--body-color);
font-family: $inter;
font-weight: 500;
margin-bottom: 8px;
margin-right: 8px;
}
}

.confirm-button {
cursor: pointer;
margin: 24px 0 8px 0;
align-self: center;
width: fit-content;
padding: 8px 12px;
border: 1px solid #ccc;
color: $mint-green-7;

&.disabled {
color: $mint-green-1;
}

.spinner-container {
display: flex;
align-items: center;
justify-content: center;
width: 57px;
height: 16px;
}
}
}
}
}
Loading

0 comments on commit 1487a2b

Please sign in to comment.