From 6914a402581eedb7a9595fa6ac84462070146e3c Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Sat, 18 Mar 2023 20:47:39 +0300 Subject: [PATCH] Init splits contract helpers --- contracts/splits/contract.ts | 165 +++++++++++++++++++++++++++ contracts/splits/index.ts | 2 + contracts/splits/messages/execute.ts | 77 +++++++++++++ contracts/splits/messages/query.ts | 43 +++++++ contracts/splits/useContract.ts | 76 ++++++++++++ 5 files changed, 363 insertions(+) create mode 100644 contracts/splits/contract.ts create mode 100644 contracts/splits/index.ts create mode 100644 contracts/splits/messages/execute.ts create mode 100644 contracts/splits/messages/query.ts create mode 100644 contracts/splits/useContract.ts diff --git a/contracts/splits/contract.ts b/contracts/splits/contract.ts new file mode 100644 index 0000000..f62a476 --- /dev/null +++ b/contracts/splits/contract.ts @@ -0,0 +1,165 @@ +import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' +import type { Coin } from '@cosmjs/proto-signing' + +export interface InstantiateResponse { + readonly contractAddress: string + readonly transactionHash: string +} + +export interface SplitsInstance { + readonly contractAddress: string + //Query + getAdmin: () => Promise + getMemberWeight: (member: string) => Promise + listMembers: (startAfter?: string, limit?: number) => Promise + getGroup: () => Promise + + //Execute + updateAdmin: (admin: string) => Promise + distribute: () => Promise +} + +export interface SplitsMessages { + updateAdmin: (admin: string) => UpdateAdminMessage + distribute: () => DistributeMessage +} + +export interface UpdateAdminMessage { + sender: string + contract: string + msg: { + update_admin: { admin: string } + } + funds: Coin[] +} + +export interface DistributeMessage { + sender: string + contract: string + msg: { distribute: Record } + funds: Coin[] +} + +export interface SplitsContract { + instantiate: ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + ) => Promise + + use: (contractAddress: string) => SplitsInstance + + messages: (contractAddress: string) => SplitsMessages +} + +export const Splits = (client: SigningCosmWasmClient, txSigner: string): SplitsContract => { + const use = (contractAddress: string): SplitsInstance => { + ///QUERY + const listMembers = async (startAfter?: string, limit?: number): Promise => { + return client.queryContractSmart(contractAddress, { + list_members: { limit, start_after: startAfter }, + }) + } + + const getMemberWeight = async (address: string): Promise => { + return client.queryContractSmart(contractAddress, { + member: { address }, + }) + } + + const getAdmin = async (): Promise => { + return client.queryContractSmart(contractAddress, { + admin: {}, + }) + } + + const getGroup = async (): Promise => { + return client.queryContractSmart(contractAddress, { + group: {}, + }) + } + /// EXECUTE + const updateAdmin = async (admin: string): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + update_admin: { + admin, + }, + }, + 'auto', + ) + return res.transactionHash + } + + const distribute = async (): Promise => { + const res = await client.execute( + txSigner, + contractAddress, + { + distribute: {}, + }, + 'auto', + ) + return res.transactionHash + } + return { + contractAddress, + updateAdmin, + distribute, + getMemberWeight, + getAdmin, + listMembers, + getGroup, + } + } + + const instantiate = async ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + ): Promise => { + const result = await client.instantiate(txSigner, codeId, initMsg, label, 'auto', { + admin, + }) + + return { + contractAddress: result.contractAddress, + transactionHash: result.transactionHash, + } + } + + const messages = (contractAddress: string) => { + const updateAdmin = (admin: string) => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + update_admin: { admin }, + }, + funds: [], + } + } + + const distribute = () => { + return { + sender: txSigner, + contract: contractAddress, + msg: { + distribute: {}, + }, + funds: [], + } + } + + return { + updateAdmin, + distribute, + } + } + + return { use, instantiate, messages } +} diff --git a/contracts/splits/index.ts b/contracts/splits/index.ts new file mode 100644 index 0000000..6dc6461 --- /dev/null +++ b/contracts/splits/index.ts @@ -0,0 +1,2 @@ +export * from './contract' +export * from './useContract' diff --git a/contracts/splits/messages/execute.ts b/contracts/splits/messages/execute.ts new file mode 100644 index 0000000..9ee2c96 --- /dev/null +++ b/contracts/splits/messages/execute.ts @@ -0,0 +1,77 @@ +import type { SplitsInstance } from '../index' +import { useSplitsContract } from '../index' + +export type ExecuteType = typeof EXECUTE_TYPES[number] + +export const EXECUTE_TYPES = ['update_admin', 'distribute'] as const + +export interface ExecuteListItem { + id: ExecuteType + name: string + description?: string +} + +export const EXECUTE_LIST: ExecuteListItem[] = [ + { + id: 'update_admin', + name: 'Update Admin', + description: `Update the splits contract admin`, + }, + { + id: 'distribute', + name: 'Distribute', + description: `Distribute the revenue to the group members`, + }, +] + +export interface DispatchExecuteProps { + type: ExecuteType + [k: string]: unknown +} + +type Select = T + +/** @see {@link SplitsInstance} */ +export type DispatchExecuteArgs = { + contract: string + messages?: SplitsInstance +} & ({ type: Select<'update_admin'>; admin: string } | { type: Select<'distribute'> }) + +export const dispatchExecute = async (args: DispatchExecuteArgs) => { + const { messages } = args + if (!messages) { + throw new Error('Cannot dispatch execute, messages are not defined') + } + switch (args.type) { + case 'update_admin': { + return messages.updateAdmin(args.admin) + } + case 'distribute': { + return messages.distribute() + } + default: { + throw new Error('Unknown execution type') + } + } +} + +export const previewExecutePayload = (args: DispatchExecuteArgs) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { messages } = useSplitsContract() + const { contract } = args + switch (args.type) { + case 'update_admin': { + return messages(contract)?.updateAdmin(args.admin) + } + case 'distribute': { + return messages(contract)?.distribute() + } + default: { + return {} + } + } +} + +export const isEitherType = (type: unknown, arr: T[]): type is T => { + return arr.some((val) => type === val) +} diff --git a/contracts/splits/messages/query.ts b/contracts/splits/messages/query.ts new file mode 100644 index 0000000..6a6b564 --- /dev/null +++ b/contracts/splits/messages/query.ts @@ -0,0 +1,43 @@ +import type { SplitsInstance } from '../contract' + +export type QueryType = typeof QUERY_TYPES[number] + +export const QUERY_TYPES = ['admin', 'group', 'member', 'list_members'] as const + +export interface QueryListItem { + id: QueryType + name: string + description?: string +} + +export const QUERY_LIST: QueryListItem[] = [ + { id: 'admin', name: 'Query Admin', description: 'View the splits contract admin' }, + { id: 'member', name: 'Query Member Weight', description: 'Check the weight of a member in the group' }, + { id: 'list_members', name: 'Query Members', description: 'View the group members' }, + { id: 'group', name: 'Query Group Contract Address', description: 'View the group contract address' }, +] + +export interface DispatchQueryProps { + messages: SplitsInstance | undefined + type: QueryType + address: string + startAfter: string + limit: number +} + +export const dispatchQuery = (props: DispatchQueryProps) => { + const { messages, type, address, startAfter, limit } = props + switch (type) { + case 'list_members': + return messages?.listMembers(startAfter, limit) + case 'admin': + return messages?.getAdmin() + case 'member': + return messages?.getMemberWeight(address) + case 'group': + return messages?.getGroup() + default: { + throw new Error('unknown query type') + } + } +} diff --git a/contracts/splits/useContract.ts b/contracts/splits/useContract.ts new file mode 100644 index 0000000..70ae0f9 --- /dev/null +++ b/contracts/splits/useContract.ts @@ -0,0 +1,76 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ + +import { useWallet } from 'contexts/wallet' +import { useCallback, useEffect, useState } from 'react' + +import type { InstantiateResponse, SplitsContract, SplitsInstance, SplitsMessages } from './contract' +import { Splits as initContract } from './contract' + +export interface UseSplitsContractProps { + instantiate: ( + codeId: number, + initMsg: Record, + label: string, + admin?: string, + ) => Promise + + use: (customAddress?: string) => SplitsInstance | undefined + + updateContractAddress: (contractAddress: string) => void + + messages: (contractAddress: string) => SplitsMessages | undefined +} + +export function useSplitsContract(): UseSplitsContractProps { + const wallet = useWallet() + + const [address, setAddress] = useState('') + const [splits, setSplits] = useState() + + useEffect(() => { + setAddress(localStorage.getItem('contract_address') || '') + }, []) + + useEffect(() => { + const splitsContract = initContract(wallet.getClient(), wallet.address) + setSplits(splitsContract) + }, [wallet]) + + const updateContractAddress = (contractAddress: string) => { + setAddress(contractAddress) + } + + const instantiate = useCallback( + (codeId: number, initMsg: Record, label: string, admin?: string): Promise => { + return new Promise((resolve, reject) => { + if (!splits) { + reject(new Error('Contract is not initialized.')) + return + } + splits.instantiate(codeId, initMsg, label, admin).then(resolve).catch(reject) + }) + }, + [splits], + ) + + const use = useCallback( + (customAddress = ''): SplitsInstance | undefined => { + return splits?.use(address || customAddress) + }, + [splits, address], + ) + + const messages = useCallback( + (customAddress = ''): SplitsMessages | undefined => { + return splits?.messages(address || customAddress) + }, + [splits, address], + ) + + return { + instantiate, + use, + updateContractAddress, + messages, + } +}