diff --git a/components/loading-summary-left.tsx b/components/loading-summary-left.tsx new file mode 100644 index 0000000..4ba0f11 --- /dev/null +++ b/components/loading-summary-left.tsx @@ -0,0 +1,83 @@ +import { Card, CardBody, Progress } from "@nextui-org/react"; + +import { LoadingItem } from "./loading-status-item"; + +import { type PositionLoadingState } from "@/pages/wallet/[walletAddress]"; +import { MeteoraPosition } from "@/services/MeteoraPosition"; +import { MeteoraPositionTransaction } from "@/services/ParseMeteoraTransactions"; + +export const LoadingSummaryLeft = (props: { + filteredPositions: MeteoraPosition[]; + filteredTransactions: MeteoraPositionTransaction[]; + loading: boolean; + positionLoadingState: PositionLoadingState; + usd: boolean; + updatedUsdValueCount: number; +}) => { + return ( + + + + + + ); +}; diff --git a/components/loading-summary-no-usd.tsx b/components/loading-summary-no-usd.tsx new file mode 100644 index 0000000..cfb820b --- /dev/null +++ b/components/loading-summary-no-usd.tsx @@ -0,0 +1,48 @@ +import { Button, Card, CardBody } from "@nextui-org/react"; + +import { type PositionLoadingState } from "@/pages/wallet/[walletAddress]"; + +export const LoadingSummaryNoUsd = (props: { + positionLoadingState: PositionLoadingState; +}) => { + const oldestTransaction = + props.positionLoadingState.transactions.length > 0 + ? props.positionLoadingState.transactions.sort( + (a, b) => a.timestamp_ms - b.timestamp_ms, + )[0] + : null; + + const oldestTransactionDate = oldestTransaction + ? new Date(oldestTransaction.timestamp_ms).toLocaleDateString() + + " " + + new Date(oldestTransaction.timestamp_ms).toLocaleTimeString() + : ""; + + return ( + + +

+ Oldest position transaction: {oldestTransactionDate} +

+

+ If you know you have no DLMM transactions prior to the date above or + you do not want to analyze on older transactions/positions, you can + safely stop loading more transactions. +

+ +
+
+ ); +}; diff --git a/components/loading-summary-usd.tsx b/components/loading-summary-usd.tsx new file mode 100644 index 0000000..795be6d --- /dev/null +++ b/components/loading-summary-usd.tsx @@ -0,0 +1,84 @@ +import { Card, CardBody } from "@nextui-org/react"; + +import { LoadingItem } from "./loading-status-item"; + +import { type PositionLoadingState } from "@/pages/wallet/[walletAddress]"; + +export const LoadingSummaryUsd = (props: { + positionLoadingState: PositionLoadingState; + estimatedPointsFromFeesAndRewards: number; + usdFeesAndRewards: number; + usdDivergenceLoss: number; + usdProfit: number; + positionsWithErrorsCount: number; +}) => { + return ( + + + + + + + + + + ); +}; diff --git a/components/loading-summary.tsx b/components/loading-summary.tsx index 73138be..03b0e87 100644 --- a/components/loading-summary.tsx +++ b/components/loading-summary.tsx @@ -1,6 +1,6 @@ -import { Card, CardBody, Progress } from "@nextui-org/react"; - -import { LoadingItem } from "./loading-status-item"; +import { LoadingSummaryLeft } from "./loading-summary-left"; +import { LoadingSummaryNoUsd } from "./loading-summary-no-usd"; +import { LoadingSummaryUsd } from "./loading-summary-usd"; import { type PositionLoadingState } from "@/pages/wallet/[walletAddress]"; import { MeteoraPosition } from "@/services/MeteoraPosition"; @@ -59,135 +59,23 @@ export const LoadingSummary = (props: { return (
- - - - - - - - - - - - - - + + +
); }; diff --git a/pages/wallet/[walletAddress].tsx b/pages/wallet/[walletAddress].tsx index a453599..fd89aef 100644 --- a/pages/wallet/[walletAddress].tsx +++ b/pages/wallet/[walletAddress].tsx @@ -35,6 +35,7 @@ export interface PositionLoadingState { updatedUsdPercent: number; usdUpdateStartTime: number; estimatedCompletionString: string; + cancel?: () => any; } export default function IndexPage() { @@ -100,7 +101,7 @@ export default function IndexPage() { return { ...currentState, tokenMap }; }); - new MeteoraPositionStream( + const meteoraPositionStream = new MeteoraPositionStream( appState.connection, walletAddress, undefined, @@ -156,49 +157,56 @@ export default function IndexPage() { } }) .on("end", () => { - setPositionLoadingState((currentState) => { - currentState.allPositionsFound = true; - currentState.rpcDataLoaded = true; - loadApiData(currentState.positions); - - return updateElapsedTime(currentState); - }); + loadApiData(); }); + + setPositionLoadingState((currentState) => { + return { + ...currentState, + cancel: () => { + meteoraPositionStream!.cancel(); + loadApiData(); + }, + }; + }); } } - async function loadApiData(positions: MeteoraPosition[]) { + async function loadApiData() { setPositionLoadingState((currentState) => { + currentState.allSignaturesFound = true; + currentState.allPositionsFound = true; + currentState.rpcDataLoaded = true; currentState.usdUpdateStartTime = new Date().getTime(); currentState.updatingUsdValues = true; - return updateElapsedTime(currentState); - }); + new UsdMeteoraPositionStream(currentState.positions) + .on("data", (data) => { + setPositionLoadingState((currentState) => { + const oldPosition = currentState.positions.find( + (position) => position.position == data.updatedPosition.position, + )!; + const index = currentState.positions.indexOf(oldPosition); - new UsdMeteoraPositionStream(positions) - .on("data", (data) => { - setPositionLoadingState((currentState) => { - const oldPosition = currentState.positions.find( - (position) => position.position == data.updatedPosition.position, - )!; - const index = currentState.positions.indexOf(oldPosition); + currentState.positions[index] = data.updatedPosition; + currentState.updatedUsdValueCount = data.updatedPositionCount; - currentState.positions[index] = data.updatedPosition; - currentState.updatedUsdValueCount = data.updatedPositionCount; + currentState.updatedUsdPercent = + (100 * data.updatedPositionCount) / currentState.positions.length; - currentState.updatedUsdPercent = - (100 * data.updatedPositionCount) / positions.length; + return updateElapsedTime(currentState); + }); + }) + .on("end", () => { + setPositionLoadingState((currentState) => { + currentState.apiDataLoaded = true; - return updateElapsedTime(currentState); + return updateElapsedTime(currentState); + }); }); - }) - .on("end", () => { - setPositionLoadingState((currentState) => { - currentState.apiDataLoaded = true; - return updateElapsedTime(currentState); - }); - }); + return updateElapsedTime(currentState); + }); } useEffect(() => { diff --git a/services/MeteoraPositionStream.ts b/services/MeteoraPositionStream.ts index 1742e68..d1b156b 100644 --- a/services/MeteoraPositionStream.ts +++ b/services/MeteoraPositionStream.ts @@ -53,6 +53,7 @@ interface MeteoraPositionStreamEvents { export class MeteoraPositionStream extends Transform { private _pairs: Map = new Map(); private _tokenList: Map = new Map(); + private _transactionStream!: ParsedTransactionStream; private _receivedAllTransactions = false; private _transactionsReceivedCount = 0; private _transactionsProcessedCount = 0; @@ -62,6 +63,8 @@ export class MeteoraPositionStream extends Transform { MeteoraPositionTransaction >; private _done = false; + private _cancelling = false; + private _cancelled = false; constructor( connection: Connection, @@ -74,6 +77,11 @@ export class MeteoraPositionStream extends Transform { this._init(connection, walletAddress, before, until, minDate); } + cancel() { + this._transactionStream.cancel(); + this._cancelling = true; + } + private async _init( connection: Connection, walletAddress: string, @@ -94,7 +102,7 @@ export class MeteoraPositionStream extends Transform { ), ); - new ParsedTransactionStream( + this._transactionStream = new ParsedTransactionStream( connection, walletAddress, before, @@ -114,28 +122,35 @@ export class MeteoraPositionStream extends Transform { } private async _processBatch(connection: Connection) { - const { inputCount, output } = await this._processor.next(); + if (!this._cancelling && !this._cancelled) { + const { inputCount, output } = await this._processor.next(); - if (inputCount > 0) { - if (output.length > 0) { - this._transactions = this._transactions.concat(output); - this.push({ - type: "transactionCount", - meteoraTransactionCount: this._transactions.length, - }); + if (inputCount > 0) { + if (output.length > 0) { + this._transactions = this._transactions.concat(output); + if (!this._cancelling && !this._cancelled) { + this.push({ + type: "transactionCount", + meteoraTransactionCount: this._transactions.length, + }); + } - await Promise.all( - output.map(async (positionTransaction) => { - if (positionTransaction.open) { - await this._createPosition(connection, positionTransaction); - } - }), - ); - } + await Promise.all( + output.map(async (positionTransaction) => { + if (positionTransaction.open) { + await this._createPosition(connection, positionTransaction); + } + }), + ); + } - this._transactionsProcessedCount += inputCount; + this._transactionsProcessedCount += inputCount; + } + this._finish(); + } else if (!this._cancelled) { + this._cancelled = true; + this.push(null); } - this._finish(); } private async _parseTransactions( @@ -151,7 +166,9 @@ export class MeteoraPositionStream extends Transform { this._transactionsReceivedCount = data.signatureCount; break; } - this.push(data); + if (!this._cancelling && !this._cancelled) { + this.push(data); + } return; } @@ -172,11 +189,13 @@ export class MeteoraPositionStream extends Transform { const newPosition = new MeteoraPosition(newPositionTransactions); if (newPosition.isClosed) { - this.push({ - type: "positionAndTransactions", - transactions: newPositionTransactions, - position: newPosition, - }); + if (!this._cancelling && !this._cancelled) { + this.push({ + type: "positionAndTransactions", + transactions: newPositionTransactions, + position: newPosition, + }); + } } else { await this._updateOpenPosition( connection, @@ -192,11 +211,13 @@ export class MeteoraPositionStream extends Transform { position: MeteoraPosition, ) { await updateOpenPosition(connection, position); - this.push({ - type: "positionAndTransactions", - transactions, - position, - }); + if (!this._cancelling && !this._cancelled) { + this.push({ + type: "positionAndTransactions", + transactions, + position, + }); + } } private async _finish() { @@ -206,7 +227,9 @@ export class MeteoraPositionStream extends Transform { !this._done ) { this._done = true; - this.push(null); + if (!this._cancelling && !this._cancelled) { + this.push(null); + } } } diff --git a/services/ParsedTransactionStream.ts b/services/ParsedTransactionStream.ts index d48e30e..e7011b1 100644 --- a/services/ParsedTransactionStream.ts +++ b/services/ParsedTransactionStream.ts @@ -44,6 +44,9 @@ export class ParsedTransactionStream extends Transform { string, ParsedTransactionWithMeta | null >; + private _signatureStream: SignatureStream; + private _cancelling = false; + private _cancelled = false; constructor( connection: Connection, @@ -60,13 +63,21 @@ export class ParsedTransactionStream extends Transform { commitment: "confirmed", }), ); - new SignatureStream(connection, walletAddress, before, until, minDate) + this._signatureStream = new SignatureStream( + connection, + walletAddress, + before, + until, + minDate, + ) .on("data", (signatures: ConfirmedSignatureInfo[]) => this._processSignatures(signatures), ) .on("end", async () => { this._allSignaturesFound = { type: "allSignaturesFound" }; - this.push(this._allSignaturesFound); + if (!this._cancelling && !this._cancelled) { + this.push(this._allSignaturesFound); + } while (!this._processor.isComplete) { await this._processBatch(); @@ -76,30 +87,44 @@ export class ParsedTransactionStream extends Transform { .on("error", (error) => this.emit("error", error)); } + cancel() { + this._signatureStream.cancel(); + this._cancelling = true; + } + private async _processBatch() { - const { inputCount, output } = await this._processor.next(); + if (!this._cancelling && !this._cancelled) { + const { inputCount, output } = await this._processor.next(); - const parsedTransactionsWithMeta = output.filter( - (parsedTransaction) => parsedTransaction != null, - ) as ParsedTransactionWithMeta[]; + const parsedTransactionsWithMeta = output.filter( + (parsedTransaction) => parsedTransaction != null, + ) as ParsedTransactionWithMeta[]; - this._processedCount += inputCount; + this._processedCount += inputCount; - if (parsedTransactionsWithMeta.length > 0) { - this.push({ - type: "parsedTransaction", - parsedTransactionsWithMeta, - }); + if (parsedTransactionsWithMeta.length > 0) { + if (!this._cancelling && !this._cancelled) { + this.push({ + type: "parsedTransaction", + parsedTransactionsWithMeta, + }); + } + } + this._finish(); + } else if (!this._cancelled) { + this._cancelled = true; + this.push(null); } - this._finish(); } private async _processSignatures(signatures: ConfirmedSignatureInfo[]) { this._signatureCount += signatures.length; - this.push({ - type: "signatureCount", - signatureCount: this._signatureCount, - }); + if (!this._cancelling && !this._cancelled) { + this.push({ + type: "signatureCount", + signatureCount: this._signatureCount, + }); + } const signatureStrings = signatures.map((signature) => signature.signature); this._processor.addBatch(signatureStrings); @@ -111,7 +136,9 @@ export class ParsedTransactionStream extends Transform { this._allSignaturesFound && this._signatureCount == this._processedCount ) { - this.push(null); + if (!this._cancelling && !this._cancelled) { + this.push(null); + } } } diff --git a/services/SignatureStream.ts b/services/SignatureStream.ts index 30e2398..0b547ea 100644 --- a/services/SignatureStream.ts +++ b/services/SignatureStream.ts @@ -20,6 +20,7 @@ export class SignatureStream extends Transform { private _before?: string; private _until?: string; private _minDate?: Date; + private _cancel = false; constructor( connection: Connection, @@ -37,6 +38,10 @@ export class SignatureStream extends Transform { this._streamSignatures().catch((error) => this.emit("error", error)); } + cancel() { + this._cancel = true; + } + private async _streamSignatures() { let newSignatures: ConfirmedSignatureInfo[] = []; let lastDate = new Date(); @@ -65,6 +70,7 @@ export class SignatureStream extends Transform { ); } } while ( + !this._cancel && newSignatures.length > 0 && (!this._minDate || (this._minDate && lastDate > this._minDate)) );