Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NNS1-3480: New util function mapIcpTransactionToReport #5895

Merged
merged 10 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions frontend/src/lib/utils/icp-transactions.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ const getTransactionInformation = (
if (data === undefined) {
throw new Error(`Unknown transaction type ${JSON.stringify(operation)}`);
}

return {
from: "from" in data ? data.from : undefined,
to: "to" in data ? data.to : undefined,
Expand All @@ -136,6 +137,67 @@ const getTransactionInformation = (
};
};

export const mapIcpTransactionToReport = ({
transaction,
accountIdentifier,
neuronAccounts,
swapCanisterAccounts,
}: {
transaction: TransactionWithId;
accountIdentifier: string;
neuronAccounts: Set<string>;
swapCanisterAccounts: Set<string>;
}) => {
const txInfo = getTransactionInformation(transaction.transaction.operation);
if (txInfo === undefined) {
throw new Error(
`Unknown transaction type ${
yhabib marked this conversation as resolved.
Show resolved Hide resolved
Object.keys(transaction.transaction.operation)[0]
}`
);
}

const { to, from, amount, fee } = txInfo;
const isSelfTransaction = isToSelf(transaction.transaction);
const isReceive = isSelfTransaction || from !== accountIdentifier;
const useFee = !isReceive;
const feeApplied = useFee && fee ? fee : 0n;

const type = getTransactionType({
transaction: transaction.transaction,
neuronAccounts,
swapCanisterAccounts,
isReceive,
});

const blockTimestampNanos = fromNullable(
transaction.transaction.timestamp
)?.timestamp_nanos;
const createdTimestampNanos = fromNullable(
transaction.transaction.created_at_time
)?.timestamp_nanos;
const timestampNanos = blockTimestampNanos ?? createdTimestampNanos;
yhabib marked this conversation as resolved.
Show resolved Hide resolved
const timestampMilliseconds = nonNullish(timestampNanos)
? Number(timestampNanos) / NANO_SECONDS_IN_MILLISECOND
: undefined;
const timestamp = nonNullish(timestampMilliseconds)
? new Date(timestampMilliseconds)
: undefined;

const tokenAmount = TokenAmountV2.fromUlps({
amount: amount + feeApplied,
token: ICPToken,
});

return {
type,
to,
from,
tokenAmount,
timestamp,
};
};

export const mapIcpTransactionToUi = ({
transaction,
accountIdentifier,
Expand Down Expand Up @@ -181,6 +243,7 @@ export const mapIcpTransactionToUi = ({
const blockTimestampNanos = fromNullable(
transaction.transaction.timestamp
)?.timestamp_nanos;

const createdTimestampNanos = fromNullable(
transaction.transaction.created_at_time
)?.timestamp_nanos;
Expand Down
218 changes: 218 additions & 0 deletions frontend/src/tests/lib/utils/icp-transactions.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import { NANO_SECONDS_IN_MILLISECOND } from "$lib/constants/constants";
import type { UiTransaction } from "$lib/types/transaction";
import {
mapIcpTransactionToReport,
mapIcpTransactionToUi,
mapToSelfTransactions,
sortTransactionsByIdDescendingOrder,
Expand All @@ -30,6 +31,10 @@ describe("icp-transactions.utils", () => {
spender: [],
},
};
const defaultTokenAmountWithOutFee = TokenAmountV2.fromUlps({
yhabib marked this conversation as resolved.
Show resolved Hide resolved
amount: amount,
token: ICPToken,
});
const defaultUiTransaction: UiTransaction = {
domKey: `${transactionId}-1`,
isIncoming: false,
Expand Down Expand Up @@ -69,6 +74,219 @@ describe("icp-transactions.utils", () => {
memo,
});

describe("mapIcpTransactionToReport", () => {
const defaultReportTransaction = {
type: "send",
to,
from,
tokenAmount: TokenAmountV2.fromUlps({
amount: amount + fee,
token: ICPToken,
}),
timestamp: defaultTimestamp,
};
it("should throw an error if no transaction information is found", () => {
const transaction = createTransaction({
operation: {
// @ts-expect-error: Even though it is not possible our implementations handles it.
Unknown: {},
},
});

expect(() =>
mapIcpTransactionToReport({
transaction,
accountIdentifier: from,
neuronAccounts: new Set<string>(),
swapCanisterAccounts: new Set<string>(),
})
).toThrowError('Unknown transaction type {"Unknown":{}}');
});

it("should return transaction information", () => {
const transaction = createTransaction({
operation: defaultTransferOperation,
});
const expectedIcpTransaction = {
...defaultReportTransaction,
};

expect(
mapIcpTransactionToReport({
transaction,
accountIdentifier: from,
neuronAccounts: new Set<string>(),
swapCanisterAccounts: new Set<string>(),
})
).toEqual(expectedIcpTransaction);
});

it("sould identify receive transactions", () => {
const transaction = createTransaction({
operation: {
Transfer: {
...defaultTransferOperation.Transfer,
to: from,
},
},
});
const expectedIcpTransaction = {
...defaultReportTransaction,
type: "receive",
to: from,
tokenAmount: defaultTokenAmountWithOutFee,
};

expect(
mapIcpTransactionToReport({
transaction,
accountIdentifier: from,
neuronAccounts: new Set<string>(),
swapCanisterAccounts: new Set<string>(),
})
).toEqual(expectedIcpTransaction);
});

describe("should map to the correct transaction type", () => {
it("maps stake neuron transaction", () => {
const transaction = createTransaction({
operation: defaultTransferOperation,
memo: 12345n,
});
const expectedIcpTransaction = {
...defaultReportTransaction,
type: "stakeNeuron",
};

expect(
mapIcpTransactionToReport({
transaction,
accountIdentifier: from,
neuronAccounts: new Set<string>([to]),
swapCanisterAccounts: new Set<string>(),
})
).toEqual(expectedIcpTransaction);
});

it("maps top up neuron transaction", () => {
const transaction = createTransaction({
operation: defaultTransferOperation,
memo: 0n,
});
const expectedIcpTransaction = {
...defaultReportTransaction,
type: "topUpNeuron",
};

expect(
mapIcpTransactionToReport({
transaction,
accountIdentifier: from,
neuronAccounts: new Set<string>([to]),
swapCanisterAccounts: new Set<string>(),
})
).toEqual(expectedIcpTransaction);
});

it("maps create canister transaction", () => {
const transaction = createTransaction({
operation: defaultTransferOperation,
memo: CREATE_CANISTER_MEMO,
});
const expectedIcpTransaction = {
...defaultReportTransaction,
type: "createCanister",
};

expect(
mapIcpTransactionToReport({
transaction,
accountIdentifier: from,
neuronAccounts: new Set<string>(),
swapCanisterAccounts: new Set<string>(),
})
).toEqual(expectedIcpTransaction);
});

it("maps top up canister transaction", () => {
yhabib marked this conversation as resolved.
Show resolved Hide resolved
const transaction = createTransaction({
operation: defaultTransferOperation,
memo: TOP_UP_CANISTER_MEMO,
});
const expectedIcpTransaction = {
...defaultReportTransaction,
type: "topUpCanister",
};

expect(
mapIcpTransactionToReport({
transaction,
accountIdentifier: from,
neuronAccounts: new Set<string>(),
swapCanisterAccounts: new Set<string>(),
})
).toEqual(expectedIcpTransaction);
});
});

describe("maps timestamps", () => {
const createdDate = new Date("2023-01-01T00:12:00.000Z");
const blockDate = new Date("2023-01-01T00:34:00.000Z");
const createTimestamps = {
timestamp_nanos:
BigInt(createdDate.getTime()) * BigInt(NANO_SECONDS_IN_MILLISECOND),
};
const blockTimestamps = {
timestamp_nanos:
BigInt(blockDate.getTime()) * BigInt(NANO_SECONDS_IN_MILLISECOND),
};

it("prefers block timestamp", () => {
const transaction = createTransaction({
operation: defaultTransferOperation,
});
transaction.transaction.created_at_time = [createTimestamps];
transaction.transaction.timestamp = [blockTimestamps];

const expectedIcpTransaction = {
...defaultReportTransaction,
timestamp: blockDate,
};

expect(
mapIcpTransactionToReport({
transaction,
accountIdentifier: from,
neuronAccounts: new Set<string>(),
swapCanisterAccounts: new Set<string>(),
})
).toEqual(expectedIcpTransaction);
});

it("falls back on created timestamps", () => {
const transaction = createTransaction({
operation: defaultTransferOperation,
});
transaction.transaction.created_at_time = [createTimestamps];
transaction.transaction.timestamp = [];

const expectedIcpTransaction = {
...defaultReportTransaction,
timestamp: createdDate,
};

expect(
mapIcpTransactionToReport({
transaction,
accountIdentifier: from,
neuronAccounts: new Set<string>(),
swapCanisterAccounts: new Set<string>(),
})
).toEqual(expectedIcpTransaction);
});
});
});

describe("mapIcpTransactionToUi", () => {
it("maps stake neuron transaction", () => {
const transaction = createTransaction({
Expand Down