From 3fd84012049c3a084ff94c38139705a2e0d6c583 Mon Sep 17 00:00:00 2001 From: BJ Vicks Date: Fri, 6 Oct 2023 12:00:32 -0700 Subject: [PATCH] enable ENS transfers --- packages/sdk/src/TokenboundClient.ts | 81 ++++----- packages/sdk/src/test/TestAll.test.ts | 167 ++++++++++++++++-- packages/sdk/src/test/config/mints.ts | 2 +- packages/sdk/src/test/utils/getWETHBalance.ts | 7 +- .../sdk/src/test/utils/getZora1155Balance.ts | 7 +- .../sdk/src/test/utils/getZora721Balance.ts | 10 +- packages/sdk/src/types/addresses.ts | 1 + packages/sdk/src/types/index.ts | 1 + packages/sdk/src/types/params.ts | 7 +- packages/sdk/src/utils/index.ts | 1 + packages/sdk/src/utils/resolvePossibleENS.ts | 21 +++ 11 files changed, 232 insertions(+), 73 deletions(-) create mode 100644 packages/sdk/src/types/addresses.ts create mode 100644 packages/sdk/src/utils/resolvePossibleENS.ts diff --git a/packages/sdk/src/TokenboundClient.ts b/packages/sdk/src/TokenboundClient.ts index 8dbe757..c4e4c47 100644 --- a/packages/sdk/src/TokenboundClient.ts +++ b/packages/sdk/src/TokenboundClient.ts @@ -52,8 +52,8 @@ import { isEthers5SignableMessage, isEthers6SignableMessage, isViemSignableMessage, + resolvePossibleENS, } from './utils' -// import { normalize } from 'viem/ens' class TokenboundClient { private chainId: number @@ -384,30 +384,32 @@ class TokenboundClient { const is1155: boolean = tokenType === NFTTokenType.ERC1155 - // Configure required args based on token type - const transferArgs: unknown[] = is1155 - ? [ - // ERC1155: safeTransferFrom(address,address,uint256,uint256,bytes) - tbAccountAddress, - recipientAddress, - tokenId, - 1, - '0x', - ] - : [ - // ERC721: safeTransferFrom(address,address,uint256) - tbAccountAddress, - recipientAddress, - tokenId, - ] - - const transferCallData = encodeFunctionData({ - abi: is1155 ? erc1155Abi : erc721Abi, - functionName: 'safeTransferFrom', - args: transferArgs, - }) - try { + const recipient = await resolvePossibleENS(this.publicClient, recipientAddress) + + // Configure required args based on token type + const transferArgs: unknown[] = is1155 + ? [ + // ERC1155: safeTransferFrom(address,address,uint256,uint256,bytes) + tbAccountAddress, + recipient, + tokenId, + 1, + '0x', + ] + : [ + // ERC721: safeTransferFrom(address,address,uint256) + tbAccountAddress, + recipient, + tokenId, + ] + + const transferCallData = encodeFunctionData({ + abi: is1155 ? erc1155Abi : erc721Abi, + functionName: 'safeTransferFrom', + args: transferArgs, + }) + return await this.executeCall({ account: tbAccountAddress, to: tokenContract, @@ -429,21 +431,11 @@ class TokenboundClient { */ public async transferETH(params: ETHTransferParams): Promise<`0x${string}`> { const { account: tbAccountAddress, amount, recipientAddress } = params - const weiValue = parseUnits(`${amount}`, 18) // convert ETH to wei - let recipient = getAddress(recipientAddress) - - // @BJ todo: debug - // const isENS = recipientAddress.endsWith(".eth") - // if (isENS) { - // recipient = await this.publicClient.getEnsResolver({name: normalize(recipientAddress)}) - // if (!recipient) { - // throw new Error('Failed to resolve ENS address'); - // } - // } - // console.log('RECIPIENT_ADDRESS', recipient) try { + const recipient = await resolvePossibleENS(this.publicClient, recipientAddress) + return await this.executeCall({ account: tbAccountAddress, to: recipient, @@ -479,17 +471,14 @@ class TokenboundClient { const amountBaseUnit = parseUnits(`${amount}`, erc20tokenDecimals) - // const recipient = recipientAddress.endsWith('.eth') - // ? await this.publicClient.getEnsResolver({ name: normalize(recipientAddress) }) - // : recipientAddress - - const callData = encodeFunctionData({ - abi: erc20Abi, - functionName: 'transfer', - args: [recipientAddress, amountBaseUnit], - }) - try { + const recipient = await resolvePossibleENS(this.publicClient, recipientAddress) + + const callData = encodeFunctionData({ + abi: erc20Abi, + functionName: 'transfer', + args: [recipient, amountBaseUnit], + }) return await this.executeCall({ account: tbAccountAddress, to: erc20tokenAddress, diff --git a/packages/sdk/src/test/TestAll.test.ts b/packages/sdk/src/test/TestAll.test.ts index ae94393..0052da4 100644 --- a/packages/sdk/src/test/TestAll.test.ts +++ b/packages/sdk/src/test/TestAll.test.ts @@ -27,7 +27,11 @@ import { ANVIL_RPC_URL, WETH_CONTRACT_ADDRESS, } from './constants' -import { walletClientToEthers5Signer, walletClientToEthers6Signer } from '../utils' +import { + resolvePossibleENS, + walletClientToEthers5Signer, + walletClientToEthers6Signer, +} from '../utils' import { ethToWei, getPublicClient, @@ -46,6 +50,7 @@ import { wethABI, // zora1155ABI } from './wagmi-cli-hooks/generated' +import { getEnsAddress } from 'viem/ens' const TIMEOUT = 60000 // default 10000 const ANVIL_USER_0 = getAddress(ANVIL_ACCOUNTS[0].address) @@ -92,7 +97,8 @@ function runTxTests({ let publicClient: PublicClient let NFT_IN_EOA: CreateAccountParams let TOKENID_IN_EOA: string - let TOKENID_IN_TBA: string + let TOKENID1_IN_TBA: string + let TOKENID2_IN_TBA: string let ZORA721_TBA_ADDRESS: `0x${string}` // Spin up a fresh anvil instance each time we run the test suite against a different signer @@ -138,11 +144,13 @@ function runTxTests({ onLogs: (logs) => { mintLogs = logs const { tokenId: eoaTokenId } = logs[0].args - const { tokenId: tbaTokenId } = logs[1].args + const { tokenId: tbaToken1Id } = logs[1].args + const { tokenId: tbaToken2Id } = logs[2].args - if (eoaTokenId && tbaTokenId) { + if (eoaTokenId && tbaToken1Id && tbaToken2Id) { TOKENID_IN_EOA = eoaTokenId.toString() - TOKENID_IN_TBA = tbaTokenId.toString() + TOKENID1_IN_TBA = tbaToken1Id.toString() + TOKENID2_IN_TBA = tbaToken2Id.toString() NFT_IN_EOA = { tokenContract: zora721.proxyContractAddress, @@ -191,7 +199,7 @@ function runTxTests({ expect(mintLogs.length).toBe(zora721.quantity) expect(mintTxHash).toMatch(ADDRESS_REGEX) expect(NFT_IN_EOA.tokenId).toBe(TOKENID_IN_EOA) - expect(zoraBalanceInAnvilWallet).toBe(2n) + expect(zoraBalanceInAnvilWallet).toBe(3n) unwatch() }) }, @@ -225,7 +233,7 @@ function runTxTests({ args: [ ANVIL_USER_0, // from ZORA721_TBA_ADDRESS, // to - BigInt(TOKENID_IN_TBA), // tokenId + BigInt(TOKENID1_IN_TBA), // tokenId ], }) @@ -271,6 +279,61 @@ function runTxTests({ TIMEOUT ) + it( + 'can transfer another minted NFT to the TBA', + async () => { + const transferCallData = encodeFunctionData({ + abi: zora721.abi, + functionName: 'safeTransferFrom', + args: [ + ANVIL_USER_0, // from + ZORA721_TBA_ADDRESS, // to + BigInt(TOKENID2_IN_TBA), // tokenId + ], + }) + + const preparedNFTTransfer = { + to: zora721.proxyContractAddress, + value: 0n, + data: transferCallData, + } + + let transferHash: `0x${string}` + + if (walletClient) { + transferHash = await walletClient.sendTransaction({ + chain: ANVIL_CONFIG.ACTIVE_CHAIN, + account: walletClient.account!.address, + ...preparedNFTTransfer, + }) + } else { + transferHash = await signer + .sendTransaction({ + chainId: ANVIL_CONFIG.ACTIVE_CHAIN.id, + ...preparedNFTTransfer, + }) + .then((tx: providers.TransactionResponse) => tx.hash) + } + + const transactionReceipt = await publicClient.getTransactionReceipt({ + hash: transferHash, + }) + + const tbaNFTBalance = await getZora721Balance({ + publicClient, + walletAddress: ZORA721_TBA_ADDRESS, + }) + console.log('# of NFTs in TBA: ', tbaNFTBalance.toString()) + + await waitFor(() => { + expect(transferHash).toMatch(ADDRESS_REGEX) + expect(transactionReceipt.status).toBe('success') + expect(tbaNFTBalance).toBe(2n) + }) + }, + TIMEOUT + ) + // To perform transactions using the SDK, we need to transfer some ETH into the TBA. it( 'can transfer ETH to the TBA', @@ -339,14 +402,14 @@ function runTxTests({ // so they provide further reinforcement that executeCall works. it('can transferETH with the TBA', async () => { const EXPECTED_BALANCE_BEFORE = parseUnits('1', 18) - const EXPECTED_BALANCE_AFTER = parseUnits('0.5', 18) + const EXPECTED_BALANCE_AFTER = parseUnits('0.75', 18) const balanceBefore = await publicClient.getBalance({ address: ZORA721_TBA_ADDRESS, }) const ethTransferHash = await tokenboundClient.transferETH({ account: ZORA721_TBA_ADDRESS, - amount: 0.5, + amount: 0.25, recipientAddress: ANVIL_USER_1, }) const balanceAfter = await publicClient.getBalance({ @@ -367,12 +430,42 @@ function runTxTests({ }) }) + it('can transferETH to an ENS with the TBA', async () => { + const EXPECTED_BALANCE_BEFORE = parseUnits('0.75', 18) + const EXPECTED_BALANCE_AFTER = parseUnits('0.5', 18) + + const balanceBefore = await publicClient.getBalance({ + address: ZORA721_TBA_ADDRESS, + }) + const ethTransferHash = await tokenboundClient.transferETH({ + account: ZORA721_TBA_ADDRESS, + amount: 0.25, + recipientAddress: 'jeebay.eth', + }) + const balanceAfter = await publicClient.getBalance({ + address: ZORA721_TBA_ADDRESS, + }) + + console.log( + 'BEFORE: ', + formatEther(balanceBefore), + 'AFTER: ', + formatEther(balanceAfter) + ) + + await waitFor(() => { + expect(ethTransferHash).toMatch(ADDRESS_REGEX) + expect(balanceBefore).toBe(EXPECTED_BALANCE_BEFORE) + expect(balanceAfter).toBe(EXPECTED_BALANCE_AFTER) + }) + }) + it('can transferNFT with the TBA', async () => { const transferNFTHash = await tokenboundClient.transferNFT({ account: ZORA721_TBA_ADDRESS, tokenType: 'ERC721', tokenContract: zora721.proxyContractAddress, - tokenId: TOKENID_IN_TBA, + tokenId: TOKENID1_IN_TBA, recipientAddress: ANVIL_USER_1, }) @@ -387,7 +480,29 @@ function runTxTests({ }) }) - it('can mint 2 Zora 721 NFTs with the TBA', async () => { + it('can transferNFT to an ENS with the TBA', async () => { + const transferNFTHash = await tokenboundClient.transferNFT({ + account: ZORA721_TBA_ADDRESS, + tokenType: 'ERC721', + tokenContract: zora721.proxyContractAddress, + tokenId: TOKENID2_IN_TBA, + recipientAddress: 'jeebay.eth', + }) + + const addr = await resolvePossibleENS(publicClient, 'jeebay.eth') + + const anvilAccount1NFTBalance = await getZora721Balance({ + publicClient, + walletAddress: addr, + }) + + await waitFor(() => { + expect(transferNFTHash).toMatch(ADDRESS_REGEX) + expect(anvilAccount1NFTBalance).toBe(1n) + }) + }) + + it('can mint 3 Zora 721 NFTs with the TBA', async () => { const encodedMintFunctionData = encodeFunctionData({ abi: zora721.abi, functionName: 'purchase', @@ -411,7 +526,7 @@ function runTxTests({ await waitFor(() => { expect(mintToTBATxHash).toMatch(ADDRESS_REGEX) expect(NFT_IN_EOA.tokenId).toBe(TOKENID_IN_EOA) - expect(zoraBalanceInTBA).toBe(2n) + expect(zoraBalanceInTBA).toBe(3n) }) }) @@ -526,8 +641,10 @@ function runTxTests({ ) it('can transferERC20 with the TBA', async () => { - const depositEthValue = 0.25 + const depositEthValue = 0.2 const depositWeiValue = ethToWei(depositEthValue) + const transferEthValue = 0.1 + const transferWeiValue = ethToWei(transferEthValue) let wethDepositHash: `0x${string}` let wethTransferHash: `0x${string}` @@ -592,12 +709,21 @@ function runTxTests({ // Transfer WETH from TBA to ANVIL_USER_1 const transferredERC20Hash = await tokenboundClient.transferERC20({ account: ZORA721_TBA_ADDRESS, - amount: depositEthValue, + amount: transferEthValue, recipientAddress: ANVIL_USER_1, erc20tokenAddress: WETH_CONTRACT_ADDRESS, erc20tokenDecimals: 18, }) + // Transfer WETH from TBA to jeebay.eth + const ensTransferredERC20Hash = await tokenboundClient.transferERC20({ + account: ZORA721_TBA_ADDRESS, + amount: transferEthValue, + recipientAddress: 'jeebay.eth', + erc20tokenAddress: WETH_CONTRACT_ADDRESS, + erc20tokenDecimals: 18, + }) + const tbaWETHFinal = await getWETHBalance({ publicClient, walletAddress: ZORA721_TBA_ADDRESS, @@ -608,6 +734,11 @@ function runTxTests({ walletAddress: ANVIL_USER_1, }) + const ensWETHBalance = await getWETHBalance({ + publicClient, + walletAddress: 'jeebay.eth', + }) + console.log( 'TBA WETH INITIAL: ', formatEther(tbaWETHInitial), @@ -616,15 +747,19 @@ function runTxTests({ 'AFTER: ', formatEther(tbaWETHFinal), 'ANVIL USER 1 BALANCE: ', - formatEther(anvilUser1WETHBalance) + formatEther(anvilUser1WETHBalance), + 'ENS BALANCE: ', + formatEther(ensWETHBalance) ) await waitFor(() => { expect(wethDepositHash).toMatch(ADDRESS_REGEX) expect(wethTransferHash).toMatch(ADDRESS_REGEX) expect(transferredERC20Hash).toMatch(ADDRESS_REGEX) + expect(ensTransferredERC20Hash).toMatch(ADDRESS_REGEX) expect(tbaWETHReceived).toBe(depositWeiValue) - expect(anvilUser1WETHBalance).toBe(depositWeiValue) + expect(anvilUser1WETHBalance).toBe(transferWeiValue) + expect(ensWETHBalance).toBe(transferWeiValue) }) }) diff --git a/packages/sdk/src/test/config/mints.ts b/packages/sdk/src/test/config/mints.ts index 2a66268..75c326c 100644 --- a/packages/sdk/src/test/config/mints.ts +++ b/packages/sdk/src/test/config/mints.ts @@ -7,7 +7,7 @@ export const zora721 = { abi: zora721DropABI, proxyContractAddress: getAddress('0x28ee638f2fcb66b4106acab7efd225aeb2bd7e8d'), mintPrice: BigInt(0), - quantity: 2, + quantity: 3, } // https://zora.co/collect/eth:0x373075bab7d668ed2473d8233ebdebcf49eb758e/1 diff --git a/packages/sdk/src/test/utils/getWETHBalance.ts b/packages/sdk/src/test/utils/getWETHBalance.ts index e06d994..5ce0be1 100644 --- a/packages/sdk/src/test/utils/getWETHBalance.ts +++ b/packages/sdk/src/test/utils/getWETHBalance.ts @@ -1,18 +1,21 @@ import { PublicClient } from 'viem' import { erc20ABI } from 'wagmi' import { WETH_CONTRACT_ADDRESS } from '../constants' +import { resolvePossibleENS } from '../../utils' +import { PossibleENSAddress } from '../../types' export async function getWETHBalance({ publicClient, walletAddress, }: { publicClient: PublicClient - walletAddress: `0x${string}` + walletAddress: PossibleENSAddress }) { + const address = await resolvePossibleENS(publicClient, walletAddress) return await publicClient.readContract({ address: WETH_CONTRACT_ADDRESS, abi: erc20ABI, functionName: 'balanceOf', - args: [walletAddress], + args: [address], }) } diff --git a/packages/sdk/src/test/utils/getZora1155Balance.ts b/packages/sdk/src/test/utils/getZora1155Balance.ts index 6bf4079..77145db 100644 --- a/packages/sdk/src/test/utils/getZora1155Balance.ts +++ b/packages/sdk/src/test/utils/getZora1155Balance.ts @@ -1,18 +1,21 @@ import { PublicClient } from 'viem' import { zora1155 } from '../config' import { zora1155ABI } from '../wagmi-cli-hooks/generated' +import { resolvePossibleENS } from '../../utils' +import { PossibleENSAddress } from '../../types' export async function getZora1155Balance({ publicClient, walletAddress, }: { publicClient: PublicClient - walletAddress: `0x${string}` + walletAddress: PossibleENSAddress }) { + const address = await resolvePossibleENS(publicClient, walletAddress) return await publicClient.readContract({ address: zora1155.proxyContractAddress, abi: zora1155ABI, functionName: 'balanceOf', - args: [walletAddress, zora1155.tokenId], + args: [address, zora1155.tokenId], }) } diff --git a/packages/sdk/src/test/utils/getZora721Balance.ts b/packages/sdk/src/test/utils/getZora721Balance.ts index 1d6f8bc..0c0c9e4 100644 --- a/packages/sdk/src/test/utils/getZora721Balance.ts +++ b/packages/sdk/src/test/utils/getZora721Balance.ts @@ -1,18 +1,22 @@ -import { PublicClient } from 'viem' +import { PublicClient, getAddress } from 'viem' import { zora721 } from '../config' import { zora721DropABI } from '../wagmi-cli-hooks/generated' +import { resolvePossibleENS } from '../../utils' +import { PossibleENSAddress } from '../../types' export async function getZora721Balance({ publicClient, walletAddress, }: { publicClient: PublicClient - walletAddress: `0x${string}` + walletAddress: PossibleENSAddress }) { + const address = await resolvePossibleENS(publicClient, walletAddress) + return await publicClient.readContract({ address: zora721.proxyContractAddress, abi: zora721DropABI, functionName: 'balanceOf', - args: [walletAddress], + args: [address], }) } diff --git a/packages/sdk/src/types/addresses.ts b/packages/sdk/src/types/addresses.ts new file mode 100644 index 0000000..f80091b --- /dev/null +++ b/packages/sdk/src/types/addresses.ts @@ -0,0 +1 @@ +export type PossibleENSAddress = `0x${string}` | `${string}.eth` diff --git a/packages/sdk/src/types/index.ts b/packages/sdk/src/types/index.ts index 691ca0f..2ed00e8 100644 --- a/packages/sdk/src/types/index.ts +++ b/packages/sdk/src/types/index.ts @@ -1,6 +1,7 @@ export * from './abstractBigNumber' export * from './abstractEthersSigner' export * from './abstractEthersTransactionResponse' +export * from './addresses' export * from './anvilAccount' export * from './erc1155Bytecode' export * from './messages' diff --git a/packages/sdk/src/types/params.ts b/packages/sdk/src/types/params.ts index 51b3c7b..2b7d50c 100644 --- a/packages/sdk/src/types/params.ts +++ b/packages/sdk/src/types/params.ts @@ -1,6 +1,7 @@ import { WalletClient, PublicClient } from 'viem' import { Prettify } from './prettify' import { UniversalSignableMessage } from './messages' +import { PossibleENSAddress } from './addresses' export const NFTTokenType = { ERC721: 'ERC721', @@ -27,20 +28,20 @@ interface TokenTypeParams { export type NFTTransferParams = Prettify< TokenTypeParams & NFTParams & { - recipientAddress: `0x${string}` + recipientAddress: PossibleENSAddress account: `0x${string}` } > export type ETHTransferParams = Prettify<{ account: `0x${string}` - recipientAddress: `0x${string}` // | `${string}.eth` + recipientAddress: PossibleENSAddress amount: number }> export type ERC20TransferParams = Prettify<{ account: `0x${string}` - recipientAddress: `0x${string}` + recipientAddress: PossibleENSAddress amount: number erc20tokenAddress: `0x${string}` erc20tokenDecimals: number diff --git a/packages/sdk/src/utils/index.ts b/packages/sdk/src/utils/index.ts index db82735..cb1e733 100644 --- a/packages/sdk/src/utils/index.ts +++ b/packages/sdk/src/utils/index.ts @@ -5,3 +5,4 @@ export * from './segmentBytecode' export * from './ethersAdaptors' export * from './normalizeEthersMessage' export * from './messageTypeguards' +export * from './resolvePossibleENS' diff --git a/packages/sdk/src/utils/resolvePossibleENS.ts b/packages/sdk/src/utils/resolvePossibleENS.ts new file mode 100644 index 0000000..8b0b57b --- /dev/null +++ b/packages/sdk/src/utils/resolvePossibleENS.ts @@ -0,0 +1,21 @@ +import { normalize } from 'viem/ens' +import { PossibleENSAddress } from '../types' +import { PublicClient, getAddress } from 'viem' + +export async function resolvePossibleENS( + publicClient: PublicClient, + possibleENSAddress: PossibleENSAddress +): Promise<`0x${string}`> { + const isENS = possibleENSAddress.endsWith('.eth') + const address = isENS + ? await publicClient.getEnsAddress({ + name: normalize(possibleENSAddress), + }) + : getAddress(possibleENSAddress) + + if (!address) { + throw new Error('Failed to resolve ENS address') + } + + return address +}