Skip to content

Commit

Permalink
[Build Transaction] Attributes view UI only (without input values and…
Browse files Browse the repository at this point in the history
… validation) (#825)

* Build transaction: basic tabs setup

* Attributes UI done

* Update button label

* Rename Attributes to Params
  • Loading branch information
quietbits authored Apr 11, 2024
1 parent 16500a2 commit fd42e8d
Show file tree
Hide file tree
Showing 16 changed files with 749 additions and 30 deletions.
8 changes: 5 additions & 3 deletions src/app/(sidebar)/endpoints/[[...pages]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { SdsLink } from "@/components/SdsLink";
import { NextLink } from "@/components/NextLink";
import { formComponentTemplate } from "@/components/formComponentTemplate";
import { PrettyJson } from "@/components/PrettyJson";
import { InputSideElement } from "@/components/InputSideElement";

import { useStore } from "@/store/useStore";
import { isEmptyObject } from "@/helpers/isEmptyObject";
Expand Down Expand Up @@ -448,12 +449,13 @@ export default function Endpoints() {
readOnly
disabled
leftElement={
<div
className="Endpoints__input__requestType"
<InputSideElement
variant="text"
placement="left"
data-testid="endpoints-url-method"
>
{pageData.requestMethod}
</div>
</InputSideElement>
}
/>
<Button
Expand Down
188 changes: 187 additions & 1 deletion src/app/(sidebar)/transaction/build/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,191 @@
"use client";

import { Alert, Button, Card } from "@stellar/design-system";

import { TabView } from "@/components/TabView";
import { PubKeyPicker } from "@/components/FormElements/PubKeyPicker";
import { Box } from "@/components/layout/Box";
import { SdsLink } from "@/components/SdsLink";
import { PositiveIntPicker } from "@/components/FormElements/PositiveIntPicker";
import { MemoPicker } from "@/components/FormElements/MemoPicker";
import { TimeBoundsPicker } from "@/components/FormElements/TimeBoundsPicker";
import { InputSideElement } from "@/components/InputSideElement";

import { useStore } from "@/store/useStore";
import { Routes } from "@/constants/routes";
import { useAccountSequenceNumber } from "@/query/useAccountSequenceNumber";

export default function BuildTransaction() {
return <div>Build Transaction</div>;
const { transaction, network } = useStore();
const { activeTab } = transaction.build;
const { updateBuildActiveTab } = transaction;

const {
// data,
// error,
refetch: fetchSequenceNumber,
} = useAccountSequenceNumber({
// TODO: pass source account
publicKey: "",
horizonUrl: network.horizonUrl,
});

// useEffect(() => {
// if (data || error) {
// // TODO: set data ?? ""
// // TODO: set error?.toString() ?? ""
// }
// }, [data, error]);

// TODO: add info links
// TODO: use store values
// TODO: validation
const renderParams = () => {
return (
<Box gap="md">
<>
<Card>
<Box gap="lg">
<>
<PubKeyPicker
id="source_account"
label="Source Account"
// TODO: handle value
value={""}
// TODO: handle error
error={""}
// TODO: handle change
onChange={() => {}}
note={
<>
If you don’t have an account yet, you can create and fund
a test net account with the{" "}
<SdsLink href={Routes.ACCOUNT_CREATE}>
account creator
</SdsLink>
.
</>
}
/>

<PositiveIntPicker
id="sequence_number"
label="Transaction Sequence Number"
placeholder="Ex: 559234806710273"
// TODO: handle value
value={""}
// TODO: handle error
error={""}
// TODO: handle change
onChange={() => {}}
note="The transaction sequence number is usually one higher than current account sequence number."
rightElement={
<InputSideElement
variant="button"
onClick={() => fetchSequenceNumber()}
placement="right"
// TODO: disable if no valid source account
// disabled={}
>
Fetch next sequence
</InputSideElement>
}
/>

<PositiveIntPicker
id="base_fee"
label="Base Fee"
// TODO: handle value
value=""
// TODO: handle error
error=""
// TODO: handle change
onChange={() => {}}
note={
<>
The{" "}
<SdsLink href="https://developers.stellar.org/docs/learn/glossary#base-fee">
network base fee
</SdsLink>{" "}
is currently set to 100 stroops (0.00001 lumens). Based on
current network activity, we suggest setting it to 100
stroops. Final transaction fee is equal to base fee times
number of operations in this transaction.
</>
}
/>

<MemoPicker
id="memo"
// TODO: handle value
value={undefined}
labelSuffix="optional"
// TODO: handle error
error=""
// TODO: handle change
onChange={() => {}}
/>

<TimeBoundsPicker
// TODO: handle value
value={undefined}
labelSuffix="optional"
// TODO: handle error
error={undefined}
// TODO: handle change
onChange={() => {}}
/>

<div>
{/* TODO: add validation */}
<Button
size="md"
variant="secondary"
onClick={() => {
updateBuildActiveTab("operations");
}}
>
Add Operations
</Button>
</div>
</>
</Box>
</Card>

<Alert variant="primary" placement="inline">
The transaction builder lets you build a new Stellar transaction.
This transaction will start out with no signatures. To make it into
the ledger, this transaction will then need to be signed and
submitted to the network.
</Alert>
</>
</Box>
);
};

const renderOperations = () => {
return <Card>Operations</Card>;
};

return (
<div>
<TabView
heading={{ title: "Build Transaction" }}
tab1={{
id: "params",
label: "Params",
content: renderParams(),
}}
tab2={{
id: "operations",
label: "Operations",
content: renderOperations(),
}}
activeTabId={activeTab}
onTabChange={(id) => {
updateBuildActiveTab(id);
}}
/>
</div>
);
}
87 changes: 87 additions & 0 deletions src/components/FormElements/MemoPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from "react";
import { MemoType, MemoValue } from "@stellar/stellar-sdk";
import { Input } from "@stellar/design-system";

import { RadioPicker } from "@/components/RadioPicker";
import { ExpandBox } from "@/components/ExpandBox";

type MemoPickerValue = { type: MemoType; value: MemoValue | undefined };

type MemoPickerProps = {
id: string;
value: MemoPickerValue | undefined;
onChange: (
optionId: string | undefined,
optionValue?: MemoPickerValue,
) => void;
labelSuffix?: string | React.ReactNode;
error: string | undefined;
};

export const MemoPicker = ({
id,
value,
onChange,
labelSuffix,
error,
}: MemoPickerProps) => {
const memoValuePlaceholder = (type?: MemoType) => {
if (!type) {
return "";
}

switch (type) {
case "id":
return "Unsigned 64-bit integer";
case "hash":
case "return":
return "32-byte hash in hexadecimal format (64 [0-9a-f] characters)";
case "text":
return "UTF-8 string of up to 28 bytes";
case "none":
default:
return "";
}
};

return (
<div className="RadioPicker__inset">
<RadioPicker
id={id}
selectedOption={value?.type}
label="Memo"
labelSuffix={labelSuffix}
onChange={onChange}
options={[
{ id: "none", label: "None", value: { type: "none", value: "" } },
{ id: "text", label: "Text", value: { type: "text", value: "" } },
{ id: "id", label: "ID", value: { type: "id", value: "" } },
{ id: "hash", label: "Hash", value: { type: "hash", value: "" } },
{
id: "return",
label: "Return",
value: { type: "return", value: "" },
},
]}
/>

<ExpandBox
isExpanded={Boolean(value?.type && value.type !== "none")}
offsetTop="sm"
>
<Input
fieldSize="md"
id="memo_value"
placeholder={memoValuePlaceholder(value?.type)}
value={value?.value?.toString() || ""}
onChange={(e) => {
if (value?.type) {
onChange(value.type, { type: value.type, value: e.target.value });
}
}}
error={error}
/>
</ExpandBox>
</div>
);
};
89 changes: 89 additions & 0 deletions src/components/FormElements/TimeBoundsPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Box } from "@/components/layout/Box";
import { SdsLink } from "@/components/SdsLink";
import { InputSideElement } from "@/components/InputSideElement";
import { PositiveIntPicker } from "./PositiveIntPicker";

type TimeBoundValue = { min: string; max: string };

type TimeBoundsPickerProps = {
value: TimeBoundValue | undefined;
labelSuffix?: string | React.ReactNode;
onChange: (value: TimeBoundValue) => void;
error: { min: string | false; max: string | false } | undefined;
};

export const TimeBoundsPicker = ({
value = { min: "", max: "" },
labelSuffix,
onChange,
error,
}: TimeBoundsPickerProps) => {
return (
<Box gap="sm">
<>
<Box gap="xs">
<>
<PositiveIntPicker
id="timebounds-min"
label="Time Bounds"
labelSuffix={labelSuffix}
placeholder="Lower time bound unix timestamp. Ex: 1479151713"
value={value.min}
error={error?.min ? `Lower time bound: ${error.min}` : ""}
onChange={(e) => {
onChange({ ...value, min: e.target.value });
}}
/>
<PositiveIntPicker
id="timebounds-max"
label=""
placeholder="Upper time bound unix timestamp. Ex: 1479151713"
value={value.max}
error={error?.max ? `Upper time bound: ${error.max}` : ""}
onChange={(e) => {
onChange({ ...value, max: e.target.value });
}}
rightElement={
<InputSideElement
variant="button"
placement="right"
onClick={() => {
onChange({
...value,
max: (
Math.ceil(new Date().getTime() / 1000) +
5 * 60
).toString(),
});
}}
>
Set to 5 min from now
</InputSideElement>
}
/>
</>
</Box>
<Box gap="xs" addlClassName="FieldNote FieldNote--note FieldNote--md">
<>
<div>
Enter{" "}
<SdsLink href="http://www.epochconverter.com/">
unix timestamp
</SdsLink>{" "}
values of time bounds when this transaction will be valid.
</div>

<div>
For regular transactions, it is highly recommended to set the
upper time bound to get a{" "}
<SdsLink href="https://github.com/stellar/stellar-core/issues/1811">
final result
</SdsLink>{" "}
of a transaction in a defined time.
</div>
</>
</Box>
</>
</Box>
);
};
Loading

0 comments on commit fd42e8d

Please sign in to comment.