From 56b9f87482a7210072eaa279960d1ff01ad5b4e0 Mon Sep 17 00:00:00 2001 From: Justin <328965+justinbarry@users.noreply.github.com> Date: Tue, 16 Jan 2024 21:17:19 -0800 Subject: [PATCH] Add grantee signer client and related updates (#44) * Add grantee signer client and related updates * granteeSigner testing flow; fix lint errors * fix MsgExec encoding issue; clean up demo app --------- Co-authored-by: Burnt Val --- .changeset/tasty-masks-kiss.md | 5 + apps/demo-app/src/app/layout.tsx | 10 +- apps/demo-app/src/app/page.tsx | 83 ++++++------- packages/abstraxion/package.json | 5 + .../abstraxion/src/GranteeSignerClient.ts | 111 ++++++++++++++++++ .../components/AbstraxionContext/index.tsx | 7 +- .../src/components/AbstraxionSignin/index.tsx | 47 +++++++- .../src/hooks/useAbstraxionAccount.ts | 21 +--- .../src/hooks/useAbstraxionSigningClient.ts | 20 +++- pnpm-lock.yaml | 57 +++++---- 10 files changed, 276 insertions(+), 90 deletions(-) create mode 100644 .changeset/tasty-masks-kiss.md create mode 100644 packages/abstraxion/src/GranteeSignerClient.ts diff --git a/.changeset/tasty-masks-kiss.md b/.changeset/tasty-masks-kiss.md new file mode 100644 index 00000000..935ee122 --- /dev/null +++ b/.changeset/tasty-masks-kiss.md @@ -0,0 +1,5 @@ +--- +"@burnt-labs/abstraxion": minor +--- + +Add grantee signer client to seamlessly handle grantor/grantee relationships diff --git a/apps/demo-app/src/app/layout.tsx b/apps/demo-app/src/app/layout.tsx index ba0578b2..6d643722 100644 --- a/apps/demo-app/src/app/layout.tsx +++ b/apps/demo-app/src/app/layout.tsx @@ -6,6 +6,10 @@ import "@burnt-labs/abstraxion/styles.css"; const inter = Inter({ subsets: ["latin"] }); +// Example XION seat contract +export const seatContractAddress = + "xion1z70cvc08qv5764zeg3dykcyymj5z6nu4sqr7x8vl4zjef2gyp69s9mmdka"; + export default function RootLayout({ children, }: { @@ -16,11 +20,7 @@ export default function RootLayout({ {children} diff --git a/apps/demo-app/src/app/page.tsx b/apps/demo-app/src/app/page.tsx index a5d20560..e1772635 100644 --- a/apps/demo-app/src/app/page.tsx +++ b/apps/demo-app/src/app/page.tsx @@ -8,9 +8,10 @@ import { } from "@burnt-labs/abstraxion"; import { Button } from "@burnt-labs/ui"; import "@burnt-labs/ui/styles.css"; -import type { InstantiateResult } from "@cosmjs/cosmwasm-stargate"; +import type { ExecuteResult } from "@cosmjs/cosmwasm-stargate"; +import { seatContractAddress } from "./layout"; -type InitiateResultOrUndefined = InstantiateResult | undefined; +type ExecuteResultOrUndefined = ExecuteResult | undefined; export default function Page(): JSX.Element { // Abstraxion hooks const { data: account } = useAbstraxionAccount(); @@ -19,54 +20,56 @@ export default function Page(): JSX.Element { // General state hooks const [isOpen, setIsOpen] = useState(false); const [loading, setLoading] = useState(false); - const [initiateResult, setInitiateResult] = - useState(undefined); + const [executeResult, setExecuteResult] = + useState(undefined); - const blockExplorerUrl = `https://explorer.burnt.com/xion-testnet-1/tx/${initiateResult?.transactionHash}`; + const blockExplorerUrl = `https://explorer.burnt.com/xion-testnet-1/tx/${executeResult?.transactionHash}`; - const instantiateTestContract = async (): Promise => { + function getTimestampInSeconds(date: Date | null) { + if (!date) return 0; + const d = new Date(date); + return Math.floor(d.getTime() / 1000); + } + + const now = new Date(); + now.setSeconds(now.getSeconds() + 15); + const oneYearFromNow = new Date(); + oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1); + + async function claimSeat() { setLoading(true); - try { - if (!client) { - setIsOpen(true); - return; - } - const initMsg = { - metadata: { - metadata: { - name: "Abstraxion House", - hub_url: "abstraxion_house", - description: "Generalized Abstraction", - tags: [], - social_links: [], - creator: account.bech32Address, - thumbnail_image_url: "https://fakeimg.pl/200/", - banner_image_url: "https://fakeimg.pl/500/", - }, - }, - ownable: { + const msg = { + sales: { + claim_item: { + token_id: String(getTimestampInSeconds(now)), owner: account.bech32Address, + token_uri: "", + extension: {}, }, - }; + }, + }; - const hubResult = await client.instantiate( - account.bech32Address || "", - 1, - initMsg, - "my-hub", + try { + const claimRes = await client?.execute( + account.bech32Address, + seatContractAddress, + msg, { amount: [{ amount: "0", denom: "uxion" }], gas: "500000", }, + "", + [], ); - setInitiateResult(hubResult); + + setExecuteResult(claimRes); } catch (error) { // eslint-disable-next-line no-console -- No UI exists yet to display errors console.log(error); } finally { setLoading(false); } - }; + } return (
@@ -80,7 +83,7 @@ export default function Page(): JSX.Element { }} structure="base" > - {account.wallet ? ( + {account.bech32Address ? (
VIEW ACCOUNT
) : ( "CONNECT" @@ -91,11 +94,11 @@ export default function Page(): JSX.Element { disabled={loading} fullWidth onClick={() => { - void instantiateTestContract(); + void claimSeat(); }} structure="base" > - {loading ? "LOADING..." : "INSTANTIATE TEST CONTRACT"} + {loading ? "LOADING..." : "CLAIM SEAT"} ) : null} - {initiateResult ? ( + {executeResult ? (

- Contract Address: + Transaction Hash

-

{initiateResult.contractAddress}

+

{executeResult.transactionHash}

Block Height:

-

{initiateResult.height}

+

{executeResult.height}

{ + const tmClient = await Tendermint37Client.connect(endpoint); + return GranteeSignerClient.createWithSigner(tmClient, signer, options); + } + + public static async createWithSigner( + cometClient: TendermintClient, + signer: OfflineSigner, + options: SigningCosmWasmClientOptions & GranteeSignerOptions, + ): Promise { + return new GranteeSignerClient(cometClient, signer, options); + } + + protected constructor( + cometClient: TendermintClient | undefined, + signer: OfflineSigner, + { + grantorAddress, + granteeAddress, + ...options + }: SigningCosmWasmClientOptions & GranteeSignerOptions, + ) { + super(cometClient, signer, options); + if (grantorAddress === undefined) { + throw new Error("grantorAddress is required"); + } + this.grantorAddress = grantorAddress; + + if (granteeAddress === undefined) { + throw new Error("granteeAddress is required"); + } + this.granteeAddress = granteeAddress; + } + + public async signAndBroadcast( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee | "auto" | number, + memo = "", + ): Promise { + // Figure out if the signerAddress is a grantor + if (signerAddress === this.grantorAddress) { + signerAddress = this.granteeAddress; + // Wrap the signerAddress in a MsgExec + messages = [ + { + typeUrl: "/cosmos.authz.v1beta1.MsgExec", + value: MsgExec.fromPartial({ + grantee: this.granteeAddress, + msgs: messages.map((msg) => this.registry.encodeAsAny(msg)), + }), + }, + ]; + } + + return super.signAndBroadcast(signerAddress, messages, fee, memo); + } + + public async sign( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + explicitSignerData?: SignerData, + ): Promise { + // Figure out if the signerAddress is a grantor + if (signerAddress === this.grantorAddress) { + signerAddress = this.granteeAddress; + // Wrap the signerAddress in a MsgExec + messages = [ + { + typeUrl: "/cosmos.authz.v1beta1.MsgExec", + value: MsgExec.fromPartial({ + grantee: signerAddress, + msgs: messages.map((msg) => this.registry.encodeAsAny(msg)), + }), + }, + ]; + } + + return super.sign(signerAddress, messages, fee, memo, explicitSignerData); + } +} diff --git a/packages/abstraxion/src/components/AbstraxionContext/index.tsx b/packages/abstraxion/src/components/AbstraxionContext/index.tsx index e5a73e78..6222f976 100644 --- a/packages/abstraxion/src/components/AbstraxionContext/index.tsx +++ b/packages/abstraxion/src/components/AbstraxionContext/index.tsx @@ -10,6 +10,8 @@ export interface AbstraxionContextProps { setAbstraxionError: React.Dispatch>; abstraxionAccount: DirectSecp256k1HdWallet | undefined; setAbstraxionAccount: React.Dispatch; + grantorAddress: string; + setGrantorAddress: React.Dispatch>; contracts?: string[]; dashboardUrl?: string; } @@ -21,7 +23,7 @@ export const AbstraxionContext = createContext( export const AbstraxionContextProvider = ({ children, contracts, - dashboardUrl, + dashboardUrl = "https://dashboard.burnt.com", }: { children: ReactNode; contracts?: string[]; @@ -33,6 +35,7 @@ export const AbstraxionContextProvider = ({ const [abstraxionAccount, setAbstraxionAccount] = useState< DirectSecp256k1HdWallet | undefined >(undefined); + const [grantorAddress, setGrantorAddress] = useState(""); return ( 0) { + const granterAddresses = data.grants.map((grant) => grant.granter); + const uniqueGranters = [...new Set(granterAddresses)]; + if (uniqueGranters.length > 1) { + console.error("More than one granter found. Taking first."); + } + + setGrantorAddress(uniqueGranters[0]); break; } } catch (error) { diff --git a/packages/abstraxion/src/hooks/useAbstraxionAccount.ts b/packages/abstraxion/src/hooks/useAbstraxionAccount.ts index 7c4137d6..e94d1c3e 100644 --- a/packages/abstraxion/src/hooks/useAbstraxionAccount.ts +++ b/packages/abstraxion/src/hooks/useAbstraxionAccount.ts @@ -1,13 +1,10 @@ -import { useContext, useEffect, useState } from "react"; -import { DirectSecp256k1HdWallet } from "graz/dist/cosmjs"; +import { useContext } from "react"; import { AbstraxionContext, AbstraxionContextProps, } from "@/src/components/AbstraxionContext"; -import { getAccountAddress } from "@/utils/get-account-address"; export interface AbstraxionAccount { - wallet?: DirectSecp256k1HdWallet; bech32Address: string; } @@ -17,25 +14,13 @@ export interface useAbstraxionAccountProps { } export const useAbstraxionAccount = (): useAbstraxionAccountProps => { - const { abstraxionAccount, isConnected } = useContext( + const { isConnected, grantorAddress } = useContext( AbstraxionContext, ) as AbstraxionContextProps; - const [bech32Address, setBech32Address] = useState(""); - - useEffect(() => { - async function updateAddress() { - const address = await getAccountAddress(); - setBech32Address(address); - } - - updateAddress(); - }, [abstraxionAccount]); - return { data: { - wallet: abstraxionAccount, - bech32Address: bech32Address, + bech32Address: grantorAddress, }, isConnected: isConnected, }; diff --git a/packages/abstraxion/src/hooks/useAbstraxionSigningClient.ts b/packages/abstraxion/src/hooks/useAbstraxionSigningClient.ts index 05ab25f3..cc5b290e 100644 --- a/packages/abstraxion/src/hooks/useAbstraxionSigningClient.ts +++ b/packages/abstraxion/src/hooks/useAbstraxionSigningClient.ts @@ -1,18 +1,19 @@ import { useContext, useEffect, useState } from "react"; -import { GasPrice, SigningCosmWasmClient } from "graz/dist/cosmjs"; +import { GasPrice } from "graz/dist/cosmjs"; import { testnetChainInfo } from "@burnt-labs/constants"; import { AbstraxionContext, AbstraxionContextProps, } from "@/src/components/AbstraxionContext"; +import { GranteeSignerClient } from "@/src/GranteeSignerClient.ts"; export const useAbstraxionSigningClient = () => { - const { isConnected, abstraxionAccount } = useContext( + const { isConnected, abstraxionAccount, grantorAddress } = useContext( AbstraxionContext, ) as AbstraxionContextProps; const [abstractClient, setAbstractClient] = useState< - SigningCosmWasmClient | undefined + GranteeSignerClient | undefined >(undefined); useEffect(() => { @@ -21,11 +22,22 @@ export const useAbstraxionSigningClient = () => { if (!abstraxionAccount) { throw new Error("No account found."); } - const directClient = await SigningCosmWasmClient.connectWithSigner( + const granteeAddress = await abstraxionAccount + .getAccounts() + .then((accounts) => { + if (accounts.length === 0) { + throw new Error("No account found."); + } + return accounts[0].address; + }); + + const directClient = await GranteeSignerClient.connectWithSigner( testnetChainInfo.rpc, abstraxionAccount, { gasPrice: GasPrice.fromString("0uxion"), + grantorAddress, + granteeAddress, }, ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d62567ab..56dc3c54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 0.5.3(prettier@3.0.3) turbo: specifier: latest - version: 1.11.2 + version: 1.11.3 apps/abstraxion-dashboard: dependencies: @@ -190,6 +190,18 @@ importers: '@burnt-labs/ui': specifier: workspace:* version: link:../ui + '@cosmjs/cosmwasm-stargate': + specifier: ^0.31.3 + version: 0.31.3 + '@cosmjs/proto-signing': + specifier: ^0.31.3 + version: 0.31.3 + '@cosmjs/stargate': + specifier: ^0.31.3 + version: 0.31.3 + '@cosmjs/tendermint-rpc': + specifier: ^0.31.1 + version: 0.31.3 '@stytch/nextjs': specifier: ^14.0.0 version: 14.0.0(@stytch/vanilla-js@3.2.1)(react-dom@18.2.0)(react@18.2.0) @@ -199,6 +211,9 @@ importers: '@types/react-dom': specifier: ^18.2.18 version: 18.2.18 + cosmjs-types: + specifier: ^0.8.0 + version: 0.8.0 graz: specifier: ^0.0.51 version: 0.0.51(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0) @@ -9909,64 +9924,64 @@ packages: yargs: 17.7.2 dev: false - /turbo-darwin-64@1.11.2: - resolution: {integrity: sha512-toFmRG/adriZY3hOps7nYCfqHAS+Ci6xqgX3fbo82kkLpC6OBzcXnleSwuPqjHVAaRNhVoB83L5njcE9Qwi2og==} + /turbo-darwin-64@1.11.3: + resolution: {integrity: sha512-IsOOg2bVbIt3o/X8Ew9fbQp5t1hTHN3fGNQYrPQwMR2W1kIAC6RfbVD4A9OeibPGyEPUpwOH79hZ9ydFH5kifw==} cpu: [x64] os: [darwin] requiresBuild: true dev: false optional: true - /turbo-darwin-arm64@1.11.2: - resolution: {integrity: sha512-FCsEDZ8BUSFYEOSC3rrARQrj7x2VOrmVcfrMUIhexTxproRh4QyMxLfr6LALk4ymx6jbDCxWa6Szal8ckldFbA==} + /turbo-darwin-arm64@1.11.3: + resolution: {integrity: sha512-FsJL7k0SaPbJzI/KCnrf/fi3PgCDCjTliMc/kEFkuWVA6Httc3Q4lxyLIIinz69q6JTx8wzh6yznUMzJRI3+dg==} cpu: [arm64] os: [darwin] requiresBuild: true dev: false optional: true - /turbo-linux-64@1.11.2: - resolution: {integrity: sha512-Vzda/o/QyEske5CxLf0wcu7UUS+7zB90GgHZV4tyN+WZtoouTvbwuvZ3V6b5Wgd3OJ/JwWR0CXDK7Sf4VEMr7A==} + /turbo-linux-64@1.11.3: + resolution: {integrity: sha512-SvW7pvTVRGsqtSkII5w+wriZXvxqkluw5FO/MNAdFw0qmoov+PZ237+37/NgArqE3zVn1GX9P6nUx9VO+xcQAg==} cpu: [x64] os: [linux] requiresBuild: true dev: false optional: true - /turbo-linux-arm64@1.11.2: - resolution: {integrity: sha512-bRLwovQRz0yxDZrM4tQEAYV0fBHEaTzUF0JZ8RG1UmZt/CqtpnUrJpYb1VK8hj1z46z9YehARpYCwQ2K0qU4yw==} + /turbo-linux-arm64@1.11.3: + resolution: {integrity: sha512-YhUfBi1deB3m+3M55X458J6B7RsIS7UtM3P1z13cUIhF+pOt65BgnaSnkHLwETidmhRh8Dl3GelaQGrB3RdCDw==} cpu: [arm64] os: [linux] requiresBuild: true dev: false optional: true - /turbo-windows-64@1.11.2: - resolution: {integrity: sha512-LgTWqkHAKgyVuLYcEPxZVGPInTjjeCnN5KQMdJ4uQZ+xMDROvMFS2rM93iQl4ieDJgidwHCxxCxaU9u8c3d/Kg==} + /turbo-windows-64@1.11.3: + resolution: {integrity: sha512-s+vEnuM2TiZuAUUUpmBHDr6vnNbJgj+5JYfnYmVklYs16kXh+EppafYQOAkcRIMAh7GjV3pLq5/uGqc7seZeHA==} cpu: [x64] os: [win32] requiresBuild: true dev: false optional: true - /turbo-windows-arm64@1.11.2: - resolution: {integrity: sha512-829aVBU7IX0c/B4G7g1VI8KniAGutHhIupkYMgF6xPkYVev2G3MYe6DMS/vsLt9GGM9ulDtdWxWrH5P2ngK8IQ==} + /turbo-windows-arm64@1.11.3: + resolution: {integrity: sha512-ZR5z5Zpc7cASwfdRAV5yNScCZBsgGSbcwiA/u3farCacbPiXsfoWUkz28iyrx21/TRW0bi6dbsB2v17swa8bjw==} cpu: [arm64] os: [win32] requiresBuild: true dev: false optional: true - /turbo@1.11.2: - resolution: {integrity: sha512-jPC7LVQJzebs5gWf8FmEvsvXGNyKbN+O9qpvv98xpNaM59aS0/Irhd0H0KbcqnXfsz7ETlzOC3R+xFWthC4Z8A==} + /turbo@1.11.3: + resolution: {integrity: sha512-RCJOUFcFMQNIGKSjC9YmA5yVP1qtDiBA0Lv9VIgrXraI5Da1liVvl3VJPsoDNIR9eFMyA/aagx1iyj6UWem5hA==} hasBin: true optionalDependencies: - turbo-darwin-64: 1.11.2 - turbo-darwin-arm64: 1.11.2 - turbo-linux-64: 1.11.2 - turbo-linux-arm64: 1.11.2 - turbo-windows-64: 1.11.2 - turbo-windows-arm64: 1.11.2 + turbo-darwin-64: 1.11.3 + turbo-darwin-arm64: 1.11.3 + turbo-linux-64: 1.11.3 + turbo-linux-arm64: 1.11.3 + turbo-windows-64: 1.11.3 + turbo-windows-arm64: 1.11.3 dev: false /type-check@0.4.0: