Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ export function HiveTransactionRow({ entry, transaction: tr }: Props) {
</div>
</div>
);
} else if (tr.type === "transfer_from_savings") {
} else if (
tr.type === "transfer_from_savings" ||
tr.type === "fill_transfer_from_savings"
) {
icon = <UilArrowRight className="size-4" />;
details = (
<div className="space-y-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ export function HiveTransactionRow({ entry, transaction: tr }: Props) {
tr.type === "transfer" ||
tr.type === "transfer_to_vesting" ||
tr.type === "transfer_to_savings" ||
tr.type === "transfer_from_savings"
tr.type === "transfer_from_savings" ||
tr.type === "fill_transfer_from_savings"
) {
flag = true;
icon = <UilArrowRight className="size-4" />;
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/entities/hive/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ export interface TransferFromSavings extends BaseTransaction {
request_id: number;
}

/** Virtual op emitted when a savings withdrawal's three-day timer completes. */
export interface FillTransferFromSavings extends BaseTransaction {
type: "fill_transfer_from_savings";
amount: string;
memo?: string;
from: string;
to: string;
request_id: number;
}

export interface LimitOrderCreate2 extends BaseTransaction {
type: "limit_order_create2";
owner: string;
Expand Down Expand Up @@ -254,6 +264,7 @@ export type Transaction =
| TransferToVesting
| TransferToSavings
| TransferFromSavings
| FillTransferFromSavings
| CancelTransferFromSavings
| WithdrawVesting
| SetWithdrawRoute
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/features/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,7 @@
"type-collateralized_convert-detail": "Request id: <strong>{{request}}</strong>",
"type-update_proposal_vote-detail": "Proposal id: <strong>{{pid}}</strong>",
"type-transfer_from_savings": "Transfer from savings",
"type-fill_transfer_from_savings": "Savings withdrawal completed",
"type-limit_order_create": "New trade created",
"type-limit_order_create2": "New trade created",
"type-limit_order_cancel": "Open trade cancelled",
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/features/shared/transactions/transaction-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,20 @@ export function TransactionRow({ entry, transaction: item }: Props) {
numbers = <span className="number">{aam}</span>;
}

if (tr.type === "fill_transfer_from_savings") {
flag = true;
// @ts-ignore
icon = <TwoUserAvatar from={tr.from} to={tr.to} size="small" />;

details = (
<span>
<strong>@{tr.from}</strong> -&gt; <strong>@{tr.to}</strong>
</span>
);

numbers = <span className="number">{tr.amount}</span>;
}

if (tr.type === "cancel_transfer_from_savings") {
flag = true;
icon = closeSvg;
Expand Down
6 changes: 6 additions & 0 deletions packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 2.3.80

### Patch Changes

- fix(sdk): correct account-history pagination and stop dropping requested ops (#1396)

## 2.3.79

### Patch Changes
Expand Down
60 changes: 51 additions & 9 deletions packages/sdk/dist/browser/index.d.ts

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/sdk/dist/browser/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/dist/browser/index.js.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/sdk/dist/node/index.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/dist/node/index.cjs.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/sdk/dist/node/index.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/dist/node/index.mjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ecency/sdk",
"private": false,
"version": "2.3.79",
"version": "2.3.80",
"description": "Ecency SDK",
"repository": {
"type": "git",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ export const ACCOUNT_OPERATION_GROUPS: Record<OperationGroup, number[]> = {
ops.transfer_to_savings,
ops.transfer_from_savings,
ops.cancel_transfer_from_savings,
// The virtual op emitted when a savings withdrawal completes. It used to be a
// second copy of fill_recurrent_transfer, so a completed savings withdrawal was
// never returned by the transfers group nor by ALL_ACCOUNT_OPERATIONS.
ops.fill_transfer_from_savings,
ops.recurrent_transfer,
ops.fill_recurrent_transfer,
ops.escrow_transfer,
ops.fill_recurrent_transfer,
],
"market-orders": [
ops.fill_convert_request,
Expand Down Expand Up @@ -48,9 +51,13 @@ export const ACCOUNT_OPERATION_GROUPS: Record<OperationGroup, number[]> = {
],
};

export const ALL_ACCOUNT_OPERATIONS = [...Object.values(ACCOUNT_OPERATION_GROUPS)].reduce(
(acc, val) => acc.concat(val),
[]
/**
* Every operation any group asks for, de-duplicated. Groups overlap (an op can be
* meaningful to more than one), and the raw concatenation used to repeat ids in the
* `operation-types` query string sent to hafah.
*/
export const ALL_ACCOUNT_OPERATIONS = Array.from(
new Set(Object.values(ACCOUNT_OPERATION_GROUPS).flat())
);

interface TxPageRaw {
Expand Down
21 changes: 21 additions & 0 deletions packages/sdk/src/modules/accounts/types/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ export interface TransferToSavings extends BaseTransaction {
to: string;
}

export interface TransferFromSavings extends BaseTransaction {
type: "transfer_from_savings";
amount: string;
memo?: string;
from: string;
to: string;
request_id: number;
}

/** Virtual op emitted when a savings withdrawal's three-day timer completes. */
export interface FillTransferFromSavings extends BaseTransaction {
type: "fill_transfer_from_savings";
amount: string;
memo?: string;
from: string;
to: string;
request_id: number;
}

export interface CancelTransferFromSavings extends BaseTransaction {
from: string;
request_id: number;
Expand Down Expand Up @@ -234,6 +253,8 @@ export type Transaction =
| Transfer
| TransferToVesting
| TransferToSavings
| TransferFromSavings
| FillTransferFromSavings
| CancelTransferFromSavings
| WithdrawVesting
| SetWithdrawRoute
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it } from "vitest";
import {
collectRequestedOperations,
getNextAccountHistoryPageParam,
} from "./get-hive-asset-transactions-query-options";
import { ALL_ACCOUNT_OPERATIONS, ACCOUNT_OPERATION_GROUPS } from "../../accounts";
import type { HiveTransaction } from "../types";

const page = (nums: number[]) =>
nums.map((num) => ({ num, type: "transfer", timestamp: "", trx_id: "" })) as
unknown as HiveTransaction[];

describe("getNextAccountHistoryPageParam", () => {
// condenser_api.get_account_history returns each page in ASCENDING num order, so the
// OLDEST row is at index 0. Reading the last element instead advances the window by a
// single operation per page.
it("walks backwards from the oldest row on the page", () => {
expect(getNextAccountHistoryPageParam(page([8842005, 8842006, 8842054]))).toBe(
8842004
);
});

it("does not overlap the page it just returned", () => {
const first = page([100, 101, 102]);
const next = getNextAccountHistoryPageParam(first)!;
expect(next).toBeLessThan(Math.min(...first.map((x) => Number(x.num))));
});

it("terminates at the start of history instead of returning the -1 sentinel", () => {
// -1 is initialPageParam ("give me the newest"), so returning it here would restart
// the walk at the head of the history and never finish.
expect(getNextAccountHistoryPageParam(page([0, 1, 2]))).toBeUndefined();
});

it("terminates on an empty or missing page", () => {
expect(getNextAccountHistoryPageParam([])).toBeUndefined();
expect(getNextAccountHistoryPageParam(undefined)).toBeUndefined();
});
});

describe("collectRequestedOperations", () => {
it("collects an explicit list", () => {
expect(collectRequestedOperations(["transfer", "fill_transfer_from_savings"])).toEqual(
new Set(["transfer", "fill_transfer_from_savings"])
);
});

it("accepts a single value", () => {
expect(collectRequestedOperations("transfer")).toEqual(new Set(["transfer"]));
});

// An empty set is what keeps the unfiltered per-asset views behaving exactly as
// before: the asset's own allow-list decides and nothing extra leaks in.
it("is empty for no filter and for the all-operations alias", () => {
expect(collectRequestedOperations([])).toEqual(new Set());
expect(collectRequestedOperations("")).toEqual(new Set());
expect(collectRequestedOperations(["", "transfer"])).toEqual(new Set(["transfer"]));
});
});

describe("ALL_ACCOUNT_OPERATIONS", () => {
it("lists every id once", () => {
expect(new Set(ALL_ACCOUNT_OPERATIONS).size).toBe(ALL_ACCOUNT_OPERATIONS.length);
});

it("covers every group", () => {
Object.values(ACCOUNT_OPERATION_GROUPS)
.flat()
.forEach((id) => expect(ALL_ACCOUNT_OPERATIONS).toContain(id));
});

// 59 used to be missing because fill_recurrent_transfer was listed twice, so a
// completed savings withdrawal never came back from the transfers group.
it("includes fill_transfer_from_savings in the transfers group", () => {
expect(ACCOUNT_OPERATION_GROUPS.transfers).toContain(59);
expect(ALL_ACCOUNT_OPERATIONS).toContain(59);
});

// Web's profile transaction list renders producer_reward, so the unfiltered default
// must keep returning it.
it("still includes producer_reward", () => {
expect(ALL_ACCOUNT_OPERATIONS).toContain(64);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from "../types";
import type { HiveTransaction } from "../types";
import {
collectRequestedOperations,
getHiveAssetTransactionsQueryOptions,
resolveHiveOperationFilters,
} from "./get-hive-asset-transactions-query-options";
Expand All @@ -18,6 +19,7 @@ export function getHbdAssetTransactionsQueryOptions(
filters: HiveOperationFilter = []
) {
const { filterKey } = resolveHiveOperationFilters(filters);
const requestedOperations = collectRequestedOperations(filters);

return infiniteQueryOptions<HiveTransaction[]>({
...getHiveAssetTransactionsQueryOptions(username, limit, filters),
Expand Down Expand Up @@ -66,7 +68,10 @@ export function getHbdAssetTransactionsQueryOptions(
case "limit_order_create2" as HiveOperationName:
return true;
default:
return false;
// See the HIVE options: keep an operation the caller named explicitly,
// otherwise the filter UI offers operations this switch throws away.
// Unfiltered requests still fall through to `false`.
return requestedOperations.has(item.type);
}
})
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,47 @@ export function resolveHiveOperationFilters(filters: HiveOperationFilter): {
};
}

/**
* The operation names the caller asked for explicitly, ignoring group aliases.
*
* Used by the per-asset `select` filters so an operation a caller deliberately
* requested is never silently dropped just because the asset filter has no opinion
* about it. Passing no filter at all keeps the historical behaviour: the asset's own
* allow-list decides, and nothing extra leaks in.
*/
export function collectRequestedOperations(
filters: HiveOperationFilter
): Set<string> {
const rawValues = Array.isArray(filters) ? filters : [filters];
return new Set(
rawValues.filter(
(value): value is HiveOperationFilterValue =>
value !== undefined && value !== null && value !== ("" as HiveOperationGroup)
)
);
}

/**
* Cursor for `condenser_api.get_account_history`.
*
* A page comes back in ASCENDING `num` order, so the OLDEST entry is at index 0 and
* walking backwards means `page[0].num - 1`. Reading the LAST entry instead takes the
* NEWEST row, which advances the window by a single operation per page (a page of 1000
* overlaps its predecessor by 999) and, once `num` reaches 0, yields -1 — the "newest"
* sentinel `initialPageParam` uses — so the walk restarts at the head of the history and
* never terminates.
*/
export function getNextAccountHistoryPageParam(
lastPage: HiveTransaction[] | undefined
): number | undefined {
if (!lastPage?.length) {
return undefined;
}

const oldest = Number(lastPage[0]?.num ?? 0);
return Number.isFinite(oldest) && oldest > 0 ? oldest - 1 : undefined;
}

function makeBitMaskFilter(allowedOperations: number[]) {
let low = 0n;
let high = 0n;
Expand All @@ -98,13 +139,12 @@ export function getHiveAssetTransactionsQueryOptions(
filters: HiveOperationFilter = []
) {
const { filterArgs, filterKey } = resolveHiveOperationFilters(filters);
const requestedOperations = collectRequestedOperations(filters);

return infiniteQueryOptions<HiveTransaction[]>({
queryKey: ["assets", "hive", "transactions", username, limit, filterKey],
initialData: { pages: [], pageParams: [] },
initialPageParam: -1,
getNextPageParam: (lastPage, __) =>
lastPage ? +(lastPage[lastPage.length - 1]?.num ?? 0) - 1 : -1,
getNextPageParam: getNextAccountHistoryPageParam,

queryFn: async ({ pageParam }) => {
const response = await callRPC(
Expand Down Expand Up @@ -165,7 +205,13 @@ export function getHiveAssetTransactionsQueryOptions(
case "limit_order_create2" as HiveOperationName:
return true;
default:
return false;
// Keep an operation the caller asked for by name. Without this the
// filter UI advertises every operation while this switch silently
// discards the ones it has no opinion about, so picking e.g.
// `fill_transfer_from_savings` returns an empty list. Requests that
// pass no filter still fall through to `false`, so the unfiltered
// HIVE view is unchanged.
return requestedOperations.has(item.type);
}
})
),
Expand Down
7 changes: 7 additions & 0 deletions packages/wallets/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @ecency/wallets

## 5.0.80

### Patch Changes

- Updated dependencies []:
- @ecency/sdk@2.3.80

## 5.0.79

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/wallets/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ecency/wallets",
"private": false,
"version": "5.0.79",
"version": "5.0.80",
"description": "Ecency wallets",
"repository": {
"type": "git",
Expand Down
Loading