Skip to content

Commit

Permalink
[Invoke Contract] Create a dropdown & retrieve function methods
Browse files Browse the repository at this point in the history
  • Loading branch information
jeesunikim committed Feb 5, 2025
1 parent b6eb1e3 commit 88a35f0
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 4 deletions.
3 changes: 3 additions & 0 deletions src/app/(sidebar)/transaction/build/components/Operations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,9 @@ export const Operations = () => {
<option value="extend_footprint_ttl">
Extend Footprint TTL (Soroban)
</option>
<option value="invoke_contract_function">
Invoke Contract Function (Soroban)
</option>
<option value="payment">Payment</option>
<option value="path_payment_strict_send">
Path Payment Strict Send
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export const SorobanOperation = ({
case "extend_ttl_to":
case "resource_fee":
case "durability":
case "invoke_contract":
return component.render({
...sorobanBaseProps,
onChange: (e: ChangeEvent<HTMLInputElement>) => {
Expand Down
2 changes: 1 addition & 1 deletion src/app/(sidebar)/transaction/submit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const isSorobanXdr = (xdrJsonString: string) => {
return (
"extend_footprint_ttl" in body ||
"restore_footprint" in body ||
"invoke_host_function" in body
"invoke_contract_function" in body
);
});
} catch (e) {
Expand Down
36 changes: 36 additions & 0 deletions src/components/FormElements/ContractMethodSelectPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState } from "react";

import { Select } from "@stellar/design-system";

export const ContractMethodSelectPicker = ({
methods,
id,
}: {
methods: string[];
id: string;
}) => {
const [selectedValue, setSelectedValue] = useState<string>("");

const onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedValue(e.target.value);
};

return (
<Select
fieldSize="md"
label="Select a function method"
id={id}
value={selectedValue}
onChange={onChange}
>
{methods.map((method) => (
<option key={method} value={method}>
{method}
</option>
))}
</Select>

// @TODO
// Render func args
);
};
117 changes: 117 additions & 0 deletions src/components/FormElements/FetchContractMethodPickerWithQuery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { useState } from "react";
import { Button } from "@stellar/design-system";

import { Box } from "@/components/layout/Box";
import { ContractMethodSelectPicker } from "@/components/FormElements/ContractMethodSelectPicker";
import { TextPicker } from "@/components/FormElements/TextPicker";
import { MessageField } from "@/components/MessageField";

import {
fetchContractFunctionMethods,
ContractFunctionMethods,
} from "@/helpers/sorobanUtils";

import { useStore } from "@/store/useStore";
import { validate } from "@/validate";

interface FetchContractMethodPickerWithQueryProps {
id: string;
value: string;
error?: string | undefined;
label?: string;
disabled?: boolean;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

export const FetchContractMethodPickerWithQuery = ({
id,
value,
error,
label,
disabled,
onChange,
}: FetchContractMethodPickerWithQueryProps) => {
const { network } = useStore();

const [contractIdError, setContractIdError] = useState<string>("");

const [contractMethods, setContractMethods] = useState<string[]>([]);
const [fetchError, setFetchError] = useState<string>("");

const onContractIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// reset the error and methods
setFetchError("");
setContractMethods([]);

// call the onChange callback
onChange?.(e);

// validate the contract id
if (e.target.value) {
const validatedContractIdError = validate.getContractIdError(
e.target.value,
);

if (validatedContractIdError) {
setContractIdError(validatedContractIdError);
} else {
setContractIdError("");
}
}
};

const handleFetchContractMethods = async () => {
const contractMethods: ContractFunctionMethods =
await fetchContractFunctionMethods({
contractId: value,
networkPassphrase: network.passphrase,
rpcUrl: network.rpcUrl,
});

if (contractMethods.methods) {
setContractMethods(contractMethods.methods);
}

if (contractMethods.error) {
setFetchError(contractMethods.error);
}
};

return (
<Box gap="md">
<TextPicker
key={id}
id={id}
label={label}
placeholder="Ex: CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
value={value || ""}
error={error || contractIdError}
onChange={onContractIdChange}
disabled={disabled}
/>

<Box gap="md" direction="row" wrap="wrap">
<Button
disabled={!value || Boolean(contractIdError)}
variant="secondary"
size="md"
onClick={handleFetchContractMethods}
type="button"
>
Fetch contract methods
</Button>

<>{fetchError ? <MessageField message={fetchError} isError /> : null}</>
</Box>

<>
{contractMethods.length ? (
<ContractMethodSelectPicker
methods={contractMethods}
id={`${id}-method`}
/>
) : null}
</>
</Box>
);
};
19 changes: 19 additions & 0 deletions src/components/formComponentTemplateTxnOps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { NumberFractionPicker } from "@/components/FormElements/NumberFractionPi
import { RevokeSponsorshipPicker } from "@/components/FormElements/RevokeSponsorshipPicker";
import { ClaimantsPicker } from "@/components/FormElements/ClaimantsPicker";
import { ResourceFeePickerWithQuery } from "@/components/FormElements/ResourceFeePickerWithQuery";
import { FetchContractMethodPickerWithQuery } from "@/components/FormElements/FetchContractMethodPickerWithQuery";

import { removeLeadingZeroes } from "@/helpers/removeLeadingZeroes";

Expand Down Expand Up @@ -476,6 +477,7 @@ export const formComponentTemplateTxnOps = ({
),
validate: null,
};
// Soroban Only
case "resource_fee":
return {
render: (templ: SorobanTemplateRenderProps) => (
Expand All @@ -502,6 +504,23 @@ export const formComponentTemplateTxnOps = ({
),
validate: validate.getPositiveNumberError,
};
// Soroban Only
case "invoke_contract":
return {
render: (templ: SorobanTemplateRenderProps) => {
return (
<FetchContractMethodPickerWithQuery
id={id}
label="Invoke Contract"
value={templ.value || ""}
error={templ.error}
onChange={templ.onChange}
disabled={templ.isDisabled}
/>
);
},
validate: null,
};
case "limit":
return {
render: (templ: TemplateRenderProps) => (
Expand Down
9 changes: 9 additions & 0 deletions src/constants/transactionOperations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,15 @@ export const TRANSACTION_OPERATIONS: { [key: string]: TransactionOperation } = {
},
},
},
invoke_contract_function: {
label: "Invoke Contract Function",
description:
"Invoke a function of the deployed contract using 'HOST_FUNCTION_TYPE_INVOKE_CONTRACT' from Invoke Host Function.",
docsUrl:
"https://developers.stellar.org/docs/learn/fundamentals/transactions/list-of-operations#invoke-host-function",
params: ["invoke_contract", "resource_fee"],
requiredParams: ["invoke_contract", "resource_fee"],
},
revoke_sponsorship: {
label: "Revoke Sponsorship",
description: "Revoke sponsorship of a ledger entry.",
Expand Down
45 changes: 43 additions & 2 deletions src/helpers/sorobanUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
contract,
Address,
Contract,
Operation,
Expand All @@ -12,9 +13,16 @@ import {
import { TransactionBuildParams } from "@/store/createStore";
import { SorobanOpType, TxnOperation } from "@/types/types";

export type ContractFunctionMethods = {
methods: string[];
error: string;
};

export const isSorobanOperationType = (operationType: string) => {
// @TODO: add restore_footprint and invoke_host_function
return ["extend_footprint_ttl"].includes(operationType);
// @TODO: add restore_footprint
return ["extend_footprint_ttl", "invoke_contract_function"].includes(
operationType,
);
};

// https://developers.stellar.org/docs/learn/glossary#ledgerkey
Expand Down Expand Up @@ -151,6 +159,39 @@ export const buildSorobanTx = ({
.build();
};

export const fetchContractFunctionMethods = async ({
contractId,
networkPassphrase,
rpcUrl,
}: {
contractId: string;
networkPassphrase: string;
rpcUrl: string;
}): Promise<ContractFunctionMethods> => {
try {
const client = await contract.Client.from({
contractId,
networkPassphrase,
rpcUrl,
});

// on how to get the function methods from the contract
// https://github.com/stellar/js-stellar-sdk/blob/master/test/unit/spec/contract_spec.ts
const spec = client.spec;
const methods = spec.funcs();

return {
methods: methods.map((method) => method.name().toString()),
error: "",
};
} catch (e) {
return {
methods: [],
error: `error while fetching contract information: ${e}`,
};
}
};

// Preparing Soroban Transaction Data
const buildSorobanData = ({
readOnlyXdrLedgerKey = [],
Expand Down
2 changes: 1 addition & 1 deletion src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ export type SponsorshipType =
// =============================================================================
// Soroban Operations
// =============================================================================
export type SorobanOpType = "extend_footprint_ttl";
export type SorobanOpType = "extend_footprint_ttl" | "invoke_contract_function";

// =============================================================================
// RPC
Expand Down

0 comments on commit 88a35f0

Please sign in to comment.