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
103 changes: 103 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,105 @@ 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;
codingki marked this conversation as resolved.
Show resolved Hide resolved
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("Stargate signing client is not ready");
codingki marked this conversation as resolved.
Show resolved Hide resolved
}
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;
codingki marked this conversation as resolved.
Show resolved Hide resolved
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("Stargate signing client is not ready");
codingki marked this conversation as resolved.
Show resolved Hide resolved
}
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> => {
codingki marked this conversation as resolved.
Show resolved Hide resolved
const { signingClients } = useGrazStore.getState();

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

if (address === undefined) {
throw new Error("Contract address is undefined");
}

if (queryMsg === undefined) {
throw new Error("Query message is undefined");
}

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

export const getQueryRaw = (keyStr: string, address?: string): Promise<Uint8Array | null> => {
codingki marked this conversation as resolved.
Show resolved Hide resolved
const { signingClients } = useGrazStore.getState();

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

if (address === undefined) {
codingki marked this conversation as resolved.
Show resolved Hide resolved
throw new Error("Contract address is undefined");
}

const key = new TextEncoder().encode(keyStr);
return signingClients.cosmWasm.queryContractRaw(address, key);
};
210 changes: 207 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 { QueryKey, UseQueryOptions, 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,191 @@ 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>) => {
const contractArgs: InstantiateContractArgs<Message> = {
...args,
fee: args.fee ?? "auto",
senderAddress: accountAddress,
codeId,
};

return instantiateContract(contractArgs);
};
codingki marked this conversation as resolved.
Show resolved Hide resolved

const queryKey = ["USE_INSTANTIATE_CONTRACT", onError, onLoading, onSuccess, codeId];
codingki marked this conversation as resolved.
Show resolved Hide resolved
const mutation = useMutation(queryKey, mutationFn, {
onError: (err, data) => Promise.resolve(onError?.(err, data)),
onMutate: onLoading,
onSuccess: (txResponse) => Promise.resolve(onSuccess?.(txResponse)),
codingki marked this conversation as resolved.
Show resolved Hide resolved
});

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>) => {
const executeArgs: ExecuteContractArgs<Message> = {
...args,
fee: args.fee ?? "auto",
senderAddress: accountAddress,
contractAddress,
};

return executeContract(executeArgs);
};

const queryKey = ["USE_EXECUTE_CONTRACT", onError, onLoading, onSuccess, contractAddress];
codingki marked this conversation as resolved.
Show resolved Hide resolved
const mutation = useMutation(queryKey, mutationFn, {
onError: (err, data) => Promise.resolve(onError?.(err, data)),
onMutate: onLoading,
onSuccess: (txResponse) => Promise.resolve(onSuccess?.(txResponse)),
codingki marked this conversation as resolved.
Show resolved Hide resolved
});

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

export type QueryOptions<TQueryFnData, TError, TData, TQueryKey extends QueryKey> = Omit<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
"queryKey" | "queryFn" | "initialData"
> & {
initialData?: () => undefined;
};
codingki marked this conversation as resolved.
Show resolved Hide resolved

export type QuerySmartKey = readonly ["USE_QUERY_SMART", string | undefined, Record<string, unknown> | undefined];
codingki marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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
* @param options - Optional react-query QueryOptions
* @returns A query result with the result returned by the smart contract.
*/
export const useQuerySmart = <TQueryFnData, TError, TData>(
address?: string,
queryMsg?: Record<string, unknown>,
options?: QueryOptions<TQueryFnData, TError, TData, QuerySmartKey>,
codingki marked this conversation as resolved.
Show resolved Hide resolved
codingki marked this conversation as resolved.
Show resolved Hide resolved
): UseQueryResult<TData, TError> => {
const argsPresent = Boolean(address) && Boolean(queryMsg);
const queryOptions: QueryOptions<TQueryFnData, TError, TData, QuerySmartKey> =
options !== undefined ? { ...options, enabled: Boolean(options.enabled) && argsPresent } : { enabled: argsPresent };

const queryKey: QuerySmartKey = ["USE_QUERY_SMART", address, queryMsg] as const;
const query = useQuery(queryKey, ({ queryKey: [, _address] }) => getQuerySmart(address, queryMsg), queryOptions);

return query;
codingki marked this conversation as resolved.
Show resolved Hide resolved
};

export type QueryRawKey = readonly ["USE_QUERY_RAW", string, string | undefined];
codingki marked this conversation as resolved.
Show resolved Hide resolved

/**
* graz query hook for dispatching a "raw" query to a CosmWasm smart contract.
*
* Note: In order to make the hook more flexible, address is 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 is known.
*
* @param key - The key to lookup in the contract storage
* @param address - The address of the contract to query
* @param options - Optional react-query QueryOptions
* @returns A query result with raw byte array stored at the key queried.
*/
export const useQueryRaw = <TError>(
key: string,
address?: string,
options?: QueryOptions<Uint8Array | null, TError, Uint8Array | null, QueryRawKey>,
codingki marked this conversation as resolved.
Show resolved Hide resolved
codingki marked this conversation as resolved.
Show resolved Hide resolved
): UseQueryResult<Uint8Array | null, TError> => {
const enabled = Boolean(address);
const queryOptions: QueryOptions<Uint8Array | null, TError, Uint8Array | null, QueryRawKey> =
options !== undefined ? { ...options, enabled: Boolean(options.enabled) && enabled } : { enabled };

const queryKey: QueryRawKey = ["USE_QUERY_RAW", key, address] as const;
const query = useQuery(queryKey, ({ queryKey: [, _address] }) => getQueryRaw(key, address), queryOptions);
codingki marked this conversation as resolved.
Show resolved Hide resolved

return query;
};