Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
129 commits
Select commit Hold shift + click to select a range
a1ea522
feat: Sovryn Perimeter Fee display on withdraw and close flows
tjcloa Aug 13, 2026
fccdf28
chore: changeset for the perimeter fee display (frontend minor)
tjcloa Aug 13, 2026
8589a5b
feat: Sovryn Perimeter withdrawal hold and vault page
tjcloa Aug 19, 2026
cd1e583
fix(perimeter): repoint the surface ids at the re-cut values
tjcloa Aug 20, 2026
b529d5e
docs(perimeter): say why the Zero surface id is declared but unused
tjcloa Aug 21, 2026
889afec
fix(perimeter): stop showing "no fee" when the fee is unknown
tjcloa Aug 21, 2026
75b53da
fix(perimeter): the Zero close path had the same conflation
tjcloa Aug 21, 2026
791a4e0
refactor(perimeter): make the unsafe charge test unreachable
tjcloa Aug 21, 2026
eb17b2a
fix(perimeter): don't print a receipt we cannot honour
tjcloa Aug 21, 2026
90ab5c0
fix(perimeter): a missing perimeter is not an unknown fee
tjcloa Aug 21, 2026
e562376
feat: say where a held withdrawal went, and let ready ones release to…
tjcloa Aug 27, 2026
b549640
review cycle 1: batch-release poisoning, false hold notice, status label
tjcloa Aug 27, 2026
6314bd1
Adopt the fee branch as authoritative for naming, addresses and deplo…
tjcloa Aug 29, 2026
b581c1c
Record the fee branch as merged into the delay line
tjcloa Aug 30, 2026
1679aa2
Let the dapp point at a local RSK fork via REACT_APP_RSK_RPC_OVERRIDE…
tjcloa Sep 2, 2026
84b2c3b
fix(perimeter): state the quote's origin in the renders-nothing cases
tjcloa Sep 3, 2026
85f851a
fix(perimeter): a fail-open Zero preview is not a zero fee
tjcloa Sep 3, 2026
1ef8701
test(perimeter): pin that a fail-open Zero preview reads as unknown
tjcloa Sep 3, 2026
1c5dbd2
fix(perimeter): fail hidden — no fee taken, no fee shown
tjcloa Sep 3, 2026
83373cc
fix(perimeter): never show the previous account's quote; exact surplu…
tjcloa Sep 4, 2026
bd8e123
fix(perimeter): a fee quoted before the protocol loaded must not be c…
tjcloa Sep 4, 2026
9a1f8fd
fix(perimeter): refuse an inconsistent Zero preview; state the pointe…
tjcloa Sep 4, 2026
d931ba3
Merge branch 'sovryn-perimeter-fee' into sovryn-perimeter-delay
tjcloa Sep 4, 2026
9bdbe2c
Merge branch 'develop' into sovryn-perimeter-delay
tjcloa Sep 4, 2026
8a14e63
fix(perimeter): a hold that could not be read is not a hold of zero
tjcloa Sep 8, 2026
a2adbed
build(frontend): make the typecheck actually check src
tjcloa Sep 8, 2026
346d7aa
fix(perimeter): the release callback reaches the transaction it settles
tjcloa Sep 8, 2026
5bb6da9
fix(perimeter): the vault page states only what it actually read
tjcloa Sep 8, 2026
f91efb5
fix(perimeter): the QA RPC override cannot reach a release, and says …
tjcloa Sep 8, 2026
0edf53e
fix(perimeter): a debt-only Zero adjust no longer announces a hold
tjcloa Sep 9, 2026
c33d271
test(perimeter): render the forms with a hold, and one that could not…
tjcloa Sep 9, 2026
d34da51
test(perimeter): the post-signature notice covers the hold it could n…
tjcloa Sep 9, 2026
15793e4
fix(test): stop the perimeter vault mock's mount effect from reading …
tjcloa Sep 11, 2026
6139f10
feat(perimeter): tell holders a contract-owned request delivers itself
tjcloa Sep 11, 2026
8177ce1
test(perimeter): the chain clock test waits past one full tick
tjcloa Sep 11, 2026
a69d138
fix(perimeter): a pointer is absent only when the node says the gette…
tjcloa Sep 11, 2026
962cdad
fix(perimeter): a delay quote that has not arrived is never shown as …
tjcloa Sep 11, 2026
53766ca
fix(perimeter): a blocked withdrawal looks ordinary until the holder …
tjcloa Sep 12, 2026
f1abb0c
fix(perimeter): release sends only what the queue will still accept
tjcloa Sep 12, 2026
1c47fca
fix(perimeter): a release is not sent from a wallet on another network
tjcloa Sep 12, 2026
dfb4efe
fix(perimeter): the Zero surplus claim shows its delay and quotes thr…
tjcloa Sep 12, 2026
f6e86ed
fix(perimeter): the Zero adjust the page uses fires the post-signatur…
tjcloa Sep 12, 2026
730753d
fix(perimeter): the vault page shows no rows against an unread chain …
tjcloa Sep 12, 2026
b7c6848
copy(perimeter): name the withdrawal delay and the Perimeter fee as d…
tjcloa Sep 12, 2026
de43912
test(perimeter): cache stand-ins start idle, like the shared cache
tjcloa Sep 12, 2026
57a025f
fix(perimeter): a release is checked again as it is sent, and never o…
tjcloa Sep 12, 2026
22c2f78
fix(perimeter): a release is not sent when the wallet's account chang…
tjcloa Sep 12, 2026
c0abbc7
fix(perimeter): a withdrawal reads as ready only once the latest bloc…
tjcloa Sep 12, 2026
4fc97e3
fix(perimeter): only a withdrawal paid to its receiver counts as deli…
tjcloa Sep 12, 2026
26c226f
fix(perimeter): a release dry run takes a yes only from a queue addre…
tjcloa Sep 12, 2026
5a11762
fix(perimeter): Release shows it is checking, takes no second press, …
tjcloa Sep 12, 2026
5da5019
copy(perimeter): name the withdrawal delay throughout the Perimeter p…
tjcloa Sep 12, 2026
fb547d9
fix(perimeter): a release is sent with gas headroom, and keeps a gas …
tjcloa Sep 12, 2026
682f1f2
fix(perimeter): a release refused before sending says nothing was sen…
tjcloa Sep 12, 2026
f0bcd30
copy(perimeter): the recovery lines say who moved a withdrawal and on…
tjcloa Sep 12, 2026
a0d7963
copy(perimeter): stop naming the withdrawal delay's operators as a mu…
tjcloa Sep 12, 2026
2df69f3
fix(perimeter): a release's gas limit never asks for more than the bl…
tjcloa Sep 12, 2026
baf6a62
fix(perimeter): a release check that passes after the dialog closes i…
tjcloa Sep 12, 2026
7c24c9e
docs(perimeter): say user, not holder, in the shared dialog's send-ch…
tjcloa Sep 12, 2026
7bc6cdb
perimeter: frozen state from chain, reason-free refusals, history fil…
tjcloa Sep 14, 2026
b39522b
Perimeter page: disable Release on the wrong network instead of faili…
tjcloa Sep 15, 2026
e27bf92
copy(perimeter): the frozen tooltip names no holding
tjcloa Sep 15, 2026
5fad4ea
Zero's adjust and close forms: wait for the Perimeter fee before Confirm
tjcloa Sep 15, 2026
fcc80e1
Surplus card: give the claim fee read a deadline like the delay quote
tjcloa Sep 15, 2026
3a84bc0
Vault: an unread block state marks the vault unknown instead of readi…
tjcloa Sep 15, 2026
b0794b0
Give the two recovery outcomes their own status, and stop naming the …
tjcloa Sep 15, 2026
8514659
Judge wrong network from the wallet too, and let the tooltip be hovered
tjcloa Sep 15, 2026
e966d5e
Send-check guard survives the dialog unmounting on close
tjcloa Sep 15, 2026
d5a8cab
History: an unread remembered withdrawal reads as unknown, not as nev…
tjcloa Sep 15, 2026
63ac9f2
Stop narrating which block state a refusal withholds; fix stale comme…
tjcloa Sep 16, 2026
61e67bb
Wrap the close form's dependency list to the line length the linter w…
tjcloa Sep 16, 2026
0fd32e5
Correct the state function's doc comment about the contract's check o…
tjcloa Sep 16, 2026
f740493
Rewrite the fee helper's private-on-purpose comment to name the conse…
tjcloa Sep 16, 2026
f87a8d3
A withdrawal the node did not state now reads as unreadable, not settled
tjcloa Sep 16, 2026
641ce56
Give the vault and the chain clock a deadline of their own, so a failed
tjcloa Sep 16, 2026
7c07eb6
Pin Confirm waiting on the fee quote on the lender, borrower and surplus
tjcloa Sep 16, 2026
c8f149a
Pin the lending fee quote's own deadline against a stalled node
tjcloa Sep 16, 2026
1cd998f
Pin that a held withdrawal stays listed, not just uncontrolled, while…
tjcloa Sep 16, 2026
dcdbdb9
Strip release plans and history from perimeter fee/delay comments and…
tjcloa Sep 16, 2026
70e002f
Hand the history hook the page's own filtered exit list
tjcloa Sep 16, 2026
d2782f4
Stop the chain clock from emptying the history table
tjcloa Sep 16, 2026
d3345a6
Show no release time on a row nothing will ever be released from
tjcloa Sep 16, 2026
f61f722
Fold a failed owner-code read into the vault's unknown flag
tjcloa Sep 16, 2026
d556d4d
Mark history unknown for any status the queue does not define
tjcloa Sep 16, 2026
f338516
Classify an inconsistent Zero fee preview as unread, not a stated zero
tjcloa Sep 16, 2026
bdc4dd8
Scope the paused banner to what is actually paused
tjcloa Sep 16, 2026
db3728e
Wrap the rows memo's dependency list to the line length the linter wants
tjcloa Sep 16, 2026
59c4055
Scope the paused banner and row tooltip to what they actually describe
tjcloa Sep 16, 2026
2c823ca
Stop the history table's loader waiting on reads history does not need
tjcloa Sep 16, 2026
f8c3b51
Stop calling the position owner and the Owner role by the same word
tjcloa Sep 16, 2026
6e6fb8a
Widen the copy suite to pin every banned term and role, not three of …
tjcloa Sep 16, 2026
3177378
test(PerimeterPage): drop a history loading-message case that could n…
tjcloa Sep 16, 2026
7058342
Drop a late completion that belongs to a superseded shared-cache read
tjcloa Sep 16, 2026
63abdb1
Stamp the history read with the account it belongs to, not only the s…
tjcloa Sep 16, 2026
9a67d31
Bound the release check's two wallet reads like every other read it m…
tjcloa Sep 16, 2026
1c6f0af
Rewrite four comments and test names to behaviour only
tjcloa Sep 16, 2026
81a4bb7
Scope the vault's "could not read" paragraph to the live list
tjcloa Sep 16, 2026
7319bbe
Show a held withdrawal as unreadable, not Ready, when its block check…
tjcloa Sep 16, 2026
82d892b
Retry a just-released withdrawal in history before leaving it out of …
tjcloa Sep 16, 2026
9cb23f1
Bound every raw chain read the withdrawal page makes, not only releas…
tjcloa Sep 16, 2026
6c974b8
Qualify the surplus card and close form when the Perimeter fee can't …
tjcloa Sep 16, 2026
fe55441
Trust a completed release over a lagging history read (owner decision…
tjcloa Sep 17, 2026
3aa0156
Scope release receipts and released-key filtering to the connected ac…
tjcloa Sep 17, 2026
1462ffe
docs(exitDelay): note the post-arming cache window next to the quote
tjcloa Sep 17, 2026
0282227
Carry a resolved withdrawal's real status into its release receipt
tjcloa Sep 17, 2026
81339fe
Let a receiver-only account release its own held withdrawal
tjcloa Sep 18, 2026
9ca74b2
Cover receiver-only release in the Perimeter vault tests
tjcloa Sep 18, 2026
fb37dd7
Re-price a configured gas limit against a fresh estimate before sending
tjcloa Sep 19, 2026
0bbc98a
Force a bounded requote so an open withdraw form is never left stale
tjcloa Sep 19, 2026
b950f4e
Scope `force` on the requote timer, not every block-driven refetch
tjcloa Sep 19, 2026
b0f8526
Add a measured gas-limit floor to the Zero surplus claim
tjcloa Sep 19, 2026
be5b0e6
Raise the borrower withdraw-collateral floor to its measured delay-ar…
tjcloa Sep 19, 2026
00251b2
Raise Zero's two close-credit-line floors to their measured delay-arm…
tjcloa Sep 19, 2026
68606d0
Remove the Perimeter vault's per-device withdrawal history
tjcloa Sep 20, 2026
054d0a3
Describe the local-storage cleanup functionally, not by its past
tjcloa Sep 20, 2026
7cc200e
Merge remote-tracking branch 'origin/develop' into sovryn-perimeter-d…
tjcloa Sep 22, 2026
d58a2c4
Force the underlying quote too, not only its outer cache entry
tjcloa Sep 22, 2026
5f495f9
Move the Perimeter vault into the account menu
tjcloa Sep 29, 2026
d73b89a
Say in the release note that the Perimeter vault is reached from the …
tjcloa Sep 29, 2026
5ca1714
Name the Perimeter page "Perimeter withdraw queue" and drop "vault" f…
tjcloa Sep 29, 2026
19245f4
Show time to unlock in whole minutes, never seconds
tjcloa Sep 29, 2026
7389747
Fix the hold toast blanking the app when a held withdrawal is confirmed
tjcloa Sep 29, 2026
e31af4e
fix(perimeter): type queue read results to restore frontend typecheck
tjcloa Oct 1, 2026
339c52a
Name the account menu entry Perimeter queue for withdrawal discovery
tjcloa Oct 1, 2026
680299b
Create approved QA-free Perimeter frontend integration
tjcloa Oct 4, 2026
28b0a89
Fix Perimeter queue menu assertion after approved label rename
tjcloa Oct 6, 2026
f3cd73e
Qualify Perimeter queue reads that reach the pagination cap
tjcloa Oct 7, 2026
b041940
Bind Perimeter releases to the configured chain at wallet submission
tjcloa Oct 7, 2026
690e5b6
Keep Perimeter queues discoverable after consumer rollback
tjcloa Oct 7, 2026
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
5 changes: 5 additions & 0 deletions .changeset/perimeter-hold-review-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'frontend': patch
---

Perimeter withdraw queue fixes: exclude a just-released hold from the batch release so it cannot revert the whole atomic transaction; stop showing the withdrawal-hold notice on a borrow or add-collateral adjust that removes nothing; correct the "Releasable by the owner" status label and use the full "Sovryn Perimeter withdraw queue" name in the hold notification.
5 changes: 5 additions & 0 deletions .changeset/perimeter-hold-visibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'frontend': minor
---

Held withdrawals now say where the money went: a visible Perimeter withdraw queue notice with a link in every charged form, and a post-signature notification on the withdrawal itself. The Perimeter withdraw queue page is reached from the Perimeter entry in the account menu and gains a one-transaction release of all ready holds.
5 changes: 5 additions & 0 deletions .changeset/perimeter-withdrawal-hold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'frontend': minor
---

feat: Sovryn Perimeter withdrawal hold. Shows how long a withdrawal will be held before it is paid out, on lending withdrawals, borrower exits and Zero collateral withdrawal/close, and adds a Perimeter withdraw queue page at /perimeter listing the withdrawals currently held for the connected account with the state each is in and a Release action for the ones that are ready. Also corrects the on-chain surface ids after the contracts renamed them, without which the existing fee display would silently stop matching any configured policy. Gated purely on on-chain state and fail-hidden: while the perimeter is undeployed, unwired or disabled every form renders exactly as it does today and the withdraw queue page reads as empty.
3 changes: 3 additions & 0 deletions apps/frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ yarn --cwd apps/frontend serve
To use custom environment variables locally, create a file named `.env.local` and add required values (see `.env.example` for sample variables and values).

For environment variables used in deployments see `netlify.toml`.

For optional deployment inputs that keep Perimeter withdrawals discoverable after
consumer rollback, see [Perimeter queue discovery](docs/perimeter-queue-discovery.md).
41 changes: 41 additions & 0 deletions apps/frontend/docs/perimeter-queue-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Perimeter queue discovery after rollback

The withdraw queue page follows the lending and Zero consumer queue pointers and
remembers discovered queues for the current tab. A fresh tab after those getters
are retired also needs a deployment queue recorded in the frontend build.

These optional public build inputs are selected only for the configured Rootstock
read network. They are compiled into the bundle and must contain no secrets.

| Network | Queue address | Expected code hash |
| ------------------ | --------------------------------------- | ---------------------------------------------------- |
| Mainnet (chain 30) | `REACT_APP_PERIMETER_QUEUE_RSK_MAINNET` | `REACT_APP_PERIMETER_QUEUE_RSK_MAINNET_RUNTIME_HASH` |
| Testnet (chain 31) | `REACT_APP_PERIMETER_QUEUE_RSK_TESTNET` | `REACT_APP_PERIMETER_QUEUE_RSK_TESTNET_RUNTIME_HASH` |

Leave both inputs unset when no Perimeter queue has been deployed. An unset pair
preserves the existing Phase 1 absence behavior. A nonzero address requires a
32-byte `0x`-prefixed hash. Mainnet inputs are never reused for testnet or another
chain. Invalid or incomplete inputs, a provider network mismatch, empty or
mismatched code, and failed validation reads produce the existing unreadable
warning; the unverified fallback address is not queried for withdrawals.

The hash is **Keccak-256 of the complete runtime bytes returned by `eth_getCode`
at the queue address**, including metadata. For an ERC1967Proxy deployment this
is the **proxy runtime code hash**, not the implementation hash. Matching that
hash does not establish the proxy's implementation, storage slots, initialization,
authority or full implementation runtime. Those require independent deployment
and release verification before these public build inputs are supplied.

Validated fallback queues are unioned with consumer-derived and remembered
queues. Duplicate queue addresses and request IDs are removed. Consumer read
failures remain visible even when fallback withdrawals can be listed. Validation
uses the existing 30-second cache and bounded reads. Each validation asks every
available JSON-RPC backend for `eth_chainId`; cached static-provider network
metadata is insufficient. A mismatching or unreadable backend rejects the
fallback validation. Keep the standalone queue
inputs in subsequent builds while it has outstanding requests after rollback.

Use the actual deployment address and independently verified expected code hash;
unit-test addresses and dummy bytecode are not deployment inputs. Changes to
these values require a new frontend build. This fallback restores discovery;
release eligibility, signing checks and on-chain payment rules remain separate.
1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"test": "craco test --watchAll=false --passWithNoTests",
"test:staged": "craco test --watchAll=false --passWithNoTests --bail --onlyChanged",
"lint": "eslint -c .eslintrc.js ./",
"typecheck": "tsc -p tsconfig.typecheck.json",
"generate:graphql": "graphql-codegen",
"generate:graphql:fetch:testnet": "env-cmd -f .env.development.local graphql-codegen -c codegen.fetch.yml",
"generate:graphql:fetch:mainnet": "env-cmd -f .env.staging graphql-codegen -c codegen.fetch.yml",
Expand Down
10 changes: 10 additions & 0 deletions apps/frontend/scripts/typecheck-stubs/opaque.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Stand-in for a dependency whose shipped types this project's TypeScript
* cannot parse. `skipLibCheck` does not help: those errors are SYNTACTIC, and
* tsc stops before reporting a single semantic diagnostic — which is why the
* typecheck passed while nothing in `src/` was being checked at all.
* Redirecting the offending modules here keeps them out of the parse, at the
* cost of typing them as `any`.
*/
declare const opaque: any;
export = opaque;
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import React from 'react';

import 'jest-canvas-mock';
import { MemoryRouter } from 'react-router-dom';

import { ChainIds } from '@sovryn/ethers-provider';

import { i18n } from '../../../locales/i18n';
import { ConnectWalletButton } from './ConnectWalletButton';

const ACCOUNT = '0x1111111111111111111111111111111111111111';
const ID = 'dapp-header-connect';

let mockChainId: string;

jest.mock('nanoid', () => ({ nanoid: () => '1234' }));

jest.mock('../../../contexts/NotificationContext', () => ({
useNotificationContext: () => ({ addNotification: jest.fn() }),
}));

jest.mock('../../../hooks/useChainStore', () => ({
useCurrentChain: () => mockChainId,
}));

const openMenu = (chainId: string = ChainIds.RSK_MAINNET) => {
mockChainId = chainId;
const view = render(
<MemoryRouter>
<ConnectWalletButton
onConnect={jest.fn()}
onDisconnect={jest.fn()}
address={ACCOUNT}
dataAttribute={ID}
/>
</MemoryRouter>,
);
userEvent.click(screen.getByText(/^0x11/));
return view;
};

const menuEntry = (name: string) =>
document.querySelector(
`[data-layout-id="${ID}-menu-${name}"]`,
) as HTMLElement;

describe('ConnectWalletButton account menu', () => {
beforeAll(async () => {
await i18n;
});

beforeEach(() => {
// The address badge draws an identicon on a canvas; the test config resets
// the canvas mock before every test, so give the canvas back a minimal
// 2d context.
HTMLCanvasElement.prototype.getContext = (() => ({
fillStyle: '',
fillRect: () => {},
})) as unknown as HTMLCanvasElement['getContext'];
HTMLCanvasElement.prototype.toDataURL = () => '';
});

it('opens the Perimeter page from the account menu', () => {
openMenu();

const entry = menuEntry('perimeter');
expect(entry).toHaveTextContent(/^Perimeter queue$/);
expect(entry.closest('a')).toHaveAttribute('href', '/perimeter');
});

it('lists the Perimeter entry after History and before Notifications', () => {
openMenu();

const order = Array.from(
document.querySelectorAll(`[data-layout-id^="${ID}-menu-"]`),
)
.map(node =>
node.getAttribute('data-layout-id')!.replace(`${ID}-menu-`, ''),
)
.filter(name =>
[
'portfolio',
'rewards',
'history',
'perimeter',
'notifications',
].includes(name),
);

expect(order).toEqual([
'portfolio',
'rewards',
'history',
'perimeter',
'notifications',
]);
});

it('shows the Perimeter entry on every chain, unlike Notifications', () => {
openMenu(ChainIds.BOB_MAINNET);

expect(menuEntry('perimeter')).not.toHaveClass('hidden');
expect(menuEntry('notifications')).toHaveClass('hidden');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ export const ConnectWalletButton: FC<
dataAttribute={`${dataAttribute}-menu-history`}
/>
</Link>
<Link to="/perimeter" className="no-underline">
<MenuItem
text={t(translations.connectWalletButton.perimeter)}
dataAttribute={`${dataAttribute}-menu-perimeter`}
/>
</Link>
<MenuItem
text={t(translations.connectWalletButton.notifications)}
onClick={handleSettingsClick}
Expand Down
123 changes: 123 additions & 0 deletions apps/frontend/src/app/2_molecules/ExitDelayRow/ExitDelayRow.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { fireEvent, render, screen } from '@testing-library/react';

import React from 'react';

import 'jest-canvas-mock';
import { MemoryRouter } from 'react-router-dom';

import { i18n } from '../../../locales/i18n';
import { ExitDelayRow } from './ExitDelayRow';

jest.mock('nanoid', () => {
return { nanoid: () => '1234' };
});

jest.mock('../../../contexts/NotificationContext', () => {
return {
useNotificationContext: () => ({
addNotification: jest.fn(),
}),
};
});

const renderRow = (delaySeconds: number, unknown = false, loading = false) =>
render(
<MemoryRouter>
<ExitDelayRow
delaySeconds={delaySeconds}
unknown={unknown}
loading={loading}
/>
</MemoryRouter>,
);

describe('ExitDelayRow', () => {
beforeAll(async () => {
await i18n;
});

it('renders nothing when the perimeter imposes no delay', () => {
const { container } = renderRow(0);
expect(container).toBeEmptyDOMElement();
});

it('shows the hold duration in whole units', () => {
const { container } = renderRow(172800);
expect(screen.getByText('Withdrawal delay')).toBeInTheDocument();
expect(
container.querySelector('[data-layout-id="exit-delay-duration"]'),
).toHaveTextContent('2 days');
});

it('uses the singular form for a one-unit hold', () => {
const { container } = renderRow(3600);
expect(
container.querySelector('[data-layout-id="exit-delay-duration"]'),
).toHaveTextContent('1 hour');
});

it('rounds a partial unit up, never down', () => {
const { container } = renderRow(5400);
expect(
container.querySelector('[data-layout-id="exit-delay-duration"]'),
).toHaveTextContent('2 hours');
});

it('names the withdraw queue and links to the Perimeter page, visibly', () => {
const { container } = renderRow(3600);
const notice = container.querySelector(
'[data-test-id="exit-delay-vault-notice"]',
);
expect(notice).toBeInTheDocument();
expect(notice).toHaveTextContent(/Perimeter withdraw queue/i);
const link = container.querySelector(
'[data-test-id="exit-delay-vault-link"]',
);
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', '/perimeter');
});

it('admits it could not check, instead of rendering as an unheld withdrawal', () => {
// The delay fails closed on chain: a quote we could not read means the
// withdrawal is held or it reverts. Rendering nothing would tell the user
// the money arrives now.
const { container } = renderRow(0, true);
expect(container).not.toBeEmptyDOMElement();
expect(
container.querySelector('[data-layout-id="exit-delay-unknown"]'),
).toHaveTextContent('Could not be checked');
const notice = container.querySelector(
'[data-test-id="exit-delay-unknown-notice"]',
);
expect(notice).toHaveTextContent(/could not check/i);
expect(notice).toHaveTextContent(/Perimeter withdraw queue/i);
expect(
container.querySelector('[data-layout-id="exit-delay-duration"]'),
).not.toBeInTheDocument();
});

it('says it is still checking while the quote is in flight, instead of rendering as an unheld withdrawal', () => {
const { container } = renderRow(0, false, true);
expect(
container.querySelector('[data-layout-id="exit-delay-checking"]'),
).toHaveTextContent(/Checking/);
expect(
container.querySelector('[data-layout-id="exit-delay-duration"]'),
).not.toBeInTheDocument();
expect(
container.querySelector('[data-layout-id="exit-delay-unknown"]'),
).not.toBeInTheDocument();
});

it('explains where the funds go, on click', () => {
const { container } = renderRow(3600);
const helper = container.querySelector(
'[data-layout-id="exit-delay-helper"]',
);
expect(helper).toBeInTheDocument();
fireEvent.click(helper!);
expect(
screen.getByText(/held in the Sovryn Perimeter withdraw queue/i),
).toBeInTheDocument();
});
});
Loading
Loading