[Dashboard/SDK] Add more Split extensions and remove Split v4 code from Dashboard (#4033)

<!-- start pr-codex -->

## PR-Codex overview
This PR adds more functionality to the `Split` contract extensions, including deploying the `Split` contract, reading recipient addresses and percentages, and testing these features.

### Detailed summary
- Added more Split contract extensions
- Updated `deploys.ts` with new exports for deploying Split contract
- Updated `split.ts` with new exports for reading recipient addresses and percentages
- Modified `page.tsx` to display contract balance using `getContract`
- Added new files for reading recipient addresses and percentages
- Updated `account-balance.tsx` to use `getContract` and `useSplitBalances`
- Added tests for getting all recipient addresses and percentages
- Added functionality to get recipient split percentage and corresponding tests

> The following files were skipped due to too many changes: `packages/thirdweb/src/extensions/split/__generated__/Split/read/totalShares.ts`, `apps/dashboard/src/contract-ui/tabs/split/components/distribute-button.tsx`, `packages/thirdweb/test/src/test-contracts.ts`, `apps/dashboard/src/@3rdweb-sdk/react/hooks/useSplit.ts`, `packages/thirdweb/src/extensions/prebuilts/deploy-split.ts`, `packages/thirdweb/src/extensions/prebuilts/__generated__/Split/write/initialize.ts`, `apps/dashboard/src/contract-ui/tabs/split/page.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
kien-ngo
2024-08-11 22:17:38 +00:00
parent 478d843975
commit 141fd53759
21 changed files with 856 additions and 153 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---
Add more Split contract extensions
@@ -1,29 +1,21 @@
import { useToast } from "@chakra-ui/react";
import { useSDKChainId } from "@thirdweb-dev/react";
import type { Split } from "@thirdweb-dev/sdk";
import type {
BalanceQueryRequest,
BalanceQueryResponse,
} from "pages/api/moralis/balances";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { distribute, distributeByToken } from "thirdweb/extensions/split";
import { useSendAndConfirmTransaction } from "thirdweb/react";
import invariant from "tiny-invariant";
import { parseErrorToMessage } from "utils/errorParser";
import { splitsKeys } from "..";
import {
useMutationWithInvalidate,
useQueryWithNetwork,
} from "./query/useQueryWithNetwork";
export function useSplitData(contract?: Split) {
return useQueryWithNetwork(
splitsKeys.list(contract?.getAddress()),
async () => contract?.getAllRecipients(),
{
enabled: !!contract,
},
);
}
export function useSplitBalances(contractAddress?: string) {
const chainId = useSDKChainId();
export function useSplitBalances(contract: ThirdwebContract) {
const chainId = contract.chain.id;
const contractAddress = contract.address;
const currencies = useQueryWithNetwork(
splitsKeys.currencies(contractAddress),
async () => {
@@ -48,72 +40,31 @@ export function useSplitBalances(contractAddress?: string) {
return currencies;
}
export function useSplitDistributeFunds(contract?: Split) {
const contractAddress = contract?.getAddress();
const balances = useSplitBalances(contractAddress);
const toast = useToast();
export function useSplitDistributeFunds(contract: ThirdwebContract) {
const contractAddress = contract.address;
const balances = useSplitBalances(contract);
const { mutateAsync } = useSendAndConfirmTransaction();
return useMutationWithInvalidate(
async () => {
invariant(contract, "split contract is not ready");
invariant(balances.data, "No balances to distribute");
const distributions = (balances.data || [])
.filter((token) => token.display_balance !== "0.0")
.map(async (currency) => {
if (currency.name === "Native Token") {
await contract
.distribute()
.then(() => {
toast({
position: "bottom",
variant: "solid",
title: "Success",
description: `Successfully distributed ${currency.name}`,
status: "success",
duration: 5000,
isClosable: true,
const transaction =
currency.name === "Native Token"
? distribute({ contract })
: distributeByToken({
contract,
tokenAddress: currency.token_address,
});
return void 0;
})
.catch((err: unknown) => {
toast({
position: "bottom",
variant: "solid",
title: `Error distributing ${currency.name}`,
description: parseErrorToMessage(err),
status: "error",
duration: 9000,
isClosable: true,
});
});
} else {
await contract
.distributeToken(currency.token_address)
.then(() => {
toast({
position: "bottom",
variant: "solid",
title: "Success",
description: `Successfully distributed ${currency.name}`,
status: "success",
duration: 5000,
isClosable: true,
});
return void 0;
})
.catch((err: unknown) => {
toast({
position: "bottom",
variant: "solid",
title: `Error distributing ${currency.name}`,
description: parseErrorToMessage(err),
status: "error",
duration: 9000,
isClosable: true,
});
});
}
const promise = mutateAsync(transaction);
toast.promise(promise, {
success: `Successfully distributed ${currency.name}`,
error: `Error distributing ${currency.name}`,
loading: "Distributing funds",
});
});
return await Promise.all(distributions);
@@ -1,21 +1,33 @@
import { thirdwebClient } from "@/constants/client";
import { useSplitBalances } from "@3rdweb-sdk/react/hooks/useSplit";
import { SimpleGrid, Stat, StatLabel, StatNumber } from "@chakra-ui/react";
import { useV5DashboardChain } from "lib/v5-adapter";
import { getContract } from "thirdweb";
import { useActiveWalletChain, useWalletBalance } from "thirdweb/react";
import { Card } from "tw-components";
interface AccountBalanceProps {
address: string;
chainId: number;
}
export const AccountBalance: React.FC<AccountBalanceProps> = ({ address }) => {
export const AccountBalance: React.FC<AccountBalanceProps> = ({
address,
chainId,
}) => {
const v5Chain = useV5DashboardChain(chainId);
const activeChain = useActiveWalletChain();
const { data: balance } = useWalletBalance({
address,
chain: activeChain,
client: thirdwebClient,
});
const balanceQuery = useSplitBalances(address);
const contract = getContract({
address,
chain: v5Chain,
client: thirdwebClient,
});
const balanceQuery = useSplitBalances(contract);
return (
<SimpleGrid spacing={{ base: 3, md: 6 }} columns={{ base: 2, md: 4 }}>
@@ -50,12 +50,19 @@ export const AccountPage: React.FC<AccountPageProps> = ({
);
}
const contract = contractQuery.contract;
return (
<Flex direction="column" gap={6}>
<Flex direction="row" justify="space-between" align="center">
<Heading size="title.sm">Balances</Heading>
</Flex>
<AccountBalance address={contractAddress || ""} />
{contract && (
<AccountBalance
address={contract.getAddress()}
chainId={contract.chainId}
/>
)}
<Flex direction="row" justify="space-between" align="center">
<Heading size="title.sm">Deposit {symbol}</Heading>
</Flex>
@@ -1,42 +1,47 @@
import { useSplitDistributeFunds } from "@3rdweb-sdk/react/hooks/useSplit";
import type { UseContractResult } from "@thirdweb-dev/react";
import type { Split } from "@thirdweb-dev/sdk";
import { MismatchButton } from "components/buttons/MismatchButton";
import { TransactionButton } from "components/buttons/TransactionButton";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
import { Button } from "tw-components";
import type { Balance } from "../page";
interface DistributeButtonProps {
contractQuery?: UseContractResult<Split>;
contract: ThirdwebContract;
balances: Balance[];
balancesIsLoading: boolean;
balancesIsError: boolean;
}
export const DistributeButton: React.FC<DistributeButtonProps> = ({
contractQuery,
contract,
balances,
balancesIsLoading,
balancesIsError,
...restButtonProps
}) => {
const trackEvent = useTrack();
const validBalances = balances.filter(
(item) => item.balance !== "0" && item.balance !== "0.0",
);
const numTransactions = useMemo(() => {
if (balances.length === 1 && balances[0].name === "Native Token") {
if (
validBalances.length === 1 &&
validBalances[0].name === "Native Token"
) {
return 1;
}
if (!balances || balancesIsLoading) {
if (!validBalances || balancesIsLoading) {
return 0;
}
return balances?.filter((b) => b.display_balance !== "0.0").length;
}, [balances, balancesIsLoading]);
return validBalances?.filter(
(b) => b.display_balance !== "0.0" && b.display_balance !== "0",
).length;
}, [validBalances, balancesIsLoading]);
const distibutedFundsMutation = useSplitDistributeFunds(
contractQuery?.contract,
);
const mutation = useSplitDistributeFunds(contract);
const { onSuccess, onError } = useTxNotifications(
"Funds splitted successfully",
@@ -44,7 +49,7 @@ export const DistributeButton: React.FC<DistributeButtonProps> = ({
);
const distributeFunds = () => {
distibutedFundsMutation.mutate(undefined, {
mutation.mutate(undefined, {
onSuccess: () => {
onSuccess();
trackEvent({
@@ -70,7 +75,7 @@ export const DistributeButton: React.FC<DistributeButtonProps> = ({
// we still want to show the button, so we'll just show the mismatch button
return (
<MismatchButton
isLoading={distibutedFundsMutation.isLoading}
isLoading={mutation.isLoading}
colorScheme="primary"
onClick={distributeFunds}
{...restButtonProps}
@@ -91,7 +96,7 @@ export const DistributeButton: React.FC<DistributeButtonProps> = ({
return (
<TransactionButton
transactionCount={numTransactions}
isLoading={distibutedFundsMutation.isLoading}
isLoading={mutation.isLoading}
colorScheme="primary"
onClick={distributeFunds}
{...restButtonProps}
@@ -1,9 +1,5 @@
import { thirdwebClient } from "@/constants/client";
import { useDashboardEVMChainId } from "@3rdweb-sdk/react";
import {
useSplitBalances,
useSplitData,
} from "@3rdweb-sdk/react/hooks/useSplit";
import { useSplitBalances } from "@3rdweb-sdk/react/hooks/useSplit";
import {
Center,
Flex,
@@ -15,11 +11,21 @@ import {
StatNumber,
} from "@chakra-ui/react";
import { useContract } from "@thirdweb-dev/react";
import { BigNumber, ethers } from "ethers";
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
import { useMemo } from "react";
import { ZERO_ADDRESS } from "thirdweb";
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
import {
type ThirdwebContract,
ZERO_ADDRESS,
getContract,
toEther,
toTokens,
} from "thirdweb";
import { getAllRecipientsPercentages } from "thirdweb/extensions/split";
import {
useActiveAccount,
useReadContract,
useWalletBalance,
} from "thirdweb/react";
import { Card, Heading, Text } from "tw-components";
import { shortenIfAddress } from "utils/usedapp-external";
import { useV5DashboardChain } from "../../../lib/v5-adapter";
@@ -40,21 +46,45 @@ interface SplitPageProps {
export const ContractSplitPage: React.FC<SplitPageProps> = ({
contractAddress,
}) => {
const address = useActiveAccount()?.address;
const contractQuery = useContract(contractAddress, "split");
const configuredChainsRecord = useSupportedChainsRecord();
const chainId = useDashboardEVMChainId();
const v4Chain = chainId ? configuredChainsRecord[chainId] : undefined;
const splitQuery = useSplitData(contractQuery.contract);
const v5Chain = useV5DashboardChain(chainId);
const v5Chain = useV5DashboardChain(contractQuery.contract?.chainId);
const nativeBalanceQuery = useWalletBalance({
address: contractAddress,
if (contractQuery.isLoading) {
// TODO build a skeleton for this
return <div>Loading...</div>;
}
if (!contractQuery?.contract || !v5Chain) {
return null;
}
const contract = getContract({
address: contractQuery.contract.getAddress(),
client: thirdwebClient,
chain: v5Chain,
});
const balanceQuery = useSplitBalances(contractAddress);
return <ContractSplitContent contract={contract} />;
};
type SplitContentProps = {
contract: ThirdwebContract;
};
const ContractSplitContent: React.FC<SplitContentProps> = ({ contract }) => {
const address = useActiveAccount()?.address;
const configuredChainsRecord = useSupportedChainsRecord();
const chainId = contract.chain.id;
const v4Chain = configuredChainsRecord[chainId];
const contractAddress = contract.address;
const nativeBalanceQuery = useWalletBalance({
address: contractAddress,
client: thirdwebClient,
chain: contract.chain,
});
const { data: allRecipientsPercentages } = useReadContract(
getAllRecipientsPercentages,
{ contract },
);
const balanceQuery = useSplitBalances(contract);
const balances = useMemo(() => {
if (!balanceQuery.data && !nativeBalanceQuery.data) {
return [];
@@ -73,37 +103,30 @@ export const ContractSplitPage: React.FC<SplitPageProps> = ({
}, [balanceQuery.data, nativeBalanceQuery.data]);
const shareOfBalancesForConnectedWallet = useMemo(() => {
const activeRecipient = splitQuery.data?.find((r) => r.address === address);
const activeRecipient = (allRecipientsPercentages || []).find(
(r) => r.address.toLowerCase() === address?.toLowerCase(),
);
if (!activeRecipient || !balances) {
return {};
}
return balances.reduce(
(acc, curr) => {
// For native token balance, Moralis returns the zero address
// this logic will potentially have to change if we decide to replace the service
const isNativeToken = curr.token_address === ZERO_ADDRESS;
const displayBalance = isNativeToken
? toEther(BigInt(curr.balance))
: toTokens(BigInt(curr.balance), curr.decimals);
return {
// biome-ignore lint/performance/noAccumulatingSpread: FIXME
...acc,
// convert to bps for BigNumber calculations
[curr.token_address]: ethers.utils.formatUnits(
BigNumber.from(curr.balance)
.mul(activeRecipient.splitPercentage * 100)
.div(10000),
curr.decimals,
),
[curr.token_address]: displayBalance,
};
},
{} as { [address: string]: string },
);
}, [splitQuery.data, balances, address]);
if (contractQuery.isLoading) {
// TODO build a skeleton for this
return <div>Loading...</div>;
}
if (!contractQuery?.contract) {
return null;
}
}, [allRecipientsPercentages, balances, address]);
return (
<Flex direction="column" gap={6}>
@@ -116,7 +139,7 @@ export const ContractSplitPage: React.FC<SplitPageProps> = ({
balanceQuery.isLoading || nativeBalanceQuery.isLoading
}
balancesIsError={balanceQuery.isError && nativeBalanceQuery.isError}
contractQuery={contractQuery}
contract={contract}
/>
</Flex>
</Flex>
@@ -188,6 +211,11 @@ export const ContractSplitPage: React.FC<SplitPageProps> = ({
The Split can receive funds in the native token or in any ERC20.
Balances may take a couple of minutes to display after being
received.
<br />
{/* We currently use Moralis and high chances are they don't recognize all ERC20 tokens in the contract */}
If you are looking to distribute an ERC20 token and it's not being
recognized on this page, you can manually call the `distribute`
method in the Explorer page
</Text>
</Flex>
@@ -195,7 +223,7 @@ export const ContractSplitPage: React.FC<SplitPageProps> = ({
<Heading size="label.lg" mb="8px">
Split Recipients
</Heading>
{splitQuery.data?.map((split) => (
{(allRecipientsPercentages || []).map((split) => (
<Card key={split.address}>
<Text>
<Text as="span" size="label.md">
@@ -0,0 +1,3 @@
[
"function initialize(address _defaultAdmin, string memory _contractURI, address[] memory _trustedForwarders, address[] memory _payees, uint256[] memory _shares)"
]
@@ -6,5 +6,6 @@
"function shares(address account) view returns (uint256)",
"function totalReleased() view returns (uint256)",
"function distribute()",
"function release(address account)"
"function release(address account)",
"function totalShares() view returns (uint256)"
]
+5
View File
@@ -33,3 +33,8 @@ export {
type PrepareDirectDeployTransactionOptions,
} from "../contract/deployment/deploy-with-abi.js";
export { computePublishedContractAddress } from "../utils/any-evm/compute-published-contract-address.js";
export {
deploySplitContract,
type SplitContractParams,
type DeploySplitContractOptions,
} from "../extensions/prebuilts/deploy-split.js";
@@ -34,6 +34,14 @@ export { totalReleased } from "../../extensions/split/__generated__/Split/read/t
export { totalReleasedByToken } from "../../extensions/split/read/totalReleasedByToken.js";
export { getAllRecipientsAddresses } from "../../extensions/split/read/getAllRecipientsAddresses.js";
export { getAllRecipientsPercentages } from "../../extensions/split/read/getAllRecipientsPercentages.js";
export {
getRecipientSplitPercentage,
type SplitRecipient,
} from "../../extensions/split/read/getRecipientSplitPercentage.js";
/**
* WRITE
*/
@@ -0,0 +1,191 @@
import type { AbiParameterToPrimitiveType } from "abitype";
import type {
BaseTransactionOptions,
WithOverrides,
} from "../../../../../transaction/types.js";
import { prepareContractCall } from "../../../../../transaction/prepare-contract-call.js";
import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js";
import { once } from "../../../../../utils/promise/once.js";
import type { ThirdwebContract } from "../../../../../contract/contract.js";
import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js";
/**
* Represents the parameters for the "initialize" function.
*/
export type InitializeParams = WithOverrides<{
defaultAdmin: AbiParameterToPrimitiveType<{
type: "address";
name: "_defaultAdmin";
}>;
contractURI: AbiParameterToPrimitiveType<{
type: "string";
name: "_contractURI";
}>;
trustedForwarders: AbiParameterToPrimitiveType<{
type: "address[]";
name: "_trustedForwarders";
}>;
payees: AbiParameterToPrimitiveType<{ type: "address[]"; name: "_payees" }>;
shares: AbiParameterToPrimitiveType<{ type: "uint256[]"; name: "_shares" }>;
}>;
export const FN_SELECTOR = "0xb1a14437" as const;
const FN_INPUTS = [
{
type: "address",
name: "_defaultAdmin",
},
{
type: "string",
name: "_contractURI",
},
{
type: "address[]",
name: "_trustedForwarders",
},
{
type: "address[]",
name: "_payees",
},
{
type: "uint256[]",
name: "_shares",
},
] as const;
const FN_OUTPUTS = [] as const;
/**
* Checks if the `initialize` method is supported by the given contract.
* @param contract The ThirdwebContract.
* @returns A promise that resolves to a boolean indicating if the `initialize` method is supported.
* @extension PREBUILTS
* @example
* ```ts
* import { isInitializeSupported } from "thirdweb/extensions/prebuilts";
*
* const supported = await isInitializeSupported(contract);
* ```
*/
export async function isInitializeSupported(contract: ThirdwebContract<any>) {
return detectMethod({
contract,
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
});
}
/**
* Encodes the parameters for the "initialize" function.
* @param options - The options for the initialize function.
* @returns The encoded ABI parameters.
* @extension PREBUILTS
* @example
* ```ts
* import { encodeInitializeParams } "thirdweb/extensions/prebuilts";
* const result = encodeInitializeParams({
* defaultAdmin: ...,
* contractURI: ...,
* trustedForwarders: ...,
* payees: ...,
* shares: ...,
* });
* ```
*/
export function encodeInitializeParams(options: InitializeParams) {
return encodeAbiParameters(FN_INPUTS, [
options.defaultAdmin,
options.contractURI,
options.trustedForwarders,
options.payees,
options.shares,
]);
}
/**
* Encodes the "initialize" function into a Hex string with its parameters.
* @param options - The options for the initialize function.
* @returns The encoded hexadecimal string.
* @extension PREBUILTS
* @example
* ```ts
* import { encodeInitialize } "thirdweb/extensions/prebuilts";
* const result = encodeInitialize({
* defaultAdmin: ...,
* contractURI: ...,
* trustedForwarders: ...,
* payees: ...,
* shares: ...,
* });
* ```
*/
export function encodeInitialize(options: InitializeParams) {
// we do a "manual" concat here to avoid the overhead of the "concatHex" function
// we can do this because we know the specific formats of the values
return (FN_SELECTOR +
encodeInitializeParams(options).slice(
2,
)) as `${typeof FN_SELECTOR}${string}`;
}
/**
* Prepares a transaction to call the "initialize" function on the contract.
* @param options - The options for the "initialize" function.
* @returns A prepared transaction object.
* @extension PREBUILTS
* @example
* ```ts
* import { initialize } from "thirdweb/extensions/prebuilts";
*
* const transaction = initialize({
* contract,
* defaultAdmin: ...,
* contractURI: ...,
* trustedForwarders: ...,
* payees: ...,
* shares: ...,
* overrides: {
* ...
* }
* });
*
* // Send the transaction
* ...
*
* ```
*/
export function initialize(
options: BaseTransactionOptions<
| InitializeParams
| {
asyncParams: () => Promise<InitializeParams>;
}
>,
) {
const asyncOptions = once(async () => {
return "asyncParams" in options ? await options.asyncParams() : options;
});
return prepareContractCall({
contract: options.contract,
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
params: async () => {
const resolvedOptions = await asyncOptions();
return [
resolvedOptions.defaultAdmin,
resolvedOptions.contractURI,
resolvedOptions.trustedForwarders,
resolvedOptions.payees,
resolvedOptions.shares,
] as const;
},
value: async () => (await asyncOptions()).overrides?.value,
accessList: async () => (await asyncOptions()).overrides?.accessList,
gas: async () => (await asyncOptions()).overrides?.gas,
gasPrice: async () => (await asyncOptions()).overrides?.gasPrice,
maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas,
maxPriorityFeePerGas: async () =>
(await asyncOptions()).overrides?.maxPriorityFeePerGas,
nonce: async () => (await asyncOptions()).overrides?.nonce,
extraGas: async () => (await asyncOptions()).overrides?.extraGas,
erc20Value: async () => (await asyncOptions()).overrides?.erc20Value,
});
}
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "~test/chains.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { isAddress } from "../../utils/address.js";
import { deploySplitContract } from "./deploy-split.js";
describe("deploy-split contract", () => {
it("should deploy Split contract", async () => {
const address = await deploySplitContract({
account: TEST_ACCOUNT_A,
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
params: {
name: "split-contract",
payees: [
"0x12345674b599ce99958242b3D3741e7b01841DF3",
"0xA6f11e47dE28B3dB934e945daeb6F538E9019694",
],
shares: [
5100n, // 51%
4900n, // 49%
],
},
});
expect(address).toBeDefined();
expect(isAddress(address)).toBe(true);
// Further tests to verify the functionality of this contract
// are done in other Split tests
});
});
@@ -0,0 +1,144 @@
import type { ThirdwebClient } from "../../client/client.js";
import type { ThirdwebContract } from "../../contract/contract.js";
import { deployViaAutoFactory } from "../../contract/deployment/deploy-via-autofactory.js";
import { getOrDeployInfraForPublishedContract } from "../../contract/deployment/utils/bootstrap.js";
import { upload } from "../../storage/upload.js";
import type { FileOrBufferOrString } from "../../storage/upload/types.js";
import type { Prettify } from "../../utils/type-utils.js";
import type { ClientAndChainAndAccount } from "../../utils/types.js";
import { initialize } from "./__generated__/Split/write/initialize.js";
/**
* @extension PREBUILT
*/
export type SplitContractParams = {
name: string;
/**
* An array of strings containing wallet addresses of the recipients
* For example:
* ```ts
* ["0x...123", "0x...456"]
* ```
*/
payees: string[];
/**
* An array of bigints containing the shared percentages of each respective payees.
* Must have the same length as `payees`
* @example
* ```ts
* [
* 5100n, // 51%
* 4900n, // 49%
* ]
* ```
*/
shares: bigint[];
description?: string;
image?: FileOrBufferOrString;
external_link?: string;
social_urls?: Record<string, string>;
symbol?: string;
contractURI?: string;
defaultAdmin?: string;
trustedForwarders?: string[];
};
/**
* @extension DEPLOY
*/
export type DeploySplitContractOptions = Prettify<
ClientAndChainAndAccount & {
params: SplitContractParams;
}
>;
/**
* Deploys a thirdweb [`Split contract`](https://thirdweb.com/thirdweb.eth/Split)
* On chains where the thirdweb infrastructure contracts are not deployed, this function will deploy them as well.
* @param options - The deployment options.
* @returns The deployed contract address.
* @extension DEPLOY
*
* @example
* ```ts
* import { deploySplitContract } from "thirdweb/deploys";
* const contractAddress = await deploySplitContract({
* chain,
* client,
* account,
* params: {
* name: "Split contract",
* payees: ["0x...123", "0x...456"],
* shares: [5100, 4900], // See type `SplitContractParams` for more context
* });
* ```
*/
export async function deploySplitContract(options: DeploySplitContractOptions) {
const { chain, client, account, params } = options;
const { cloneFactoryContract, implementationContract } =
await getOrDeployInfraForPublishedContract({
chain,
client,
account,
contractId: "Split",
constructorParams: [],
});
const initializeTransaction = await getInitializeTransaction({
client,
implementationContract,
params,
accountAddress: account.address,
});
return deployViaAutoFactory({
client,
chain,
account,
cloneFactoryContract,
initializeTransaction,
});
}
async function getInitializeTransaction(options: {
client: ThirdwebClient;
implementationContract: ThirdwebContract;
params: SplitContractParams;
accountAddress: string;
}) {
const { client, implementationContract, params, accountAddress } = options;
const {
name,
description,
symbol,
image,
external_link,
social_urls,
payees,
shares,
} = params;
const contractURI =
params.contractURI ||
(await upload({
client,
files: [
{
name,
description,
symbol,
image,
external_link,
social_urls,
},
],
})) ||
"";
return initialize({
contract: implementationContract,
defaultAdmin: params.defaultAdmin || accountAddress,
contractURI,
trustedForwarders: params.trustedForwarders || [],
payees,
shares,
});
}
@@ -0,0 +1,72 @@
import { readContract } from "../../../../../transaction/read-contract.js";
import type { BaseTransactionOptions } from "../../../../../transaction/types.js";
import { decodeAbiParameters } from "viem";
import type { Hex } from "../../../../../utils/encoding/hex.js";
import type { ThirdwebContract } from "../../../../../contract/contract.js";
import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js";
export const FN_SELECTOR = "0x3a98ef39" as const;
const FN_INPUTS = [] as const;
const FN_OUTPUTS = [
{
type: "uint256",
},
] as const;
/**
* Checks if the `totalShares` method is supported by the given contract.
* @param contract The ThirdwebContract.
* @returns A promise that resolves to a boolean indicating if the `totalShares` method is supported.
* @extension SPLIT
* @example
* ```ts
* import { isTotalSharesSupported } from "thirdweb/extensions/split";
*
* const supported = await isTotalSharesSupported(contract);
* ```
*/
export async function isTotalSharesSupported(contract: ThirdwebContract<any>) {
return detectMethod({
contract,
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
});
}
/**
* Decodes the result of the totalShares function call.
* @param result - The hexadecimal result to decode.
* @returns The decoded result as per the FN_OUTPUTS definition.
* @extension SPLIT
* @example
* ```ts
* import { decodeTotalSharesResult } from "thirdweb/extensions/split";
* const result = decodeTotalSharesResult("...");
* ```
*/
export function decodeTotalSharesResult(result: Hex) {
return decodeAbiParameters(FN_OUTPUTS, result)[0];
}
/**
* Calls the "totalShares" function on the contract.
* @param options - The options for the totalShares function.
* @returns The parsed result of the function call.
* @extension SPLIT
* @example
* ```ts
* import { totalShares } from "thirdweb/extensions/split";
*
* const result = await totalShares({
* contract,
* });
*
* ```
*/
export async function totalShares(options: BaseTransactionOptions) {
return readContract({
contract: options.contract,
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
params: [],
});
}
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "~test/chains.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { getContract } from "../../../contract/contract.js";
import { deploySplitContract } from "../../../extensions/prebuilts/deploy-split.js";
import { getAllRecipientsAddresses } from "./getAllRecipientsAddresses.js";
const chain = ANVIL_CHAIN;
const client = TEST_CLIENT;
describe("getAllRecipientsAddresses", () => {
it("should work", async () => {
const payees = [
"0x12345674b599ce99958242b3D3741e7b01841DF3",
"0xA6f11e47dE28B3dB934e945daeb6F538E9019694",
];
const address = await deploySplitContract({
account: TEST_ACCOUNT_A,
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
params: {
name: "split-contract",
payees,
shares: [
5100n, // 51%
4900n, // 49%
],
},
});
const contract = getContract({
address,
chain,
client,
});
const result = await getAllRecipientsAddresses({ contract });
expect(result).toStrictEqual(payees);
});
});
@@ -0,0 +1,26 @@
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { payee } from "../__generated__/Split/read/payee.js";
import { payeeCount } from "../__generated__/Split/read/payeeCount.js";
/**
* Get the addresses of all recipients of a [`thirdweb Split contract`](https://thirdweb.com/thirdweb.eth/Split)
* @extension SPLIT
* @returns an array of wallet addresses
*
* @example
* ```ts
* import { getAllRecipientsAddresses } from "thirdweb/extensions/split";
*
* const addresses = await getAllRecipientsAddresses({ contract });
*/
export async function getAllRecipientsAddresses(
options: BaseTransactionOptions,
): Promise<string[]> {
const { contract } = options;
const _totalRecipients = await payeeCount(options);
const indexes = Array.from({ length: Number(_totalRecipients) }, (_, i) => i);
const recipientAddresses = await Promise.all(
indexes.map((index) => payee({ contract, index: BigInt(index) })),
);
return recipientAddresses;
}
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "~test/chains.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { getContract } from "../../../contract/contract.js";
import { deploySplitContract } from "../../../extensions/prebuilts/deploy-split.js";
import { getAllRecipientsPercentages } from "./getAllRecipientsPercentages.js";
const chain = ANVIL_CHAIN;
const client = TEST_CLIENT;
describe("getAllRecipientsPercentages", () => {
it("should work", async () => {
const payees = [
"0x12345674b599ce99958242b3D3741e7b01841DF3",
"0xA6f11e47dE28B3dB934e945daeb6F538E9019694",
];
const address = await deploySplitContract({
account: TEST_ACCOUNT_A,
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
params: {
name: "split-contract",
payees,
shares: [
5100n, // 51%
4900n, // 49%
],
},
});
const contract = getContract({
address,
chain,
client,
});
const result = await getAllRecipientsPercentages({ contract });
expect(result).toStrictEqual([
{
address: "0x12345674b599ce99958242b3D3741e7b01841DF3",
splitPercentage: 51,
},
{
address: "0xA6f11e47dE28B3dB934e945daeb6F538E9019694",
splitPercentage: 49,
},
]);
});
});
@@ -0,0 +1,41 @@
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { getAllRecipientsAddresses } from "./getAllRecipientsAddresses.js";
import {
type SplitRecipient,
getRecipientSplitPercentage,
} from "./getRecipientSplitPercentage.js";
/**
* Get all the recipients of a Split contracts
* @extension SPLIT
* @returns an array of recipients' addresses and split percentage of each
*
* @example
* ```ts
* import { getAllRecipientsPercentages } from "thirdweb/extensions/split";
*
* const allRecipients = await getAllRecipientsPercentages({ contract });
* // Example result:
* [
* {
* address: "0x1...",
* splitPercentage: 25, // 25%
* },
* {
* address: "0x2...",
* splitPercentage: 75, // 75%
* },
* ];
* ```
*/
export async function getAllRecipientsPercentages(
options: BaseTransactionOptions,
): Promise<SplitRecipient[]> {
const { contract } = options;
const recipientAddresses = await getAllRecipientsAddresses({ contract });
return await Promise.all(
recipientAddresses.map((recipientAddress) =>
getRecipientSplitPercentage({ contract, recipientAddress }),
),
);
}
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "~test/chains.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { getContract } from "../../../contract/contract.js";
import { deploySplitContract } from "../../../extensions/prebuilts/deploy-split.js";
import { getRecipientSplitPercentage } from "./getRecipientSplitPercentage.js";
const chain = ANVIL_CHAIN;
const client = TEST_CLIENT;
describe("getRecipientSplitPercentage", () => {
it("should work", async () => {
const payees = [
"0x12345674b599ce99958242b3D3741e7b01841DF3",
"0xA6f11e47dE28B3dB934e945daeb6F538E9019694",
];
const address = await deploySplitContract({
account: TEST_ACCOUNT_A,
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
params: {
name: "split-contract",
payees,
shares: [
5100n, // 51%
4900n, // 49%
],
},
});
const contract = getContract({
address,
chain,
client,
});
const result = await getRecipientSplitPercentage({
contract,
recipientAddress: "0x12345674b599ce99958242b3D3741e7b01841DF3",
});
expect(result).toStrictEqual({
address: "0x12345674b599ce99958242b3D3741e7b01841DF3",
splitPercentage: 51,
});
});
});
@@ -0,0 +1,40 @@
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { shares } from "../__generated__/Split/read/shares.js";
import { totalShares } from "../__generated__/Split/read/totalShares.js";
/**
* @extension SPLIT
*/
export interface SplitRecipient {
/**
* The address of the recipient
*/
address: string;
/**
* The split of the recipient as a percentage of the total amount
*
* I.e. If a recipient has a split of 50%, and the asset sells for 100 ETH,
* the recipient will receive 50 ETH.
*/
splitPercentage: number;
}
export async function getRecipientSplitPercentage(
options: BaseTransactionOptions<{ recipientAddress: string }>,
): Promise<SplitRecipient> {
const { contract, recipientAddress } = options;
const [_totalShares, walletsShares] = await Promise.all([
totalShares({ contract }),
shares({ contract, account: recipientAddress }),
]);
// We convert to basis points to avoid floating point loss of precision
// 7544n -> 75.44 (75.44 %)
// also we don't have to worry about number overflow in this particular context
const splitPercentage =
(Number(walletsShares) * 1e7) / Number(_totalShares) / 1e5;
return {
address: recipientAddress,
splitPercentage,
};
}
+27 -26
View File
@@ -1,24 +1,24 @@
import { TEST_CLIENT } from "./test-clients.js";
import { getContract } from "../../src/contract/contract.js";
import { USDT_ABI } from "./abis/usdt.js";
import { FORKED_ETHEREUM_CHAIN } from "./chains.js";
import { getContract } from "../../src/contract/contract.js";
import { TEST_CLIENT } from "./test-clients.js";
// ERC20
export const USDT_CONTRACT_ADDRESS =
"0xdAC17F958D2ee523a2206206994597C13D831ec7";
"0xdAC17F958D2ee523a2206206994597C13D831ec7";
export const USDT_CONTRACT = getContract({
client: TEST_CLIENT,
address: USDT_CONTRACT_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
client: TEST_CLIENT,
address: USDT_CONTRACT_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
});
export const USDT_CONTRACT_WITH_ABI = getContract({
client: TEST_CLIENT,
address: USDT_CONTRACT_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
abi: USDT_ABI,
client: TEST_CLIENT,
address: USDT_CONTRACT_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
abi: USDT_ABI,
});
// ERC721
@@ -26,18 +26,19 @@ export const USDT_CONTRACT_WITH_ABI = getContract({
const DOODLES_ADDRESS = "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e";
export const DOODLES_CONTRACT = getContract({
client: TEST_CLIENT,
address: DOODLES_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
client: TEST_CLIENT,
address: DOODLES_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
});
const NFT_DROP_ADDRESS = "0xE333cD2f6e26A949Ce1F3FB15d7BfAc2871cc9e4";
export const NFT_DROP_IMPLEMENTATION = "0x6f6010fb5da6f757d5b1822aadf1d3b806d6546d"; // https://etherscan.io/address/0x6f6010fb5da6f757d5b1822aadf1d3b806d6546d#code
export const NFT_DROP_IMPLEMENTATION =
"0x6f6010fb5da6f757d5b1822aadf1d3b806d6546d"; // https://etherscan.io/address/0x6f6010fb5da6f757d5b1822aadf1d3b806d6546d#code
export const NFT_DROP_CONTRACT = getContract({
client: TEST_CLIENT,
address: NFT_DROP_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
client: TEST_CLIENT,
address: NFT_DROP_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
});
// ERC1155
@@ -45,9 +46,9 @@ export const NFT_DROP_CONTRACT = getContract({
const AURA_ADDRESS = "0x42d3641255C946CC451474295d29D3505173F22A";
export const DROP1155_CONTRACT = getContract({
client: TEST_CLIENT,
address: AURA_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
client: TEST_CLIENT,
address: AURA_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
});
// Uniswap
@@ -55,14 +56,14 @@ export const DROP1155_CONTRACT = getContract({
const UNISWAPV3_FACTORY_ADDRESS = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
export const OHM_CONTRACT_ADDRESS =
"0x64aa3364F17a4D01c6f1751Fd97C2BD3D7e7f1D5";
"0x64aa3364F17a4D01c6f1751Fd97C2BD3D7e7f1D5";
export const WETH_CONTRACT_ADDRESS =
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
export const MOG_CONTRACT_ADDRESS =
"0xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a";
"0xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a";
export const UNISWAPV3_FACTORY_CONTRACT = getContract({
client: TEST_CLIENT,
address: UNISWAPV3_FACTORY_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
client: TEST_CLIENT,
address: UNISWAPV3_FACTORY_ADDRESS,
chain: FORKED_ETHEREUM_CHAIN,
});