├── .env.example ├── test ├── helpers │ ├── constants.ts │ ├── network.ts │ ├── deploy.ts │ └── testIpfsPath.ts └── fndCart.ts ├── .gitignore ├── .prettierignore ├── contracts ├── mocks │ ├── EmptyMockContract.sol │ └── MockNFT.sol ├── importsFoundationCollections.sol ├── importFoundationMarket.sol └── FNDCart.sol ├── .prettierrc ├── tsconfig.json ├── hardhat.test-mainnet-fork.config.ts ├── .solhint.json ├── .eslintrc.yaml ├── .circleci ├── commit.sh └── config.yml ├── hardhat.config.ts ├── test-mainnet-fork ├── helpers │ ├── mainnet.ts │ └── getFoundationContracts.ts └── fndCart.ts ├── scripts ├── rpc │ ├── getMintedAt.ts │ ├── availableMarketActions.ts │ └── getSaleHistory.ts └── subgraph │ ├── getMintedAt.ts │ └── getSaleHistory.ts ├── README.md └── package.json /.env.example: -------------------------------------------------------------------------------- 1 | RPC_URL_MAINNET=TODO -------------------------------------------------------------------------------- /test/helpers/constants.ts: -------------------------------------------------------------------------------- 1 | export const ONE_DAY = 24 * 60 * 60; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain-types 6 | 7 | #Hardhat files 8 | cache 9 | artifacts 10 | 11 | yarn-error.log 12 | 13 | .openzeppelin/ 14 | 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts/ 3 | build/ 4 | cache/ 5 | coverage/ 6 | dist/ 7 | docs/ 8 | lib/ 9 | node_modules/ 10 | typechain-types/ 11 | .vscode/ 12 | deployments/ 13 | 14 | # files 15 | coverage.json 16 | -------------------------------------------------------------------------------- /contracts/mocks/EmptyMockContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | contract EmptyMockContract { 6 | // Something must be included in order to generate the typechain file 7 | event DummyEvent(); 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "endOfLine":"auto", 5 | "printWidth": 120, 6 | "singleQuote": false, 7 | "tabWidth": 2, 8 | "trailingComma": "all", 9 | "overrides": [ 10 | { 11 | "files": "*.sol", 12 | "options": { 13 | "tabWidth": 2 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /contracts/importsFoundationCollections.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | // Import contracts so that their typechain files are made available 6 | import "@f8n/fnd-protocol/contracts/NFTCollectionFactory.sol"; 7 | import "@f8n/fnd-protocol/contracts/NFTCollection.sol"; 8 | import "@f8n/fnd-protocol/contracts/NFTDropCollection.sol"; 9 | -------------------------------------------------------------------------------- /contracts/importFoundationMarket.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | // Import contracts so that their typechain files are made available 6 | import "@f8n/fnd-protocol/contracts/FoundationTreasury.sol"; 7 | import "@f8n/fnd-protocol/contracts/PercentSplitETH.sol"; 8 | import "@f8n/fnd-protocol/contracts/NFTMarket.sol"; 9 | import "@f8n/fnd-protocol/contracts/FNDMiddleware.sol"; 10 | import "@f8n/fnd-protocol/contracts/FETH.sol"; 11 | -------------------------------------------------------------------------------- /test/helpers/network.ts: -------------------------------------------------------------------------------- 1 | import { providers } from "ethers"; 2 | import { ethers } from "hardhat"; 3 | 4 | export async function increaseTime(seconds: number): Promise { 5 | const provider: providers.JsonRpcProvider = ethers.provider; 6 | await provider.send("evm_increaseTime", [seconds]); 7 | await advanceBlock(); 8 | } 9 | 10 | export async function advanceBlock() { 11 | const provider: providers.JsonRpcProvider = ethers.provider; 12 | await provider.send("evm_mine", []); 13 | } 14 | -------------------------------------------------------------------------------- /contracts/mocks/MockNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 7 | 8 | contract MockNFT is Ownable, ERC721 { 9 | uint256 private nextTokenId; 10 | 11 | constructor() 12 | ERC721("MockNFT", "mNFT") // solhint-disable-next-line no-empty-blocks 13 | {} 14 | 15 | function mint() external onlyOwner { 16 | _mint(msg.sender, ++nextTokenId); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["es5", "es6"], 6 | "jsx": "react", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "noImplicitAny": true, 10 | "outDir": "dist", 11 | "resolveJsonModule": true, 12 | "sourceMap": true, 13 | "strict": true, 14 | "target": "es5" 15 | }, 16 | "exclude": ["artifacts/**/*", "node_modules"], 17 | "include": ["hardhat*.config.ts", "test/**/*", "test-mainnet-fork/**/*", "typechain-types/**/*", "scripts/**/*"] 18 | } 19 | -------------------------------------------------------------------------------- /hardhat.test-mainnet-fork.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/types"; 2 | import baseConfig from "./hardhat.config"; 3 | 4 | if (!process.env[`RPC_URL_MAINNET`]) { 5 | throw new Error("Missing .env RPC_URL_MAINNET"); 6 | } 7 | 8 | const config: HardhatUserConfig = { 9 | ...baseConfig, 10 | networks: { 11 | hardhat: { 12 | forking: { 13 | url: process.env[`RPC_URL_MAINNET`], 14 | }, 15 | chainId: 1, 16 | gasPrice: "auto", 17 | }, 18 | }, 19 | paths: { 20 | ...baseConfig.paths, 21 | tests: "./test-mainnet-fork", 22 | }, 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 7], 6 | "compiler-version": ["error", "^0.8.0"], 7 | "const-name-snakecase": "off", 8 | "constructor-syntax": "error", 9 | "func-visibility": ["error", { "ignoreConstructors": true }], 10 | "max-line-length": ["error", 120], 11 | "not-rely-on-time": "off", 12 | "prettier/prettier": [ 13 | "error", 14 | { 15 | "endOfLine": "auto" 16 | } 17 | ], 18 | "reason-string": ["warn", { "maxLength": 64 }], 19 | "var-name-mixedcase": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - eslint:recommended 3 | - plugin:@typescript-eslint/eslint-recommended 4 | - plugin:@typescript-eslint/recommended 5 | - prettier 6 | parser: "@typescript-eslint/parser" 7 | parserOptions: 8 | project: tsconfig.json 9 | plugins: 10 | - "@typescript-eslint" 11 | - "no-only-tests" 12 | root: true 13 | rules: 14 | "@typescript-eslint/no-floating-promises": 15 | - error 16 | - ignoreIIFE: true 17 | ignoreVoid: true 18 | "@typescript-eslint/no-inferrable-types": "off" 19 | "@typescript-eslint/no-unused-vars": 20 | - error 21 | - argsIgnorePattern: _ 22 | varsIgnorePattern: _ 23 | "sort-imports": 24 | - error 25 | - ignoreCase: true 26 | ignoreDeclarationSort: true 27 | "no-only-tests/no-only-tests": "error" 28 | overrides: 29 | - files: subgraph/**/*.ts 30 | rules: 31 | prefer-const: 0 32 | -------------------------------------------------------------------------------- /.circleci/commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Based on https://github.com/eldarlabs/ghpages-deploy-script/blob/master/scripts/deploy-ghpages.sh 3 | 4 | echo "Auto-commit starting..." 5 | 6 | # abort the script if there is a non-zero error 7 | set -e 8 | 9 | remote=$(git config remote.origin.url) 10 | 11 | # now lets setup a new repo so we can update the branch 12 | echo "git config email" 13 | git config --global user.email "$GH_EMAIL" > /dev/null 2>&1 14 | echo "git config name" 15 | git config --global user.name "$GH_NAME" > /dev/null 2>&1 16 | 17 | cd ~/repo 18 | 19 | # stage any changes and new files 20 | echo "git add" 21 | git add -A 22 | if ! git diff-index --quiet origin/$CIRCLE_BRANCH --; then 23 | echo "Changes detected. Committing..." 24 | # now commit 25 | git commit -m "auto lint/docs/gas/size update" 26 | echo "Pushing commit..." 27 | # and push 28 | git push --set-upstream origin $CIRCLE_BRANCH 29 | else 30 | echo "No changes to commit." 31 | fi 32 | 33 | echo "Auto-commit done." -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { config as dotenvConfig } from "dotenv"; 2 | import { resolve } from "path"; 3 | dotenvConfig({ path: resolve(__dirname, "./.env") }); 4 | 5 | import "@nomiclabs/hardhat-waffle"; 6 | import "@typechain/hardhat"; 7 | import "@nomiclabs/hardhat-ethers"; 8 | import "@nomiclabs/hardhat-waffle"; 9 | import "hardhat-gas-reporter"; 10 | import "hardhat-contract-sizer"; 11 | import "@openzeppelin/hardhat-upgrades"; 12 | import { HardhatUserConfig } from "hardhat/types"; 13 | import "hardhat-tracer"; 14 | 15 | const config: HardhatUserConfig = { 16 | solidity: { 17 | version: "0.8.15", 18 | settings: { 19 | optimizer: { 20 | enabled: true, 21 | runs: 1337, 22 | }, 23 | }, 24 | }, 25 | networks: { 26 | mainnet: { 27 | url: process.env[`RPC_URL_MAINNET`], 28 | }, 29 | }, 30 | typechain: { 31 | target: "ethers-v5", 32 | externalArtifacts: ["node_modules/@manifoldxyz/royalty-registry-solidity/build/contracts/*.json"], 33 | }, 34 | gasReporter: { 35 | excludeContracts: ["mocks/", "FoundationTreasury.sol", "ERC721.sol"], 36 | }, 37 | mocha: { 38 | timeout: 1200000, 39 | }, 40 | }; 41 | 42 | export default config; 43 | -------------------------------------------------------------------------------- /test-mainnet-fork/helpers/mainnet.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { ethers, network } from "hardhat"; 3 | 4 | export async function resetNodeState() { 5 | await network.provider.request({ 6 | method: "hardhat_reset", 7 | params: [ 8 | { 9 | forking: { 10 | jsonRpcUrl: process.env.RPC_URL_MAINNET, 11 | }, 12 | }, 13 | ], 14 | }); 15 | } 16 | 17 | export async function addSomeETH(address: SignerWithAddress) { 18 | const balance = ethers.utils.hexStripZeros(ethers.utils.parseEther("1000").toHexString()); 19 | await network.provider.send("hardhat_setBalance", [address.address, balance]); 20 | } 21 | 22 | export async function impersonate(address: string): Promise { 23 | const signer = await ethers.getSigner(address); 24 | await network.provider.request({ 25 | method: "hardhat_impersonateAccount", 26 | params: [signer.address], 27 | }); 28 | await addSomeETH(signer); 29 | return signer; 30 | } 31 | 32 | export async function stopImpersonate(address: SignerWithAddress) { 33 | await network.provider.request({ 34 | method: "hardhat_stopImpersonatingAccount", 35 | params: [address.address], 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /scripts/rpc/getMintedAt.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * An example of how to query the the time an NFT was minted using RPC (e.g. Alchemy) requests. 3 | * Run with `yarn run-script scripts/rpc/getMintedAt.ts --network mainnet` 4 | * 5 | * See `scripts/subgraph/getMintedAt.ts` for an alternative approach to querying this value. 6 | */ 7 | 8 | import { ethers } from "hardhat"; 9 | import { IERC721__factory } from "../../typechain-types"; 10 | 11 | // The example NFT to query for, this should be updated with the NFT of interest 12 | const collection = "0x3B3ee1931Dc30C1957379FAc9aba94D1C48a5405"; 13 | const tokenId = 105912; 14 | 15 | async function main(): Promise { 16 | const nft = IERC721__factory.connect(collection, ethers.provider); 17 | 18 | const mintedEvents = await nft.queryFilter(nft.filters.Transfer(ethers.constants.AddressZero, undefined, tokenId)); 19 | if (mintedEvents.length === 0) { 20 | console.log(`No mint events found for ${collection} #${tokenId}`); 21 | return; 22 | } 23 | 24 | // It is possible for an NFT to be burned and then minted again, in that case only the most recent mint is relevant. 25 | const mostRecentMint = mintedEvents[mintedEvents.length - 1]; 26 | const mintedAt = (await ethers.provider.getBlock(mostRecentMint.blockNumber)).timestamp; 27 | 28 | console.log(`NFT ${collection} #${tokenId} was minted at ${mintedAt}`); 29 | } 30 | 31 | main() 32 | .then(() => process.exit(0)) 33 | .catch((error: Error) => { 34 | console.error(error); 35 | process.exit(1); 36 | }); 37 | -------------------------------------------------------------------------------- /test-mainnet-fork/helpers/getFoundationContracts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FETH, 3 | FETH__factory, 4 | FoundationTreasury, 5 | FoundationTreasury__factory, 6 | NFTCollectionFactory, 7 | NFTCollectionFactory__factory, 8 | NFTMarket, 9 | NFTMarket__factory, 10 | PercentSplitETH, 11 | PercentSplitETH__factory, 12 | } from "../../typechain-types/index"; 13 | import { ethers } from "hardhat"; 14 | import { resetNodeState } from "./mainnet"; 15 | // eslint-disable-next-line @typescript-eslint/no-var-requires 16 | const addresses = require("@f8n/fnd-protocol/addresses.js"); 17 | 18 | export type FoundationContracts = { 19 | treasury: FoundationTreasury; 20 | market: NFTMarket; 21 | percentSplitFactory: PercentSplitETH; 22 | collectionFactory: NFTCollectionFactory; 23 | feth: FETH; 24 | }; 25 | 26 | export async function getFoundationContracts(): Promise { 27 | await resetNodeState(); 28 | const [defaultAccount] = await ethers.getSigners(); 29 | 30 | const treasury = FoundationTreasury__factory.connect(addresses.prod[1].treasury, defaultAccount); 31 | const market = NFTMarket__factory.connect(addresses.prod[1].nftMarket, defaultAccount); 32 | const percentSplitFactory = PercentSplitETH__factory.connect(addresses.prod[1].percentSplit, defaultAccount); 33 | const collectionFactory = NFTCollectionFactory__factory.connect( 34 | addresses.prod[1].nftCollectionFactoryV2, 35 | defaultAccount, 36 | ); 37 | const feth = FETH__factory.connect(addresses.prod[1].feth, defaultAccount); 38 | 39 | return { 40 | treasury, 41 | market, 42 | percentSplitFactory, 43 | collectionFactory, 44 | feth, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /scripts/subgraph/getMintedAt.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * An example of how to query the time an NFT was minted from our hosted subgraph. 3 | * Run with `yarn run-script scripts/subgraph/getMintedAt.ts` 4 | * 5 | * Note: due to a limit in our subgraph implementation ATM this only works for Foundation NFTs 6 | * (a Foundation created collection or the shared collection contract). 7 | * 8 | * See `scripts/rpc/getMintedAt.ts` for an alternative approach to querying this value. 9 | */ 10 | 11 | import gql from "graphql-tag"; 12 | import { GraphQLClient } from "graphql-request"; 13 | // eslint-disable-next-line @typescript-eslint/no-var-requires 14 | const subgraphEndpoints = require("@f8n/fnd-protocol/subgraphEndpoints.js"); 15 | 16 | // The example NFT to query for, this should be updated with the NFT of interest 17 | const collection = "0x3B3ee1931Dc30C1957379FAc9aba94D1C48a5405"; 18 | const tokenId = 105912; 19 | 20 | async function main(): Promise { 21 | // Connect to the Foundation subgraph endpoint 22 | const graphClient = new GraphQLClient(subgraphEndpoints.prod[1]); 23 | 24 | // Subgraph queries are case sensitive and addresses are always lowercase (instead of in the checksum format) 25 | const mintedAtQuery = gql`{ 26 | nft(id: "${collection.toLowerCase()}-${tokenId}") 27 | { 28 | dateMinted 29 | } 30 | }`; 31 | 32 | // Query for the date minted 33 | const results = await graphClient.request(mintedAtQuery); 34 | console.log(results); 35 | 36 | if (!results?.nft?.dateMinted) { 37 | console.log(`NFT not found: ${collection} #${tokenId}`); 38 | return; 39 | } 40 | 41 | console.log(`NFT ${collection} #${tokenId} was minted at ${results.nft.dateMinted}`); 42 | } 43 | 44 | main() 45 | .then(() => process.exit(0)) 46 | .catch((error: Error) => { 47 | console.error(error); 48 | process.exit(1); 49 | }); 50 | -------------------------------------------------------------------------------- /scripts/subgraph/getSaleHistory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * An example of how to query the Foundation sale history from our hosted subgraph. 3 | * Run with `yarn run-script scripts/subgraph/getSaleHistory.ts` 4 | * 5 | * See `scripts/rpc/getSaleHistory.ts` for an alternative approach to querying the Foundation sale history. 6 | */ 7 | 8 | import gql from "graphql-tag"; 9 | import { GraphQLClient } from "graphql-request"; 10 | // eslint-disable-next-line @typescript-eslint/no-var-requires 11 | const subgraphEndpoints = require("@f8n/fnd-protocol/subgraphEndpoints.js"); 12 | 13 | // The example NFT to query for, this should be updated with the NFT of interest 14 | const collection = "0x3B3ee1931Dc30C1957379FAc9aba94D1C48a5405"; 15 | const tokenId = 105912; 16 | 17 | async function main(): Promise { 18 | // Connect to the Foundation subgraph endpoint 19 | const graphClient = new GraphQLClient(subgraphEndpoints.prod[1]); 20 | 21 | // Subgraph queries are case sensitive and addresses are always lowercase (instead of in the checksum format) 22 | const saleHistoryQuery = gql`{ 23 | nftHistories( 24 | where: { 25 | nft: "${collection.toLowerCase()}-${tokenId}" 26 | event_in: [Sold, OfferAccepted, BuyPriceAccepted] 27 | }, 28 | orderBy: date, 29 | orderDirection: asc 30 | ) { 31 | event 32 | amountInETH 33 | } 34 | }`; 35 | 36 | // Query for the sales history 37 | const saleHistory = await graphClient.request(saleHistoryQuery); 38 | 39 | if (saleHistory.nftHistories.length === 0) { 40 | console.log(`No sales found for ${collection} #${tokenId}`); 41 | return; 42 | } 43 | 44 | const mostRecentSale = saleHistory.nftHistories[saleHistory.nftHistories.length - 1]; 45 | 46 | console.log(`Most recent sale for ${collection} #${tokenId} was for ${mostRecentSale.amountInETH} ETH`); 47 | } 48 | 49 | main() 50 | .then(() => process.exit(0)) 51 | .catch((error: Error) => { 52 | console.error(error); 53 | process.exit(1); 54 | }); 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Repo — Protocol 2 | 3 | Examples showing how you can use the @f8n/fnd-protocol npm package and integrate with [Foundation smart contracts](https://github.com/f8n/fnd-protocol). 4 | 5 | Set up the repository with: 6 | 7 | ``` 8 | yarn 9 | yarn build 10 | ``` 11 | 12 | ## Testing 13 | 14 | To test using a local deployment of the Foundation contracts, run: 15 | 16 | ``` 17 | yarn test 18 | ``` 19 | 20 | This uses the `test/helpers/deploy.ts` helper file to deploy Foundation contracts to your local machine for testing. 21 | 22 | ### Mainnet fork 23 | 24 | Alternatively you can test your contracts using a [mainnet fork](https://hardhat.org/hardhat-network/guides/mainnet-forking.html#mainnet-forking). Once forked, new contract deployments and any other transactions sent happen on your local machine only. This is a great way to confirm that your contracts work as expected with the latest Foundation contracts. 25 | 26 | First create a `.env` file (you can use `.env.example` as a template) and set the RPC endpoint. We use [Alchemy](https://www.alchemyapi.io/) but any RPC provider should work. 27 | 28 | Then you can test with: 29 | 30 | ``` 31 | yarn test-mainnet-fork 32 | ``` 33 | 34 | This uses the `test-mainnet-fork/helpers/getFoundationContracts.ts` helper file to get Typechain instances of each of the Foundation contracts, reading the addresses from our NPM package. 35 | 36 | We have also included helper functions such as `impersonate` to ease interacting with the fork. 37 | 38 | ## Example scripts 39 | 40 | Some example scripts are included in the `scripts` directory to demonstrate various ways you may integrate with Foundation contracts or the data it logs on-chain. The `scripts/rpc` directory shows integrations using just an RPC endpoint such as Alchemy and the `scripts/subgraph` directory shows an alternative approach using our hosted subgraph instance. 41 | 42 | If you have a question on how to use the Foundation contracts, please post to the [discussions](https://github.com/f8n/fnd-protocol-examples/discussions) - maybe we could create another example script to help you and others. 43 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | wait-for: cobli/wait-for@0.0.2 4 | jobs: 5 | build: 6 | docker: 7 | - image: circleci/node:16 8 | working_directory: ~/repo 9 | steps: 10 | - checkout 11 | 12 | - run: yarn 13 | - run: yarn build 14 | 15 | - persist_to_workspace: 16 | root: ~/repo 17 | paths: . 18 | test: 19 | docker: 20 | - image: circleci/node:16 21 | working_directory: ~/repo 22 | steps: 23 | - attach_workspace: 24 | at: ~/repo 25 | 26 | - run: yarn test 27 | test-mainnet-fork: 28 | docker: 29 | - image: circleci/node:16 30 | working_directory: ~/repo 31 | steps: 32 | - attach_workspace: 33 | at: ~/repo 34 | 35 | - run: yarn test-mainnet-fork 36 | lint: 37 | docker: 38 | - image: circleci/node:16 39 | working_directory: ~/repo 40 | steps: 41 | - attach_workspace: 42 | at: ~/repo 43 | 44 | - run: yarn lint 45 | 46 | # Auto-commit any lint changes 47 | - run: mkdir -p ~/.ssh 48 | - run: ssh-keyscan -H github.com >> ~/.ssh/known_hosts 49 | - run: bash .circleci/commit.sh 50 | scripts: 51 | docker: 52 | - image: circleci/node:16 53 | working_directory: ~/repo 54 | steps: 55 | - attach_workspace: 56 | at: ~/repo 57 | 58 | # Run scripts to confirm they still compile and execute without error 59 | - run: yarn run-script scripts/rpc/getSaleHistory.ts --network mainnet 60 | - run: yarn run-script scripts/rpc/availableMarketActions.ts --network mainnet 61 | - run: yarn run-script scripts/rpc/getMintedAt.ts --network mainnet 62 | - run: yarn run-script scripts/subgraph/getSaleHistory.ts 63 | - run: yarn run-script scripts/subgraph/getMintedAt.ts 64 | 65 | workflows: 66 | build: 67 | jobs: 68 | - build: 69 | filters: 70 | tags: 71 | only: /.*/ 72 | branches: 73 | ignore: 74 | - gh-pages 75 | - artifacts 76 | - test: 77 | requires: 78 | - build 79 | - test-mainnet-fork: 80 | requires: 81 | - build 82 | - lint: 83 | requires: 84 | - build 85 | - scripts: 86 | requires: 87 | - build 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@f8n/fnd-protocol-examples", 3 | "description": "Examples building with the Foundation smart contracts", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "hardhat typechain", 7 | "lint": "yarn prettier && yarn lint:sol && yarn lint:ts", 8 | "lint:sol": "solhint --config ./.solhint.json --fix \"contracts/**/*.sol\"", 9 | "lint:ts": "eslint --config ./.eslintrc.yaml --fix --ext .js,.ts .", 10 | "prettier": "prettier --config .prettierrc --write --list-different \"**/*.{js,json,md,sol,ts}\"", 11 | "test": "hardhat test", 12 | "test-mainnet-fork": "hardhat --config hardhat.test-mainnet-fork.config.ts test", 13 | "run-script": "hardhat run", 14 | "size-contracts": "hardhat size-contracts" 15 | }, 16 | "devDependencies": { 17 | "@f8n/fnd-protocol": "2.3.0", 18 | "@manifoldxyz/royalty-registry-solidity": "1.0.9", 19 | "@nomiclabs/hardhat-ethers": "2.1.1", 20 | "@nomiclabs/hardhat-waffle": "2.0.3", 21 | "@openzeppelin/contracts": "4.7.3", 22 | "@openzeppelin/contracts-upgradeable": "4.7.3", 23 | "@openzeppelin/hardhat-upgrades": "1.17.0", 24 | "@primitivefi/hardhat-dodoc": "0.2.3", 25 | "@typechain/ethers-v5": "9.0.0", 26 | "@typechain/hardhat": "5.0.0", 27 | "@types/chai": "4.3.3", 28 | "@types/mocha": "10.0.0", 29 | "@typescript-eslint/eslint-plugin": "5.38.1", 30 | "@typescript-eslint/parser": "5.38.1", 31 | "chai": "4.3.6", 32 | "dotenv": "16.0.3", 33 | "eslint": "8.23.1", 34 | "eslint-config-prettier": "8.5.0", 35 | "eslint-plugin-no-only-tests": "3.0.0", 36 | "ethereum-waffle": "3.4.4", 37 | "ethers": "5.7.1", 38 | "graphql": "16.6.0", 39 | "graphql-request": "5.0.0", 40 | "graphql-tag": "2.12.6", 41 | "hardhat": "2.11.2", 42 | "hardhat-contract-sizer": "2.6.1", 43 | "hardhat-gas-reporter": "1.0.9", 44 | "hardhat-tracer": "1.0.0-alpha.6", 45 | "prettier": "2.7.1", 46 | "prettier-plugin-solidity": "1.0.0-dev.23", 47 | "solc": "0.8.17", 48 | "solhint": "3.3.7", 49 | "solhint-plugin-prettier": "0.0.5", 50 | "ts-node": "10.9.1", 51 | "typechain": "7.0.1", 52 | "typescript": "4.8.4" 53 | }, 54 | "license": "MIT", 55 | "repository": { 56 | "type": "git", 57 | "url": "https://github.com/f8n/fnd-protocol-examples" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/f8n/fnd-protocol-examples/issues" 61 | }, 62 | "homepage": "https://github.com/f8n/fnd-protocol-examples" 63 | } 64 | -------------------------------------------------------------------------------- /scripts/rpc/availableMarketActions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * An example of how to query the current state of an NFT on Foundation and which actions are available to users. 3 | * This script uses our "middleware" contract to query state in a single call, but the same information is available 4 | * via direct calls to the market contract. 5 | * 6 | * Run with `yarn run-script scripts/rpc/availableMarketActions.ts --network mainnet` 7 | */ 8 | 9 | import { ethers } from "hardhat"; 10 | import { FNDMiddleware__factory } from "../../typechain-types"; 11 | // eslint-disable-next-line @typescript-eslint/no-var-requires 12 | const addresses = require("@f8n/fnd-protocol/addresses.js"); 13 | 14 | // The example NFT to query for, this should be updated with the NFT of interest 15 | const collection = "0xfb856F0AeD8b23dcED99484031f8FE471b36B0C7"; 16 | const tokenId = 3; 17 | 18 | async function main(): Promise { 19 | // Connect to the Foundation middleware contract 20 | const middleware = FNDMiddleware__factory.connect(addresses.prod[1].middleware, ethers.provider); 21 | 22 | const nftDetails = await middleware.getNFTDetails(collection, tokenId); 23 | 24 | // Auction state 25 | if (nftDetails.auctionPrice.eq(0)) { 26 | console.log(`No auction found`); 27 | } else if (nftDetails.auctionEndTime.eq(0)) { 28 | console.log( 29 | `An auction is listed with a reserve price of ${ethers.utils.formatEther(nftDetails.auctionPrice)} ETH`, 30 | ); 31 | } else { 32 | const currentTime = (await ethers.provider.getBlock("latest")).timestamp; 33 | if (nftDetails.auctionEndTime.gt(currentTime)) { 34 | console.log( 35 | `An auction is in progress with the highest bid of ${ethers.utils.formatEther( 36 | nftDetails.auctionPrice, 37 | )} ETH from ${nftDetails.auctionBidder}`, 38 | ); 39 | } else { 40 | console.log( 41 | `The auction has ended, selling to ${nftDetails.auctionBidder} for ${ethers.utils.formatEther( 42 | nftDetails.auctionPrice, 43 | )} ETH, and is pending finalization (settle auction)`, 44 | ); 45 | } 46 | } 47 | 48 | // Buy price 49 | if (nftDetails.buyPrice.lt(ethers.constants.MaxUint256)) { 50 | console.log(`The NFT may be bought for ${ethers.utils.formatEther(nftDetails.buyPrice)} ETH`); 51 | } else { 52 | console.log(`No buy price found`); 53 | } 54 | 55 | // Offer 56 | if (nftDetails.offerAmount.gt(0)) { 57 | console.log( 58 | `There's an offer to purchase this NFT for ${ethers.utils.formatEther(nftDetails.offerAmount)} ETH from ${ 59 | nftDetails.offerBuyer 60 | } which expires at ${nftDetails.offerExpiration}`, 61 | ); 62 | } else { 63 | console.log(`No valid (unexpired) offer found`); 64 | } 65 | 66 | // Single string for debugging purposes 67 | console.log(await middleware.getNFTDetailString(collection, tokenId)); 68 | } 69 | 70 | main() 71 | .then(() => process.exit(0)) 72 | .catch((error: Error) => { 73 | console.error(error); 74 | process.exit(1); 75 | }); 76 | -------------------------------------------------------------------------------- /scripts/rpc/getSaleHistory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * An example of how to query the Foundation sale history of an NFT using RPC (e.g. Alchemy) requests. 3 | * Run with `yarn run-script scripts/rpc/getSaleHistory.ts --network mainnet` 4 | * 5 | * See `scripts/subgraph/getSaleHistory.ts` for an alternative approach to querying the Foundation sale history. 6 | */ 7 | 8 | import { ethers } from "hardhat"; 9 | import { NFTMarket__factory } from "../../typechain-types"; 10 | import { ReserveAuctionFinalizedEvent } from "../../typechain-types/NFTMarket"; 11 | // eslint-disable-next-line @typescript-eslint/no-var-requires 12 | const addresses = require("@f8n/fnd-protocol/addresses.js"); 13 | 14 | // The example NFT to query for, this should be updated with the NFT of interest 15 | const collection = "0x3B3ee1931Dc30C1957379FAc9aba94D1C48a5405"; 16 | const tokenId = 105912; 17 | 18 | async function main(): Promise { 19 | // Connect to the Foundation market contract 20 | const market = NFTMarket__factory.connect(addresses.prod[1].nftMarket, ethers.provider); 21 | 22 | // Query for all sales via Buy Price and via Offers 23 | const buyPricesAccepted = await market.queryFilter(market.filters.BuyPriceAccepted(collection, tokenId)); 24 | const offersAccepted = await market.queryFilter(market.filters.OfferAccepted(collection, tokenId)); 25 | // It is possible to query for all buyPricesAccepted and offersAccepted in a single request, but for simplicity we do it separately here 26 | 27 | const auctionsCreated = await market.queryFilter(market.filters.ReserveAuctionCreated(null, collection, tokenId)); 28 | // Due to use of auctionIds, each auction listing needs to be checked independently to see if it sold (and for how much) 29 | const auctionsFinalized: ReserveAuctionFinalizedEvent[] = []; 30 | for (const auction of auctionsCreated) { 31 | // It is possible that an auction has ended but has not been finalized yet, and therefore be missed with this query 32 | const finalized = await market.queryFilter(market.filters.ReserveAuctionFinalized(auction.args.auctionId)); 33 | if (finalized.length > 0) { 34 | // If the auction was finalized, that event will only be emitted once for the given auctionId 35 | auctionsFinalized.push(finalized[0]); 36 | } 37 | } 38 | 39 | let allSales = [...buyPricesAccepted, ...offersAccepted, ...auctionsFinalized]; 40 | 41 | if (allSales.length === 0) { 42 | console.log(`No sales found for ${collection} #${tokenId}`); 43 | return; 44 | } 45 | 46 | allSales = allSales.sort((a, b) => { 47 | // If there were multiple sales in the same block (which should be rare), then sort by log index 48 | if (a.blockNumber === b.blockNumber) { 49 | return a.logIndex - b.logIndex; 50 | } 51 | // Otherwise sort by block number 52 | return a.blockNumber - b.blockNumber; 53 | }); 54 | 55 | const mostRecentSale = allSales[allSales.length - 1]; 56 | 57 | // The sale price is the sum of the 3 fees emitted 58 | const mostRecentSalePrice = mostRecentSale.args.sellerRev 59 | .add(mostRecentSale.args.creatorFee) 60 | .add(mostRecentSale.args.protocolFee); 61 | 62 | console.log( 63 | `Most recent sale for ${collection} #${tokenId} was for ${ethers.utils.formatEther(mostRecentSalePrice)} ETH`, 64 | ); 65 | } 66 | 67 | main() 68 | .then(() => process.exit(0)) 69 | .catch((error: Error) => { 70 | console.error(error); 71 | process.exit(1); 72 | }); 73 | -------------------------------------------------------------------------------- /test/helpers/deploy.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { upgrades } from "hardhat"; 3 | import { 4 | EmptyMockContract__factory, 5 | FETH, 6 | FETH__factory, 7 | FoundationTreasury, 8 | FoundationTreasury__factory, 9 | MockNFT, 10 | MockNFT__factory, 11 | NFTMarket, 12 | NFTMarket__factory, 13 | RoyaltyRegistry, 14 | RoyaltyRegistry__factory, 15 | } from "../../typechain-types/index"; 16 | import { ONE_DAY } from "./constants"; 17 | 18 | export type Contracts = { 19 | treasury: FoundationTreasury; 20 | market: NFTMarket; 21 | feth: FETH; 22 | royaltyRegistry: RoyaltyRegistry; 23 | nft: MockNFT; 24 | }; 25 | 26 | export async function deployContracts({ 27 | deployer, 28 | defaultAdmin, 29 | defaultOperator, 30 | creator, 31 | }: { 32 | deployer: SignerWithAddress; 33 | defaultAdmin?: SignerWithAddress; 34 | defaultOperator?: SignerWithAddress; 35 | creator?: SignerWithAddress; 36 | }): Promise { 37 | upgrades.silenceWarnings(); 38 | const treasury = await deployTreasury({ deployer, defaultAdmin, defaultOperator }); 39 | const royaltyRegistry = await deployRoyaltyRegistry(deployer); 40 | const { market, feth } = await deployMarketAndFETH({ deployer, treasury, royaltyRegistry }); 41 | const nft = await deployMockNFT(creator ?? deployer); 42 | 43 | return { treasury, market, feth, royaltyRegistry, nft }; 44 | } 45 | 46 | export async function deployTreasury({ 47 | deployer, 48 | defaultAdmin, 49 | defaultOperator, 50 | }: { 51 | deployer: SignerWithAddress; 52 | defaultAdmin?: SignerWithAddress; 53 | defaultOperator?: SignerWithAddress; 54 | }): Promise { 55 | const Treasury = new FoundationTreasury__factory(deployer); 56 | const admin = defaultAdmin ?? deployer; 57 | const treasury = (await upgrades.deployProxy(Treasury, [admin.address], { 58 | unsafeAllow: ["constructor"], 59 | })) as FoundationTreasury; 60 | const operator = defaultOperator ?? admin; 61 | await treasury.connect(admin).grantOperator(operator.address); 62 | 63 | return treasury; 64 | } 65 | 66 | export async function deployRoyaltyRegistry(deployer: SignerWithAddress): Promise { 67 | // Manually deploy proxy and set implementation, deploy helpers assume building from source 68 | const factoryProxy = await upgrades.deployProxy(new EmptyMockContract__factory(deployer)); 69 | const proxyAdmin = await upgrades.admin.getInstance(); 70 | const registryFactory = new RoyaltyRegistry__factory(deployer); 71 | let royaltyRegistry = await registryFactory.deploy(); 72 | await proxyAdmin.upgrade(factoryProxy.address, royaltyRegistry.address); 73 | royaltyRegistry = RoyaltyRegistry__factory.connect(factoryProxy.address, deployer); 74 | 75 | return royaltyRegistry; 76 | } 77 | 78 | export async function deployFETH({ 79 | deployer, 80 | marketAddress, 81 | }: { 82 | deployer: SignerWithAddress; 83 | marketAddress: string; 84 | }): Promise { 85 | const FETH = new FETH__factory(deployer); 86 | return (await upgrades.deployProxy(FETH, [], { 87 | unsafeAllow: ["state-variable-immutable", "constructor"], // https://docs.openzeppelin.com/upgrades-plugins/1.x/faq#why-cant-i-use-immutable-variables 88 | // TODO: update the second market address here to be the drop market 89 | constructorArgs: [marketAddress, marketAddress, ONE_DAY], 90 | })) as FETH; 91 | } 92 | 93 | export async function deployMarketAndFETH({ 94 | deployer, 95 | treasury, 96 | royaltyRegistry, 97 | }: { 98 | deployer: SignerWithAddress; 99 | treasury: FoundationTreasury; 100 | royaltyRegistry: RoyaltyRegistry; 101 | }): Promise<{ market: NFTMarket; feth: FETH }> { 102 | // Create a proxy to an empty mock in order to determine the proxy address to be used in constructor args 103 | const mockFactory = new EmptyMockContract__factory(deployer); 104 | const marketProxy = await upgrades.deployProxy(mockFactory); 105 | const feth = await deployFETH({ deployer, marketAddress: marketProxy.address }); 106 | const Market = new NFTMarket__factory(deployer); 107 | const market = (await upgrades.upgradeProxy(marketProxy, Market, { 108 | unsafeAllow: ["state-variable-immutable", "constructor"], // https://docs.openzeppelin.com/upgrades-plugins/1.x/faq#why-cant-i-use-immutable-variables 109 | constructorArgs: [ 110 | treasury.address, 111 | feth.address, 112 | royaltyRegistry.address, 113 | ONE_DAY, // duration 114 | ], 115 | })) as NFTMarket; 116 | await market.initialize(); 117 | 118 | return { market, feth }; 119 | } 120 | 121 | export async function deployMockNFT(deployer: SignerWithAddress): Promise { 122 | const MockNFT = new MockNFT__factory(deployer); 123 | return await MockNFT.deploy(); 124 | } 125 | -------------------------------------------------------------------------------- /test-mainnet-fork/fndCart.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { ethers } from "hardhat"; 3 | import { expect } from "chai"; 4 | import { ContractTransaction } from "ethers"; 5 | import { 6 | FETH, 7 | FNDCart, 8 | FNDCart__factory, 9 | NFTCollection, 10 | NFTCollection__factory, 11 | NFTCollectionFactory, 12 | NFTMarket, 13 | } from "../typechain-types/index"; 14 | import { getFoundationContracts } from "./helpers/getFoundationContracts"; 15 | import { increaseTime } from "../test/helpers/network"; 16 | import { ONE_DAY } from "../test/helpers/constants"; 17 | import { testIpfsPath } from "../test/helpers/testIpfsPath"; 18 | 19 | describe("FNDCart", function () { 20 | let creator: SignerWithAddress; 21 | let bidder: SignerWithAddress; 22 | let referrerTreasury: SignerWithAddress; 23 | let market: NFTMarket; 24 | let feth: FETH; 25 | let collectionFactory: NFTCollectionFactory; 26 | let nft: NFTCollection; 27 | let fndCart: FNDCart; 28 | const reservePrice = ethers.utils.parseEther("0.1"); 29 | const listedTokenIds = [1, 5, 6, 7]; 30 | const buyPrice = ethers.utils.parseEther("0.42"); 31 | const pricedTokenIds = [3, 5, 9]; 32 | let tx: ContractTransaction; 33 | 34 | beforeEach(async () => { 35 | [, creator, bidder, referrerTreasury] = await ethers.getSigners(); 36 | ({ market, feth, collectionFactory } = await getFoundationContracts()); 37 | fndCart = await new FNDCart__factory(bidder).deploy(market.address, feth.address, referrerTreasury.address); 38 | 39 | await collectionFactory.connect(creator).createNFTCollection("Collection", "COL", 42); 40 | nft = NFTCollection__factory.connect( 41 | await collectionFactory.predictNFTCollectionAddress(creator.address, 42), 42 | creator, 43 | ); 44 | 45 | // Mint 10 tokens 46 | for (let i = 0; i < 10; i++) { 47 | await nft.connect(creator).mint(testIpfsPath[i]); 48 | } 49 | await nft.connect(creator).setApprovalForAll(market.address, true); 50 | 51 | // List a few 52 | for (const tokenId of listedTokenIds) { 53 | await market.connect(creator).createReserveAuction(nft.address, tokenId, reservePrice); 54 | } 55 | 56 | // Set some buy prices 57 | for (const tokenId of pricedTokenIds) { 58 | await market.connect(creator).setBuyPrice(nft.address, tokenId, buyPrice); 59 | } 60 | }); 61 | 62 | describe("Try to buy the entire collection", () => { 63 | const cart: FNDCart.CartItemStruct[] = []; 64 | 65 | beforeEach(async () => { 66 | for (const tokenId of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) { 67 | cart.push({ nft: { nftContract: nft.address, tokenId }, maxPrice: buyPrice }); 68 | } 69 | 70 | tx = await fndCart.checkout(cart, { value: ethers.utils.parseEther("5") }); 71 | }); 72 | 73 | it("Emits buy for all purchased NFTs", async () => { 74 | for (const tokenId of pricedTokenIds) { 75 | await expect(tx) 76 | .to.emit(market, "BuyPriceAccepted") 77 | .withArgs( 78 | nft.address, 79 | tokenId, 80 | creator.address, 81 | fndCart.address, 82 | buyPrice.mul(5).div(100), 83 | buyPrice.mul(95).div(100), 84 | 0, 85 | ); 86 | await expect(tx).to.emit(market, "BuyReferralPaid").withArgs( 87 | nft.address, // token 88 | tokenId, // tokenID 89 | referrerTreasury.address, // buyReferrer 90 | buyPrice.mul(1).div(100), // buyReferrerProtocolFee 91 | 0, // buyReferrerSellerFee 92 | ); 93 | } 94 | await expect(tx).to.changeEtherBalance(referrerTreasury, buyPrice.mul(3).div(100)); 95 | }); 96 | 97 | it("Bought NFTs were transferred to the bidder", async () => { 98 | for (const tokenId of pricedTokenIds) { 99 | const ownerOf = await nft.ownerOf(tokenId); 100 | expect(ownerOf).to.eq(bidder.address); 101 | } 102 | }); 103 | 104 | it("Emits bid for all bids placed", async () => { 105 | for (const tokenId of listedTokenIds) { 106 | if (pricedTokenIds.includes(tokenId)) { 107 | // This was purchased directly instead 108 | continue; 109 | } 110 | const auctionId = await market.getReserveAuctionIdFor(nft.address, tokenId); 111 | const auctionInfo = await market.getReserveAuction(auctionId); 112 | await expect(tx) 113 | .to.emit(market, "ReserveAuctionBidPlaced") 114 | .withArgs(auctionId, fndCart.address, reservePrice, auctionInfo.endTime); 115 | } 116 | }); 117 | 118 | describe("Withdraw the auction ends", () => { 119 | beforeEach(async () => { 120 | await increaseTime(ONE_DAY + 1); 121 | 122 | const nfts: FNDCart.NFTStruct[] = []; 123 | for (const tokenId of listedTokenIds) { 124 | nfts.push({ nftContract: nft.address, tokenId }); 125 | } 126 | tx = await fndCart.withdrawNFTs(nfts, { gasLimit: 1_000_000 }); 127 | }); 128 | 129 | it("Winning bids were settled and transferred to the bidder", async () => { 130 | for (const tokenId of listedTokenIds) { 131 | if (pricedTokenIds.includes(tokenId)) { 132 | // This was purchased directly instead 133 | continue; 134 | } 135 | const ownerOf = await nft.ownerOf(tokenId); 136 | expect(ownerOf).to.eq(bidder.address); 137 | } 138 | }); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /test/fndCart.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 2 | import { ethers } from "hardhat"; 3 | import { expect } from "chai"; 4 | import { ContractTransaction } from "ethers"; 5 | import { FETH, FNDCart, FNDCart__factory, MockNFT, NFTMarket } from "../typechain-types"; 6 | import { deployContracts } from "./helpers/deploy"; 7 | import { ONE_DAY } from "./helpers/constants"; 8 | import { increaseTime } from "./helpers/network"; 9 | 10 | describe("FNDCart", function () { 11 | let deployer: SignerWithAddress; 12 | let creator: SignerWithAddress; 13 | let bidder: SignerWithAddress; 14 | let referrerTreasury: SignerWithAddress; 15 | let market: NFTMarket; 16 | let feth: FETH; 17 | let nft: MockNFT; 18 | let fndCart: FNDCart; 19 | const reservePrice = ethers.utils.parseEther("0.1"); 20 | const listedTokenIds = [1, 5, 6, 7]; 21 | const buyPrice = ethers.utils.parseEther("0.42"); 22 | const pricedTokenIds = [3, 5, 9]; 23 | let tx: ContractTransaction; 24 | 25 | beforeEach(async () => { 26 | [deployer, creator, bidder, referrerTreasury] = await ethers.getSigners(); 27 | ({ market, feth, nft } = await deployContracts({ deployer, creator })); 28 | fndCart = await new FNDCart__factory(bidder).deploy(market.address, feth.address, referrerTreasury.address); 29 | 30 | // Mint 10 tokens 31 | for (let i = 0; i < 10; i++) { 32 | await nft.connect(creator).mint(); 33 | } 34 | await nft.connect(creator).setApprovalForAll(market.address, true); 35 | 36 | // List a few 37 | for (const tokenId of listedTokenIds) { 38 | await market.connect(creator).createReserveAuction(nft.address, tokenId, reservePrice); 39 | } 40 | 41 | // Set some buy prices 42 | for (const tokenId of pricedTokenIds) { 43 | await market.connect(creator).setBuyPrice(nft.address, tokenId, buyPrice); 44 | } 45 | }); 46 | 47 | describe("Try to buy the entire collection", () => { 48 | const cart: FNDCart.CartItemStruct[] = []; 49 | 50 | beforeEach(async () => { 51 | for (const tokenId of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) { 52 | cart.push({ nft: { nftContract: nft.address, tokenId }, maxPrice: buyPrice }); 53 | } 54 | 55 | tx = await fndCart.checkout(cart, { value: ethers.utils.parseEther("5") }); 56 | }); 57 | 58 | it("Emits buy for all purchased NFTs", async () => { 59 | for (const tokenId of pricedTokenIds) { 60 | await expect(tx) 61 | .to.emit(market, "BuyPriceAccepted") 62 | .withArgs( 63 | nft.address, 64 | tokenId, 65 | creator.address, 66 | fndCart.address, 67 | buyPrice.mul(5).div(100), 68 | buyPrice.mul(95).div(100), 69 | 0, 70 | ); 71 | await expect(tx).to.emit(market, "BuyReferralPaid").withArgs( 72 | nft.address, // token 73 | tokenId, // tokenID 74 | referrerTreasury.address, // buyReferrer 75 | buyPrice.mul(1).div(100), // buyReferrerProtocolFee 76 | 0, // buyReferrerSellerFee 77 | ); 78 | } 79 | await expect(tx).to.changeEtherBalance(referrerTreasury, buyPrice.mul(3).div(100)); 80 | }); 81 | 82 | it("Bought NFTs were transferred to the bidder", async () => { 83 | for (const tokenId of pricedTokenIds) { 84 | const ownerOf = await nft.ownerOf(tokenId); 85 | expect(ownerOf).to.eq(bidder.address); 86 | } 87 | }); 88 | 89 | it("Emits bid for all bids placed", async () => { 90 | for (const tokenId of listedTokenIds) { 91 | if (pricedTokenIds.includes(tokenId)) { 92 | // This was purchased directly instead 93 | continue; 94 | } 95 | const auctionId = await market.getReserveAuctionIdFor(nft.address, tokenId); 96 | const auctionInfo = await market.getReserveAuction(auctionId); 97 | await expect(tx) 98 | .to.emit(market, "ReserveAuctionBidPlaced") 99 | .withArgs(auctionId, fndCart.address, reservePrice, auctionInfo.endTime); 100 | } 101 | }); 102 | 103 | describe("Withdraw the auction ends", () => { 104 | beforeEach(async () => { 105 | await increaseTime(ONE_DAY + 1); 106 | 107 | const nfts: FNDCart.NFTStruct[] = []; 108 | for (const tokenId of listedTokenIds) { 109 | nfts.push({ nftContract: nft.address, tokenId }); 110 | } 111 | tx = await fndCart.withdrawNFTs(nfts, { gasLimit: 1_000_000 }); 112 | }); 113 | 114 | it("Winning bids were settled and transferred to the bidder", async () => { 115 | for (const tokenId of listedTokenIds) { 116 | if (pricedTokenIds.includes(tokenId)) { 117 | // This was purchased directly instead 118 | continue; 119 | } 120 | const ownerOf = await nft.ownerOf(tokenId); 121 | expect(ownerOf).to.eq(bidder.address); 122 | } 123 | }); 124 | 125 | it("Emits BuyReferralPaid when finalized", async () => { 126 | for (const tokenId of listedTokenIds) { 127 | if (pricedTokenIds.includes(tokenId)) { 128 | // This was purchased directly instead 129 | continue; 130 | } 131 | await expect(tx).to.emit(market, "BuyReferralPaid").withArgs( 132 | nft.address, // token 133 | tokenId, // tokenID 134 | referrerTreasury.address, // buyReferrer 135 | reservePrice.mul(1).div(100), // buyReferrerProtocolFee 136 | 0, // buyReferrerSellerFee 137 | ); 138 | } 139 | }); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/helpers/testIpfsPath.ts: -------------------------------------------------------------------------------- 1 | export const testIpfsPath = [ 2 | "QmWEFuQSbZbVgCFVHRfbtqdPYB88nGg6xwWyEdavstT5JV/metadata.json", 3 | "QmcSwtx4YP1n8y9eM7XsQdhEABJ4tNmuuEb9KmE8Zc3EYB/metadata.json", 4 | "QmUUUucyqbY4qk7tLps75NyAH3v1bhbW6XeHLeUzKVnNcE/metadata.json", 5 | "QmS8Yu35HJ4s54ZM264YAcQHUKctMjukfkQkSHHjkAKrxk/metadata.json", 6 | "QmRh84kkdbmvj4Ty2CdedHwBJtfajN3ajZqn1M37cZdt4P/metadata.json", 7 | "QmYJiQDYn9KujU3KPAnGHYUidXGXWfnUaMEhPkhBAeuvp4/metadata.json", 8 | "QmVxiZFcMSKQuCe5st1ttmNrUrPQeU696NdFwA6FLzvvbx/metadata.json", 9 | "QmaFsmgGHtLd65dwwwSioSybemghmDszQkYDuY4CH66UV9/metadata.json", 10 | "Qmf9R5LzJSDvcD6V7bJJEqGRpMyXWPN7bk2FE2F5ohuvB9/metadata.json", 11 | "QmSFze4acWPscpEdACt32kiFzdtgG78F9UsV2ZSisoRCLT/metadata.json", 12 | "QmeZECuZ3sUbqceXX2ST9CiCbXdM1hG1wLziNZ62qFzUPT/metadata.json", 13 | "QmeX1fH4vRZFKxNCrcyrzJAPa8jMwZXimAgVikaDPVeMv9/metadata.json", 14 | "Qmcs1XKAdQ3EyULcQXeK9HTE42Z6271K81iaTWXBfLCZNA/metadata.json", 15 | "QmVdmj9TG4eBnJPwt6GUaz2se5qMN6uy9odAyzvuAA9ET5/metadata.json", 16 | "QmZ3AXdbnMSzaW936G6UcT36ww1eahmPmNsrPWvUi1vy99/metadata.json", 17 | "QmYh6VKh4R5fsErcnSBVvx7A334teoWyLY4SdRuD1HsZvR/metadata.json", 18 | "QmbEobLqxUaXCUCdSBHyq5Ki1qFfhac26kCaWVzqWiZTTN/metadata.json", 19 | "QmUxiaovup5N6GRkitTK1A1EuAc8Et39QonWMv3qJRawiF/metadata.json", 20 | "QmQZaZad9GhV9gdgQbmFHj1qFXT8jrbWVYDEyKSdjvt76q/metadata.json", 21 | "QmYx4M3ghd3RiiCycy9v93jCVVcJ3DZTNXf6Xp7Gt7RwsS/metadata.json", 22 | "QmQnJabSpxtBfQuFM4Vcc3rDTzd5inTqKYV5hpMZzMyjJf/metadata.json", 23 | "QmUyzyeobY8tyCPsSmpN8XkJ6zNwtuEpXZby6GnHp35tY4/metadata.json", 24 | "QmdtKCeAyZdTLWiDC85L6rY21z8xzgkSLQtyYvbn89ncob/metadata.json", 25 | "QmcqsXh9JS2bVFRx6ZLSqYHkCA7VvKepevoiJzcKnrYYbe/metadata.json", 26 | "QmNtQVhL3FWzwZVdsYFvYCsrp6GDBCNuBEWHjKXkYnkZBa/metadata.json", 27 | "QmXLLGctLfwrMdMsDsfQAWLENqFWToM6nmFeQf17vTymaC/metadata.json", 28 | "Qmf2yQ2Gqe9hAWmEqfixE57Kn76ismhULMnv7R6X8Kg5u1/metadata.json", 29 | "QmUAEZETaYa7TxuRdVHmVwKxxWHMA6PvEJu67TEsJV71Gz/metadata.json", 30 | "Qmc3G21Er4TyZxqJe93ArbpWsBvaWox4RABXLVkUKcTW6H/metadata.json", 31 | "QmWvo9hkvpLBjDX9WkqiqFShnJky3znDY4DNbs98tyn9dY/metadata.json", 32 | "Qmb6BRrsmobicuf21Ai8oL2mJHEh5xd1o7ejhEPEXk2hwu/metadata.json", 33 | "QmT46uHnkUoXwu3pvdznU3x378Ap8xEbcwAZCCnVNeYwtY/metadata.json", 34 | "QmYoySQ2zzwFubCmncwUUuhUZHPmUjveVmfnmhG8J4LRex/metadata.json", 35 | "QmRG8TAX3r45jVDWRfbQ3EU5DLXCUbAyzVHkkzzQbPDC9J/metadata.json", 36 | "QmQCLbaMhS9rTbNsWUwNK79K5P9SquWMaFEz2sPjswNoRj/metadata.json", 37 | "QmSc31tmge86Y26QQdnXq2ctUuCGUH5yq5tFjzsi5jaV4i/metadata.json", 38 | "Qmaef6wNdEuQDWBrhkNjnHzEHWgqjB3L7Ca4n2EEYjmza5/metadata.json", 39 | "QmfUxpiTjA1rKh4yTwCtZiq9RV41hTKM3PzyzV9QMvQyN6/metadata.json", 40 | "QmR9zNBKayJRvcnrYWuqm69YPJ1pFhNuM71pGsQEDgf8zw/metadata.json", 41 | "QmPiYzpqifEK3KdxZV7zUcmjAW6yDT1SiDyTeeTaF94WDv/metadata.json", 42 | "QmNWcX9kjbXygweYyr4JtihcuCjASrxp3fbj5kzfuka2TA/metadata.json", 43 | "QmS7PpYSKpdepcBk1MxBZ4xshPjThU58iYneUEwBhxepLh/metadata.json", 44 | "QmVVqCFFNPi65fcFbt4LmdigU1nGPef9esa1T4dk8iqMts/metadata.json", 45 | "QmaFbm48JPayXVT3mHGsPNAkUdmeUuzPqe7ibtPR2cBYBw/metadata.json", 46 | "QmfSs2J2eHKdXNpHfi8pb5LE4TToUWG9e7NvJVH4TbmANq/metadata.json", 47 | "QmdzDB44fEFx57x7dseimPeUguu4voZ3fsCNW1stywQHue/metadata.json", 48 | "QmQGeD3oYQZ3XhZAJLGJgtiNNzi6xdSRe276xDaCJXuHhS/metadata.json", 49 | "QmcUnr52XTM9vZxF5umqaQn26QdsuVuSJxKFpWSGVEuuMF/metadata.json", 50 | "QmNPs1douSmEanWQeZU8Da1v8S1h5tFfAAvsPA2y48phU9/metadata.json", 51 | "QmaV7LxnfCnUPqbHE4Ek6rnif3YP6JzQgXVSiYsi6q3AKK/metadata.json", 52 | "QmUUTwZaQY6WVZkLJCohw5oURgJPDc6pijxxye4LFCSKN7/metadata.json", 53 | "QmQW2fG9FomBYrYMFWuChMKcskwM11A2ox4d3q5dcUoQzg/metadata.json", 54 | "QmZ6zAwhm6uWTrehycVx18o7bSfHzvczq5zqEon7LF63Bw/metadata.json", 55 | "QmZyhB41aYmyY35NzX3WQt7vhQVDxMnaNoNrKrwYSsjE69/metadata.json", 56 | "QmVYgbLxVDMsAteHj5QML6crvmWmVzmPDMEsab658JYh5P/metadata.json", 57 | "QmQZRJvrkoTpRoKQcr4B9xEX2AL3qkGzjssNRrJHvUCrMS/metadata.json", 58 | "QmUgdUdQ2uX66SgwTgco2utxR9dEVSWXJiwckhz6XanaHo/metadata.json", 59 | "QmNiXJTvLLJmQeaA11aDK4U4W6jpSDEqMdAkHQH3ME9Hvu/metadata.json", 60 | "QmbCqCjhVc4QSuw7MF8BmNyQfVnpXYmU1Xc7hfUedPBaRD/metadata.json", 61 | "QmPoWPYFRzxjUJUt9S9C7XcDNuKKzBaAngCbeiHPd7EHBW/metadata.json", 62 | "QmSoEuqYcf7XJ7qo6APNKuRiFhy1Nsm4P9Y8X3WCsycq28/metadata.json", 63 | "QmVtAk1Bp3jukznnaitdbCKCiRzD4pmqsswtBasGnmounR/metadata.json", 64 | "QmYxXDxy8mgeRbsscY7mftruodpM8JZE2rdakXAbXhVKAd/metadata.json", 65 | "QmazHKocTQs5sQQeK7WJC8VTeTNXwS1K29xG8qpfsauW7W/metadata.json", 66 | "QmPadqyXwR8txG5ocTSvUhf2xvqkiVCPeEAvFGWfvEK4EU/metadata.json", 67 | "QmY5kgvf2FVMzexBYFubSGrbXthH4DQnih2KcohLtPvdkD/metadata.json", 68 | "QmawFBkDfw8qtEF7Tu9rMGPdS6HpMkaQbqczt9Jq2E8Jih/metadata.json", 69 | "QmcKxRbQwAhPcZjkzSZXX6edJet89opa3BfgtUGExRwBLc/metadata.json", 70 | "QmVfPuwEmbD32CHG7Q8LxCV2b3jk3vSdmKggN3BnBk6AwB/metadata.json", 71 | "QmRmrz77dgjBMX44eYpChTBew29dmirYeHTaUaMXM2Pskz/metadata.json", 72 | "QmfGS9fBjsM8NcmrnBRfToRxRJLka4sgeDTeG83Zb4y5Ty/metadata.json", 73 | "QmTBaayreXpMdrTXQ3zRYXCAdhnFUZk3DsWYkoS3An74h1/metadata.json", 74 | "Qmb4AnZCw19TYoRWPhSJnA77beVSbK8sd4AcJixJcnd3ck/metadata.json", 75 | "QmcZv2UX4Ad6qoewq4AafgjbFfdooapN2Ahm36U8Tiasjy/metadata.json", 76 | "QmUhcPvnq3zqyJCHPyiuPi6816aopjNbGmLSjN9drJYJYE/metadata.json", 77 | "QmXL79CWxS2oGWC7xNk5jz5Ab8G1kfqZNNLNELEDK9Wybq/metadata.json", 78 | "QmSEPyXkz1V7sBFvrmUs6Tf6tMDAiAh7mAsYVD3CPz1WS2/metadata.json", 79 | "QmWEyAjRfPgoAMv9j8JnyDsVzsM5pctaRWc3yzH3WBS9HZ/metadata.json", 80 | "QmSNzXPrDSSVjmDP5VHRenkgDn7cZUNTpZVPD5wcezkkxQ/metadata.json", 81 | "QmPW3gj4TyK7gbA2KHEqvdnLmCds54gU1HqGZjVyhg233c/metadata.json", 82 | "QmZ3jiYtzzGpWid5TdRtHCeXn1SyuKcCfDRyBK1mzzzgN4/metadata.json", 83 | "QmVdy1s8GjpwyT4nvFmPipmaAb2xiAW7nktyQPpmPRNc2S/metadata.json", 84 | "QmehVtp3sq4yKVakbLjrbtPgYfL5XLgKrUJoN2bVKJvq5j/metadata.json", 85 | "QmR3mSiwzDP2vXZN3mREmZpjPtLwje4nLYV3w7kFPGwRUp/metadata.json", 86 | "QmeKwrbP3samKW4Xhv4Lm8sZLTco8e8WwuwpmFiEmF3TwK/metadata.json", 87 | "Qme9qQXTy3w4eW1kgsSBkHBP8pV9Wjh8r5xvVmPZvLjJeL/metadata.json", 88 | "QmSE4mu1bwQCYWRuJi9GGWb7euRFwRkkcUDit68Sz1xvxw/metadata.json", 89 | "QmUzKULBrGvFPQ3Nzi2vo3ms3raz1AbxkZm512cF8Cmwjd/metadata.json", 90 | "Qmd5KDFMP7YioNYGfocHqksQGgbryCTfY8PWEhcFzTzfjg/metadata.json", 91 | "QmUtLhydN9WnuJS8FiRZf9HZKxR6WR55PVJBun1X4aiENt/metadata.json", 92 | "QmQCjdptgB3zFygZk1VhMPWKZVcewfhao3dU8WTyMdmHK3/metadata.json", 93 | "QmUkWJ9WjHQutdqPN4z4RFaFXD2DuucuZzdxLFrWuYPn7U/metadata.json", 94 | "QmSitMw6cqVPwXo6qPTg5CAWTm5PKkA9M3Kpi5eTR1fkHn/metadata.json", 95 | "QmS2FAwwKsKraU4kCur663k2A1q4mKop5fgRuq2DyC2ubq/metadata.json", 96 | "QmeS8B9raSCoamQS7Z8oPtZnnuJRvHUqiSudvAseNMfNrd/metadata.json", 97 | "Qmbk6JYUQZ2HCJRmpwEJsFV5TuC1iGCAdBetK1DUY68F6r/metadata.json", 98 | "QmfSa6xeRi9umxWdXx1581LCPQkuTrbE77ANcE22sww6kZ/metadata.json", 99 | "QmNYS5GK8PoHyFKHAP1kFJAFpcjH6oUMXP645fFdnpskEQ/metadata.json", 100 | "QmT2b4puXmKRBYqvph1i43y1z9YLe9Ro66rPQdQXTWCZyu/metadata.json", 101 | "QmNj9Ya5ha8GpM16Gg8NVkSNG7my5LFkFjyZQ2fjsajQon/metadata.json", 102 | ]; 103 | -------------------------------------------------------------------------------- /contracts/FNDCart.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/utils/Address.sol"; 7 | import "@openzeppelin/contracts/utils/math/Math.sol"; 8 | import "@f8n/fnd-protocol/contracts/NFTMarket.sol"; 9 | import "@f8n/fnd-protocol/contracts/FETH.sol"; 10 | 11 | /** 12 | * @title A contract to buy, bid on, or make an offer for multiple NFTs with a single transaction. 13 | * @dev This contract has not been fully tested or audited. 14 | */ 15 | contract FNDCart is Ownable { 16 | using Address for address payable; 17 | using Math for uint256; 18 | 19 | NFTMarket public immutable market; 20 | FETH public immutable feth; 21 | address payable public immutable referrerTreasury; 22 | 23 | struct NFT { 24 | address nftContract; 25 | uint256 tokenId; 26 | } 27 | 28 | struct CartItem { 29 | NFT nft; 30 | uint256 maxPrice; 31 | } 32 | 33 | /** 34 | * @notice Initialize the contract. 35 | * @param _market The Foundation market contract address on this network. 36 | * @param _referrerTreasury A treasury address to receive a referral kick-back fee. 37 | */ 38 | constructor( 39 | address payable _market, 40 | address payable _feth, 41 | address payable _referrerTreasury 42 | ) { 43 | market = NFTMarket(_market); 44 | feth = FETH(_feth); 45 | referrerTreasury = _referrerTreasury; 46 | } 47 | 48 | /** 49 | * @notice Accept native currency payments (e.g. bid refunds from the market) and forward them to the owner. 50 | */ 51 | receive() external payable { 52 | payable(owner()).sendValue(address(this).balance); 53 | } 54 | 55 | /** 56 | * @notice Buy instantly or place the minimum bid on each of the tokens provided, 57 | * if the price is less than the max specified; otherwise make an offer if possible. 58 | * Any funds remaining will be instantly refunded. 59 | */ 60 | function checkout(CartItem[] calldata cart) external payable onlyOwner { 61 | // Transfer any FETH available to consider in the available funds calculations. 62 | uint256 balance = feth.balanceOf(address(this)); 63 | if (balance != 0) { 64 | feth.withdrawAvailableBalance(); 65 | } 66 | 67 | // Attempt to purchase each item in the cart. 68 | for (uint256 i = 0; i < cart.length; ++i) { 69 | CartItem memory item = cart[i]; 70 | 71 | // 1) Buy instantly 72 | if (!_tryBuy(item)) { 73 | // 2) Place a bid 74 | if (!_tryBid(item)) { 75 | // 3) Make an offer 76 | _tryOffer(item); 77 | } 78 | } 79 | } 80 | 81 | // Refund any ETH remaining. 82 | if (address(this).balance != 0) { 83 | payable(msg.sender).sendValue(address(this).balance); 84 | } 85 | } 86 | 87 | /** 88 | * @notice Withdraw any FETH & ETH currently held by this contract. 89 | * @dev This may apply if one of your bids was later outbid. 90 | */ 91 | function withdrawBalance() external onlyOwner { 92 | // Transfer FETH if there's any available. 93 | uint256 balance = feth.balanceOf(address(this)); 94 | if (balance != 0) { 95 | feth.withdrawFrom(address(this), payable(msg.sender), balance); 96 | } 97 | 98 | // Withdraw ETH. 99 | if (address(this).balance != 0) { 100 | payable(msg.sender).sendValue(address(this).balance); 101 | } 102 | } 103 | 104 | /** 105 | * @notice Withdraw an NFT from this contract. 106 | * This will settle the auction won by this contract first, if required. 107 | */ 108 | function withdrawNFTs(NFT[] calldata nfts) external onlyOwner { 109 | for (uint256 i = 0; i < nfts.length; ++i) { 110 | NFT memory nft = nfts[i]; 111 | _trySettleAuction(nft); 112 | _tryTransferNFT(nft); 113 | } 114 | } 115 | 116 | /** 117 | * @notice Make any arbitrary calls. 118 | * @dev This should not be necessary, but here just in case you need to recover other assets. 119 | */ 120 | function proxyCall( 121 | address payable target, 122 | bytes memory callData, 123 | uint256 value 124 | ) external onlyOwner { 125 | target.functionCallWithValue(callData, value); 126 | } 127 | 128 | /** 129 | * @notice Place the minimum bid possible if there's an auction available <= the max price for this item. 130 | */ 131 | function _tryBid(CartItem memory item) internal returns (bool bidPlaced) { 132 | uint256 auctionId = market.getReserveAuctionIdFor(item.nft.nftContract, item.nft.tokenId); 133 | if (auctionId != 0) { 134 | uint256 bid = market.getMinBidAmount(auctionId); 135 | if (bid <= item.maxPrice && bid <= address(this).balance) { 136 | try 137 | market.placeBidV2{ value: bid }(auctionId, bid, referrerTreasury) // solhint-disable-next-line no-empty-blocks 138 | { 139 | // Successfully placed the bid. 140 | bidPlaced = true; 141 | } catch // solhint-disable-next-line no-empty-blocks 142 | { 143 | // The bid may fail if the auction has ended of if this contract is already the highest bidder. 144 | } 145 | } 146 | } 147 | } 148 | 149 | /** 150 | * @notice Buy if the NFT has a buy price set <= the max price for this item. 151 | */ 152 | function _tryBuy(CartItem memory item) internal returns (bool bought) { 153 | (, uint256 price) = market.getBuyPrice(item.nft.nftContract, item.nft.tokenId); 154 | // The price would be MAX_UINT256 if the token is not for sale with buy now. 155 | if (price <= item.maxPrice && price <= address(this).balance) { 156 | // Buy NFT with a referral kick-back to the cart's treasury. 157 | market.buyV2{ value: price }(item.nft.nftContract, item.nft.tokenId, price, referrerTreasury); 158 | // Transfer the NFT to the end user. 159 | IERC721(item.nft.nftContract).transferFrom(address(this), msg.sender, item.nft.tokenId); 160 | bought = true; 161 | } 162 | } 163 | 164 | /** 165 | * @notice Attempt to offer the max price specified for this item. 166 | */ 167 | function _tryOffer(CartItem memory item) internal returns (bool offerMade) { 168 | uint256 offer = item.maxPrice.min(address(this).balance); 169 | try market.makeOfferV2(item.nft.nftContract, item.nft.tokenId, offer, referrerTreasury) { 170 | // Successfully made the offer. 171 | offerMade = true; 172 | } catch // solhint-disable-next-line no-empty-blocks 173 | { 174 | // The offer may fail if there's an auction in progress or another user already outbid us. 175 | } 176 | } 177 | 178 | function _trySettleAuction(NFT memory nft) internal { 179 | // Check if the NFT is pending an auction settle first 180 | uint256 auctionId = market.getReserveAuctionIdFor(nft.nftContract, nft.tokenId); 181 | if (auctionId != 0) { 182 | try 183 | market.finalizeReserveAuction(auctionId) // solhint-disable-next-line no-empty-blocks 184 | { 185 | // Successfully settled the auction. 186 | } catch // solhint-disable-next-line no-empty-blocks 187 | { 188 | // If the auction is not over, this call will revert and then NFT is not ready to withdraw. 189 | } 190 | } 191 | } 192 | 193 | function _tryTransferNFT(NFT memory nft) internal { 194 | try 195 | IERC721(nft.nftContract).transferFrom(address(this), msg.sender, nft.tokenId) 196 | // solhint-disable-next-line no-empty-blocks 197 | { 198 | 199 | } catch // solhint-disable-next-line no-empty-blocks 200 | { 201 | 202 | } 203 | } 204 | } 205 | --------------------------------------------------------------------------------