From 4fc46ac98ea0721b50d0b658219875e311f68f70 Mon Sep 17 00:00:00 2001 From: 0xaptosj <129789810+0xaptosj@users.noreply.github.com> Date: Mon, 12 Aug 2024 00:59:59 -0700 Subject: [PATCH] wip --- .eslintignore | 3 +- example2/.gitignore | 36 + example2/.prettierignore | 2 + example2/.prettierrc | 4 + example2/LICENSE | 201 + example2/README.md | 7 + example2/components.json | 17 + example2/frontend/App.tsx | 50 + example2/frontend/components/Header.tsx | 13 + .../frontend/components/LabelValueGrid.tsx | 36 + example2/frontend/components/Message.tsx | 103 + .../components/PostMessageWithSurf.tsx | 338 ++ .../frontend/components/WalletProvider.tsx | 26 + .../frontend/components/WalletSelector.tsx | 188 + example2/frontend/components/ui/button.tsx | 59 + example2/frontend/components/ui/card.tsx | 84 + .../frontend/components/ui/collapsible.tsx | 9 + example2/frontend/components/ui/dialog.tsx | 120 + .../frontend/components/ui/dropdown-menu.tsx | 198 + example2/frontend/components/ui/form.tsx | 178 + example2/frontend/components/ui/input.tsx | 25 + example2/frontend/components/ui/label.tsx | 26 + .../frontend/components/ui/radio-group.tsx | 44 + example2/frontend/components/ui/toast.tsx | 127 + example2/frontend/components/ui/toaster.tsx | 33 + example2/frontend/components/ui/use-toast.ts | 187 + example2/frontend/constants.ts | 1 + example2/frontend/index.css | 77 + example2/frontend/lib/utils.ts | 6 + example2/frontend/main.tsx | 18 + example2/frontend/utils/abi.ts | 90 + example2/frontend/utils/aptosClient.ts | 17 + example2/frontend/utils/helpers.ts | 11 + example2/index.html | 19 + example2/move/Move.toml | 15 + example2/move/sources/message_board.move | 230 ++ example2/package.json | 71 + example2/postcss.config.js | 6 + example2/public/aptos.png | Bin 0 -> 8699 bytes example2/scripts/init.js | 13 + example2/scripts/move/compile.js | 22 + example2/scripts/move/get_abi.js | 31 + example2/scripts/move/publish.js | 49 + example2/scripts/move/test.js | 13 + example2/scripts/move/upgrade.js | 23 + example2/tailwind.config.js | 184 + example2/tsconfig.json | 30 + example2/tsconfig.node.json | 11 + example2/vite-env.d.ts | 1 + example2/vite.config.ts | 20 + example2/yarn.lock | 3501 +++++++++++++++++ package.json | 8 +- yarn.lock | 236 +- 53 files changed, 6749 insertions(+), 68 deletions(-) create mode 100644 example2/.gitignore create mode 100644 example2/.prettierignore create mode 100644 example2/.prettierrc create mode 100644 example2/LICENSE create mode 100644 example2/README.md create mode 100644 example2/components.json create mode 100644 example2/frontend/App.tsx create mode 100644 example2/frontend/components/Header.tsx create mode 100644 example2/frontend/components/LabelValueGrid.tsx create mode 100644 example2/frontend/components/Message.tsx create mode 100644 example2/frontend/components/PostMessageWithSurf.tsx create mode 100644 example2/frontend/components/WalletProvider.tsx create mode 100644 example2/frontend/components/WalletSelector.tsx create mode 100644 example2/frontend/components/ui/button.tsx create mode 100644 example2/frontend/components/ui/card.tsx create mode 100644 example2/frontend/components/ui/collapsible.tsx create mode 100644 example2/frontend/components/ui/dialog.tsx create mode 100644 example2/frontend/components/ui/dropdown-menu.tsx create mode 100644 example2/frontend/components/ui/form.tsx create mode 100644 example2/frontend/components/ui/input.tsx create mode 100644 example2/frontend/components/ui/label.tsx create mode 100644 example2/frontend/components/ui/radio-group.tsx create mode 100644 example2/frontend/components/ui/toast.tsx create mode 100644 example2/frontend/components/ui/toaster.tsx create mode 100644 example2/frontend/components/ui/use-toast.ts create mode 100644 example2/frontend/constants.ts create mode 100644 example2/frontend/index.css create mode 100644 example2/frontend/lib/utils.ts create mode 100644 example2/frontend/main.tsx create mode 100644 example2/frontend/utils/abi.ts create mode 100644 example2/frontend/utils/aptosClient.ts create mode 100644 example2/frontend/utils/helpers.ts create mode 100644 example2/index.html create mode 100644 example2/move/Move.toml create mode 100644 example2/move/sources/message_board.move create mode 100644 example2/package.json create mode 100644 example2/postcss.config.js create mode 100644 example2/public/aptos.png create mode 100644 example2/scripts/init.js create mode 100644 example2/scripts/move/compile.js create mode 100644 example2/scripts/move/get_abi.js create mode 100644 example2/scripts/move/publish.js create mode 100644 example2/scripts/move/test.js create mode 100644 example2/scripts/move/upgrade.js create mode 100644 example2/tailwind.config.js create mode 100644 example2/tsconfig.json create mode 100644 example2/tsconfig.node.json create mode 100644 example2/vite-env.d.ts create mode 100644 example2/vite.config.ts create mode 100644 example2/yarn.lock diff --git a/.eslintignore b/.eslintignore index 5af2584..6471044 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,5 @@ /**/*.test.ts build/**/* bindings/**/* -example/ \ No newline at end of file +example/ +example2/ diff --git a/example2/.gitignore b/example2/.gitignore new file mode 100644 index 0000000..82d0b06 --- /dev/null +++ b/example2/.gitignore @@ -0,0 +1,36 @@ +.env +.aptos + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/example2/.prettierignore b/example2/.prettierignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/example2/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/example2/.prettierrc b/example2/.prettierrc new file mode 100644 index 0000000..3773725 --- /dev/null +++ b/example2/.prettierrc @@ -0,0 +1,4 @@ +{ + "trailingComma": "all", + "printWidth": 120 +} diff --git a/example2/LICENSE b/example2/LICENSE new file mode 100644 index 0000000..41d985b --- /dev/null +++ b/example2/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Copyright 2023 Aptos Labs + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/example2/README.md b/example2/README.md new file mode 100644 index 0000000..d52aae2 --- /dev/null +++ b/example2/README.md @@ -0,0 +1,7 @@ +# Full stack example to demonstrate Surf + +Surf is a type safe tool for Move contracts in TypeScript environment. [Learn more about Surf on the doc](https://aptos.dev/en/build/sdks/ts-sdk/type-safe-contract#what-is-surf). + +We built a simple message board contract that allows anyone to read and write message to the board. Note: message can be overwritten by anyone. It demonstrate how the type safety is provided by Surf. + +Demo contract deployed at https://explorer.aptoslabs.com/object/0xd01f58c69ccac6569e13a7cfdf68409d513c1492b3f13399a7ba3415dff36fa4?network=testnet. diff --git a/example2/components.json b/example2/components.json new file mode 100644 index 0000000..49358c4 --- /dev/null +++ b/example2/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "frontend/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/example2/frontend/App.tsx b/example2/frontend/App.tsx new file mode 100644 index 0000000..fc5e08e --- /dev/null +++ b/example2/frontend/App.tsx @@ -0,0 +1,50 @@ +import { useEffect, useState } from "react"; +import { useWallet } from "@aptos-labs/wallet-adapter-react"; +// Internal Components +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Header } from "@/components/Header"; +import { PostMessageWithSurf } from "@/components/PostMessageWithSurf"; +import { Message } from "@/components/Message"; +import { surfClient } from "@/utils/aptosClient"; + +function App() { + const { connected } = useWallet(); + const [messageExist, setMessageExist] = useState(false); + + useEffect(() => { + if (!connected) { + return; + } + surfClient() + .view.exist_message({ + typeArguments: [], + functionArguments: [], + }) + .then((result) => { + console.log("message exists", result); + setMessageExist(result[0]); + }); + }, [connected]); + + return ( + <> +
+
+ {connected ? ( + + + {messageExist ? :

Message not exists

} + +
+
+ ) : ( + + To get started Connect a wallet + + )} +
+ + ); +} + +export default App; diff --git a/example2/frontend/components/Header.tsx b/example2/frontend/components/Header.tsx new file mode 100644 index 0000000..4cfb432 --- /dev/null +++ b/example2/frontend/components/Header.tsx @@ -0,0 +1,13 @@ +import { WalletSelector } from "./WalletSelector"; + +export function Header() { + return ( +
+

CAD Boilerplate Template

+ +
+ +
+
+ ); +} diff --git a/example2/frontend/components/LabelValueGrid.tsx b/example2/frontend/components/LabelValueGrid.tsx new file mode 100644 index 0000000..2f0a06b --- /dev/null +++ b/example2/frontend/components/LabelValueGrid.tsx @@ -0,0 +1,36 @@ +import { Fragment, ReactNode } from "react"; + +export interface LabelValueGridProps { + items: Array<{ label: string; subLabel?: string; value: ReactNode }>; +} + +export function LabelValueGrid({ items }: LabelValueGridProps) { + return ( +
+ {items.map(({ label, subLabel, value }) => ( + +
+
{label}
+ {subLabel &&
{subLabel}
} +
+ {value} +
+ ))} +
+ ); +} + +export interface DisplayValueProps { + value: string; + isCorrect: boolean; + expected?: string; +} + +export function DisplayValue({ value, isCorrect, expected }: DisplayValueProps) { + return ( +
+

{value}

+ {!isCorrect && expected ?

Expected: {expected}

: null} +
+ ); +} diff --git a/example2/frontend/components/Message.tsx b/example2/frontend/components/Message.tsx new file mode 100644 index 0000000..484b68d --- /dev/null +++ b/example2/frontend/components/Message.tsx @@ -0,0 +1,103 @@ +import { useEffect, useState } from "react"; + +import { surfClient } from "@/utils/aptosClient"; +import { LabelValueGrid } from "@/components/LabelValueGrid"; + +export function Message() { + const [booleanContent, setBooleanContent] = useState(); + const [stringContent, setStringContent] = useState(); + const [numberContent, setNumberContent] = useState(); + const [addressContent, setAddressContent] = useState<`0x${string}`>(); + const [objectContent, setObjectContent] = useState<{ inner: `0x${string}` }>(); + const [vectorContent, setVectorContent] = useState(); + const [optionalBooleanContent, setOptionalBooleanContent] = useState<{}>(); + const [optionalStringContent, setOptionalStringContent] = useState<{}>(); + const [optionalNumberContent, setOptionalNumberContent] = useState<{}>(); + const [optionalAddressContent, setOptionalAddressContent] = useState<{}>(); + const [optionalObjectContent, setOptionalObjectContent] = useState<{}>(); + const [optionalVectorContent, setOptionalVectorContent] = useState<{}>(); + + useEffect(() => { + surfClient() + .view.get_message_content({ + typeArguments: [], + functionArguments: [], + }) + .then((result) => { + console.log("message content", result); + setBooleanContent(result[0]); + setStringContent(result[1]); + setNumberContent(parseInt(result[2])); + setAddressContent(result[3]); + setObjectContent(result[4]); + setVectorContent(result[5]); + setOptionalBooleanContent(result[6]); + setOptionalStringContent(result[7]); + setOptionalNumberContent(result[8]); + setOptionalAddressContent(result[9]); + setOptionalObjectContent(result[10]); + setOptionalVectorContent(result[11]); + }) + .catch((error) => { + console.error(error); + }); + }, []); + + return ( +
+

Message content

+ {String(booleanContent)}

, + }, + { + label: "String Content", + value:

{stringContent}

, + }, + { + label: "Number Content", + value:

{numberContent}

, + }, + { + label: "Address Content", + value:

{addressContent}

, + }, + { + label: "Object Content", + value:

{JSON.stringify(objectContent)}

, + }, + { + label: "Vector Content", + value:

{JSON.stringify(vectorContent)}

, + }, + { + label: "Optional Boolean Content", + value:

{String(optionalBooleanContent?.vec)}

, + }, + { + label: "Optional String Content", + value:

{optionalStringContent?.vec}

, + }, + { + label: "Optional Number Content", + value:

{optionalNumberContent?.vec}

, + }, + { + label: "Optional Address Content", + value:

{optionalAddressContent?.vec}

, + }, + { + label: "Optional Object Content", + value:

{JSON.stringify(optionalObjectContent?.vec)}

, + }, + { + label: "Optional Vector Content", + value:

{JSON.stringify(optionalVectorContent?.vec)}

, + }, + ]} + /> +
+ ); +} diff --git a/example2/frontend/components/PostMessageWithSurf.tsx b/example2/frontend/components/PostMessageWithSurf.tsx new file mode 100644 index 0000000..b9aab39 --- /dev/null +++ b/example2/frontend/components/PostMessageWithSurf.tsx @@ -0,0 +1,338 @@ +"use client"; + +import { useWalletClient } from "@thalalabs/surf/hooks"; +import { useWallet } from "@aptos-labs/wallet-adapter-react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { ABI } from "@/utils/abi"; + +import { aptosClient } from "@/utils/aptosClient"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/components/ui/use-toast"; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; + +const FormSchema = z.object({ + booleanContent: z.boolean(), + stringContent: z.string(), + numberContent: z.number().int(), + addressContent: z.string().startsWith("0x"), + objectContent: z.string().startsWith("0x"), + vectorContent: z.array(z.string()), + optionalBooleanContent: z.boolean().optional(), + optionalStringContent: z.string().optional(), + optionalNumberContent: z.number().int().optional(), + optionalAddressContent: z.string().startsWith("0x").optional(), + optionalObjectContent: z.string().startsWith("0x").optional(), + optionalVectorContent: z.array(z.string()).optional(), +}); + +export function PostMessageWithSurf() { + const { toast } = useToast(); + const { account } = useWallet(); + const { client: walletClient } = useWalletClient(); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + booleanContent: true, + stringContent: "hello world", + numberContent: 1, + addressContent: "0x1", + objectContent: "0x577f6d824628353ce99463e2b85e76160db72c3445215ccb1ecbbe332dc631e8", + vectorContent: ["hello", "world"], + optionalBooleanContent: undefined, + optionalStringContent: undefined, + optionalNumberContent: undefined, + optionalAddressContent: undefined, + optionalObjectContent: undefined, + optionalVectorContent: undefined, + }, + }); + + const onSignAndSubmitTransaction = async (data: z.infer) => { + if (!account || !walletClient) { + console.error("Account or wallet client not available"); + return; + } + + try { + const committedTransaction = await walletClient?.useABI(ABI).post_message({ + type_arguments: [], + arguments: [ + data.booleanContent, + data.stringContent, + data.numberContent, + data.addressContent as `0x${string}`, + data.objectContent as `0x${string}`, + data.vectorContent, + data.optionalBooleanContent, + data.optionalStringContent, + data.optionalNumberContent, + data.optionalAddressContent as `0x${string}`, + data.optionalObjectContent as `0x${string}`, + data.optionalVectorContent, + ], + }); + const executedTransaction = await aptosClient().waitForTransaction({ + transactionHash: committedTransaction.hash, + }); + toast({ + title: "Success", + description: `Transaction succeeded, hash: ${executedTransaction.hash}`, + }); + } catch (error) { + console.error(error); + } + }; + + return ( +
+

Post a message

+ + ( + { + if (value === "true") { + field.onChange(true); + } else { + field.onChange(false); + } + }} + className="flex flex-col space-y-1" + > + Boolean Content + + + + + True + + + + + + False + + Store a boolean content + + )} + /> + ( + + String Content + + + + Store a string content + + + )} + /> + ( + + Number Content + + { + field.onChange(parseInt(event.target.value)); + }} + /> + + Store a number content + + + )} + /> + ( + + Address Content + + + + Store an address content + + + )} + /> + ( + + Object Content + + + + Store an object content + + + )} + /> + ( + + Vector Content + + { + field.onChange(event.target.value.split(",")); + }} + /> + + Store a vector content + + + )} + /> + ( + { + if (value === "true") { + field.onChange(true); + } else if (value === "false") { + field.onChange(false); + } else { + field.onChange(undefined); + } + }} + className="flex flex-col space-y-1" + > + Optional Boolean Content + + + + + Null + + + + + + True + + + + + + False + + Store an optional boolean content + + )} + /> + ( + + Optional String Content + + + + Store an optional string content + + + )} + /> + ( + + Optional Number Content + + { + if (event.target.value === "") { + field.onChange(undefined); + } else { + field.onChange(parseInt(event.target.value)); + } + }} + /> + + Store an optional number content + + + )} + /> + ( + + Optional Address Content + + + + Store an optional address content + + + )} + /> + ( + + Optional Object Content + + + + Store an optional object content + + + )} + /> + ( + + Optional Vector Content + + + + Store an optional vector content + + + )} + /> + + + + ); +} diff --git a/example2/frontend/components/WalletProvider.tsx b/example2/frontend/components/WalletProvider.tsx new file mode 100644 index 0000000..9a5b66a --- /dev/null +++ b/example2/frontend/components/WalletProvider.tsx @@ -0,0 +1,26 @@ +import { PropsWithChildren } from "react"; +import { AptosWalletAdapterProvider } from "@aptos-labs/wallet-adapter-react"; +// Internal components +import { useToast } from "@/components/ui/use-toast"; +// Internal constants +import { NETWORK } from "@/constants"; + +export function WalletProvider({ children }: PropsWithChildren) { + const { toast } = useToast(); + + return ( + { + toast({ + variant: "destructive", + title: "Error", + description: error || "Unknown wallet error", + }); + }} + > + {children} + + ); +} diff --git a/example2/frontend/components/WalletSelector.tsx b/example2/frontend/components/WalletSelector.tsx new file mode 100644 index 0000000..27ccf25 --- /dev/null +++ b/example2/frontend/components/WalletSelector.tsx @@ -0,0 +1,188 @@ +import { + APTOS_CONNECT_ACCOUNT_URL, + AnyAptosWallet, + WalletItem, + getAptosConnectWallets, + isAptosConnectWallet, + isInstallRequired, + partitionWallets, + truncateAddress, + useWallet, +} from "@aptos-labs/wallet-adapter-react"; +import { ChevronDown, Copy, LogOut, User } from "lucide-react"; +import { useCallback, useState } from "react"; +// Internal components +import { Button } from "@/components/ui/button"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useToast } from "@/components/ui/use-toast"; + +export function WalletSelector() { + const { account, connected, disconnect, wallet } = useWallet(); + const { toast } = useToast(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + const closeDialog = useCallback(() => setIsDialogOpen(false), []); + + const copyAddress = useCallback(async () => { + if (!account?.address) return; + try { + await navigator.clipboard.writeText(account.address); + toast({ + title: "Success", + description: "Copied wallet address to clipboard.", + }); + } catch { + toast({ + variant: "destructive", + title: "Error", + description: "Failed to copy wallet address.", + }); + } + }, [account?.address, toast]); + + return connected ? ( + + + + + + + Copy address + + {wallet && isAptosConnectWallet(wallet) && ( + + + Account + + + )} + + Disconnect + + + + ) : ( + + + + + + + ); +} + +interface ConnectWalletDialogProps { + close: () => void; +} + +function ConnectWalletDialog({ close }: ConnectWalletDialogProps) { + const { wallets = [] } = useWallet(); + + const { + /** Wallets that use social login to create an account on the blockchain */ + aptosConnectWallets, + /** Wallets that use traditional wallet extensions */ + otherWallets, + } = getAptosConnectWallets(wallets); + + const { + /** Wallets that are currently installed or loadable. */ + defaultWallets, + /** Wallets that are NOT currently installed or loadable. */ + moreWallets, + } = partitionWallets(otherWallets); + + return ( + + {/* AptosConnect does not support file uploads, so we only show it on the public mint page when people want to mint an NFT */} + + <> + + + Log in or sign up + with Social + Aptos Connect + + +
+ {aptosConnectWallets.map((wallet) => ( + + ))} +
+
+
+ Or +
+
+ + +
+ {defaultWallets.map((wallet) => ( + + ))} + {!!moreWallets.length && ( + + + + + + {moreWallets.map((wallet) => ( + + ))} + + + )} +
+ + ); +} + +interface WalletRowProps { + wallet: AnyAptosWallet; + onConnect?: () => void; +} + +function WalletRow({ wallet, onConnect }: WalletRowProps) { + return ( + +
+ + +
+ {isInstallRequired(wallet) ? ( + + ) : ( + + + + )} +
+ ); +} + +function AptosConnectWalletRow({ wallet, onConnect }: WalletRowProps) { + return ( + + + + + + ); +} diff --git a/example2/frontend/components/ui/button.tsx b/example2/frontend/components/ui/button.tsx new file mode 100644 index 0000000..2608ba5 --- /dev/null +++ b/example2/frontend/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 gap-1", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + green: + "focus:outline-none text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + icon: "border bg-background hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-10 px-4 py-2 rounded-md", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10 rounded-full", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/example2/frontend/components/ui/card.tsx b/example2/frontend/components/ui/card.tsx new file mode 100644 index 0000000..e41d93c --- /dev/null +++ b/example2/frontend/components/ui/card.tsx @@ -0,0 +1,84 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + + +interface CardProps extends React.HTMLAttributes { + shadow?: "sm" | "md"; +} + +const Card = React.forwardRef( + ({ className, shadow, ...props }, ref) => ( +
+ ) +); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +interface CardContentProps extends React.HTMLAttributes { + fullPadding?: boolean; +} + +const CardContent = React.forwardRef( + ({ className, fullPadding, ...props }, ref) => ( +

+ ) +); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/example2/frontend/components/ui/collapsible.tsx b/example2/frontend/components/ui/collapsible.tsx new file mode 100644 index 0000000..a23e7a2 --- /dev/null +++ b/example2/frontend/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/example2/frontend/components/ui/dialog.tsx b/example2/frontend/components/ui/dialog.tsx new file mode 100644 index 0000000..c23630e --- /dev/null +++ b/example2/frontend/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/example2/frontend/components/ui/dropdown-menu.tsx b/example2/frontend/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..769ff7a --- /dev/null +++ b/example2/frontend/components/ui/dropdown-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/example2/frontend/components/ui/form.tsx b/example2/frontend/components/ui/form.tsx new file mode 100644 index 0000000..ce264ae --- /dev/null +++ b/example2/frontend/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +