Skip to content

Commit

Permalink
Merge branch 'main' into sc-contract-storage-csv
Browse files Browse the repository at this point in the history
  • Loading branch information
quietbits authored Feb 4, 2025
2 parents 9373207 + f101038 commit f3fe2f3
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 126 deletions.
190 changes: 108 additions & 82 deletions src/app/(sidebar)/transaction/saved/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ import { SavedItemTimestampAndDelete } from "@/components/SavedItemTimestampAndD
import { PageCard } from "@/components/layout/PageCard";
import { SaveToLocalStorageModal } from "@/components/SaveToLocalStorageModal";

import { TRANSACTION_OPERATIONS } from "@/constants/transactionOperations";
import { useStore } from "@/store/useStore";
import { localStorageSavedTransactions } from "@/helpers/localStorageSavedTransactions";
import { arrayItem } from "@/helpers/arrayItem";
import { isSorobanOperationType } from "@/helpers/sorobanUtils";

import {
TRANSACTION_OPERATIONS,
INITIAL_OPERATION,
} from "@/constants/transactionOperations";
import { SavedTransaction, SavedTransactionPage } from "@/types/types";

export default function SavedTransactions() {
Expand Down Expand Up @@ -50,18 +54,42 @@ export default function SavedTransactions() {
const found = findLocalStorageTx(timestamp);

if (found) {
let isSorobanTx = false;

// reset both the classic and soroban related states
transaction.updateBuildOperations([INITIAL_OPERATION]);
transaction.updateBuildXdr("");
transaction.updateSorobanBuildOperation(INITIAL_OPERATION);
transaction.updateSorobanBuildXdr("");

router.push(Routes.BUILD_TRANSACTION);

if (found.params) {
transaction.setBuildParams(found.params);
}

if (found.operations) {
transaction.updateBuildOperations(found.operations);
isSorobanTx = isSorobanOperationType(
found?.operations?.[0]?.operation_type,
);

if (isSorobanTx) {
// reset the classic operation
transaction.updateBuildOperations([INITIAL_OPERATION]);
transaction.updateSorobanBuildOperation(found.operations[0]);
} else {
// reset the soroban operation
transaction.updateSorobanBuildOperation(INITIAL_OPERATION);
transaction.updateBuildOperations(found.operations);
}
}

if (found.xdr) {
transaction.updateBuildXdr(found.xdr);
if (isSorobanTx) {
transaction.updateSorobanBuildXdr(found.xdr);
} else {
transaction.updateBuildXdr(found.xdr);
}
}
}
};
Expand Down Expand Up @@ -107,89 +135,87 @@ export default function SavedTransactions() {
}
};

const SavedTxn = ({ txn }: { txn: SavedTransaction }) => {
return (
const SavedTxn = ({ txn }: { txn: SavedTransaction }) => (
<Box
gap="sm"
addlClassName="PageBody__content"
data-testid="saved-transactions-item"
>
<Input
id={`saved-txn-${txn.timestamp}`}
data-testid="saved-transactions-name"
fieldSize="md"
value={txn.name}
readOnly
rightElement={
<InputSideElement
variant="button"
placement="right"
onClick={() => {
setCurrentTxnTimestamp(txn.timestamp);
}}
icon={<Icon.Edit05 />}
data-testid="saved-transactions-edit"
/>
}
/>

<>
{!txn.operations || txn.operations.length === 0
? null
: txn.operations.map((o, idx) => (
<Input
key={`saved-txn-${txn.timestamp}-op-${idx}`}
id={`saved-txn-${txn.timestamp}-op-${idx}`}
data-testid="saved-transactions-op"
fieldSize="md"
value={
TRANSACTION_OPERATIONS[o.operation_type]?.label ||
"Operation type not selected"
}
readOnly
leftElement={idx + 1}
/>
))}
</>

<Box
gap="sm"
addlClassName="PageBody__content"
data-testid="saved-transactions-item"
gap="lg"
direction="row"
align="center"
justify="space-between"
addlClassName="Endpoints__urlBar__footer"
>
<Input
id={`saved-txn-${txn.timestamp}`}
data-testid="saved-transactions-name"
fieldSize="md"
value={txn.name}
readOnly
rightElement={
<InputSideElement
variant="button"
placement="right"
onClick={() => {
setCurrentTxnTimestamp(txn.timestamp);
}}
icon={<Icon.Edit05 />}
data-testid="saved-transactions-edit"
/>
}
/>

<>
{!txn.operations || txn.operations.length === 0
? null
: txn.operations.map((o, idx) => (
<Input
key={`saved-txn-${txn.timestamp}-op-${idx}`}
id={`saved-txn-${txn.timestamp}-op-${idx}`}
data-testid="saved-transactions-op"
fieldSize="md"
value={
TRANSACTION_OPERATIONS[o.operation_type]?.label ||
"Operation type not selected"
}
readOnly
leftElement={idx + 1}
/>
))}
</>

<Box
gap="lg"
direction="row"
align="center"
justify="space-between"
addlClassName="Endpoints__urlBar__footer"
>
<Box gap="sm" direction="row">
<>
{renderActionButton(txn.timestamp, txn.page)}
{txn.shareableUrl ? (
<ShareUrlButton shareableUrl={txn.shareableUrl} />
) : null}
</>
</Box>

<Box gap="sm" direction="row" align="center" justify="end">
<SavedItemTimestampAndDelete
timestamp={txn.timestamp}
onDelete={() => {
const allTxns = localStorageSavedTransactions.get();
const indexToUpdate = allTxns.findIndex(
(t) => t.timestamp === txn.timestamp,
);

if (indexToUpdate >= 0) {
const updatedList = arrayItem.delete(allTxns, indexToUpdate);

localStorageSavedTransactions.set(updatedList);
updateSavedTxns();
}
}}
/>
</Box>
<Box gap="sm" direction="row">
<>
{renderActionButton(txn.timestamp, txn.page)}
{txn.shareableUrl ? (
<ShareUrlButton shareableUrl={txn.shareableUrl} />
) : null}
</>
</Box>

<Box gap="sm" direction="row" align="center" justify="end">
<SavedItemTimestampAndDelete
timestamp={txn.timestamp}
onDelete={() => {
const allTxns = localStorageSavedTransactions.get();
const indexToUpdate = allTxns.findIndex(
(t) => t.timestamp === txn.timestamp,
);

if (indexToUpdate >= 0) {
const updatedList = arrayItem.delete(allTxns, indexToUpdate);

localStorageSavedTransactions.set(updatedList);
updateSavedTxns();
}
}}
/>
</Box>
</Box>
);
};
</Box>
);

return (
<Box gap="md" data-testid="saved-transactions-container">
Expand Down
77 changes: 51 additions & 26 deletions src/app/(sidebar)/transaction/submit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,30 @@ const SUBMIT_OPTIONS = [
},
];

// traverse the xdr to json string and check if
// it contains a soroban operation
const isSorobanXdr = (xdrJsonString: string) => {
if (!xdrJsonString) {
return false;
}

try {
const parsedJson = JSON.parse(xdrJsonString);
const operations = parsedJson?.tx?.tx?.operations || [];

return operations.some((op: any) => {
const body = op?.body || {};
return (
"extend_footprint_ttl" in body ||
"restore_footprint" in body ||
"invoke_host_function" in body
);
});
} catch (e) {
return false;
}
};

export default function SubmitTransaction() {
const { network, xdr, transaction } = useStore();
const { blob, updateXdrBlob } = xdr;
Expand Down Expand Up @@ -117,6 +141,31 @@ export default function SubmitTransaction() {
);
const isError = Boolean(submitRpcError || submitHorizonError);

const getXdrJson = () => {
const xdrType = XDR_TYPE_TRANSACTION_ENVELOPE;

if (!(isXdrInit && blob)) {
return null;
}

try {
const xdrJson = StellarXdr.decode(xdrType, blob);

return {
jsonString: xdrJson,
error: "",
};
} catch (e) {
return {
jsonString: "",
error: `Unable to decode input as ${xdrType}`,
};
}
};

const xdrJson = getXdrJson();
const isSoroban = isSorobanXdr(xdrJson?.jsonString || "");

const handleClickOutside = useCallback((event: MouseEvent) => {
if (dropdownRef?.current?.contains(event.target as Node)) {
return;
Expand All @@ -132,13 +181,13 @@ export default function SubmitTransaction() {
if (localStorageMethod) {
setSubmitMethod(localStorageMethod);
} else {
setSubmitMethod(isRpcAvailable ? "rpc" : "horizon");
setSubmitMethod(isSoroban && isRpcAvailable ? "rpc" : "horizon");
}

resetSubmitState();
// Not including resetSubmitState
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isRpcAvailable]);
}, [isRpcAvailable, isSoroban]);

// Scroll to response
useScrollIntoView(isSuccess, responseSuccessEl);
Expand Down Expand Up @@ -231,37 +280,13 @@ export default function SubmitTransaction() {
setIsSaveTxnModalVisible(true);
};

const getXdrJson = () => {
const xdrType = XDR_TYPE_TRANSACTION_ENVELOPE;

if (!(isXdrInit && blob)) {
return null;
}

try {
const xdrJson = StellarXdr.decode(xdrType, blob);

return {
jsonString: xdrJson,
error: "",
};
} catch (e) {
return {
jsonString: "",
error: `Unable to decode input as ${xdrType}`,
};
}
};

const getButtonLabel = () => {
return (
SUBMIT_OPTIONS.find((s) => s.id === submitMethod)?.title ||
"Select submit method"
);
};

const xdrJson = getXdrJson();

const isSubmitDisabled = !submitMethod || !blob || Boolean(xdrJson?.error);

const XdrLink = ({ xdr, type }: { xdr: string; type: string }) => (
Expand Down
14 changes: 3 additions & 11 deletions src/validate/methods/getContractIdError.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
// https://developers.stellar.org/docs/learn/encyclopedia/contract-development/types/built-in-types#bytes-strings-bytes-bytesn-string
// contract IDs are fixed 32-byte byte arrays, and are represented as BytesN<32>
// A 32 byte binary array is equal to 256 bits (32 * 8)
// To represent 256 bits in base32, we need at least 52 characters
const CONTRACT_MIN_LENGTH = 52;
import { StrKey } from "@stellar/stellar-sdk";

export const getContractIdError = (value: string) => {
if (value.charAt(0) !== "C") {
return "The string must start with 'C'.";
if (!StrKey.isValidContract(value)) {
return "Invalid contract ID. Please enter a valid contract ID.";
}
if (value.length < CONTRACT_MIN_LENGTH) {
return `The string length should be at least ${CONTRACT_MIN_LENGTH} characters long.`;
}
// skipping max length check in case it includes metadata

return false;
};
5 changes: 3 additions & 2 deletions tests/buildTransaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1365,7 +1365,8 @@ test.describe("Build Transaction Page", () => {
isSorobanOp: true,
label: "Contract ID",
value: "aaa",
errorMessage: "The string must start with 'C'.",
errorMessage:
"Invalid contract ID. Please enter a valid contract ID.",
});

await testInputError({
Expand All @@ -1374,7 +1375,7 @@ test.describe("Build Transaction Page", () => {
label: "Contract ID",
value: "CAQP53Z2GMZ6WVOKJWXMCVDL",
errorMessage:
"The string length should be at least 52 characters long.",
"Invalid contract ID. Please enter a valid contract ID.",
});

await testInputError({
Expand Down
Loading

0 comments on commit f3fe2f3

Please sign in to comment.