Skip to content

Commit 79ce1da

Browse files
authored
Merge pull request #1280 from input-output-hk/ensemble/1185/spo-tickers-in-explorer
SPO tickers in explorer
2 parents 8e70245 + 83fb23f commit 79ce1da

File tree

13 files changed

+331
-8
lines changed

13 files changed

+331
-8
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {render, screen} from '@testing-library/react'
2+
import '@testing-library/jest-dom'
3+
import {initStore} from "./helpers";
4+
import {Provider} from "react-redux";
5+
import {poolsSlice} from "../src/store/poolsSlice";
6+
import PoolTicker from "../src/components/PoolTicker";
7+
import {getCExplorerUrlForPool} from "../src/utils";
8+
9+
function renderPoolTickerComponent(aggregator, partyId, default_state = undefined) {
10+
const store = initStore(default_state);
11+
return [
12+
render(
13+
<Provider store={store}>
14+
<PoolTicker aggregator={aggregator} partyId={partyId}/>
15+
</Provider>
16+
),
17+
store
18+
];
19+
}
20+
21+
describe('PoolTicker', () => {
22+
it('Pool ticker not on the three main network doesn\'t show link to cexplorer', () => {
23+
const partyId = "pool1zmtm8yef33z2n7x4nn0kvv9xpzjuj7725p9y9m5t960g5qy51ua";
24+
const poolTicker = "[MITHRIL] Mithril Signer";
25+
renderPoolTickerComponent(
26+
"myaggregator",
27+
partyId,
28+
{
29+
pools: {
30+
...poolsSlice.getInitialState(),
31+
list: [
32+
{
33+
aggregator: "myaggregator",
34+
network: "devnet",
35+
pools: [{
36+
"party_id": partyId,
37+
"pool_ticker": poolTicker,
38+
"has_registered": true,
39+
}],
40+
}],
41+
},
42+
});
43+
44+
expect(screen.getByText(poolTicker));
45+
expect(screen.queryByRole('link')).toBe(null);
46+
});
47+
48+
it.each(["mainnet", "preprod", "preview"])
49+
('Pool ticker on %s network link to cexplorer', (network) => {
50+
const partyId = "pool1zmtm8yef33z2n7x4nn0kvv9xpzjuj7725p9y9m5t960g5qy51ua";
51+
const poolTicker = "[MITHRIL] Mithril Signer";
52+
renderPoolTickerComponent(
53+
"myaggregator",
54+
partyId,
55+
{
56+
pools: {
57+
...poolsSlice.getInitialState(),
58+
list: [
59+
{
60+
aggregator: "myaggregator",
61+
network: network,
62+
pools: [{
63+
"party_id": partyId,
64+
"pool_ticker": poolTicker,
65+
"has_registered": true,
66+
}],
67+
}],
68+
},
69+
});
70+
71+
expect(screen.getByText(poolTicker));
72+
expect(screen.getByRole('link')).toHaveAttribute('href', getCExplorerUrlForPool(network, partyId));
73+
});
74+
75+
it.each(["mainnet", "preprod", "preview"])
76+
('Not available Pool ticker on %s network still show link to cexplorer', (network) => {
77+
const partyId = "pool1zmtm8yef33z2n7x4nn0kvv9xpzjuj7725p9y9m5t960g5qy51ua";
78+
renderPoolTickerComponent(
79+
"myaggregator",
80+
partyId,
81+
{
82+
pools: {
83+
...poolsSlice.getInitialState(),
84+
list: [
85+
{
86+
aggregator: "myaggregator",
87+
network: network,
88+
pools: [{
89+
"party_id": partyId,
90+
"has_registered": true,
91+
}],
92+
}],
93+
},
94+
});
95+
96+
expect(screen.getByText("Not available"));
97+
expect(screen.getByRole('link')).toHaveAttribute('href', getCExplorerUrlForPool(network, partyId));
98+
});
99+
100+
it.each(["mainnet", "preprod", "preview"])
101+
('Not available Pool ticker on %s network still show link to cexplorer even without pools data', (network) => {
102+
const partyId = "pool1zmtm8yef33z2n7x4nn0kvv9xpzjuj7725p9y9m5t960g5qy51ua";
103+
renderPoolTickerComponent(
104+
"myaggregator",
105+
partyId,
106+
{
107+
pools: {
108+
...poolsSlice.getInitialState(),
109+
list: [
110+
{
111+
aggregator: "myaggregator",
112+
network: network,
113+
pools: [],
114+
}],
115+
},
116+
});
117+
118+
expect(screen.getByText("Not available"));
119+
expect(screen.getByRole('link')).toHaveAttribute('href', getCExplorerUrlForPool(network, partyId));
120+
});
121+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {formatPartyId} from "../src/utils";
2+
3+
describe('Stake formatting', () => {
4+
it.each([
5+
["pool1zmtm8yef33z2n7x4nn0kvv9xpzjuj7725p9y9m5t960g5qy51ua", "pool1zmtm8…y51ua"],
6+
["pool23kk0fksdayg23htnj372avmnwwql9c2zz0ah8jt63rjdzyjr95n", "pool23kk0f…jr95n"],
7+
])('formatting party id remove all but the 10 first and the last 5 characters', (partyId, expected) => {
8+
expect(formatPartyId(partyId)).toEqual(expected);
9+
});
10+
11+
it.each([
12+
["pool1zmtm8yef33zuj7725p9y9m5t960g5qy51ua", "pool1zmtm8…y51ua"],
13+
["pool1zmtm8yef33z22n7x4nn0kvv9xpzjn7x4nn0kvv9xpzjuj7725p9y9m5t960g5qy51ua", "pool1zmtm8…y51ua"],
14+
["pool23kk0fyjr95n", "pool23kk0f…jr95n"],
15+
["pool23kk0fjr95n", "pool23kk0fjr95n"],
16+
["pool23kk0jr95n", "pool23kk0jr95n"],
17+
["pool25n", "pool25n"],
18+
])('formatting party id remove all but the 10 first and the last 5 characters even for non usual length', (partyId, expected) => {
19+
expect(formatPartyId(partyId)).toEqual(expected);
20+
});
21+
});

mithril-explorer/__tests__/store.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "../src/store/settingsSlice";
99
import default_available_aggregators from "../src/aggregators-list";
1010
import {initStore} from "./helpers";
11+
import {poolsSlice} from "../src/store/poolsSlice";
1112

1213
describe('Store Initialization', () => {
1314
it('init with settings initialState without local storage', () => {
@@ -19,6 +20,7 @@ describe('Store Initialization', () => {
1920
it('init with local storage saved state', () => {
2021
let aggregators = [...default_available_aggregators, "https://aggregator.test"];
2122
let expected = {
23+
pools: poolsSlice.getInitialState(),
2224
settings: {
2325
...settingsSlice.getInitialState(),
2426
selectedAggregator: aggregators.at(aggregators.length - 1),
@@ -36,6 +38,7 @@ describe('Store Initialization', () => {
3638
const initialAggregator = default_available_aggregators.at(1);
3739
let aggregators = [...default_available_aggregators, "https://aggregator.test"];
3840
let expected = {
41+
pools: poolsSlice.getInitialState(),
3942
settings: {
4043
...settingsSlice.getInitialState(),
4144
selectedAggregator: aggregators.at(aggregators.length - 1),
5.15 KB
Loading

mithril-explorer/src/app/page.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import SnapshotsList from '../components/Artifacts/SnapshotsList';
1111
import MithrilStakeDistributionsList from "../components/Artifacts/MithrilStakeDistributionsList";
1212
import {aggregatorSearchParam} from "../constants";
1313
import {selectAggregator, selectedAggregator as currentlySelectedAggregator} from "../store/settingsSlice";
14+
import {updatePoolsForAggregator} from "../store/poolsSlice";
1415

1516
// Disable SSR for the following components since they use data from the store that are not
1617
// available server sides (because those data can be read from the local storage).
@@ -26,6 +27,7 @@ export default function Explorer() {
2627
const [isUpdatingAggregatorInUrl, setIsUpdatingAggregatorInUrl] = useState(false);
2728
const selectedAggregator = useSelector(currentlySelectedAggregator);
2829

30+
2931
// Update the aggregator in the url query
3032
useEffect(() => {
3133
const aggregatorInUrl = searchParams.get(aggregatorSearchParam);
@@ -37,6 +39,8 @@ export default function Explorer() {
3739
setIsUpdatingAggregatorInUrl(true);
3840
router.push("?" + params.toString(), undefined, {shallow: true});
3941
}
42+
43+
dispatch(updatePoolsForAggregator(selectedAggregator));
4044
}, [selectedAggregator]);
4145

4246
// Allow navigation to work (previous, next)

mithril-explorer/src/app/registrations/page.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import {useSearchParams} from "next/navigation";
44
import {useCallback, useEffect, useState} from "react";
5+
import {useDispatch} from "react-redux";
56
import {checkUrl, setChartJsDefaults, toAda} from "../../utils";
67
import {Alert, ButtonGroup, Col, Row, Spinner, Stack, Table} from "react-bootstrap";
78
import {ArcElement, BarElement, CategoryScale, Chart, Legend, LinearScale, Title, Tooltip} from 'chart.js';
@@ -11,6 +12,9 @@ import LinkButton from "../../components/LinkButton";
1112
import Stake from "../../components/Stake";
1213
import RawJsonButton from "../../components/RawJsonButton";
1314
import VerifiedBadge from "../../components/VerifiedBadge";
15+
import PoolTicker from "../../components/PoolTicker";
16+
import {updatePoolsForAggregator} from "../../store/poolsSlice";
17+
import PartyId from "../../components/PartyId";
1418

1519
Chart.register(
1620
ArcElement,
@@ -25,6 +29,7 @@ Chart.register(
2529
setChartJsDefaults(Chart);
2630

2731
export default function Registrations() {
32+
const dispatch = useDispatch();
2833
const searchParams = useSearchParams();
2934
const [isLoading, setIsLoading] = useState(true);
3035
const [currentError, setCurrentError] = useState(undefined);
@@ -74,6 +79,8 @@ export default function Registrations() {
7479
setCurrentEpoch(undefined);
7580
console.error("Fetch current epoch in epoch-settings error:", error);
7681
});
82+
83+
dispatch(updatePoolsForAggregator(aggregator));
7784
} else {
7885
setCurrentError(error);
7986
}
@@ -244,14 +251,20 @@ export default function Registrations() {
244251
<tr>
245252
<th>#</th>
246253
<th>Party id</th>
254+
<th>Pool Ticker</th>
247255
<th style={{textAlign: "end"}}>Stake</th>
248256
</tr>
249257
</thead>
250258
<tbody>
251259
{registrations.map((signer, index) =>
252260
<tr key={signer.party_id}>
253261
<td>{index}</td>
254-
<td><VerifiedBadge tooltip="Verified Signer"/>{' '}{signer.party_id}</td>
262+
<td className="text-break">
263+
<VerifiedBadge tooltip="Verified Signer"/>
264+
{' '}
265+
<PartyId partyId={signer.party_id}/>
266+
</td>
267+
<td><PoolTicker aggregator={aggregator} partyId={signer.party_id}/></td>
255268
<td style={{textAlign: "end"}}><Stake lovelace={signer.stake}/></td>
256269
</tr>
257270
)}

mithril-explorer/src/components/CertificateModal/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import {Badge, Button, Col, Container, ListGroup, Modal, Row, Table} from "react
33
import {useSelector} from "react-redux";
44
import RawJsonButton from "../RawJsonButton";
55
import Stake from "../Stake";
6-
import VerifiedBadge from '../VerifiedBadge';
76
import ProtocolParameters from "../ProtocolParameters";
7+
import PoolTicker from "../PoolTicker";
8+
import VerifiedBadge from '../VerifiedBadge';
89
import {selectedAggregator} from "../../store/settingsSlice";
10+
import PartyId from "../PartyId";
911

1012
export default function CertificateModal(props) {
1113
const [certificate, setCertificate] = useState({});
1214
const certificateEndpoint = useSelector((state) => `${selectedAggregator(state)}/certificate/${props.hash}`);
15+
const aggregator = useSelector(selectedAggregator);
1316

1417
useEffect(() => {
1518
if (!props.hash) {
@@ -82,6 +85,7 @@ export default function CertificateModal(props) {
8285
<tr>
8386
<th></th>
8487
<th>Party id</th>
88+
<th>Pool ticker</th>
8589
<th style={{textAlign: "end"}}>Stake</th>
8690
</tr>
8791
</thead>
@@ -93,7 +97,8 @@ export default function CertificateModal(props) {
9397
<VerifiedBadge tooltip="Verified Signer"/>
9498
}
9599
</td>
96-
<td>{signer.party_id}</td>
100+
<td><PartyId partyId={signer.party_id}/></td>
101+
<td><PoolTicker aggregator={aggregator} partyId={signer.party_id}/></td>
97102
<td style={{textAlign: "end"}}><Stake lovelace={signer.stake}/></td>
98103
</tr>
99104
)}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import {Button, OverlayTrigger, Tooltip} from "react-bootstrap";
3+
import {formatPartyId} from "../utils";
4+
5+
export default function PartyId({partyId}) {
6+
function copyToClipboard() {
7+
if (window.isSecureContext && partyId) {
8+
navigator.clipboard.writeText(partyId).then(() => {
9+
});
10+
}
11+
}
12+
13+
return (
14+
<span className="text-break">
15+
{partyId}<> </>
16+
<OverlayTrigger overlay={<Tooltip>Copy</Tooltip>}>
17+
<Button variant="link" onClick={copyToClipboard} size="md" className="p-0">
18+
<i className="bi bi-copy" style={{color: 'black'}}></i>
19+
</Button>
20+
</OverlayTrigger>
21+
</span>
22+
);
23+
}

mithril-explorer/src/components/PendingCertificate/index.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React, {useEffect, useState} from 'react';
22
import {Card, CardGroup, ListGroup} from "react-bootstrap";
33
import {useSelector} from "react-redux";
4+
import PartyId from "../PartyId";
5+
import PoolTicker from "../PoolTicker";
46
import RawJsonButton from "../RawJsonButton";
57
import SignedEntityType from "../SignedEntityType";
68
import VerifiedBadge from '../VerifiedBadge';
@@ -9,6 +11,7 @@ import {selectedAggregator} from "../../store/settingsSlice";
911
export default function PendingCertificate(props) {
1012
const [pendingCertificate, setPendingCertificate] = useState({});
1113
const pendingCertificateEndpoint = useSelector((state) => `${selectedAggregator(state)}/certificate-pending`);
14+
const aggregator = useSelector(selectedAggregator);
1215
const autoUpdate = useSelector((state) => state.settings.autoUpdate);
1316
const updateInterval = useSelector((state) => state.settings.updateInterval);
1417

@@ -68,10 +71,11 @@ export default function PendingCertificate(props) {
6871
? <div>No Signers registered</div>
6972
: <>
7073
<ListGroup variant="flush">
71-
<ListGroup.Item><b>Party id</b></ListGroup.Item>
74+
<ListGroup.Item><b>Pools</b></ListGroup.Item>
7275
{pendingCertificate.signers.map(signer =>
7376
<ListGroup.Item key={signer.party_id}>
74-
{signer.party_id}
77+
<PoolTicker partyId={signer.party_id} aggregator={aggregator}/><br/>
78+
<PartyId partyId={signer.party_id}/>
7579
{signer.verification_key_signature &&
7680
<div className="float-end">
7781
<VerifiedBadge tooltip="Verified Signer"/>
@@ -91,10 +95,11 @@ export default function PendingCertificate(props) {
9195
? <div>No Signers registered for next epoch</div>
9296
: <>
9397
<ListGroup variant="flush">
94-
<ListGroup.Item><b>Party id</b></ListGroup.Item>
98+
<ListGroup.Item><b>Pools</b></ListGroup.Item>
9599
{pendingCertificate.next_signers.map(signer =>
96100
<ListGroup.Item key={signer.party_id}>
97-
{signer.party_id}
101+
<PoolTicker partyId={signer.party_id} aggregator={aggregator}/><br/>
102+
<PartyId partyId={signer.party_id}/>
98103
{signer.verification_key_signature &&
99104
<div className="float-end">
100105
<VerifiedBadge tooltip="Verified Signer"/>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, {useEffect, useState} from "react";
2+
import {useSelector} from "react-redux";
3+
import {getPool} from "../store/poolsSlice";
4+
import {getCExplorerUrlForPool} from "../utils";
5+
import Image from "next/image";
6+
import {OverlayTrigger, Tooltip} from "react-bootstrap";
7+
8+
export default function PoolTicker({aggregator, partyId, ...props}) {
9+
const pool = useSelector((state) => getPool(state, aggregator, partyId));
10+
const [url, setUrl] = useState(undefined);
11+
12+
useEffect(() => {
13+
if (pool?.network) {
14+
setUrl(getCExplorerUrlForPool(pool.network, partyId));
15+
} else {
16+
setUrl(undefined);
17+
}
18+
}, [partyId, pool.network])
19+
20+
return (url !== undefined)
21+
? <>
22+
<a href={url} target="_blank" className="link-dark link-underline-light">
23+
<OverlayTrigger overlay={<Tooltip>See in CExplorer</Tooltip>}>
24+
<span>
25+
<Image src="/explorer/cexplorer_logo.png"
26+
alt="CExplorer Logo"
27+
style={{verticalAlign:"text-top"}}
28+
width={20} height={20}/>
29+
<> </>
30+
{pool.pool_ticker ?? "Not available"}
31+
</span>
32+
</OverlayTrigger>
33+
</a>
34+
</>
35+
: <span>{pool.pool_ticker}</span>;
36+
}

0 commit comments

Comments
 (0)