Skip to content

Commit 37919d9

Browse files
authored
Merge branch 'main' into bugfix-build-tx-soroban
2 parents 4ccfd8d + cd7fd2d commit 37919d9

15 files changed

Lines changed: 488 additions & 26 deletions

File tree

src/app/(sidebar)/smart-contracts/contract-explorer/components/ContractInfo.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -375,11 +375,7 @@ export const ContractInfo = ({
375375
</Card>
376376

377377
<Card>
378-
<Box
379-
gap="lg"
380-
data-testid="contract-info-contract-container"
381-
addlClassName="ContractInfo__tabs"
382-
>
378+
<Box gap="lg" addlClassName="ContractInfo__tabs">
383379
<Box gap="sm" direction="row" align="center">
384380
<Text as="h2" size="md" weight="semi-bold">
385381
Contract
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { TransactionBuilder } from "@stellar/stellar-sdk";
2+
import { Icon, Text } from "@stellar/design-system";
3+
4+
import { Box } from "@/components/layout/Box";
5+
6+
import { useStore } from "@/store/useStore";
7+
8+
import { getTxData } from "@/helpers/getTxData";
9+
import { shortenStellarAddress } from "@/helpers/shortenStellarAddress";
10+
import {
11+
findKeyBySignatureHint,
12+
verifySignature,
13+
} from "@/helpers/signatureHint";
14+
import * as StellarXdr from "@/helpers/StellarXdr";
15+
16+
import { useIsXdrInit } from "@/hooks/useIsXdrInit";
17+
18+
import { RpcTxJsonResponse } from "@/types/types";
19+
20+
export const Signatures = ({
21+
txDetails,
22+
}: {
23+
txDetails: RpcTxJsonResponse | null;
24+
}) => {
25+
const { transaction, feeBumpTx, signatures, txHash } = getTxData(txDetails);
26+
const isXdrInit = useIsXdrInit();
27+
const { network } = useStore();
28+
29+
if (!signatures || signatures.length === 0 || !txHash) {
30+
return null;
31+
}
32+
33+
const feeBumpInnerTxXdr =
34+
feeBumpTx?.tx?.inner_tx &&
35+
isXdrInit &&
36+
StellarXdr.encode(
37+
"TransactionEnvelope",
38+
JSON.stringify(feeBumpTx.tx.inner_tx),
39+
);
40+
41+
const feeBumpInnerTxHash = feeBumpInnerTxXdr
42+
? TransactionBuilder.fromXDR(feeBumpInnerTxXdr, network.passphrase)
43+
.hash()
44+
.toString("hex")
45+
: undefined;
46+
47+
const possibleSigners = getPossibleSigners(
48+
signatures,
49+
transaction,
50+
feeBumpTx,
51+
);
52+
53+
const renderTableBody = () => {
54+
return signatures.map(
55+
({ hint, signature }: { hint: string; signature: string }) => {
56+
const rowKey = `table-row-${hint}`;
57+
58+
const signerPubKey = findKeyBySignatureHint(hint, possibleSigners);
59+
60+
const isVerified = verifySignature(
61+
{ hint, signature },
62+
signerPubKey,
63+
feeBumpInnerTxHash ? feeBumpInnerTxHash : txHash,
64+
);
65+
66+
return (
67+
<tr role="row" key={rowKey}>
68+
<td>
69+
<SignatureCell>
70+
{signerPubKey ? renderSigner(isVerified, signerPubKey) : "-"}
71+
</SignatureCell>
72+
</td>
73+
<td>
74+
<SignatureCell isSignature={true}>
75+
<code>{signature}</code>
76+
</SignatureCell>
77+
</td>
78+
<td>
79+
<SignatureCell>{hint}</SignatureCell>
80+
</td>
81+
</tr>
82+
);
83+
},
84+
);
85+
};
86+
87+
return (
88+
<Box gap="md" addlClassName="Signatures">
89+
<div className="Signatures__gridTableContainer">
90+
<table>
91+
<thead>
92+
<tr>
93+
<th>
94+
<SignatureCell isHeader={true}>Signer</SignatureCell>
95+
</th>
96+
<th>
97+
<SignatureCell isHeader={true}>Signature</SignatureCell>
98+
</th>
99+
<th>
100+
<SignatureCell isHeader={true}>Hint</SignatureCell>
101+
</th>
102+
</tr>
103+
</thead>
104+
<tbody>{renderTableBody()}</tbody>
105+
</table>
106+
</div>
107+
</Box>
108+
);
109+
};
110+
111+
const SignatureCell = ({
112+
children,
113+
isHeader,
114+
isSignature,
115+
}: {
116+
children: React.ReactNode;
117+
isHeader?: boolean;
118+
isSignature?: boolean;
119+
}) => {
120+
return (
121+
<Text
122+
size="sm"
123+
as="div"
124+
weight="medium"
125+
addlClassName="Signatures__cell"
126+
{...(isHeader ? { "data-is-header": true } : {})}
127+
{...(isSignature ? { "data-is-signature": true } : {})}
128+
>
129+
{children}
130+
</Text>
131+
);
132+
};
133+
134+
const renderSigner = (isVerified: boolean, signer: string) => {
135+
return isVerified ? (
136+
<Box
137+
gap="xs"
138+
direction="row"
139+
align="center"
140+
addlClassName="success-message"
141+
>
142+
<Icon.CheckCircle />
143+
<span>{shortenStellarAddress(signer)}</span>
144+
</Box>
145+
) : (
146+
<Box gap="xs" direction="row" align="center" addlClassName="error-message">
147+
<Icon.XCircle />
148+
<span>{shortenStellarAddress(signer)}</span>
149+
</Box>
150+
);
151+
};
152+
153+
// Helper function to get possible signers
154+
const getPossibleSigners = (
155+
signatures: any[] | undefined,
156+
transaction: any,
157+
feeBumpTx: any,
158+
) => {
159+
if (!signatures || signatures.length === 0 || !transaction) {
160+
return [];
161+
}
162+
163+
const allPossibleSigners: string[] = [];
164+
165+
const addSigner = (key: string | undefined) => {
166+
if (key && !allPossibleSigners.includes(key)) {
167+
allPossibleSigners.push(key);
168+
}
169+
};
170+
171+
// #1: Single signature case - only source account
172+
if (
173+
signatures.length === 1 &&
174+
findKeyBySignatureHint(signatures[0].hint, [transaction?.source_account])
175+
) {
176+
addSigner(transaction?.source_account);
177+
}
178+
179+
// #2: Fee bump transaction
180+
if (feeBumpTx?.tx?.fee_source) {
181+
addSigner(feeBumpTx?.tx?.fee_source);
182+
} else {
183+
// #3: Regular transaction - source account + operation sources
184+
addSigner(transaction.source_account);
185+
186+
for (const operation of transaction.operations) {
187+
addSigner(operation.source || transaction.source_account);
188+
}
189+
190+
// Soroban Transaction doesn't use extra signers
191+
}
192+
return allPossibleSigners;
193+
};

src/app/(sidebar)/transaction-dashboard/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { trackEvent, TrackingEvent } from "@/metrics/tracking";
2222
import { TransactionInfo } from "./components/TransactionInfo";
2323
import { StateChange } from "./components/StateChange";
2424
import { FeeBreakdown } from "./components/FeeBreakdown";
25+
import { Signatures } from "./components/Signatures";
2526

2627
import "./styles.scss";
2728

@@ -261,7 +262,7 @@ export default function TransactionDashboard() {
261262
tab6={{
262263
id: "tx-signatures",
263264
label: "Signatures",
264-
content: <ComingSoonText />,
265+
content: <Signatures txDetails={txDetails || null} />,
265266
isDisabled: !isDataLoaded,
266267
}}
267268
tab7={{

src/app/(sidebar)/transaction-dashboard/styles.scss

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,75 @@
228228
}
229229
}
230230
}
231+
232+
// =============================================================================
233+
// Signatures
234+
// =============================================================================
235+
.Signatures {
236+
color: var(--sds-clr-gray-11);
237+
238+
&__cell {
239+
&[data-is-header="true"] {
240+
text-align: left;
241+
font-size: pxToRem(12px);
242+
line-height: pxToRem(18px);
243+
color: var(--sds-clr-gray-11);
244+
}
245+
246+
&[data-is-signature="true"] {
247+
white-space: normal;
248+
word-break: break-all;
249+
overflow-wrap: break-word;
250+
font-family: var(--sds-ff-monospace);
251+
}
252+
}
253+
254+
&__gridTableContainer {
255+
width: 100%;
256+
overflow-y: auto;
257+
258+
border: 1px solid var(--sds-clr-gray-06);
259+
border-radius: pxToRem(8px);
260+
background-color: var(--sds-clr-base-00);
261+
262+
table {
263+
tr {
264+
display: grid;
265+
grid-template-columns:
266+
minmax(pxToRem(160px), 1fr)
267+
minmax(pxToRem(160px), 1fr)
268+
minmax(pxToRem(160px), 1fr);
269+
270+
&[data-is-highlighted="true"] {
271+
background-color: var(--sds-clr-gray-03);
272+
}
273+
}
274+
275+
td,
276+
th {
277+
display: flex;
278+
flex-direction: column;
279+
padding: pxToRem(8px) pxToRem(12px);
280+
justify-content: center;
281+
282+
&:not(:last-child) {
283+
border-right: 1px solid var(--sds-clr-gray-06);
284+
}
285+
286+
.Badge {
287+
--Badge-padding-vertical: 0;
288+
--Badge-padding-horizontal: #{pxToRem(6px)};
289+
290+
font-family: var(--sds-ff-monospace);
291+
letter-spacing: pxToRem(-0.24px);
292+
}
293+
}
294+
295+
tbody {
296+
tr {
297+
border-top: 1px solid var(--sds-clr-gray-06);
298+
}
299+
}
300+
}
301+
}
302+
}

src/components/DataTable/index.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
Label,
88
Loader,
99
} from "@stellar/design-system";
10-
import { stringify } from "lossless-json";
10+
import { stringify, parse } from "lossless-json";
1111

1212
import { Box } from "@/components/layout/Box";
1313
import { Dropdown } from "@/components/Dropdown";
@@ -18,6 +18,9 @@ import { exportJsonToCsvFile } from "@/helpers/exportJsonToCsvFile";
1818
import { capitalizeString } from "@/helpers/capitalizeString";
1919
import { formatNumber } from "@/helpers/formatNumber";
2020
import { formatEpochToDate } from "@/helpers/formatEpochToDate";
21+
import { decodeXdr } from "@/helpers/decodeXdr";
22+
23+
import { useIsXdrInit } from "@/hooks/useIsXdrInit";
2124

2225
import { getPublicKeyError } from "@/validate/methods/getPublicKeyError";
2326
import { getContractIdError } from "@/validate/methods/getContractIdError";
@@ -102,6 +105,8 @@ export const DataTable = <T extends AnyObject>({
102105
const [currentPage, setCurrentPage] = useState(1);
103106
const [totalPageCount, setTotalPageCount] = useState(1);
104107

108+
const isXdrInit = useIsXdrInit();
109+
105110
const hasAppliedFilters =
106111
appliedFilters.key.length > 0 || appliedFilters.value.length > 0;
107112

@@ -261,13 +266,21 @@ export const DataTable = <T extends AnyObject>({
261266
}
262267

263268
if (format === "json") {
264-
fileData = processedData.map((p) => ({
265-
key: stringify(p.keyJson),
266-
value: stringify(p.valueJson),
267-
durability: capitalizeString(p.durability),
268-
ttl: formatNumber(p.ttl),
269-
updated: formatEpochToDate(p.updated, "short") || "-",
270-
}));
269+
fileData = processedData.map((p) => {
270+
const decodedValue = decodeXdr({
271+
xdrType: "ScVal",
272+
xdrBlob: p.value,
273+
isReady: isXdrInit,
274+
})?.jsonString;
275+
276+
return {
277+
key: stringify(p.keyJson),
278+
value: decodedValue ? stringify(parse(decodedValue)) : "",
279+
durability: capitalizeString(p.durability),
280+
ttl: formatNumber(p.ttl),
281+
updated: formatEpochToDate(p.updated, "short") || "-",
282+
};
283+
});
271284
}
272285

273286
if (!fileData) {

src/components/Home/Tutorials.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const Tutorials = () => {
99
title: "Create Account",
1010
description: "Creates and funds a new Stellar account.",
1111
youTubeLink:
12-
"https://www.youtube.com/embed/Sou5wUsfsZw?si=dtQFORe7YBkmApHe",
12+
"https://www.youtube.com/embed/7j5t69f40dM?si=4svimk6VRVqnGmCj",
1313
},
1414
{
1515
title: "Payment",

src/components/TabView/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const TabView = ({
6767
<div className="TabView__content">
6868
{tabContent.map((tc) => (
6969
<div key={tc.id} data-is-active={activeTabId === tc.id}>
70-
{tc.content}
70+
{activeTabId === tc.id ? tc.content : null}
7171
</div>
7272
))}
7373
</div>

0 commit comments

Comments
 (0)