Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CosmWasm smart contract hooks #51

Merged
merged 15 commits into from
Nov 10, 2022
91 changes: 91 additions & 0 deletions packages/graz/src/actions/methods.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { InstantiateOptions } from "@cosmjs/cosmwasm-stargate";
import type { Coin } from "@cosmjs/proto-signing";
import type { DeliverTxResponse, StdFee } from "@cosmjs/stargate";
import type { Height } from "cosmjs-types/ibc/core/client/v1/client";
Expand Down Expand Up @@ -98,3 +99,93 @@ export const sendIbcTokens = async ({
memo,
);
};

export interface InstantiateContractArgs<Message extends Record<string, unknown>> {
msg: Message;
label: string;
fee: StdFee | "auto" | number;
options?: InstantiateOptions;
senderAddress: string;
codeId: number;
}

export type InstantiateContractMutationArgs<Message extends Record<string, unknown>> = Omit<
InstantiateContractArgs<Message>,
"codeId" | "senderAddress" | "fee"
> & {
fee?: StdFee | "auto" | number;
};

export const instantiateContract = async <Message extends Record<string, unknown>>({
senderAddress,
msg,
fee,
options,
label,
codeId,
}: InstantiateContractArgs<Message>) => {
const { signingClients } = useGrazStore.getState();

if (!signingClients?.cosmWasm) {
throw new Error("CosmWasm signing client is not ready");
}
if (!senderAddress) {
codingki marked this conversation as resolved.
Show resolved Hide resolved
throw new Error("senderAddress is not defined");
}
codingki marked this conversation as resolved.
Show resolved Hide resolved

return signingClients.cosmWasm.instantiate(senderAddress, codeId, msg, label, fee, options);
};

export interface ExecuteContractArgs<Message extends Record<string, unknown>> {
msg: Message;
fee: StdFee | "auto" | number;
senderAddress: string;
contractAddress: string;
}

export type ExecuteContractMutationArgs<Message extends Record<string, unknown>> = Omit<
ExecuteContractArgs<Message>,
"contractAddress" | "senderAddress" | "fee"
> & {
fee?: StdFee | "auto" | number;
};

export const executeContract = async <Message extends Record<string, unknown>>({
senderAddress,
msg,
fee,
contractAddress,
}: ExecuteContractArgs<Message>) => {
const { signingClients } = useGrazStore.getState();

if (!signingClients?.cosmWasm) {
throw new Error("CosmWasm signing client is not ready");
}
if (!senderAddress) {
codingki marked this conversation as resolved.
Show resolved Hide resolved
throw new Error("senderAddress is not defined");
}
codingki marked this conversation as resolved.
Show resolved Hide resolved

return signingClients.cosmWasm.execute(senderAddress, contractAddress, msg, fee);
};

export const getQuerySmart = async <TData>(address: string, queryMsg: Record<string, unknown>): Promise<TData> => {
const { signingClients } = useGrazStore.getState();

if (!signingClients?.cosmWasm) {
throw new Error("CosmWasm signing client is not ready");
}

const result = (await signingClients.cosmWasm.queryContractSmart(address, queryMsg)) as TData;
return result;
};

export const getQueryRaw = (address: string, keyStr: string): Promise<Uint8Array | null> => {
const { signingClients } = useGrazStore.getState();

if (!signingClients?.cosmWasm) {
throw new Error("CosmWasm signing client is not ready");
}

const key = new TextEncoder().encode(keyStr);
return signingClients.cosmWasm.queryContractRaw(address, key);
};
204 changes: 201 additions & 3 deletions packages/graz/src/hooks/methods.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import type { ExecuteResult, InstantiateResult } from "@cosmjs/cosmwasm-stargate";
import type { DeliverTxResponse } from "@cosmjs/stargate";
import { useMutation } from "@tanstack/react-query";
import type { UseQueryResult } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";

import type { SendIbcTokensArgs, SendTokensArgs } from "../actions/methods";
import { sendIbcTokens, sendTokens } from "../actions/methods";
import type {
ExecuteContractArgs,
ExecuteContractMutationArgs,
InstantiateContractArgs,
InstantiateContractMutationArgs,
SendIbcTokensArgs,
SendTokensArgs,
} from "../actions/methods";
import {
executeContract,
getQueryRaw,
getQuerySmart,
instantiateContract,
sendIbcTokens,
sendTokens,
} from "../actions/methods";
import type { MutationEventArgs } from "../types/hooks";
import { useAccount } from "./account";

Expand Down Expand Up @@ -99,3 +115,185 @@ export const useSendIbcTokens = ({
status: mutation.status,
};
};

export type UseInstantiateContractArgs<Message extends Record<string, unknown>> = {
codeId: number;
} & MutationEventArgs<InstantiateContractMutationArgs<Message>, InstantiateResult>;

/**
* graz mutation hook to instantiate a CosmWasm smart contract when supported.
*
* @example
* ```ts
* import { useInstantiateContract } from "graz"
*
* const { instantiateContract: instantiateMyContract } = useInstantiateContract({
* codeId: 4,
* onSuccess: ({ contractAddress }) => console.log('Address:', contractAddress)
* })
*
* const instantiateMessage = { foo: 'bar' };
* instantiateMyContract(instantiateMessage);
* ```
*/
export const useInstantiateContract = <Message extends Record<string, unknown>>({
codeId,
onError,
onLoading,
onSuccess,
}: UseInstantiateContractArgs<Message>) => {
const { data: account } = useAccount();
const accountAddress = account?.bech32Address;

const mutationFn = (args: InstantiateContractMutationArgs<Message>) => {
if (!accountAddress) throw new Error("senderAddress is undefined");
const contractArgs: InstantiateContractArgs<Message> = {
...args,
fee: args.fee ?? "auto",
senderAddress: accountAddress,
codeId,
};

return instantiateContract(contractArgs);
};

const queryKey = ["USE_INSTANTIATE_CONTRACT", onError, onLoading, onSuccess, codeId, accountAddress];
const mutation = useMutation(queryKey, mutationFn, {
onError: (err, data) => Promise.resolve(onError?.(err, data)),
onMutate: onLoading,
onSuccess: (instantiateResult) => Promise.resolve(onSuccess?.(instantiateResult)),
});

return {
error: mutation.error,
isLoading: mutation.isLoading,
isSuccess: mutation.isSuccess,
instantiateContract: mutation.mutate,
instantiateContractAsync: mutation.mutateAsync,
status: mutation.status,
};
};

export type UseExecuteContractArgs<Message extends Record<string, unknown>> = {
contractAddress: string;
} & MutationEventArgs<ExecuteContractMutationArgs<Message>, ExecuteResult>;

/**
* graz mutation hook for executing transactions against a CosmWasm smart
* contract.
*
* @example
* ```ts
* import { useExecuteContract } from "graz"
*
* interface GreetMessage {
* name: string;
* }
*
* interface GreetResponse {
* message: string;
* }
*
* const contractAddress = "cosmosfoobarbaz";
* const { executeContract } = useExecuteContract<ExecuteMessage>({ contractAddress });
* executeContract({ name: 'CosmWasm' }, {
* onSuccess: (data: GreetResponse) => console.log('Got message:', data.message);
* });
* ```
*/
export const useExecuteContract = <Message extends Record<string, unknown>>({
contractAddress,
onError,
onLoading,
onSuccess,
}: UseExecuteContractArgs<Message>) => {
const { data: account } = useAccount();
const accountAddress = account?.bech32Address;

const mutationFn = (args: ExecuteContractMutationArgs<Message>) => {
if (!accountAddress) throw new Error("senderAddress is undefined");
const executeArgs: ExecuteContractArgs<Message> = {
...args,
fee: args.fee ?? "auto",
senderAddress: accountAddress,
contractAddress,
};

return executeContract(executeArgs);
};

const queryKey = ["USE_EXECUTE_CONTRACT", onError, onLoading, onSuccess, contractAddress, accountAddress];
const mutation = useMutation(queryKey, mutationFn, {
onError: (err, data) => Promise.resolve(onError?.(err, data)),
onMutate: onLoading,
onSuccess: (executeResult) => Promise.resolve(onSuccess?.(executeResult)),
});

return {
error: mutation.error,
isLoading: mutation.isLoading,
isSuccess: mutation.isSuccess,
executeContract: mutation.mutate,
executeContractAsync: mutation.mutateAsync,
status: mutation.status,
};
};

/**
* graz query hook for dispatching a "smart" query to a CosmWasm smart
* contract.
*
* Note: In order to make the hook more flexible, address and queryMsg are
* optional, but the query will be automatically disabled if either of them are
* not present. This makes it possible to register the hook before the address
* or queryMsg are known.
codingki marked this conversation as resolved.
Show resolved Hide resolved
*
* @param address - The address of the contract to query
* @param queryMsg - The query message to send to the contract
* @returns A query result with the result returned by the smart contract.
*/
export const useQuerySmart = <TData, TError>(
address?: string,
queryMsg?: Record<string, unknown>,
): UseQueryResult<TData, TError> => {
const queryKey = ["USE_QUERY_SMART", address, queryMsg] as const;
const query: UseQueryResult<TData, TError> = useQuery(
queryKey,
({ queryKey: [, _address] }) => {
if (!address || !queryMsg) throw new Error("address or queryMsg undefined");
return getQuerySmart(address, queryMsg);
},
{
enabled: Boolean(address) && Boolean(queryMsg),
},
);

return query;
};

/**
* graz query hook for dispatching a "raw" query to a CosmWasm smart contract.
*
* Note: In order to make the hook more flexible, address and key are optional, but
* the query will be automatically disabled if either of them are not present.
* This makes it possible to register the hook before the address or key are known.
codingki marked this conversation as resolved.
Show resolved Hide resolved
*
* @param address - The address of the contract to query
* @param key - The key to lookup in the contract storage
* @returns A query result with raw byte array stored at the key queried.
*/
export const useQueryRaw = <TError>(address?: string, key?: string): UseQueryResult<Uint8Array | null, TError> => {
const queryKey = ["USE_QUERY_RAW", key, address] as const;
const query: UseQueryResult<Uint8Array | null, TError> = useQuery(
queryKey,
({ queryKey: [, _address] }) => {
if (!address || !key) throw new Error("address or key undefined");
return getQueryRaw(address, key);
},
{
enabled: Boolean(address) && Boolean(key),
},
);

return query;
};