├── .env.example ├── tests ├── e2e │ ├── runner │ │ ├── .gitignore │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ ├── __tests__ │ │ │ ├── graphql │ │ │ │ ├── get-current-block-number.graphql │ │ │ │ ├── get-contracts.graphql │ │ │ │ ├── get-core-registries.graphql │ │ │ │ ├── get-minters.graphql │ │ │ │ ├── get-receipts.graphql │ │ │ │ ├── get-bids.graphql │ │ │ │ ├── get-split-atomic-factory.graphql │ │ │ │ ├── get-split-atomic-contract.graphql │ │ │ │ ├── get-minter-filters.graphql │ │ │ │ ├── get-project-pmp-configuration.graphql │ │ │ │ ├── get-token-pmp-configuration.graphql │ │ │ │ ├── get-project-minter-configurations.graphql │ │ │ │ └── get-projects.graphql │ │ │ ├── base.test.ts │ │ │ ├── subgraph-config.d.ts │ │ │ ├── minter-suite-v2 │ │ │ │ ├── min-price-lib.test.ts │ │ │ │ ├── merkle-lib.test.ts │ │ │ │ ├── split-funds-lib.test.ts │ │ │ │ ├── max-invocations-lib.test.ts │ │ │ │ ├── da-lin-lib.test.ts │ │ │ │ ├── da-exp-lib.test.ts │ │ │ │ └── set-price-lib.test.ts │ │ │ └── split-atomic │ │ │ │ └── split-atomic-factory.test.ts │ │ ├── jest.config.js │ │ ├── tsconfig.json │ │ ├── supplemental_abis │ │ │ └── readme.md │ │ ├── graphql.config.js │ │ └── package.json │ ├── seed │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ ├── .gitignore │ │ ├── tsconfig.json │ │ ├── supplemental_abis │ │ │ └── readme.md │ │ └── package.json │ ├── hardhat │ │ ├── package.json │ │ ├── Dockerfile │ │ └── hardhat.config.js │ ├── docker-compose.ci.yml │ ├── docker-compose.local.yml │ ├── docker-compose.test.yml │ ├── setup-scripts │ │ └── subgraph.sh │ └── docker-compose.yml └── subgraph │ ├── .docker │ └── Dockerfile │ ├── unit-tests.test.ts │ ├── legacy-minter-suite │ └── helpers.ts │ ├── mapping-v2-core │ └── helpers.ts │ └── mapping-v0-core │ └── helpers.ts ├── matchstick.yaml ├── .prettierrc ├── .dockerignore ├── CODEOWNERS ├── docker └── Dockerfile ├── config ├── deprecated │ ├── palm.json │ ├── palm-testnet.json │ ├── arbitrum-goerli-dev.json │ ├── arbitrum-goerli-staging.json │ ├── ropsten-dev.json │ ├── ropsten-staging.json │ └── mainnet-with-secondary.json └── generic.json ├── .gitignore ├── .vscode └── settings.json ├── posterity ├── src │ └── secondary │ │ ├── secondary-helpers.ts │ │ ├── looksrare │ │ └── looksrare-mapping.ts │ │ └── opensea │ │ └── os-seaport-mapping.ts └── tests │ └── secondary │ ├── opensea │ └── seaportHelpers.ts │ └── looksrare │ └── looksrareHelpers.ts ├── .github ├── workflows │ └── create-asana-attachment.yaml └── pull_request_template.md ├── src ├── util-types.ts ├── min-price-lib-mapping.ts ├── max-invocations-lib-mapping.ts ├── merkle-lib-mapping.ts ├── split-funds-lib-mapping.ts ├── da-lib-mapping.ts ├── da-lin-lib-mapping.ts ├── constants.ts ├── set-price-lib-mapping.ts ├── da-exp-lib-mapping.ts ├── token-holder-lib-mapping.ts ├── split-atomic-mapping.ts ├── extra-minter-details-helpers.ts ├── core-registry.ts └── settlement-exp-lib-mapping.ts ├── abis ├── _generate-abis.sh └── _include-artblocks-abis.txt ├── abis-supplemental └── readme.md ├── .circleci └── config.yml ├── package.json └── LICENSE /.env.example: -------------------------------------------------------------------------------- 1 | GOLDSKY_API_KEY= -------------------------------------------------------------------------------- /tests/e2e/runner/.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /matchstick.yaml: -------------------------------------------------------------------------------- 1 | testsFolder: tests/subgraph -------------------------------------------------------------------------------- /tests/e2e/seed/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | contracts 3 | .env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /tests/e2e/runner/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | generated 3 | .env -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | contracts 4 | generated 5 | tests 6 | !tests/e2e/setup-scripts/* -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Request review from the best fit approvers group for this repo. 2 | * @ArtBlocks/Eng-Approvers-Contracts 3 | -------------------------------------------------------------------------------- /tests/e2e/runner/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | WORKDIR /usr/runner 3 | COPY . . 4 | RUN ["yarn"] 5 | CMD ["yarn", "test"] -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-current-block-number.graphql: -------------------------------------------------------------------------------- 1 | query GetCurrentBlockNumber { 2 | _meta { 3 | block { 4 | number 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /tests/e2e/hardhat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "ethers": "^5.0.0", 4 | "hardhat": "^2.12.6", 5 | "@nomiclabs/hardhat-ethers": "^2.0.0" 6 | } 7 | } -------------------------------------------------------------------------------- /tests/e2e/hardhat/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | RUN ["apk", "add", "--no-cache", "curl"] 3 | WORKDIR /usr/hardhat 4 | COPY . . 5 | RUN ["yarn"] 6 | CMD ["yarn", "hardhat", "node"] -------------------------------------------------------------------------------- /tests/e2e/seed/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | RUN apk add --no-cache bash 3 | WORKDIR /usr/seed 4 | COPY . . 5 | RUN ["yarn"] 6 | RUN ["yarn", "generate:typechain"] 7 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /tests/e2e/seed/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | contracts/ 5 | coverage.json 6 | typechain 7 | typechain-types 8 | 9 | # Hardhat files 10 | cache 11 | artifacts 12 | 13 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | RUN apk add --no-cache curl 3 | RUN apk add --no-cache git 4 | WORKDIR /usr/subgraph 5 | COPY . . 6 | COPY ./tests/e2e/setup-scripts /usr/subgraph/setup-scripts 7 | CMD [ "yarn" ] 8 | 9 | -------------------------------------------------------------------------------- /config/deprecated/palm.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "palm", 3 | "genArt721Core2Contracts": [ 4 | { 5 | "address": "0x8De4e517A6F0B84654625228D8293b70AB49cF6C", 6 | "startBlock": 700000 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tests/e2e/runner/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | testPathIgnorePatterns: [".*/*.d.ts", "__tests__/utils/helpers.ts"], 6 | }; 7 | -------------------------------------------------------------------------------- /tests/e2e/runner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/e2e/seed/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .DS_Store 4 | subgraph.yaml 5 | .bin 6 | tests/.latest.json 7 | .env 8 | 9 | # Ignore generated files 10 | generated/ 11 | contracts 12 | tests/e2e/data 13 | tests/subgraph/.latest.json 14 | 15 | # Ignore imported abi files 16 | abis/*.json 17 | -------------------------------------------------------------------------------- /config/deprecated/palm-testnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "palm-testnet", 3 | "pbabContracts": [ 4 | { 5 | "address": "0x4C7E1e1Ba5e35934C8323d75AF838fDa26588177", 6 | "name": "Art Blocks Engine Palm Testnet Demo", 7 | "startBlock": 8732253 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-contracts.graphql: -------------------------------------------------------------------------------- 1 | fragment ContractDetails on Contract { 2 | id 3 | } 4 | 5 | query GetContracts { 6 | contracts { 7 | ...ContractDetails 8 | } 9 | } 10 | 11 | query GetTargetContracts($targetId: ID!) { 12 | contracts(where: { id: $targetId }) { 13 | ...ContractDetails 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/e2e/docker-compose.ci.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | runner: 4 | build: runner 5 | environment: 6 | - SUBGRAPH_GRAPHQL_URL=http://graph-node:8000/subgraphs/name/artblocks/art-blocks 7 | depends_on: 8 | subgraph: 9 | condition: service_healthy 10 | volumes: 11 | - shared-data:/usr/runner/shared 12 | -------------------------------------------------------------------------------- /tests/e2e/docker-compose.local.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | hardhat: 4 | ports: 5 | - "8545:8545" 6 | graph-node: 7 | ports: 8 | - '8000:8000' 9 | - '8001:8001' 10 | - '8020:8020' 11 | - '8030:8030' 12 | - '8040:8040' 13 | subgraph: 14 | volumes: 15 | - ./setup-scripts:/usr/subgraph/setup-scripts -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-core-registries.graphql: -------------------------------------------------------------------------------- 1 | fragment CoreRegistryDetails on CoreRegistry { 2 | id 3 | } 4 | 5 | query GetCoreRegistries { 6 | coreRegistries { 7 | ...CoreRegistryDetails 8 | } 9 | } 10 | 11 | query GetTargetCoreRegistries($targetId: ID!) { 12 | coreRegistries(where: { id: $targetId }) { 13 | ...CoreRegistryDetails 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true 5 | }, 6 | "[assemblyscript]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "editor.formatOnSave": true 9 | }, 10 | "graphql-config.load.rootDir": "./tests/e2e/runner", 11 | "graphql-config.dotEnvPath": "./tests/e2e/runner/.env", 12 | } -------------------------------------------------------------------------------- /posterity/src/secondary/secondary-helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param projectId The projectId id 4 | * @param tokenId The token id 5 | * @param saleId The sale id (eth tx hash) 6 | * @returns The corresponding lookup table id 7 | */ 8 | export function buildTokenSaleLookupTableId( 9 | projectId: string, 10 | tokenId: string, 11 | saleId: string 12 | ): string { 13 | return projectId + "::" + tokenId + "::" + saleId; 14 | } -------------------------------------------------------------------------------- /tests/e2e/runner/supplemental_abis/readme.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This directory is intended to contain minter contracts that are not included in the latest @artblocks/contracts package, but are needed for end-to-end testing. 4 | 5 | For example, if a new contract is still in development, but is needed for testing, it can be added here. 6 | 7 | Once a contract is included in the latest @artblocks/contracts package, it should be removed from this directory. 8 | -------------------------------------------------------------------------------- /tests/e2e/seed/supplemental_abis/readme.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This directory is intended to contain minter contracts that are not included in the latest @artblocks/contracts package, but are needed for end-to-end testing. 4 | 5 | For example, if a new contract is still in development, but is needed for testing, it can be added here. 6 | 7 | Once a contract is included in the latest @artblocks/contracts package, it should be removed from this directory. 8 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-minters.graphql: -------------------------------------------------------------------------------- 1 | fragment MinterDetails on Minter { 2 | id 3 | type 4 | minterFilter { 5 | id 6 | } 7 | isGloballyAllowlistedOnMinterFilter 8 | extraMinterDetails 9 | updatedAt 10 | } 11 | 12 | query GetMinters { 13 | minters { 14 | ...MinterDetails 15 | } 16 | } 17 | 18 | query GetTargetMinters($targetId: ID!) { 19 | minters(where: { id: $targetId }) { 20 | ...MinterDetails 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/e2e/docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | runner: 4 | build: runner 5 | environment: 6 | - SUBGRAPH_GRAPHQL_URL=http://graph-node:8000/subgraphs/name/artblocks/art-blocks 7 | depends_on: 8 | subgraph: 9 | condition: service_healthy 10 | volumes: 11 | - shared-data:/usr/runner/shared 12 | - ./runner/__tests__:/usr/runner/__tests__ 13 | subgraph: 14 | volumes: 15 | - ./setup-scripts:/usr/subgraph/setup-scripts -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-receipts.graphql: -------------------------------------------------------------------------------- 1 | fragment ReceiptDetails on Receipt { 2 | id 3 | project { 4 | id 5 | } 6 | minter { 7 | id 8 | } 9 | account { 10 | id 11 | } 12 | netPosted 13 | numPurchased 14 | updatedAt 15 | } 16 | 17 | query GetReceipts { 18 | receipts { 19 | ...ReceiptDetails 20 | } 21 | } 22 | 23 | query GetTargetReceipts($targetId: ID!) { 24 | receipts(where: { id: $targetId }) { 25 | ...ReceiptDetails 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/create-asana-attachment.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | types: [opened, reopened] 4 | 5 | jobs: 6 | create-asana-attachment-job: 7 | runs-on: ubuntu-latest 8 | name: Create pull request attachments on Asana tasks 9 | steps: 10 | - name: Create pull request attachments 11 | uses: Asana/create-app-attachment-github-action@latest 12 | id: postAttachment 13 | with: 14 | asana-secret: ${{ secrets.ASANA_SECRET }} 15 | - name: Log output status 16 | run: echo "Status is ${{ steps.postAttachment.outputs.status }}" 17 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-bids.graphql: -------------------------------------------------------------------------------- 1 | fragment BidDetails on Bid { 2 | id 3 | bidType 4 | project { 5 | id 6 | } 7 | minter { 8 | id 9 | } 10 | token { 11 | id 12 | } 13 | bidder { 14 | id 15 | } 16 | isRemoved 17 | settled 18 | slotIndex 19 | value 20 | winningBid 21 | txHash 22 | timestamp 23 | logIndex 24 | updatedAt 25 | } 26 | 27 | query GetBids { 28 | bids { 29 | ...BidDetails 30 | } 31 | } 32 | 33 | query GetTargetBids($targetId: ID!) { 34 | bids(where: { id: $targetId }) { 35 | ...BidDetails 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-split-atomic-factory.graphql: -------------------------------------------------------------------------------- 1 | fragment SplitAtomicFactoryDetails on SplitAtomicFactory { 2 | id 3 | type 4 | implementation 5 | splitAtomicContracts { 6 | id 7 | } 8 | requiredSplitAddress 9 | requiredSplitBasisPoints 10 | abandoned 11 | updatedAt 12 | } 13 | 14 | query GetSplitAtomicFactories { 15 | splitAtomicFactories { 16 | ...SplitAtomicFactoryDetails 17 | } 18 | } 19 | 20 | query GetTargetSplitAtomicFactories($targetId: ID!) { 21 | splitAtomicFactories(where: { id: $targetId }) { 22 | ...SplitAtomicFactoryDetails 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/e2e/runner/graphql.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overwrite: true, 3 | schema: process.env.SUBGRAPH_GRAPHQL_URL, 4 | documents: ['./__tests__/graphql/*.graphql'], 5 | generates: { 6 | 'generated/graphql.ts': { 7 | plugins: ['typescript', 'typescript-operations', 'typescript-urql'], 8 | config: { 9 | maybeValue: 'T | null | undefined', 10 | scalars: { 11 | BigInt: 'string', 12 | BigDecimal: 'string', 13 | Bytes: 'string', 14 | timestamptz: 'string' 15 | }, 16 | withHooks: false 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description of the change 2 | 3 | \ 4 | 5 | >reminder: Any subgraph deployments should be documented as [Releases](https://github.com/ArtBlocks/artblocks-subgraph/releases) in this repository. 6 | 7 | >reminder: Any changes to subgraph schema should be accompanied by corresponding changes to the Art Blocks documentation site, available at https://docs.artblocks.io/ (specifically, see the [Subgraph Entities](https://docs.artblocks.io/creator-docs/art-blocks-api/entities/) and [Subgraph Querying and API Overview](https://docs.artblocks.io/creator-docs/art-blocks-api/queries/) sections). 8 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-split-atomic-contract.graphql: -------------------------------------------------------------------------------- 1 | fragment SplitAtomicContractDetails on SplitAtomicContract { 2 | id 3 | type 4 | splitAtomicFactory { 5 | id 6 | } 7 | implementation 8 | splits { 9 | id 10 | splitAtomicContract { 11 | id 12 | } 13 | index 14 | recipient 15 | basisPoints 16 | } 17 | updatedAt 18 | } 19 | 20 | query GetSplitAtomicContracts { 21 | splitAtomicContracts { 22 | ...SplitAtomicContractDetails 23 | } 24 | } 25 | 26 | query GetTargetSplitAtomicContracts($targetId: ID!) { 27 | splitAtomicContracts(where: { id: $targetId }) { 28 | ...SplitAtomicContractDetails 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/util-types.ts: -------------------------------------------------------------------------------- 1 | import { Address, Bytes, ethereum } from "@graphprotocol/graph-ts"; 2 | 3 | export class MinterConfigSetAddressEvent extends ethereum.Event { 4 | get params(): MinterConfigSetAddressEvent__Params { 5 | return new MinterConfigSetAddressEvent__Params(this); 6 | } 7 | } 8 | 9 | class MinterConfigSetAddressEvent__Params { 10 | _event: MinterConfigSetAddressEvent; 11 | 12 | constructor(event: MinterConfigSetAddressEvent) { 13 | this._event = event; 14 | } 15 | 16 | get _key(): Bytes { 17 | return this._event.parameters[0].value.toBytes(); 18 | } 19 | 20 | get _value(): Address { 21 | return this._event.parameters[1].value.toAddress(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/subgraph/.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/x86_64 ubuntu:22.04 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | ENV ARGS="" 6 | 7 | RUN apt update \ 8 | && apt install -y sudo curl postgresql postgresql-contrib 9 | 10 | RUN curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - \ 11 | && sudo apt-get install -y nodejs 12 | 13 | RUN curl -OL https://github.com/LimeChain/matchstick/releases/download/0.6.0/binary-linux-22 \ 14 | && chmod a+x binary-linux-22 15 | 16 | RUN mkdir matchstick 17 | WORKDIR /matchstick 18 | 19 | # Commenting out for now as it seems there's no need to copy when using bind mount 20 | # COPY ./ . 21 | 22 | CMD ../binary-linux-22 ${ARGS} 23 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-minter-filters.graphql: -------------------------------------------------------------------------------- 1 | fragment MinterFilterDetails on MinterFilter { 2 | id 3 | minterGlobalAllowlist { 4 | id 5 | } 6 | minterFilterContractAllowlists { 7 | id 8 | contract { 9 | id 10 | } 11 | minterContractAllowlist { 12 | id 13 | } 14 | } 15 | knownMinters { 16 | id 17 | } 18 | coreRegistry { 19 | id 20 | } 21 | type 22 | updatedAt 23 | } 24 | 25 | query GetMinterFilters { 26 | minterFilters { 27 | ...MinterFilterDetails 28 | } 29 | } 30 | 31 | query GetTargetMinterFilters($targetId: ID!) { 32 | minterFilters(where: { id: $targetId }) { 33 | ...MinterFilterDetails 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/e2e/setup-scripts/subgraph.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | yarn; 3 | cp ./shared/test-config.json ./config/local.json; 4 | yarn prepare:local; 5 | yarn graph create --node $GRAPH_NODE artblocks/art-blocks; 6 | yarn graph deploy --node $GRAPH_NODE --ipfs $IPFS artblocks/art-blocks --version-label=latest; 7 | # We check for the presence of this file in a healthcheck that 8 | # is used by a dependent service (runner) to determine when the 9 | # subgraph has been deployed. 10 | touch ./shared/subgraph-complete; 11 | # This is a hack to keep the container running so that the 12 | # the docker compose exit-code-from command can be used to 13 | # exit with the exit code of the test runner container. 14 | tail -f /dev/null; -------------------------------------------------------------------------------- /tests/e2e/hardhat/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomiclabs/hardhat-ethers"); 2 | const fs = require("fs"); 3 | 4 | /** 5 | * This overrides the default hardhat task to write the 6 | * mnemonic to a file on a shardd volume so that it can 7 | * be used by the test runner. 8 | */ 9 | task('node', 'Starts a Hardhat node', async (args, hre, runSuper) => { 10 | const accounts = config.networks.hardhat.accounts; 11 | fs.writeFileSync("./shared/accounts.json", JSON.stringify({mnemonic: accounts.mnemonic})); 12 | 13 | await runSuper(args) 14 | }); 15 | 16 | module.exports = { 17 | networks: { 18 | hardhat: { 19 | mining: { 20 | auto: true, 21 | interval: 1000 22 | } 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /abis/_generate-abis.sh: -------------------------------------------------------------------------------- 1 | # clear existing ABIs 2 | rm -f ./*.json 3 | # import ABIs from artblocks npm dependency 4 | while IFS="" read -r contractName || [ -n "$contractName" ] 5 | do 6 | # copy ABI json file from @artblocks npm dependency to the current directory 7 | find '../node_modules/@artblocks/contracts/artifacts/contracts' -regex .*/$contractName.json | xargs -I {} cp {} . 8 | done < _include-artblocks-abis.txt 9 | # import additional abis from supplemental ABIs directory 10 | cp ../abis-supplemental/*.json . 11 | # import required openzeppelin ABIs 12 | cp ../node_modules/@openzeppelin-4.7/contracts/build/contracts/Ownable.json . 13 | cp ../node_modules/@openzeppelin-4.8/contracts-upgradeable/build/contracts/OwnableUpgradeable.json . 14 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-project-pmp-configuration.graphql: -------------------------------------------------------------------------------- 1 | fragment PmpProjectConfigDetails on PmpProjectConfig { 2 | id 3 | project { 4 | id 5 | } 6 | pmpConfigCount 7 | pmpConfigKeys 8 | pmpAddress 9 | tokenPMPPostConfigHook 10 | tokenPMPReadAugmentationHook 11 | updatedAt 12 | pmpConfigs { 13 | id 14 | pmpProjectConfig { 15 | id 16 | } 17 | authOption 18 | paramType 19 | key 20 | pmpLockedAfterTimestamp 21 | authAddress 22 | selectOptions 23 | minRange 24 | maxRange 25 | createdAt 26 | } 27 | } 28 | 29 | query GetProjectPmpConfigs($targetId: ID!) { 30 | pmpProjectConfigs(where: { id: $targetId }) { 31 | ...PmpProjectConfigDetails 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-token-pmp-configuration.graphql: -------------------------------------------------------------------------------- 1 | fragment PmpDetails on Pmp { 2 | id 3 | key 4 | token { 5 | id 6 | } 7 | tokenIdString 8 | tokenPMPNonce 9 | configuredParamType 10 | configuredValue 11 | artistConfiguredValueString 12 | nonArtistConfiguredValueString 13 | configuringAddress 14 | createdAt 15 | } 16 | 17 | fragment PmpLatestStateDetails on PmpLatestState { 18 | id 19 | latestTokenPMPNonce 20 | } 21 | 22 | query GetTokenLatestPmpStates($targetId: ID!) { 23 | pmpLatestStates(where: { id: $targetId }) { 24 | ...PmpLatestStateDetails 25 | } 26 | } 27 | 28 | query GetTokenPmps($targetId: ID!) { 29 | pmps(where: { id: $targetId }) { 30 | ...PmpDetails 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-project-minter-configurations.graphql: -------------------------------------------------------------------------------- 1 | fragment ProjectMinterConfigurationDetails on ProjectMinterConfiguration { 2 | id 3 | project { 4 | id 5 | } 6 | minter { 7 | id 8 | } 9 | priceIsConfigured 10 | currencySymbol 11 | currencyAddress 12 | currencyDecimals 13 | purchaseToDisabled 14 | basePrice 15 | extraMinterDetails 16 | maxInvocations 17 | } 18 | 19 | query GetProjectMinterConfigurations { 20 | projectMinterConfigurations { 21 | ...ProjectMinterConfigurationDetails 22 | } 23 | } 24 | 25 | query GetTargetProjectMinterConfigurations($targetId: ID!) { 26 | projectMinterConfigurations(where: { id: $targetId }) { 27 | ...ProjectMinterConfigurationDetails 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /abis-supplemental/readme.md: -------------------------------------------------------------------------------- 1 | ## Supplemental ABIs 2 | 3 | These ABIs are intended to represent two types of ABIs not included in the latest @artblocks/artblocks-contracts release: 4 | 5 | - legacy ABIs for contracts that are included in the subgraph, but are no longer in the @artblocks/artblocks-contracts repo 6 | - ABIs for new contracts that are not yet included in the @artblocks/artblocks-contracts package 7 | 8 | The ABIs in this directory will overwrite the ABIs in the @artblocks/artblocks-contracts repo when the subgraph is built. 9 | 10 | Once ABIs for new contracts are included in the @artblocks/artblocks-contracts package, they should be removed from this directory, and the ABI should instead be included in the `../abis/_include-artblocks-abis.txt` file. 11 | -------------------------------------------------------------------------------- /tests/e2e/seed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "node -r ts-node/register scripts/deploy.ts && tail -f /dev/null", 4 | "generate:typechain": "typechain --target ethers-v5 --out-dir ./contracts './node_modules/@artblocks/contracts//artifacts/contracts/**/!(*.dbg)*.json' && yarn generate-supplemental-abis", 5 | "generate-supplemental-abis": "path-exists ./supplemental_abis/*.json && typechain --target ethers-v5 --out-dir ./contracts './supplemental_abis/*.json' || echo 'No supplemental ABIs found.'" 6 | }, 7 | "devDependencies": { 8 | "@artblocks/contracts": "1.3.2", 9 | "@typechain/ethers-v5": "^9.0.0", 10 | "@types/node": "^18.13.0", 11 | "ethers": "^5.0.0", 12 | "path-exists-cli": "^2.0.0", 13 | "ts-node": "^10.9.1", 14 | "typechain": "^7.0.0", 15 | "typescript": "^4.9.5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/graphql/get-projects.graphql: -------------------------------------------------------------------------------- 1 | fragment ProjectDetails on Project { 2 | id 3 | projectId 4 | pmpProjectConfig { 5 | id 6 | } 7 | minterConfiguration { 8 | id 9 | project { 10 | id 11 | } 12 | minter { 13 | id 14 | type 15 | minterFilter { 16 | id 17 | } 18 | isGloballyAllowlistedOnMinterFilter 19 | extraMinterDetails 20 | updatedAt 21 | } 22 | priceIsConfigured 23 | currencySymbol 24 | currencyAddress 25 | currencyDecimals 26 | purchaseToDisabled 27 | basePrice 28 | extraMinterDetails 29 | maxInvocations 30 | } 31 | updatedAt 32 | } 33 | 34 | query GetProjects { 35 | projects { 36 | ...ProjectDetails 37 | } 38 | } 39 | 40 | query GetTargetProjects($targetId: ID!) { 41 | projects(where: { id: $targetId }) { 42 | ...ProjectDetails 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@5.1.0 5 | 6 | jobs: 7 | test: 8 | docker: 9 | - image: "cimg/node:20.17" 10 | steps: 11 | - checkout 12 | - node/install: 13 | install-yarn: true 14 | - node/install-packages: 15 | pkg-manager: yarn 16 | - run: node --version 17 | - run: 18 | name: Run tests 19 | command: yarn test 20 | test_e2e: 21 | docker: 22 | - image: "cimg/base:current" 23 | resource_class: medium+ 24 | steps: 25 | - checkout 26 | - setup_remote_docker 27 | - node/install: 28 | install-yarn: true 29 | node-version: "18.15" 30 | - node/install-packages: 31 | pkg-manager: yarn 32 | - run: node --version 33 | - run: 34 | name: Run e2e tests 35 | command: yarn test:e2e:ci 36 | 37 | workflows: 38 | run-ci: 39 | jobs: 40 | - test 41 | - test_e2e 42 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/base.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "@jest/globals"; 2 | import { 3 | GetTargetContractsDocument, 4 | GetTargetContractsQuery, 5 | GetTargetContractsQueryVariables, 6 | } from "../generated/graphql"; 7 | import { 8 | createSubgraphClient, 9 | waitUntilSubgraphIsSynced, 10 | getSubgraphConfig, 11 | } from "./utils/helpers"; 12 | 13 | const config = getSubgraphConfig(); 14 | const client = createSubgraphClient(); 15 | 16 | describe("expected contract exist", () => { 17 | beforeAll(async () => { 18 | await waitUntilSubgraphIsSynced(client); 19 | }); 20 | 21 | test("expected contracts exist", async () => { 22 | if (!config.iGenArt721CoreContractV3_BaseContracts) { 23 | throw new Error("No iGenArt721CoreContractV3_BaseContracts in config"); 24 | } 25 | // target contract is the contract in the subgraph config 26 | const targetId = 27 | config.iGenArt721CoreContractV3_BaseContracts[0].address.toLowerCase(); 28 | const contractsRes = await client 29 | .query( 30 | GetTargetContractsDocument, 31 | { targetId } 32 | ) 33 | .toPromise(); 34 | 35 | // expect to have found the target contract 36 | expect(contractsRes.data?.contracts.length).toBe(1); 37 | expect(contractsRes.data?.contracts[0].id).toBe(targetId); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /abis/_include-artblocks-abis.txt: -------------------------------------------------------------------------------- 1 | AdminACLV0 2 | GenArt721 3 | GenArt721Core 4 | GenArt721Core2 5 | GenArt721Core2EngineFlex 6 | GenArt721Core2PBAB 7 | GenArt721CoreV3 8 | GenArt721CoreV3_Engine 9 | GenArt721CoreV3_Engine_Flex 10 | IAdminACLV0 11 | ICoreRegistryV1 12 | IDependencyRegistryCompatibleV0 13 | IDependencyRegistryV0 14 | IERC721 15 | IFilteredMinterDAExpSettlementV1 16 | IFilteredMinterDAExpV1 17 | IFilteredMinterDALinV1 18 | IFilteredMinterHolderV2 19 | IFilteredMinterMerkleV2 20 | IFilteredMinterSEAV0 21 | IFilteredMinterV2 22 | IGenArt721CoreContractV3 23 | IGenArt721CoreContractV3_Base 24 | IGenArt721CoreContractV3_Engine 25 | IGenArt721CoreContractV3_Engine_Flex 26 | IGenArt721CoreContractV3_Engine_PreV3p2 27 | IGenArt721CoreContractV3_ProjectFinance 28 | IMinterFilterV0 29 | IMinterFilterV1 30 | MinterFilterV0 31 | MinterFilterV1 32 | Ownable 33 | OwnableUpgradeable 34 | ISharedMinterDAExpSettlementV0 35 | ISharedMinterDAExpV0 36 | ISharedMinterDALinV0 37 | ISharedMinterDAV0 38 | ISharedMinterHolderV0 39 | ISharedMinterMerkleV0 40 | ISharedMinterSEAV0 41 | ISharedMinterV0 42 | DAExpLib 43 | DALib 44 | DALinLib 45 | GenericMinterEventsLib 46 | MaxInvocationsLib 47 | MerkleLib 48 | PolyptychLib 49 | SEALib 50 | SetPriceLib 51 | SettlementExpLib 52 | SplitFundsLib 53 | TokenHolderLib 54 | ISharedMinterRAMV0 55 | ISplitAtomicFactoryV0 56 | ISplitAtomicV0 57 | RAMLib 58 | MinPriceLib 59 | IGenArt721CoreContractV3_Base 60 | IPMPV0 -------------------------------------------------------------------------------- /src/min-price-lib-mapping.ts: -------------------------------------------------------------------------------- 1 | import { MinMintFeeUpdated } from "../generated/MinPriceLib/MinPriceLib"; 2 | import { loadOrCreateMinter } from "./helpers"; 3 | import { setMinterExtraMinterDetailsValue } from "./extra-minter-details-helpers"; 4 | 5 | /////////////////////////////////////////////////////////////////////////////// 6 | // EVENT HANDLERS start here 7 | /////////////////////////////////////////////////////////////////////////////// 8 | 9 | /** 10 | * Handles the update of contract-level min mint fee. 11 | * Loads or creates the minter, updates the minter's extra minter details with 12 | * the updated value, and induces a sync of the minter entity. 13 | * @param event The event carrying new min mint fee value. 14 | */ 15 | export function handleMinMintFeeUpdated(event: MinMintFeeUpdated): void { 16 | // load minter 17 | const minter = loadOrCreateMinter(event.address, event.block.timestamp); 18 | 19 | // update minter entity 20 | setMinterExtraMinterDetailsValue( 21 | "minMintFee", 22 | event.params.minMintFee.toString(), 23 | minter 24 | ); 25 | 26 | // update minter's updatedAt timestamp to induce a sync, and save 27 | minter.updatedAt = event.block.timestamp; 28 | minter.save(); 29 | } 30 | 31 | /////////////////////////////////////////////////////////////////////////////// 32 | // EVENT HANDLERS end here 33 | /////////////////////////////////////////////////////////////////////////////// 34 | -------------------------------------------------------------------------------- /tests/e2e/runner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "codegen": "yarn graphql-codegen --require dotenv/config --config ./graphql.config.js dotenv_config_path=./.env", 4 | "test": "yarn generate:typechain && yarn codegen && yarn jest --runInBand", 5 | "generate:typechain": "typechain --target ethers-v5 --out-dir ./contracts './node_modules/@artblocks/contracts//artifacts/contracts/**/!(*.dbg)*.json' && yarn generate-supplemental-abis", 6 | "generate-supplemental-abis": "path-exists ./supplemental_abis/*.json && typechain --target ethers-v5 --out-dir ./contracts './supplemental_abis/*.json' || echo 'No supplemental ABIs found.'" 7 | }, 8 | "devDependencies": { 9 | "@artblocks/contracts": "1.3.2", 10 | "@graphql-codegen/cli": "^3.0.0", 11 | "@graphql-codegen/typescript": "^3.0.0", 12 | "@graphql-codegen/typescript-document-nodes": "^3.0.0", 13 | "@graphql-codegen/typescript-operations": "^3.0.0", 14 | "@graphql-codegen/typescript-urql": "^3.7.3", 15 | "@typechain/ethers-v5": "^9.0.0", 16 | "@types/jest": "^29.4.0", 17 | "@types/node": "^18.13.0", 18 | "@urql/exchange-retry": "^1.0.0", 19 | "cross-fetch": "^3.1.5", 20 | "ethers": "^5.0.0", 21 | "graphql": "^16.8.1", 22 | "jest": "^29.4.2", 23 | "path-exists-cli": "^2.0.0", 24 | "ts-jest": "^29.0.5", 25 | "ts-node": "^10.9.1", 26 | "typechain": "^7.0.0", 27 | "typescript": "^4.9.5", 28 | "urql": "^3.0.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /config/deprecated/arbitrum-goerli-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "arbitrum-goerli", 3 | "iGenArt721CoreContractV3_BaseContracts": [], 4 | "ownableGenArt721CoreV3Contracts": [], 5 | "iERC721GenArt721CoreV3Contracts": [], 6 | "iGenArt721CoreContractV3_Engine_FlexContracts": [], 7 | "minterFilterV1Contracts": [ 8 | { 9 | "address": "0xf7BdA2D6283f7f06ca39f370cAe6Ed1bb2822D5A", 10 | "name": "Minter Filter V1", 11 | "startBlock": 18481284 12 | } 13 | ], 14 | "minterSetPriceContracts": [ 15 | { 16 | "address": "0x9E74C652E449d4c7A3a885d70F1D3F7ed2153Dc9", 17 | "name": "V4 Set Price Minter", 18 | "startBlock": 18481284 19 | } 20 | ], 21 | "minterSetPriceERC20Contracts": [], 22 | "minterDAExpContracts": [], 23 | "minterDALinContracts": [], 24 | "minterDAExpSettlementContracts": [ 25 | { 26 | "address": "0xcC3Ca4065129B5988fcEdc128CD5d6c3FbC6648D", 27 | "name": "V1 DA Settlement Minter", 28 | "startBlock": 18481284 29 | } 30 | ], 31 | "minterMerkleContracts": [], 32 | "minterHolderContracts": [], 33 | "minterSEAContracts": [], 34 | "adminACLV0Contracts": [], 35 | "ICoreRegistryContracts": [ 36 | { 37 | "address": "0x429af8eE97750aaddE1e8df9e921e11406ff9ed2", 38 | "name": "Arbitrum Goerli dev engine registry contract, heylinds as Deployer", 39 | "startBlock": 18481284 40 | } 41 | ], 42 | "iDependencyRegistryV0Contracts": [], 43 | "ownableUpgradeableDependencyRegistryContracts": [] 44 | } 45 | -------------------------------------------------------------------------------- /config/deprecated/arbitrum-goerli-staging.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "arbitrum-goerli", 3 | "iGenArt721CoreContractV3_BaseContracts": [], 4 | "ownableGenArt721CoreV3Contracts": [], 5 | "iERC721GenArt721CoreV3Contracts": [], 6 | "iGenArt721CoreContractV3_Engine_FlexContracts": [], 7 | "minterFilterV1Contracts": [ 8 | { 9 | "address": "0x5E9EDa0F8a6f14e2f900357D1aec1F53507605da", 10 | "name": "Minter Filter V1", 11 | "startBlock": 21284790 12 | } 13 | ], 14 | "minterSetPriceContracts": [ 15 | { 16 | "address": "0x9F1F56C19BA2D64dfa6A3164eFF6B9B8590796D9", 17 | "name": "V4 Set Price Minter", 18 | "startBlock": 21284800 19 | } 20 | ], 21 | "minterSetPriceERC20Contracts": [], 22 | "minterDAExpContracts": [], 23 | "minterDALinContracts": [], 24 | "minterDAExpSettlementContracts": [ 25 | { 26 | "address": "0x58C3B1fCF6c8Ef3a71BE318525b63121eC65E301", 27 | "name": "V1 DA Settlement Minter", 28 | "startBlock": 21284840 29 | } 30 | ], 31 | "minterMerkleContracts": [], 32 | "minterHolderContracts": [], 33 | "minterSEAContracts": [], 34 | "adminACLV0Contracts": [], 35 | "ICoreRegistryContracts": [ 36 | { 37 | "address": "0x25841600e79E9A5263Ec4badcC328AD9CFE5f8C8", 38 | "name": "Arbitrum Goerli dev engine registry contract, heylinds as Deployer", 39 | "startBlock": 20565900 40 | }, 41 | { 42 | "address": "0x3b30d421a6dA95694EaaE09971424F15Eb375269", 43 | "name": "Arbitrum Goerli staging Engine registry for partner deployments", 44 | "startBlock": 25125001 45 | } 46 | ], 47 | "iDependencyRegistryV0Contracts": [], 48 | "ownableUpgradeableDependencyRegistryContracts": [] 49 | } 50 | -------------------------------------------------------------------------------- /src/max-invocations-lib-mapping.ts: -------------------------------------------------------------------------------- 1 | import { BigInt, log, Address } from "@graphprotocol/graph-ts"; 2 | import { Project } from "../generated/schema"; 3 | 4 | import { ProjectMaxInvocationsLimitUpdated } from "../generated/MaxInvocationsLib/MaxInvocationsLib"; 5 | 6 | import { updateProjectIfMinterConfigIsActive } from "./helpers"; 7 | 8 | import { loadOrCreateMinterProjectAndConfigIfProject } from "./generic-minter-events-lib-mapping"; 9 | 10 | /////////////////////////////////////////////////////////////////////////////// 11 | // EVENT HANDLERS start here 12 | /////////////////////////////////////////////////////////////////////////////// 13 | 14 | export function handleProjectMaxInvocationsLimitUpdated( 15 | event: ProjectMaxInvocationsLimitUpdated 16 | ): void { 17 | const minterProjectAndConfig = loadOrCreateMinterProjectAndConfigIfProject( 18 | event.address, // minter 19 | event.params.coreContract, 20 | event.params.projectId, 21 | event.block.timestamp 22 | ); 23 | if (!minterProjectAndConfig) { 24 | // project wasn't found, warning already logged in helper function 25 | return; 26 | } 27 | const projectMinterConfig = minterProjectAndConfig.projectMinterConfiguration; 28 | projectMinterConfig.maxInvocations = event.params.maxInvocations; 29 | projectMinterConfig.save(); 30 | 31 | // induce sync if the project minter configuration is the active one 32 | updateProjectIfMinterConfigIsActive( 33 | minterProjectAndConfig.project, 34 | projectMinterConfig, 35 | event.block.timestamp 36 | ); 37 | } 38 | 39 | /////////////////////////////////////////////////////////////////////////////// 40 | // EVENT HANDLERS end here 41 | /////////////////////////////////////////////////////////////////////////////// 42 | -------------------------------------------------------------------------------- /tests/subgraph/unit-tests.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | clearStore, 4 | test, 5 | newMockEvent, 6 | describe, 7 | beforeEach, 8 | createMockedFunction 9 | } from "matchstick-as/assembly/index"; 10 | import { 11 | Address, 12 | BigInt, 13 | Bytes, 14 | json, 15 | JSONValue, 16 | JSONValueKind, 17 | TypedMap, 18 | store, 19 | log, 20 | ethereum 21 | } from "@graphprotocol/graph-ts"; 22 | 23 | // helper src imports 24 | import { getTotalDAExpAuctionTime } from "../../src/helpers"; 25 | 26 | describe(`getTotalDAExpAuctionTime`, () => { 27 | // note that calculations are also tested in e2e tests: i-filtered-shared-da-exp.test.ts, see function 28 | // `getApproxDAExpLength` (written in typescript, not assemblyscript) 29 | 30 | test("should return correct known-values", () => { 31 | // known values come from Hasura database, with values known to be accurate in prod 32 | const auctionLength = getTotalDAExpAuctionTime( 33 | BigInt.fromString("3000000000000000000"), // start price 34 | BigInt.fromString("90000000000000000"), // base price 35 | BigInt.fromString("354") // half life seconds 36 | ); 37 | // start time = 1691600400, approx end time = 1691602198 38 | const targetAuctionLength = BigInt.fromString("1691602198").minus( 39 | BigInt.fromString("1691600400") 40 | ); 41 | assert.assertTrue(auctionLength.equals(targetAuctionLength)); 42 | }); 43 | 44 | test("should return correct known-values case 2", () => { 45 | // known values come from Hasura database, with values known to be accurate in prod 46 | const auctionLength = getTotalDAExpAuctionTime( 47 | BigInt.fromString("2000000000000000000"), // start price 48 | BigInt.fromString("80000000000000000"), // base price 49 | BigInt.fromString("381") // half life seconds 50 | ); 51 | // start time = 1685638800, approx end time = 1685640598 52 | const targetAuctionLength = BigInt.fromString("1685640598").minus( 53 | BigInt.fromString("1685638800") 54 | ); 55 | assert.assertTrue(auctionLength.equals(targetAuctionLength)); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /config/deprecated/ropsten-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "ropsten", 3 | "genArt721CoreContracts": [ 4 | { 5 | "address": "0x87c6E93Fc0B149ec59AD595e2E187a4e1d7fDC25", 6 | "name": "Main Dev Ropsten Contract", 7 | "startBlock": 10216817 8 | }, 9 | { 10 | "address": "0x3f073062271F86711f851df57F31D2DEc026E3da", 11 | "name": "Test Dev Ropsten Contract", 12 | "startBlock": 10216817 13 | } 14 | ], 15 | "minterFilterV0Contracts": [ 16 | { 17 | "address": "0xDDc77d8f935b255aD8b5651392D1284E29478b5b", 18 | "startBlock": 12070104 19 | } 20 | ], 21 | "minterSetPriceV0Contracts": [ 22 | { 23 | "address": "0x7CaD4e26e378FaBe8884b6609445BD4EDC099C65", 24 | "startBlock": 12070107 25 | } 26 | ], 27 | "minterSetPriceERC20V0Contracts": [ 28 | { 29 | "address": "0xE8D934100B560c50EE3bB2f83fC14C97dc403e44", 30 | "startBlock": 12070106 31 | } 32 | ], 33 | "minterDALinV0Contracts": [ 34 | { 35 | "address": "0xd2971Fa03F6b9aa2c88C1496F9e3AdBb1e1AaB64", 36 | "startBlock": 12070108 37 | } 38 | ], 39 | "minterDAExpV0Contracts": [ 40 | { 41 | "address": "0x631d3Bb3f360a7ed5f6De194206aBa4c4999f2c8", 42 | "startBlock": 12070109 43 | } 44 | ], 45 | "minterSetPriceV1Contracts": [ 46 | { 47 | "address": "0x0e2Bb2Bb67cb43136c63D61829ff59FD2FC872C8", 48 | "startBlock": 12240973 49 | } 50 | ], 51 | "minterSetPriceERC20V1Contracts": [ 52 | { 53 | "address": "0xd6d15Aa90207fD07d62bAd155838859b6f65dE07", 54 | "startBlock": 12240972 55 | } 56 | ], 57 | "minterDALinV1Contracts": [ 58 | { 59 | "address": "0xFcaBB2dc7e5abfBD9EB36b0052F8A20cdF2830df", 60 | "startBlock": 12240974 61 | } 62 | ], 63 | "minterDAExpV1Contracts": [ 64 | { 65 | "address": "0xcfe42B288a9299f9214373D505418899ebAa5Fb4", 66 | "startBlock": 12240975 67 | } 68 | ], 69 | "minterHolderV0Contracts": [ 70 | { 71 | "address": "0xcaE34c37ed4f47CE74CF3E7ad799272528d1B069", 72 | "startBlock": 12499903 73 | } 74 | ], 75 | "minterMerkleV0Contracts": [ 76 | { 77 | "address": "0x25170a18d984721f16d525b83537568D4448776B", 78 | "startBlock": 12240975 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/subgraph-config.d.ts: -------------------------------------------------------------------------------- 1 | export type SubgraphConfig = Partial<{ 2 | network: string; 3 | iGenArt721CoreContractV3_BaseContracts: { 4 | address: string; 5 | }[]; 6 | ownableGenArt721CoreV3Contracts?: { address: string }[]; 7 | iERC721GenArt721CoreV3Contracts?: { address: string }[]; 8 | genArt721CoreContracts: { address: string }[]; 9 | genArt721Core2Contracts: { address: string }[]; 10 | genArt721Contracts: { address: string }[]; 11 | pbabContracts: { address: string }[]; 12 | engineFlexContracts: { address: string }[]; 13 | minterFilterV0Contracts: { address: string }[]; 14 | minterFilterV1Contracts: { address: string }[]; 15 | sharedMinterFilterContracts: { address: string }[]; 16 | minterSetPriceContracts: { address: string }[]; 17 | minterSetPriceERC20Contracts: { address: string }[]; 18 | minterDAExpContracts: { address: string }[]; 19 | minterDALinContracts: { address: string }[]; 20 | minterDAExpSettlementContracts: { address: string }[]; 21 | minterMerkleContracts: { address: string }[]; 22 | minterHolderContracts: { address: string }[]; 23 | adminACLV0Contracts: { address: string }[]; 24 | iDependencyRegistryV0Contracts: { address: string }[]; 25 | ownableUpgradeableDependencyRegistryContracts: { address: string }[]; 26 | ICoreRegistryContracts: { address: string }[]; 27 | genericMinterEventsLibContracts: { address: string }[]; 28 | splitFundsLibContracts: { address: string }[]; 29 | setPriceLibContracts: { address: string }[]; 30 | maxInvocationsLibContracts: { address: string }[]; 31 | merkleLibContracts: { address: string }[]; 32 | holderLibContracts: { address: string }[]; 33 | minPriceLibContracts: { address: string }[]; 34 | SEALibContracts: { address: string }[]; 35 | RAMLibContracts: { address: string }[]; 36 | DALibContracts: { address: string }[]; 37 | DAExpLibContracts: { address: string }[]; 38 | DALinLibContracts: { address: string }[]; 39 | settlementExpLibContracts: { address: string }[]; 40 | iPMPV0Contracts: { address: string }[]; 41 | iSplitAtomicFactoryV0Contracts: { address: string }[]; 42 | metadata: { 43 | minterFilterAdminACLAddress: string; 44 | coreRegistryAddress: string; 45 | bytecodeStorageReaderAddress: string; 46 | delegationRegistryAddress: string; 47 | pmpV0Address: string; 48 | splitAtomicImplementationAddress: string; 49 | }; 50 | }>; 51 | -------------------------------------------------------------------------------- /src/merkle-lib-mapping.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultMaxInvocationsPerAddress, 3 | DelegationRegistryUpdated 4 | } from "../generated/MerkleLib/MerkleLib"; 5 | 6 | import { loadOrCreateMinter } from "./helpers"; 7 | 8 | import { setMinterExtraMinterDetailsValue } from "./extra-minter-details-helpers"; 9 | 10 | /////////////////////////////////////////////////////////////////////////////// 11 | // EVENT HANDLERS start here 12 | /////////////////////////////////////////////////////////////////////////////// 13 | 14 | /** 15 | * Handles the event that updates the default max invocations per address. 16 | * Loads or creates the minter, updates the minter's details, and refreshes 17 | * the minter's timestamp to induce a sync. 18 | * @param event The event containing the updated max invocations per address. 19 | */ 20 | export function handleDefaultMaxInvocationsPerAddress( 21 | event: DefaultMaxInvocationsPerAddress 22 | ): void { 23 | // load minter 24 | const minter = loadOrCreateMinter(event.address, event.block.timestamp); 25 | 26 | // update minter extra minter details value 27 | setMinterExtraMinterDetailsValue( 28 | "defaultMaxInvocationsPerAddress", 29 | event.params.defaultMaxInvocationsPerAddress, 30 | minter 31 | ); 32 | 33 | // update minter's updatedAt timestamp to induce a sync, and save 34 | minter.updatedAt = event.block.timestamp; 35 | minter.save(); 36 | } 37 | 38 | /** 39 | * Handles the event that updates the delegation registry address. 40 | * Loads or creates the minter, updates the minter's details, and refreshes 41 | * the minter's timestamp to induce a sync. 42 | * @param event The event containing the updated delegation registry address. 43 | */ 44 | export function handleDelegationRegistryUpdated( 45 | event: DelegationRegistryUpdated 46 | ): void { 47 | // load minter 48 | const minter = loadOrCreateMinter(event.address, event.block.timestamp); 49 | 50 | // update minter extra minter details value 51 | setMinterExtraMinterDetailsValue( 52 | "delegationRegistryAddress", 53 | event.params.delegationRegistry, 54 | minter 55 | ); 56 | 57 | // update minter's updatedAt timestamp to induce a sync, and save 58 | minter.updatedAt = event.block.timestamp; 59 | minter.save(); 60 | } 61 | 62 | /////////////////////////////////////////////////////////////////////////////// 63 | // EVENT HANDLERS end here 64 | /////////////////////////////////////////////////////////////////////////////// 65 | -------------------------------------------------------------------------------- /src/split-funds-lib-mapping.ts: -------------------------------------------------------------------------------- 1 | import { BigInt, log, Address } from "@graphprotocol/graph-ts"; 2 | import { Project } from "../generated/schema"; 3 | 4 | import { ProjectCurrencyInfoUpdated } from "../generated/SplitFundsLib/SplitFundsLib"; 5 | 6 | import { updateProjectIfMinterConfigIsActive } from "./helpers"; 7 | 8 | import { loadOrCreateMinterProjectAndConfigIfProject } from "./generic-minter-events-lib-mapping"; 9 | import { ERC20 } from "../generated/MinterSetPriceERC20/ERC20"; 10 | 11 | /////////////////////////////////////////////////////////////////////////////// 12 | // EVENT HANDLERS start here 13 | /////////////////////////////////////////////////////////////////////////////// 14 | 15 | /** 16 | * Handles the update of a project's currency information. Attempts to load associated 17 | * project and its minter configuration, then updates currency address and symbol. 18 | * @param event The event carrying updated currency information 19 | */ 20 | export function handleProjectCurrencyInfoUpdated( 21 | event: ProjectCurrencyInfoUpdated 22 | ): void { 23 | const minterProjectAndConfig = loadOrCreateMinterProjectAndConfigIfProject( 24 | event.address, // minter 25 | event.params.coreContract, 26 | event.params.projectId, 27 | event.block.timestamp 28 | ); 29 | if (!minterProjectAndConfig) { 30 | // project wasn't found, warning already logged in helper function 31 | return; 32 | } 33 | 34 | const projectMinterConfig = minterProjectAndConfig.projectMinterConfiguration; 35 | projectMinterConfig.currencyAddress = event.params.currencyAddress; 36 | projectMinterConfig.currencySymbol = event.params.currencySymbol; 37 | projectMinterConfig.currencyDecimals = 18; 38 | if (event.params.currencyAddress != Address.zero()) { 39 | let currencyContract = ERC20.bind(event.params.currencyAddress); 40 | let decimals = currencyContract.try_decimals(); 41 | if (!decimals.reverted) { 42 | projectMinterConfig.currencyDecimals = decimals.value; 43 | } 44 | } 45 | 46 | projectMinterConfig.save(); 47 | 48 | // induce sync if the project minter configuration is the active one 49 | updateProjectIfMinterConfigIsActive( 50 | minterProjectAndConfig.project, 51 | projectMinterConfig, 52 | event.block.timestamp 53 | ); 54 | } 55 | 56 | /////////////////////////////////////////////////////////////////////////////// 57 | // EVENT HANDLERS end here 58 | /////////////////////////////////////////////////////////////////////////////// 59 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/minter-suite-v2/min-price-lib.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "@jest/globals"; 2 | import { 3 | getSubgraphConfig, 4 | createSubgraphClient, 5 | getAccounts, 6 | waitUntilSubgraphIsSynced, 7 | getMinterDetails, 8 | } from "../utils/helpers"; 9 | 10 | import { MinterMinPriceV0__factory } from "../../contracts/factories/MinterMinPriceV0__factory"; 11 | // hide nuisance logs about event overloading 12 | import { Logger } from "@ethersproject/logger"; 13 | Logger.setLogLevel(Logger.levels.ERROR); 14 | 15 | // waiting for subgraph to sync can take longer than the default 5s timeout 16 | // @dev For this file specifically, one test takes >60s due to hard-coded minimum 17 | // auction duration on SEA minter 18 | jest.setTimeout(100 * 1000); 19 | 20 | const config = getSubgraphConfig(); 21 | 22 | const client = createSubgraphClient(); 23 | const { deployer } = getAccounts(); 24 | 25 | // get min price lib contract from the subgraph config 26 | if (!config.minPriceLibContracts) { 27 | throw new Error("No minPriceLibContracts in config"); 28 | } 29 | const minterMinPriceV0Address = config.minPriceLibContracts[0].address; 30 | const minterMinPriceV0Contract = new MinterMinPriceV0__factory(deployer).attach( 31 | minterMinPriceV0Address 32 | ); 33 | 34 | describe("MinPriceLib event handling", () => { 35 | beforeAll(async () => { 36 | await waitUntilSubgraphIsSynced(client); 37 | }); 38 | 39 | describe("Indexed after setup", () => { 40 | test("created new Minter during deployment and allowlisting", async () => { 41 | const targetId = minterMinPriceV0Address.toLowerCase(); 42 | const minterRes = await getMinterDetails(client, targetId); 43 | expect(minterRes.id).toBe(targetId); 44 | }); 45 | }); 46 | 47 | describe("MinMintFeeUpdated", () => { 48 | // this is a minter-level, value set in the constructor and other function(s), 49 | // so we can inspect it by checking the Minter entity in the subgraph 50 | // that was created during deployment 51 | test("value was populated during deployment", async () => { 52 | // query public constant for the expected value (>0) 53 | const expectedValue = await minterMinPriceV0Contract.minMintFee(); 54 | expect(expectedValue.gt(0)).toBe(true); 55 | // validate minter's extraMinterDetails in subgraph 56 | const targetId = minterMinPriceV0Address.toLowerCase(); 57 | const minterRes = await getMinterDetails(client, targetId); 58 | const extraMinterDetails = JSON.parse(minterRes.extraMinterDetails); 59 | expect(extraMinterDetails.minMintFee).toBe(expectedValue.toString()); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/da-lib-mapping.ts: -------------------------------------------------------------------------------- 1 | import { ResetAuctionDetails } from "../generated/DALib/DALib"; 2 | 3 | import { loadOrCreateMinterProjectAndConfigIfProject } from "./generic-minter-events-lib-mapping"; 4 | 5 | import { updateProjectIfMinterConfigIsActive } from "./helpers"; 6 | 7 | import { removeProjectMinterConfigExtraMinterDetailsEntry } from "./extra-minter-details-helpers"; 8 | 9 | /////////////////////////////////////////////////////////////////////////////// 10 | // EVENT HANDLERS start here 11 | /////////////////////////////////////////////////////////////////////////////// 12 | 13 | // project-level configuration events 14 | 15 | /** 16 | * Handles the event that resets the auction details for a project. 17 | * @param event The event carrying the project's core address and project number. 18 | */ 19 | export function handleResetAuctionDetails(event: ResetAuctionDetails): void { 20 | const minterProjectAndConfig = loadOrCreateMinterProjectAndConfigIfProject( 21 | event.address, // minter 22 | event.params.coreContract, 23 | event.params.projectId, 24 | event.block.timestamp 25 | ); 26 | if (!minterProjectAndConfig) { 27 | // project wasn't found, warning already logged in helper function 28 | return; 29 | } 30 | 31 | const projectMinterConfig = minterProjectAndConfig.projectMinterConfiguration; 32 | // remove the auction details from the project minter config 33 | projectMinterConfig.basePrice = null; 34 | projectMinterConfig.priceIsConfigured = false; 35 | // @dev we remove all possible auction details for both Exp and Lin auctions, 36 | // because they share a common interface 37 | removeProjectMinterConfigExtraMinterDetailsEntry( 38 | "startPrice", 39 | projectMinterConfig 40 | ); 41 | removeProjectMinterConfigExtraMinterDetailsEntry( 42 | "startTime", 43 | projectMinterConfig 44 | ); 45 | // @dev only populated in linear auctions 46 | removeProjectMinterConfigExtraMinterDetailsEntry( 47 | "endTime", 48 | projectMinterConfig 49 | ); 50 | // @dev only populated in exponential auctions 51 | removeProjectMinterConfigExtraMinterDetailsEntry( 52 | "halfLifeSeconds", 53 | projectMinterConfig 54 | ); 55 | // @dev only populated in exponential auctions 56 | removeProjectMinterConfigExtraMinterDetailsEntry( 57 | "approximateDAExpEndTime", 58 | projectMinterConfig 59 | ); 60 | 61 | projectMinterConfig.save(); 62 | 63 | // induce sync if the project minter configuration is the active one 64 | updateProjectIfMinterConfigIsActive( 65 | minterProjectAndConfig.project, 66 | projectMinterConfig, 67 | event.block.timestamp 68 | ); 69 | } 70 | 71 | /////////////////////////////////////////////////////////////////////////////// 72 | // EVENT HANDLERS end here 73 | /////////////////////////////////////////////////////////////////////////////// 74 | -------------------------------------------------------------------------------- /config/deprecated/ropsten-staging.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "ropsten", 3 | "genArt721Core2Contracts": [ 4 | { "address": "0x1CD623a86751d4C4f20c96000FEC763941f098A2" } 5 | ], 6 | "pbabContracts": [ 7 | { 8 | "address": "0xd9f14781f6cba1f4ffb0743bcfd5fc860d1da847", 9 | "name": "Doodle Labs" 10 | }, 11 | { 12 | "address": "0x06710498339b30834653459Ac90F52Cbd2F1D085", 13 | "name": "Flamingo Flutter" 14 | }, 15 | { 16 | "address": "0xd10e3DEe203579FcEE90eD7d0bDD8086F7E53beB", 17 | "name": "Plottables" 18 | }, 19 | { 20 | "address": "0x7DC2c066251A21520Ae03cd2C3567a3c073Be60A", 21 | "name": "Tboa Club" 22 | }, 23 | { 24 | "address": "0x86732cd7DC0A6fc559C62736083298E78310B8dC", 25 | "name": "Bright Moments" 26 | }, 27 | { 28 | "address": "0x24CCA47Fcc4fe09b51aBE8A8124C3da01765bE14", 29 | "name": "Crypto Citizens" 30 | }, 31 | { 32 | "address": "0xf17A2577eFaA269E02d2cFBEB15dfE9860C90A38", 33 | "name": "ARTCODE" 34 | }, 35 | { 36 | "address": "0x0583379345586d5219cA842c6eC463f8cdDdBC84", 37 | "name": "MechSuit" 38 | }, 39 | { 40 | "address": "0xD06A5109bEdEf92696B29FcC0F6184640b19c310", 41 | "name": "Colors and Shapes" 42 | }, 43 | { 44 | "address": "0x0711A9767a41290D7f52714204618345C584f450", 45 | "name": "Legends of Metaterra" 46 | }, 47 | { 48 | "address": "0xb17e8d4cBcd0Ff0F8891e8d01Faeed41025C6B2e", 49 | "name": "Fireworks by Samsung" 50 | } 51 | ], 52 | "minterFilterV0Contracts": [ 53 | { 54 | "address": "0xF3d2a642640c928A33a087545939e5df3d0d657f", 55 | "startBlock": 12175917 56 | } 57 | ], 58 | "minterSetPriceV0Contracts": [ 59 | { 60 | "address": "0xFd50294f1ddEcCdf6ea55bC57a15916328177653", 61 | "startBlock": 12175920 62 | } 63 | ], 64 | "minterSetPriceERC20V0Contracts": [ 65 | { 66 | "address": "0xcA92128156772A424CEE092954C064B57eC481Fb", 67 | "startBlock": 12175918 68 | } 69 | ], 70 | "minterDALinV0Contracts": [ 71 | { 72 | "address": "0x84795B0FF2e0cEa1657A6A86d42623441EfCa38d", 73 | "startBlock": 12175921 74 | } 75 | ], 76 | "minterDAExpV0Contracts": [ 77 | { 78 | "address": "0x1e0235eD161A99C59FB33dE8Dd5374aB7CC3D57b", 79 | "startBlock": 12175922 80 | } 81 | ], 82 | "minterSetPriceV1Contracts": [ 83 | { 84 | "address": "0x9bC71a0380f2cc5C099e58b0d836bEddB1B5CE1C", 85 | "startBlock": 12278126 86 | } 87 | ], 88 | "minterSetPriceERC20V1Contracts": [ 89 | { 90 | "address": "0xa504BF82Db6347165A39294FB0CaE761074661Ee", 91 | "startBlock": 12278126 92 | } 93 | ], 94 | "minterDALinV1Contracts": [ 95 | { 96 | "address": "0xfe2D6Fc2E4914CD520353F91E0c214cF1e478eeb", 97 | "startBlock": 12278126 98 | } 99 | ], 100 | "minterDAExpV1Contracts": [ 101 | { 102 | "address": "0x00D59a0e11b9b8a878bacd94cb03dEe5eBfDfe1E", 103 | "startBlock": 12278126 104 | } 105 | ], 106 | "startBlock": 10760915 107 | } 108 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/minter-suite-v2/merkle-lib.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "@jest/globals"; 2 | import { 3 | getSubgraphConfig, 4 | createSubgraphClient, 5 | waitUntilSubgraphIsSynced, 6 | getMinterDetails, 7 | } from "../utils/helpers"; 8 | 9 | // hide nuisance logs about event overloading 10 | import { Logger } from "@ethersproject/logger"; 11 | Logger.setLogLevel(Logger.levels.ERROR); 12 | 13 | // waiting for subgraph to sync can take longer than the default 5s timeout 14 | jest.setTimeout(30 * 1000); 15 | 16 | const config = getSubgraphConfig(); 17 | 18 | const client = createSubgraphClient(); 19 | 20 | // set up delegation registry address 21 | const delegationRegistryAddress = config.metadata?.delegationRegistryAddress; 22 | if (!delegationRegistryAddress) 23 | throw new Error("No delegation registry address found in config metadata"); 24 | 25 | // set up contract instances and/or addresses 26 | const coreRegistryAddress = config.metadata?.coreRegistryAddress; 27 | if (!coreRegistryAddress) 28 | throw new Error("No core registry address found in config metadata"); 29 | 30 | const sharedMinterFilter = config.sharedMinterFilterContracts?.[0]; 31 | if (!sharedMinterFilter) { 32 | throw new Error("No shared minter filter found in config metadata"); 33 | } 34 | 35 | // get MinterSetPriceMerkleV5 contract from the subgraph config 36 | if (!config.merkleLibContracts) { 37 | throw new Error("No merkleLibContracts in config"); 38 | } 39 | const minterSetPriceMerkleV5Address = config.merkleLibContracts[0].address; 40 | 41 | describe("MerkleLib event handling", () => { 42 | beforeAll(async () => { 43 | await waitUntilSubgraphIsSynced(client); 44 | }); 45 | 46 | describe("Indexed after setup", () => { 47 | test("created new Minter during deployment and allowlisting", async () => { 48 | const targetId = minterSetPriceMerkleV5Address.toLowerCase(); 49 | const minterRes = await getMinterDetails(client, targetId); 50 | expect(minterRes.id).toBe(targetId); 51 | }); 52 | }); 53 | 54 | describe("DefaultMaxInvocationsPerAddress", () => { 55 | // this is a minter-level, immutable value set in the constructor, 56 | // so we can only inspect it by checking the Minter entity in the subgraph 57 | // that was created during deployment 58 | test("value was populated during deployment", async () => { 59 | // validate minter's extraMinterDetails in subgraph 60 | const targetId = minterSetPriceMerkleV5Address.toLowerCase(); 61 | const minterRes = await getMinterDetails(client, targetId); 62 | const extraMinterDetails = JSON.parse(minterRes.extraMinterDetails); 63 | expect(extraMinterDetails.defaultMaxInvocationsPerAddress).toBe(1); 64 | }); 65 | }); 66 | 67 | describe("DelegationRegistryUpdated", () => { 68 | // this is a minter-level, immutable value set in the constructor, 69 | // so we can only inspect it by checking the Minter entity in the subgraph 70 | // that was created during deployment 71 | test("value was populated during deployment", async () => { 72 | // validate minter's extraMinterDetails in subgraph 73 | const targetId = minterSetPriceMerkleV5Address.toLowerCase(); 74 | const minterRes = await getMinterDetails(client, targetId); 75 | const extraMinterDetails = JSON.parse(minterRes.extraMinterDetails); 76 | expect(extraMinterDetails.delegationRegistryAddress).toBe( 77 | delegationRegistryAddress.toLowerCase() 78 | ); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/da-lin-lib-mapping.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuctionMinimumLengthSecondsUpdated, 3 | SetAuctionDetailsLin 4 | } from "../generated/DALinLib/DALinLib"; 5 | 6 | import { loadOrCreateMinterProjectAndConfigIfProject } from "./generic-minter-events-lib-mapping"; 7 | 8 | import { 9 | loadOrCreateMinter, 10 | updateProjectIfMinterConfigIsActive 11 | } from "./helpers"; 12 | 13 | import { 14 | setMinterExtraMinterDetailsValue, 15 | setProjectMinterConfigExtraMinterDetailsValue 16 | } from "./extra-minter-details-helpers"; 17 | 18 | /////////////////////////////////////////////////////////////////////////////// 19 | // EVENT HANDLERS start here 20 | /////////////////////////////////////////////////////////////////////////////// 21 | 22 | // minter-level configuration events 23 | 24 | /** 25 | * handles the update of a Linear Dutch Auction's minimum auction length in seconds. 26 | * @param event The event containing the updated minimum auction length in seconds. 27 | */ 28 | export function handleAuctionMinimumLengthSecondsUpdated( 29 | event: AuctionMinimumLengthSecondsUpdated 30 | ): void { 31 | // load minter 32 | const minter = loadOrCreateMinter(event.address, event.block.timestamp); 33 | 34 | // update minter extra minter details value 35 | setMinterExtraMinterDetailsValue( 36 | "minimumAuctionLengthInSeconds", 37 | event.params.minimumAuctionLengthSeconds, 38 | minter 39 | ); 40 | 41 | // update minter's updatedAt timestamp to induce a sync, and save 42 | minter.updatedAt = event.block.timestamp; 43 | minter.save(); 44 | } 45 | 46 | // project-level configuration events 47 | 48 | /** 49 | * Handles the event that updates the auction details for a project using an 50 | * linear Dutch auction minter. 51 | * @param event The event carrying the updated lin auction data. 52 | */ 53 | export function handleSetAuctionDetailsLin(event: SetAuctionDetailsLin): void { 54 | const minterProjectAndConfig = loadOrCreateMinterProjectAndConfigIfProject( 55 | event.address, // minter 56 | event.params.coreContract, 57 | event.params.projectId, 58 | event.block.timestamp 59 | ); 60 | if (!minterProjectAndConfig) { 61 | // project wasn't found, warning already logged in helper function 62 | return; 63 | } 64 | 65 | const projectMinterConfig = minterProjectAndConfig.projectMinterConfiguration; 66 | // remove the auction details from the project minter config 67 | 68 | projectMinterConfig.basePrice = event.params.basePrice; 69 | projectMinterConfig.priceIsConfigured = true; 70 | setProjectMinterConfigExtraMinterDetailsValue( 71 | "startPrice", 72 | event.params.startPrice.toString(), // Price is likely to overflow js Number.MAX_SAFE_INTEGER so store as string 73 | projectMinterConfig 74 | ); 75 | setProjectMinterConfigExtraMinterDetailsValue( 76 | "startTime", 77 | event.params.auctionTimestampStart, 78 | projectMinterConfig 79 | ); 80 | setProjectMinterConfigExtraMinterDetailsValue( 81 | "endTime", 82 | event.params.auctionTimestampEnd, 83 | projectMinterConfig 84 | ); 85 | 86 | projectMinterConfig.save(); 87 | 88 | // induce sync if the project minter configuration is the active one 89 | updateProjectIfMinterConfigIsActive( 90 | minterProjectAndConfig.project, 91 | projectMinterConfig, 92 | event.block.timestamp 93 | ); 94 | } 95 | 96 | /////////////////////////////////////////////////////////////////////////////// 97 | // EVENT HANDLERS end here 98 | /////////////////////////////////////////////////////////////////////////////// 99 | -------------------------------------------------------------------------------- /config/generic.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "sepolia", 3 | "iGenArt721CoreContractV3_BaseContracts": [ 4 | { 5 | "address": "" 6 | } 7 | ], 8 | "ownableGenArt721CoreV3Contracts": [ 9 | { 10 | "address": "" 11 | } 12 | ], 13 | "iERC721GenArt721CoreV3Contracts": [ 14 | { 15 | "address": "" 16 | } 17 | ], 18 | "iGenArt721CoreContractV3_Engine_FlexContracts": [ 19 | { 20 | "address": "" 21 | } 22 | ], 23 | "genArt721CoreContracts": [ 24 | { 25 | "address": "" 26 | } 27 | ], 28 | "genArt721Core2Contracts": [ 29 | { 30 | "address": "" 31 | } 32 | ], 33 | "genArt721Contracts": [ 34 | { 35 | "address": "" 36 | } 37 | ], 38 | "pbabContracts": [ 39 | { 40 | "address": "" 41 | } 42 | ], 43 | "engineFlexContracts": [ 44 | { 45 | "address": "" 46 | } 47 | ], 48 | "minterFilterV0Contracts": [ 49 | { 50 | "address": "" 51 | } 52 | ], 53 | "minterFilterV1Contracts": [ 54 | { 55 | "address": "" 56 | } 57 | ], 58 | "sharedMinterFilterContracts": [ 59 | { 60 | "address": "" 61 | } 62 | ], 63 | "genericMinterEventsLibContracts": [ 64 | { 65 | "address": "" 66 | } 67 | ], 68 | "splitFundsLibContracts": [ 69 | { 70 | "address": "" 71 | } 72 | ], 73 | "setPriceLibContracts": [ 74 | { 75 | "address": "" 76 | } 77 | ], 78 | "minPriceLibContracts": [ 79 | { 80 | "address": "" 81 | } 82 | ], 83 | "maxInvocationsLibContracts": [ 84 | { 85 | "address": "" 86 | } 87 | ], 88 | "merkleLibContracts": [ 89 | { 90 | "address": "" 91 | } 92 | ], 93 | "holderLibContracts": [ 94 | { 95 | "address": "" 96 | } 97 | ], 98 | "SEALibContracts": [ 99 | { 100 | "address": "" 101 | } 102 | ], 103 | "RAMLibContracts": [ 104 | { 105 | "address": "" 106 | } 107 | ], 108 | "DALibContracts": [ 109 | { 110 | "address": "" 111 | } 112 | ], 113 | "DAExpLibContracts": [ 114 | { 115 | "address": "" 116 | } 117 | ], 118 | "DALinLibContracts": [ 119 | { 120 | "address": "" 121 | } 122 | ], 123 | "settlementExpLibContracts": [ 124 | { 125 | "address": "" 126 | } 127 | ], 128 | "minterSetPriceContracts": [ 129 | { 130 | "address": "" 131 | } 132 | ], 133 | "minterSetPriceERC20Contracts": [ 134 | { 135 | "address": "" 136 | } 137 | ], 138 | "minterDAExpContracts": [ 139 | { 140 | "address": "" 141 | } 142 | ], 143 | "minterDALinContracts": [ 144 | { 145 | "address": "" 146 | } 147 | ], 148 | "minterDAExpSettlementContracts": [ 149 | { 150 | "address": "" 151 | } 152 | ], 153 | "minterMerkleContracts": [ 154 | { 155 | "address": "" 156 | } 157 | ], 158 | "minterHolderContracts": [ 159 | { 160 | "address": "" 161 | } 162 | ], 163 | "minterSEAContracts": [ 164 | { 165 | "address": "" 166 | } 167 | ], 168 | "adminACLV0Contracts": [ 169 | { 170 | "address": "" 171 | } 172 | ], 173 | "iDependencyRegistryV0Contracts": [ 174 | { 175 | "address": "" 176 | } 177 | ], 178 | "ownableUpgradeableDependencyRegistryContracts": [ 179 | { 180 | "address": "" 181 | } 182 | ], 183 | "ICoreRegistryContracts": [ 184 | { 185 | "address": "" 186 | } 187 | ], 188 | "iSplitAtomicFactoryV0Contracts": [ 189 | { 190 | "address": "" 191 | } 192 | ], 193 | "iPMPV0Contracts": [ 194 | { 195 | "address": "" 196 | } 197 | ] 198 | } 199 | -------------------------------------------------------------------------------- /posterity/src/secondary/looksrare/looksrare-mapping.ts: -------------------------------------------------------------------------------- 1 | import { Address } from "@graphprotocol/graph-ts"; 2 | 3 | import { 4 | Token, 5 | Sale, 6 | SaleLookupTable, 7 | Contract, 8 | Payment 9 | } from "../../../generated/schema"; 10 | 11 | import { 12 | TakerAsk, 13 | TakerBid 14 | } from "../../../generated/LooksRareExchange/LooksRareExchange"; 15 | 16 | import { generateContractSpecificId } from "../../helpers"; 17 | import { buildTokenSaleLookupTableId } from "../secondary-helpers"; 18 | import { LR_PRIVATE_SALE_STRATEGY, LR_V1, NATIVE } from "../../constants"; 19 | 20 | /** 21 | * 22 | * @param event TakerAsk 23 | * @description Event handler for the TakerAsk event. Forward call to `handleLooksRareEvents` 24 | */ 25 | export function handleTakerAsk(event: TakerAsk): void { 26 | handleSale(event); 27 | } 28 | 29 | /** 30 | * 31 | * @param event TakerBid 32 | * @description Event handler for the TakerBid event. Forward call to `handleLooksRareEvents` 33 | */ 34 | export function handleTakerBid(event: TakerBid): void { 35 | handleSale(event); 36 | } 37 | 38 | /** 39 | * 40 | * @param event TakerAsk or TakerBid event from looksrare, beeing emitted when a seller accept 41 | * an offer from a buyer (TakerAsk) or when a buyer accept to buy from a seller (TakerBid) 42 | * @description This function handle TakerAsk/TakerBid events from LooksRare, build the associated Sale and 43 | * SaleLookUpTable entities and store them 44 | */ 45 | function handleSale(event: T): void { 46 | // Invalid call, not a valid event 47 | if (!(event instanceof TakerBid) && !(event instanceof TakerAsk)) { 48 | return; 49 | } 50 | 51 | let contract = Contract.load(event.params.collection.toHexString()); 52 | 53 | // Only interested in Art Blocks sells 54 | if (!contract) { 55 | return; 56 | } 57 | 58 | // The token must already exists (minted) to be sold on LooksRare 59 | let token = Token.load( 60 | generateContractSpecificId( 61 | Address.fromString(event.params.collection.toHexString()), 62 | event.params.tokenId 63 | ) 64 | ); 65 | 66 | // The token must already exist (minted) to be sold on LooksRare 67 | if (!token) { 68 | return; 69 | } 70 | 71 | // Create sale 72 | let saleId = event.params.orderHash.toHexString(); 73 | let sale = new Sale(saleId); 74 | sale.txHash = event.transaction.hash; 75 | sale.exchange = LR_V1; 76 | sale.saleType = "Single"; 77 | sale.blockNumber = event.block.number; 78 | sale.blockTimestamp = event.block.timestamp; 79 | 80 | if (event instanceof TakerAsk) { 81 | sale.buyer = event.params.maker; 82 | sale.seller = event.params.taker; 83 | } else { 84 | sale.buyer = event.params.taker; 85 | sale.seller = event.params.maker; 86 | } 87 | sale.summaryTokensSold = token.id; 88 | sale.isPrivate = 89 | event.params.strategy.toHexString() == LR_PRIVATE_SALE_STRATEGY; 90 | sale.save(); 91 | 92 | // Create the associated entry in the Nft <=> lookup table 93 | let tableEntryId = buildTokenSaleLookupTableId( 94 | token.project, 95 | token.id, 96 | saleId 97 | ); 98 | 99 | let payment = new Payment(saleId + "-0"); 100 | payment.sale = saleId; 101 | payment.paymentType = NATIVE; 102 | payment.paymentToken = event.params.currency; 103 | payment.price = event.params.price; 104 | payment.recipient = event.params.taker; 105 | payment.save(); 106 | 107 | // Create saleLookUpTable with sale and token info 108 | let saleLookUpTable = new SaleLookupTable(tableEntryId); 109 | saleLookUpTable.token = token.id; 110 | saleLookUpTable.project = token.project; 111 | saleLookUpTable.sale = sale.id; 112 | saleLookUpTable.timestamp = sale.blockTimestamp; 113 | saleLookUpTable.blockNumber = sale.blockNumber; 114 | saleLookUpTable.save(); 115 | } 116 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { BigInt } from "@graphprotocol/graph-ts"; 2 | 3 | export const NULL_ADDRESS = "0x0000000000000000000000000000000000000000"; 4 | 5 | export const ARTBLOCKS_PLATFORM_ID = "ArtBlocks"; 6 | export const ARTBLOCKS_ORIGINAL_ADDRESS = 7 | "0x059edd72cd353df5106d2b9cc5ab83a52287ac3a"; 8 | export const ARTBLOCKS_ADDRESS = "0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270"; 9 | 10 | // Opensea V1 contract 11 | export const WYVERN_EXCHANGE_ADDRESS = 12 | "0x7be8076f4ea4a4ad08075c2508e481d6c946d12b"; 13 | 14 | // Opensea V2 contract 15 | export const WYVERN_EXCHANGE_WITH_BULK_CANCELLATIONS_ADDRESS = 16 | "0x7f268357a8c2552623316e2562d90e642bb538e5"; 17 | 18 | // Opensea V2 contract handling the transfers 19 | export const WYVERN_MERKLE_ADDRESS = 20 | "0xbaf2127b49fc93cbca6269fade0f7f31df4c88a7"; 21 | 22 | // Opensea atomicizer contract for bundle 23 | export const WYVERN_ATOMICIZER_ADDRESS = 24 | "0xc99f70bfd82fb7c8f8191fdfbfb735606b15e5c5"; 25 | 26 | // LooksRare private sale strategy contract 27 | export const LR_PRIVATE_SALE_STRATEGY = 28 | "0x58d83536d3efedb9f7f2a1ec3bdaad2b1a4dd98c"; 29 | 30 | // Function selectors 31 | export const TRANSFER_FROM_SELECTOR = "0x23b872dd"; 32 | export const ERC721_SAFE_TRANSFER_FROM_SELECTOR = "0x42842e0e"; 33 | export const ERC155_SAFE_TRANSFER_FROM_SELECTOR = "0xf242432a"; 34 | 35 | // Core Types 36 | export const GEN_ART_721_CORE_V0 = "GenArt721CoreV0"; 37 | export const GEN_ART_721_CORE_V1 = "GenArt721CoreV1"; 38 | export const GEN_ART_721_CORE_V2 = "GenArt721CoreV2"; 39 | // V3 and-on source core type from contract itself 40 | 41 | export const OS_V1 = "OS_V1"; 42 | export const OS_V2 = "OS_V2"; 43 | export const LR_V1 = "LR_V1"; 44 | export const OS_SP = "OS_SP"; 45 | 46 | export const NATIVE = "Native"; 47 | export const ERC20 = "ERC20"; 48 | export const ERC721 = "ERC721"; 49 | export const ERC1155 = "ERC1155"; 50 | 51 | // This is directly tied to the ExternalAssetDependencyType enum on the Engine Flex Core contract 52 | export const FLEX_CONTRACT_EXTERNAL_ASSET_DEP_TYPES = [ 53 | "IPFS", 54 | "ARWEAVE", 55 | "ONCHAIN", 56 | "ART_BLOCKS_DEPENDENCY_REGISTRY" 57 | ]; 58 | 59 | // This is directly tied to the MinterFilterType enum defined in the subgraph schema 60 | // @dev does not include the "UNKNOWN" type, because this is a list of only known types 61 | export const KNOWN_MINTER_FILTER_TYPES = [ 62 | "MinterFilterV0", 63 | "MinterFilterV1", 64 | "MinterFilterV2" 65 | ]; 66 | 67 | export const JS_MAX_SAFE_INTEGER = BigInt.fromString("9007199254740991"); 68 | 69 | // @dev these are used to stop indexing of new engine contracts registered 70 | // on a compromised core registry contract 71 | export const COMPROMISED_ENGINE_REGISTRY_ADDRESS_GOERLI = 72 | "0xea698596b6009a622c3ed00dd5a8b5d1cae4fc36"; 73 | export const COMPROMISED_ENGINE_REGISTRY_CUTOFF_BLOCK_GOERLI = BigInt.fromI32( 74 | 10050500 75 | ); 76 | export const COMPROMISED_ENGINE_REGISTRY_ADDRESS_MAINNET = 77 | "0x652490c8bb6e7ec3fd798537d2f348d7904bbbc2"; 78 | export const COMPROMISED_ENGINE_REGISTRY_CUTOFF_BLOCK_MAINNET = BigInt.fromI32( 79 | 18580700 80 | ); 81 | export const COMPROMISED_ENGINE_REGISTRY_ADDRESS_ARBITRUM = 82 | "0xdae755c2944ec125a0d8d5cb082c22837593441a"; 83 | export const COMPROMISED_ENGINE_REGISTRY_CUTOFF_BLOCK_ARBITRUM = BigInt.fromI32( 84 | 150114684 85 | ); 86 | 87 | // PMP enum values for auth options and param types 88 | export const PMP_AUTH_OPTIONS: string[] = [ 89 | "Artist", 90 | "TokenOwner", 91 | "Address", 92 | "ArtistAndTokenOwner", 93 | "ArtistAndAddress", 94 | "TokenOwnerAndAddress", 95 | "ArtistAndTokenOwnerAndAddress" 96 | ]; 97 | 98 | export const PMP_PARAM_TYPES: string[] = [ 99 | "Unconfigured", 100 | "Select", 101 | "Bool", 102 | "Uint256Range", 103 | "Int256Range", 104 | "DecimalRange", 105 | "HexColor", 106 | "Timestamp", 107 | "String" 108 | ]; 109 | -------------------------------------------------------------------------------- /tests/e2e/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | hardhat: 4 | build: hardhat 5 | volumes: 6 | - shared-data:/usr/hardhat/shared 7 | healthcheck: 8 | test: ["CMD-SHELL", "curl --fail http://localhost:8545 || exit 1"] 9 | interval: 10s 10 | timeout: 5s 11 | retries: 5 12 | seed: 13 | build: seed 14 | volumes: 15 | - shared-data:/usr/seed/shared 16 | depends_on: 17 | hardhat: 18 | condition: service_healthy 19 | graph-node: 20 | build: 21 | context: . 22 | dockerfile_inline: | 23 | FROM ${GRAPH_NODE_BASE_IMAGE} 24 | RUN ["apt", "update"] 25 | RUN ["apt-get", "install", "-y", "curl"] 26 | depends_on: 27 | ipfs: 28 | condition: service_healthy 29 | postgres: 30 | condition: service_healthy 31 | extra_hosts: 32 | - host.docker.internal:host-gateway 33 | environment: 34 | postgres_host: postgres 35 | postgres_user: graph-node 36 | postgres_pass: let-me-in 37 | postgres_db: graph-node 38 | ipfs: 'ipfs:5001' 39 | ethereum: 'mainnet:http://hardhat:8545' 40 | fork_base: 'https://api.thegraph.com/subgraphs/id/' 41 | GRAPH_LOG: info 42 | healthcheck: 43 | test: ["CMD-SHELL", "curl -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' http://localhost:8020 || exit 1"] 44 | interval: 10s 45 | timeout: 5s 46 | retries: 5 47 | start_period: 60s 48 | ipfs: 49 | image: ipfs/go-ipfs:v0.10.0 50 | healthcheck: 51 | test: ["CMD-SHELL", "wget --post-data='' http://127.0.0.1:5001/api/v0/version -O - || exit 1"] 52 | interval: 30s 53 | timeout: 10s 54 | retries: 5 55 | volumes: 56 | - ipfs:/data/ipfs 57 | postgres: 58 | image: postgres 59 | command: 60 | [ 61 | "postgres", 62 | "-cshared_preload_libraries=pg_stat_statements" 63 | ] 64 | environment: 65 | POSTGRES_USER: graph-node 66 | POSTGRES_PASSWORD: let-me-in 67 | POSTGRES_DB: graph-node 68 | # FIXME: remove this env. var. which we shouldn't need. Introduced by 69 | # , maybe as a 70 | # workaround for https://github.com/docker/for-mac/issues/6270? 71 | PGDATA: "/var/lib/postgresql/data" 72 | POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C" 73 | healthcheck: 74 | test: ["CMD-SHELL", "pg_isready -U graph-node"] 75 | interval: 10s 76 | timeout: 5s 77 | retries: 5 78 | volumes: 79 | - postgres:/var/lib/postgresql/data 80 | subgraph: 81 | build: 82 | context: ../../ 83 | dockerfile: ./docker/Dockerfile 84 | depends_on: 85 | graph-node: 86 | condition: service_healthy 87 | environment: 88 | GRAPH_NODE: 'http://graph-node:8020' 89 | IPFS: 'http://ipfs:5001' 90 | volumes: 91 | - shared-data:/usr/subgraph/shared 92 | command: ./setup-scripts/subgraph.sh 93 | # We create an empty file called `subgraph-complete` in the shared 94 | # volume when the subgraph is deployed. We use this file as a 95 | # healthcheck to ensure that the subgraph is deployed before 96 | # running the e2e tests. 97 | healthcheck: 98 | test: ["CMD-SHELL", "cat ./shared/subgraph-complete"] 99 | interval: 10s 100 | timeout: 5s 101 | retries: 5 102 | start_period: 120s 103 | volumes: 104 | # By setting all of the volumes to be tmpfs, we can avoid the need to 105 | # clean up the volumes after each run. In this way our e2e test suite 106 | # can be run in a CI environment without needing to worry about the 107 | # state of the volumes between runs. 108 | shared-data: 109 | driver_opts: 110 | type: tmpfs 111 | device: tmpfs 112 | postgres: 113 | driver_opts: 114 | type: tmpfs 115 | device: tmpfs 116 | ipfs: 117 | driver_opts: 118 | type: tmpfs 119 | device: tmpfs -------------------------------------------------------------------------------- /src/set-price-lib-mapping.ts: -------------------------------------------------------------------------------- 1 | import { BigInt, log, Address } from "@graphprotocol/graph-ts"; 2 | import { Project } from "../generated/schema"; 3 | 4 | import { PricePerTokenUpdated } from "../generated/SetPriceLib/SetPriceLib"; 5 | 6 | import { 7 | loadOrCreateMinter, 8 | loadOrCreateProjectMinterConfiguration, 9 | updateProjectIfMinterConfigIsActive 10 | } from "./helpers"; 11 | 12 | import { loadProjectByCoreAddressAndProjectNumber } from "./generic-minter-events-lib-mapping"; 13 | 14 | /////////////////////////////////////////////////////////////////////////////// 15 | // EVENT HANDLERS start here 16 | /////////////////////////////////////////////////////////////////////////////// 17 | 18 | /** 19 | * Handles the update of price per token. Attempts to load associated project and 20 | * its minter configuration, then updates base price in the configuration. 21 | * @param event The event carrying new price per token (no decimals) 22 | */ 23 | export function handlePricePerTokenUpdated(event: PricePerTokenUpdated): void { 24 | // attempt to load project, if it doesn't exist, log a warning and return 25 | // @dev we don't support or allow minters to pre-configure projects that do 26 | // not yet exist 27 | const project = loadProjectByCoreAddressAndProjectNumber( 28 | event.params.coreContract, 29 | event.params.projectId 30 | ); 31 | if (!project) { 32 | log.warning("Project {} not found for core contract {}", [ 33 | event.params.projectId.toString(), 34 | event.params.coreContract.toHexString() 35 | ]); 36 | return; 37 | } 38 | 39 | // load minter 40 | const minter = loadOrCreateMinter(event.address, event.block.timestamp); 41 | 42 | // load or create project minter configuration 43 | const projectMinterConfig = loadOrCreateProjectMinterConfiguration( 44 | project, 45 | minter 46 | ); 47 | 48 | projectMinterConfig.basePrice = event.params.pricePerToken; 49 | projectMinterConfig.priceIsConfigured = true; 50 | projectMinterConfig.save(); 51 | 52 | // induce sync if the project minter configuration is the active one 53 | updateProjectIfMinterConfigIsActive( 54 | project, 55 | projectMinterConfig, 56 | event.block.timestamp 57 | ); 58 | } 59 | 60 | /** 61 | * Handles the reset of price per token. Attempts to load associated project and 62 | * its minter configuration, then resets base price in the configuration, 63 | * marking its price as not configured and setting price to 0. 64 | * @param event The event carrying which project was reset 65 | */ 66 | export function handlePricePerTokenReset(event: PricePerTokenUpdated): void { 67 | // attempt to load project, if it doesn't exist, log a warning and return 68 | // @dev we don't support or allow minters to pre-configure projects that do 69 | // not yet exist 70 | const project = loadProjectByCoreAddressAndProjectNumber( 71 | event.params.coreContract, 72 | event.params.projectId 73 | ); 74 | if (!project) { 75 | log.warning("Project {} not found for core contract {}", [ 76 | event.params.projectId.toString(), 77 | event.params.coreContract.toHexString() 78 | ]); 79 | return; 80 | } 81 | 82 | // load minter 83 | const minter = loadOrCreateMinter(event.address, event.block.timestamp); 84 | 85 | // load or create project minter configuration 86 | const projectMinterConfig = loadOrCreateProjectMinterConfiguration( 87 | project, 88 | minter 89 | ); 90 | 91 | projectMinterConfig.basePrice = BigInt.fromI32(0); 92 | projectMinterConfig.priceIsConfigured = false; 93 | projectMinterConfig.save(); 94 | 95 | // induce sync if the project minter configuration is the active one 96 | updateProjectIfMinterConfigIsActive( 97 | project, 98 | projectMinterConfig, 99 | event.block.timestamp 100 | ); 101 | } 102 | 103 | /////////////////////////////////////////////////////////////////////////////// 104 | // EVENT HANDLERS end here 105 | /////////////////////////////////////////////////////////////////////////////// 106 | -------------------------------------------------------------------------------- /src/da-exp-lib-mapping.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuctionMinHalfLifeSecondsUpdated, 3 | SetAuctionDetailsExp 4 | } from "../generated/DAExpLib/DAExpLib"; 5 | 6 | import { loadOrCreateMinterProjectAndConfigIfProject } from "./generic-minter-events-lib-mapping"; 7 | 8 | import { 9 | loadOrCreateMinter, 10 | updateProjectIfMinterConfigIsActive, 11 | getTotalDAExpAuctionTime 12 | } from "./helpers"; 13 | 14 | import { 15 | setMinterExtraMinterDetailsValue, 16 | setProjectMinterConfigExtraMinterDetailsValue 17 | } from "./extra-minter-details-helpers"; 18 | 19 | /////////////////////////////////////////////////////////////////////////////// 20 | // EVENT HANDLERS start here 21 | /////////////////////////////////////////////////////////////////////////////// 22 | 23 | // minter-level configuration events 24 | 25 | /** 26 | * handles the update of Exponential Dutch Auction's minimum price decay half life in seconds. 27 | * @param event The event containing the updated minimum price decay half life in seconds. 28 | */ 29 | export function handleAuctionMinHalfLifeSecondsUpdated( 30 | event: AuctionMinHalfLifeSecondsUpdated 31 | ): void { 32 | // load minter 33 | const minter = loadOrCreateMinter(event.address, event.block.timestamp); 34 | 35 | // update minter extra minter details value 36 | setMinterExtraMinterDetailsValue( 37 | "minimumHalfLifeInSeconds", 38 | event.params.minimumPriceDecayHalfLifeSeconds, 39 | minter 40 | ); 41 | 42 | // update minter's updatedAt timestamp to induce a sync, and save 43 | minter.updatedAt = event.block.timestamp; 44 | minter.save(); 45 | } 46 | 47 | // project-level configuration events 48 | 49 | /** 50 | * Handles the event that updates the auction details for a project using an 51 | * exponential Dutch auction minter. 52 | * @param event The event carrying the updated exp auction data. 53 | */ 54 | export function handleSetAuctionDetailsExp(event: SetAuctionDetailsExp): void { 55 | const minterProjectAndConfig = loadOrCreateMinterProjectAndConfigIfProject( 56 | event.address, // minter 57 | event.params.coreContract, 58 | event.params.projectId, 59 | event.block.timestamp 60 | ); 61 | if (!minterProjectAndConfig) { 62 | // project wasn't found, warning already logged in helper function 63 | return; 64 | } 65 | 66 | const projectMinterConfig = minterProjectAndConfig.projectMinterConfiguration; 67 | // remove the auction details from the project minter config 68 | 69 | projectMinterConfig.basePrice = event.params.basePrice; 70 | projectMinterConfig.priceIsConfigured = true; 71 | setProjectMinterConfigExtraMinterDetailsValue( 72 | "startPrice", 73 | event.params.startPrice.toString(), // Price is likely to overflow js Number.MAX_SAFE_INTEGER so store as string 74 | projectMinterConfig 75 | ); 76 | setProjectMinterConfigExtraMinterDetailsValue( 77 | "startTime", 78 | event.params.auctionTimestampStart, 79 | projectMinterConfig 80 | ); 81 | setProjectMinterConfigExtraMinterDetailsValue( 82 | "halfLifeSeconds", 83 | event.params.priceDecayHalfLifeSeconds, 84 | projectMinterConfig 85 | ); 86 | 87 | // pre-calculate the approximate DA end time 88 | const totalAuctionTime = getTotalDAExpAuctionTime( 89 | event.params.startPrice, 90 | event.params.basePrice, 91 | event.params.priceDecayHalfLifeSeconds 92 | ); 93 | setProjectMinterConfigExtraMinterDetailsValue( 94 | "approximateDAExpEndTime", 95 | event.params.auctionTimestampStart.plus(totalAuctionTime), 96 | projectMinterConfig 97 | ); 98 | 99 | projectMinterConfig.save(); 100 | 101 | // induce sync if the project minter configuration is the active one 102 | updateProjectIfMinterConfigIsActive( 103 | minterProjectAndConfig.project, 104 | projectMinterConfig, 105 | event.block.timestamp 106 | ); 107 | } 108 | 109 | /////////////////////////////////////////////////////////////////////////////// 110 | // EVENT HANDLERS end here 111 | /////////////////////////////////////////////////////////////////////////////// 112 | -------------------------------------------------------------------------------- /src/token-holder-lib-mapping.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from "@graphprotocol/graph-ts"; 2 | 3 | import { 4 | DelegationRegistryUpdated, 5 | AllowedHoldersOfProjects, 6 | RemovedHoldersOfProjects 7 | } from "../generated/TokenHolderLib/TokenHolderLib"; 8 | 9 | import { loadOrCreateMinterProjectAndConfigIfProject } from "./generic-minter-events-lib-mapping"; 10 | 11 | import { 12 | loadOrCreateMinter, 13 | updateProjectIfMinterConfigIsActive 14 | } from "./helpers"; 15 | 16 | import { 17 | setMinterExtraMinterDetailsValue, 18 | addProjectMinterConfigExtraMinterDetailsManyValue, 19 | removeProjectMinterConfigExtraMinterDetailsManyValue 20 | } from "./extra-minter-details-helpers"; 21 | 22 | /////////////////////////////////////////////////////////////////////////////// 23 | // EVENT HANDLERS start here 24 | /////////////////////////////////////////////////////////////////////////////// 25 | 26 | export function handleDelegationRegistryUpdated( 27 | event: DelegationRegistryUpdated 28 | ): void { 29 | // load minter 30 | const minter = loadOrCreateMinter(event.address, event.block.timestamp); 31 | 32 | // update minter extra minter details value 33 | setMinterExtraMinterDetailsValue( 34 | "delegationRegistryAddress", 35 | event.params.delegationRegistry, 36 | minter 37 | ); 38 | 39 | // update minter's updatedAt timestamp to induce a sync, and save 40 | minter.updatedAt = event.block.timestamp; 41 | minter.save(); 42 | } 43 | 44 | export function handleAllowedHoldersOfProjects( 45 | event: AllowedHoldersOfProjects 46 | ): void { 47 | handleHoldersOfProjectsGeneric(event); 48 | } 49 | 50 | export function handleRemovedHoldersOfProjects( 51 | event: RemovedHoldersOfProjects 52 | ): void { 53 | handleHoldersOfProjectsGeneric(event); 54 | } 55 | 56 | /** 57 | * Helper function for both AllowedHoldersOfProjects and RemovedHoldersOfProjects. 58 | * @dev This function is used because the two events induce nearly the same 59 | * behavior, except one adds to the allowlist and the other removes from it, 60 | * therefore much of the event handling logic can be shared. 61 | */ 62 | function handleHoldersOfProjectsGeneric(event: EventType): void { 63 | if ( 64 | !( 65 | event instanceof AllowedHoldersOfProjects || 66 | event instanceof RemovedHoldersOfProjects 67 | ) 68 | ) { 69 | return; 70 | } 71 | // load projectMinterConfiguration 72 | let minterProjectAndConfig = loadOrCreateMinterProjectAndConfigIfProject( 73 | event.address, // minter 74 | event.params.coreContract, 75 | event.params.projectId, 76 | event.block.timestamp 77 | ); 78 | if (!minterProjectAndConfig) { 79 | // project wasn't found, warning already logged in helper function 80 | return; 81 | } 82 | 83 | const projectMinterConfig = minterProjectAndConfig.projectMinterConfiguration; 84 | for (let i = 0; i < event.params.ownedNFTAddresses.length; i++) { 85 | let address = event.params.ownedNFTAddresses[i].toHexString(); 86 | let holderProjectId = event.params.ownedNFTProjectIds[i].toString(); 87 | let bytesValueCombined = Bytes.fromUTF8(address + "-" + holderProjectId); 88 | 89 | if (event instanceof AllowedHoldersOfProjects) { 90 | addProjectMinterConfigExtraMinterDetailsManyValue( 91 | projectMinterConfig, 92 | "allowlistedAddressAndProjectId", 93 | bytesValueCombined 94 | ); 95 | } else { 96 | // event instanceof RemovedHoldersOfProjects 97 | removeProjectMinterConfigExtraMinterDetailsManyValue( 98 | projectMinterConfig, 99 | "allowlistedAddressAndProjectId", 100 | bytesValueCombined 101 | ); 102 | } 103 | } 104 | 105 | // induce sync if the project minter configuration is the active one 106 | updateProjectIfMinterConfigIsActive( 107 | minterProjectAndConfig.project, 108 | projectMinterConfig, 109 | event.block.timestamp 110 | ); 111 | } 112 | 113 | /////////////////////////////////////////////////////////////////////////////// 114 | // EVENT HANDLERS end here 115 | /////////////////////////////////////////////////////////////////////////////// 116 | -------------------------------------------------------------------------------- /tests/subgraph/legacy-minter-suite/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Address, ethereum, BigInt } from "@graphprotocol/graph-ts"; 2 | import { createMockedFunction } from "matchstick-as"; 3 | 4 | export const DAExpMintersToTest: string[] = [ 5 | "MinterDAExpV0", 6 | "MinterDAExpV1", 7 | "MinterDAExpV2", 8 | "MinterDAExpV3", 9 | "MinterDAExpV4", 10 | "MinterDAExpSettlementV0", 11 | "MinterDAExpSettlementV1" 12 | ]; 13 | export const DALinMintersToTest: string[] = [ 14 | "MinterDALinV0", 15 | "MinterDALinV1", 16 | "MinterDALinV2", 17 | "MinterDALinV3", 18 | "MinterDALinV4" 19 | ]; 20 | export const HolderMintersToTest: string[] = [ 21 | "MinterHolderV0", 22 | "MinterHolderV1", 23 | "MinterHolderV2", 24 | "MinterHolderV3", 25 | "MinterHolderV4" 26 | ]; 27 | 28 | export function mockMinterType( 29 | minterAddress: Address, 30 | minterType: string 31 | ): void { 32 | createMockedFunction( 33 | minterAddress, 34 | "minterType", 35 | "minterType():(string)" 36 | ).returns([ethereum.Value.fromString(minterType)]); 37 | } 38 | 39 | export function mockDAExpHalfLifeMinMax( 40 | minterAddress: Address, 41 | halfLifeMin: BigInt, 42 | halfLifeMax: BigInt 43 | ): void { 44 | createMockedFunction( 45 | minterAddress, 46 | "minimumPriceDecayHalfLifeSeconds", 47 | "minimumPriceDecayHalfLifeSeconds():(uint256)" 48 | ).returns([ethereum.Value.fromUnsignedBigInt(halfLifeMin)]); 49 | createMockedFunction( 50 | minterAddress, 51 | "maximumPriceDecayHalfLifeSeconds", 52 | "maximumPriceDecayHalfLifeSeconds():(uint256)" 53 | ).returns([ethereum.Value.fromUnsignedBigInt(halfLifeMax)]); 54 | } 55 | 56 | export function mockDALinMinAuctionLength( 57 | minterAddress: Address, 58 | minAuctionLength: BigInt 59 | ): void { 60 | createMockedFunction( 61 | minterAddress, 62 | "minimumAuctionLengthSeconds", 63 | "minimumAuctionLengthSeconds():(uint256)" 64 | ).returns([ethereum.Value.fromUnsignedBigInt(minAuctionLength)]); 65 | } 66 | 67 | export function mockMinterFilterAddress( 68 | minterAddress: Address, 69 | minterFilterAddress: Address 70 | ): void { 71 | createMockedFunction( 72 | minterAddress, 73 | "minterFilterAddress", 74 | "minterFilterAddress():(address)" 75 | ).returns([ethereum.Value.fromAddress(minterFilterAddress)]); 76 | } 77 | 78 | export function mockCoreContract( 79 | minterAddress: Address, 80 | coreContract: Address 81 | ): void { 82 | createMockedFunction( 83 | minterAddress, 84 | "genArt721CoreAddress", 85 | "genArt721CoreAddress():(address)" 86 | ).returns([ethereum.Value.fromAddress(coreContract)]); 87 | } 88 | 89 | export function mockGetProjectAndMinterInfoAt( 90 | minterFilterAddress: Address, 91 | index: BigInt, 92 | projectId: BigInt, 93 | minterAddress: Address, 94 | minterType: string 95 | ): void { 96 | createMockedFunction( 97 | minterFilterAddress, 98 | "getProjectAndMinterInfoAt", 99 | "getProjectAndMinterInfoAt(uint256):(uint256,address,string)" 100 | ) 101 | .withArgs([ethereum.Value.fromUnsignedBigInt(index)]) 102 | .returns([ 103 | ethereum.Value.fromUnsignedBigInt(projectId), // project id 104 | ethereum.Value.fromAddress(minterAddress), // minter address 105 | ethereum.Value.fromString(minterType) // minter type 106 | ]); 107 | } 108 | 109 | export function mockPurchaseToDisabled( 110 | minterAddress: Address, 111 | projectId: BigInt, 112 | purchaseToDisabled: boolean 113 | ): void { 114 | createMockedFunction( 115 | minterAddress, 116 | "purchaseToDisabled", 117 | "purchaseToDisabled(uint256):(bool)" 118 | ) 119 | .withArgs([ethereum.Value.fromUnsignedBigInt(projectId)]) 120 | .returns([ethereum.Value.fromBoolean(purchaseToDisabled)]); 121 | } 122 | 123 | export function mockGetPriceInfo( 124 | minterAddress: Address, 125 | projectId: BigInt, 126 | priceIsConfigured: boolean, 127 | basePrice: BigInt, 128 | currencySymbol: string, 129 | currencyAddress: Address 130 | ): void { 131 | createMockedFunction( 132 | minterAddress, 133 | "getPriceInfo", 134 | "getPriceInfo(uint256):(bool,uint256,string,address)" 135 | ) 136 | .withArgs([ethereum.Value.fromUnsignedBigInt(projectId)]) 137 | .returns([ 138 | ethereum.Value.fromBoolean(priceIsConfigured), 139 | ethereum.Value.fromUnsignedBigInt(basePrice), 140 | ethereum.Value.fromString(currencySymbol), 141 | ethereum.Value.fromAddress(currencyAddress) 142 | ]); 143 | } 144 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "artblocks-subgraph", 3 | "license": "LGPL-3.0-only", 4 | "scripts": { 5 | "build": "yarn codegen:generic && graph build", 6 | "test": "yarn codegen:generic && graph test -r", 7 | "test:e2e": "cd tests/e2e && GRAPH_NODE_BASE_IMAGE=graphprotocol/graph-node:latest docker compose -f docker-compose.yml -f docker-compose.test.yml up --exit-code-from runner", 8 | "test:e2e:m1": "cd tests/e2e && GRAPH_NODE_BASE_IMAGE=graph-node:latest docker compose -f docker-compose.yml -f docker-compose.test.yml up --exit-code-from runner", 9 | "test:e2e:ci": "cd tests/e2e && GRAPH_NODE_BASE_IMAGE=graphprotocol/graph-node:latest docker compose -f docker-compose.yml -f docker-compose.ci.yml up --exit-code-from runner", 10 | "run:local": "cd tests/e2e && GRAPH_NODE_BASE_IMAGE=graph-node:latest docker compose -f docker-compose.yml -f docker-compose.local.yml up", 11 | "coverage": "graph test -- -c", 12 | "deploy:studio": "yarn prepare:mainnet && graph deploy --studio art-blocks", 13 | "deploy:studio-base": "yarn prepare:base && graph deploy --studio art-blocks-base", 14 | "deploy:studio-sepolia-dev": "yarn prepare:sepolia-dev && graph deploy --studio art-blocks-dev-sepolia", 15 | "deploy:studio-sepolia-staging": "yarn prepare:sepolia-staging && graph deploy --studio art-blocks-staging-sepolia", 16 | "deploy:studio-arbitrum-one": "yarn prepare:arbitrum-one && graph deploy --studio art-blocks-arbitrum", 17 | "deploy:goldsky-sepolia-dev": "sh -c 'read -p \"Enter subgraph version: \" version; if [ -z \"$version\" ]; then echo \"Version is required\" >&2; exit 1; fi; yarn prepare:sepolia-dev && graph build && set -a && source .env && set +a && goldsky login --token \"$GOLDSKY_API_KEY\" && goldsky subgraph deploy artblocks-sepolia-dev/\"$version\"'", 18 | "deploy:goldsky-sepolia-staging": "sh -c 'read -p \"Enter subgraph version: \" version; if [ -z \"$version\" ]; then echo \"Version is required\" >&2; exit 1; fi; yarn prepare:sepolia-staging && graph build && set -a && source .env && set +a && goldsky login --token \"$GOLDSKY_API_KEY\" && goldsky subgraph deploy artblocks-sepolia-staging/\"$version\"'", 19 | "tag-latest:goldsky-sepolia-dev": "sh -c 'read -p \"Enter dev subgraph version you want to tag as latest: \" version; if [ -z \"$version\" ]; then echo \"Version is required\" >&2; exit 1; fi; source .env && set +a && goldsky login --token \"$GOLDSKY_API_KEY\" && goldsky subgraph tag create artblocks-sepolia-dev/\"$version\" --tag latest'", 20 | "tag-latest:goldsky-sepolia-staging": "sh -c 'read -p \"Enter staging subgraph version you want to tag as latest: \" version; if [ -z \"$version\" ]; then echo \"Version is required\" >&2; exit 1; fi; source .env && set +a && goldsky login --token \"$GOLDSKY_API_KEY\" && goldsky subgraph tag create artblocks-sepolia-staging/\"$version\" --tag latest'", 21 | "create:local": "graph create --node http://localhost:8020/ artblocks/art-blocks", 22 | "remove:local": "graph remove --node http://localhost:8020/ artblocks/art-blocks", 23 | "deploy:local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 artblocks/art-blocks", 24 | "prepare:mainnet": "yarn codegen:generic && mustache config/mainnet.json subgraph.template.yaml > subgraph.yaml", 25 | "prepare:sepolia-staging": "yarn codegen:generic && mustache config/sepolia-staging.json subgraph.template.yaml > subgraph.yaml", 26 | "prepare:sepolia-dev": "yarn codegen:generic && mustache config/sepolia-dev.json subgraph.template.yaml > subgraph.yaml", 27 | "prepare:arbitrum-sepolia-staging": "yarn codegen:generic && mustache config/arbitrum-sepolia-staging.json subgraph.template.yaml > subgraph.yaml", 28 | "prepare:arbitrum-one": "yarn codegen:generic && mustache config/arbitrum-one.json subgraph.template.yaml > subgraph.yaml", 29 | "prepare:base": "yarn codegen:generic && mustache config/base.json subgraph.template.yaml > subgraph.yaml", 30 | "prepare:local": "yarn codegen:generic && mustache config/local.json subgraph.template.yaml > subgraph.yaml", 31 | "prepare:generic": "yarn codegen:generic", 32 | "codegen:generic": "yarn generate:abis && mkdir -p ./generated && rm -r ./generated && mustache config/generic.json subgraph.template.yaml > subgraph.yaml && graph codegen", 33 | "generate:abis": "cd ./abis && ./_generate-abis.sh && cd ../" 34 | }, 35 | "dependencies": { 36 | "@artblocks/contracts": "1.3.2", 37 | "@assemblyscript/loader": "^0.19.22", 38 | "@graphprotocol/graph-cli": "^0.56.0", 39 | "@graphprotocol/graph-ts": "^0.27.0", 40 | "matchstick-as": "^0.6.0" 41 | }, 42 | "devDependencies": { 43 | "assemblyscript": "^0.19.22", 44 | "mustache": "^4.1.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/split-atomic-mapping.ts: -------------------------------------------------------------------------------- 1 | import { log, BigInt } from "@graphprotocol/graph-ts"; 2 | 3 | import { 4 | SplitAtomicFactory, 5 | SplitAtomicContract, 6 | SplitAtomicSplit 7 | } from "../generated/schema"; 8 | 9 | import { 10 | Deployed, 11 | SplitAtomicCreated, 12 | Abandoned 13 | } from "../generated/ISplitAtomicFactory/ISplitAtomicFactoryV0"; 14 | 15 | import { ISplitAtomicV0 } from "../generated/ISplitAtomicFactory/ISplitAtomicV0"; 16 | 17 | /*** EVENT HANDLERS ***/ 18 | export function handleDeployed(event: Deployed): void { 19 | // create the split atomic factory entity 20 | const splitAtomicFactory = new SplitAtomicFactory( 21 | event.address.toHexString() 22 | ); 23 | // update the implementation and type 24 | splitAtomicFactory.implementation = event.params.implementation; 25 | splitAtomicFactory.type = event.params.type_.toString(); 26 | // update the required splits 27 | splitAtomicFactory.requiredSplitAddress = event.params.requiredSplitAddress; 28 | splitAtomicFactory.requiredSplitBasisPoints = BigInt.fromI32( 29 | event.params.requiredSplitBasisPoints 30 | ); 31 | // set as not abandoned 32 | splitAtomicFactory.abandoned = false; 33 | // update the updated at timestamp 34 | splitAtomicFactory.updatedAt = event.block.timestamp; 35 | // save the split atomic factory 36 | splitAtomicFactory.save(); 37 | } 38 | 39 | export function handleAbandoned(event: Abandoned): void { 40 | // load the split atomic factory 41 | const splitAtomicFactory = SplitAtomicFactory.load( 42 | event.address.toHexString() 43 | ); 44 | if (!splitAtomicFactory) { 45 | // This should never happen 46 | log.warning("SplitAtomicCreated: split atomic factory {} not found", [ 47 | event.address.toHexString() 48 | ]); 49 | return; 50 | } 51 | // set as abandoned 52 | splitAtomicFactory.abandoned = true; 53 | // update the updated at timestamp 54 | splitAtomicFactory.updatedAt = event.block.timestamp; 55 | // save the split atomic factory 56 | splitAtomicFactory.save(); 57 | } 58 | 59 | export function handleSplitAtomicCreated(event: SplitAtomicCreated): void { 60 | // load the split atomic factory 61 | const splitAtomicFactory = SplitAtomicFactory.load( 62 | event.address.toHexString() 63 | ); 64 | if (!splitAtomicFactory) { 65 | // This should never happen 66 | log.warning("SplitAtomicCreated: split atomic factory {} not found", [ 67 | event.address.toHexString() 68 | ]); 69 | return; 70 | } 71 | // update the updated at timestamp 72 | splitAtomicFactory.updatedAt = event.block.timestamp; 73 | // save the split atomic factory 74 | splitAtomicFactory.save(); 75 | 76 | // create the split atomic contract 77 | const splitAtomicContract = new SplitAtomicContract( 78 | event.params.splitAtomic.toHexString() 79 | ); 80 | // set the split atomic factory 81 | splitAtomicContract.splitAtomicFactory = splitAtomicFactory.id; 82 | // set the implementation and type 83 | splitAtomicContract.implementation = splitAtomicFactory.implementation; 84 | // @dev web3 call to get the type 85 | const splitAtomicContractInstance = ISplitAtomicV0.bind( 86 | event.params.splitAtomic 87 | ); 88 | // use a try catch block to catch a possible revert, even though we never expect this to happen 89 | const splitAtomicFactoryTypeResult = splitAtomicContractInstance.try_type_(); 90 | if (splitAtomicFactoryTypeResult.reverted) { 91 | // This should never happen 92 | log.warning( 93 | "SplitAtomicCreated: type_ call reverted for split atomic contract {}", 94 | [event.params.splitAtomic.toHexString()] 95 | ); 96 | return; 97 | } 98 | splitAtomicContract.type = splitAtomicFactoryTypeResult.value.toString(); 99 | splitAtomicContract.updatedAt = event.block.timestamp; 100 | // safe the split atomic contract 101 | splitAtomicContract.save(); 102 | 103 | // create the splits 104 | // @dev web3 call to get the splits 105 | // use a try catch block to catch a possible revert, even though we never expect this to happen 106 | const splitsResult = splitAtomicContractInstance.try_getSplits(); 107 | if (splitsResult.reverted) { 108 | // This should never happen 109 | log.warning( 110 | "SplitAtomicCreated: getSplits call reverted for split atomic contract {}", 111 | [event.params.splitAtomic.toHexString()] 112 | ); 113 | return; 114 | } 115 | const splits = splitsResult.value; 116 | // loop over the splits array and add each split to the store 117 | for (let i = 0; i < splits.length; i++) { 118 | const split = new SplitAtomicSplit( 119 | event.params.splitAtomic.toHexString() + "-" + i.toString() 120 | ); 121 | split.splitAtomicContract = splitAtomicContract.id; 122 | split.index = BigInt.fromI32(i); 123 | split.recipient = splits[i].recipient; 124 | split.basisPoints = BigInt.fromI32(splits[i].basisPoints); 125 | split.save(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /posterity/src/secondary/opensea/os-seaport-mapping.ts: -------------------------------------------------------------------------------- 1 | import { log } from "matchstick-as"; 2 | 3 | import { 4 | Token, 5 | Sale, 6 | SaleLookupTable, 7 | Contract, 8 | Payment 9 | } from "../../../generated/schema"; 10 | 11 | import { 12 | OrderFulfilled, 13 | OrderFulfilledOfferStruct, 14 | OrderFulfilledConsiderationStruct 15 | } from "../../../generated/SeaportExchange/SeaportExchange"; 16 | import { OS_SP, NATIVE, ERC20, ERC721, ERC1155 } from "../../constants"; 17 | 18 | import { generateContractSpecificId } from "../../helpers"; 19 | import { buildTokenSaleLookupTableId } from "../secondary-helpers"; 20 | 21 | // Enum for Seaport's ItemType 22 | export enum ItemType { 23 | NATIVE, 24 | ERC20, 25 | ERC721, 26 | ERC1155, 27 | ERC721_WITH_CRITERIA, 28 | ERC1155_WITH_CRITERIA 29 | } 30 | 31 | // Array to map the above ItemType enum to their string counterparts 32 | // (With_Criteria entries map to their regular counterpart) 33 | const itemTypeMapping = [NATIVE, ERC20, ERC721, ERC1155, ERC721, ERC1155]; 34 | 35 | // Checks if Order is a private sale - returns true if so 36 | // Seaport private sales have the offer token in the consideration struct as well 37 | function isPrivateSale( 38 | offer: OrderFulfilledOfferStruct[], 39 | consideration: OrderFulfilledConsiderationStruct[] 40 | ): boolean { 41 | let priv = false; 42 | for (let i = 0; i < offer.length; i++) { 43 | let o = offer[i]; 44 | for (let j = 0; j < consideration.length; j++) { 45 | let c = consideration[j]; 46 | // Private sale if offer item also in consideration array 47 | if ( 48 | o.itemType == c.itemType && 49 | o.token == c.token && 50 | o.identifier == c.identifier 51 | ) { 52 | priv = true; 53 | } 54 | } 55 | } 56 | return priv; 57 | } 58 | 59 | /** 60 | * @param event OrderFulfilled 61 | * @description Event handler for the Seaport OrderFulfilled event (which happens on sale) 62 | */ 63 | export function handleOrderFulfilled(event: OrderFulfilled): void { 64 | let bundleIncludesArtBlocks = false; 65 | 66 | for (let i = 0; i < event.params.offer.length; i++) { 67 | let contract = Contract.load(event.params.offer[i].token.toHexString()); 68 | if (contract) { 69 | bundleIncludesArtBlocks = true; 70 | break; 71 | } 72 | } 73 | 74 | // Only interested in Art Blocks sells 75 | if (!bundleIncludesArtBlocks) { 76 | return; 77 | } 78 | 79 | // Loop over every item sold in the trade 80 | let summaryTokensSold = ""; 81 | let numValidTokens = 0; 82 | let saleId = event.params.orderHash.toHexString(); 83 | let saleLookupTables: SaleLookupTable[] = []; 84 | for (let i = 0; i < event.params.offer.length; i++) { 85 | let offerItem = event.params.offer[i]; 86 | 87 | let fullTokenId = generateContractSpecificId( 88 | offerItem.token, 89 | offerItem.identifier 90 | ); 91 | 92 | if (summaryTokensSold.length == 0) { 93 | summaryTokensSold += fullTokenId; 94 | } else { 95 | summaryTokensSold += "::" + fullTokenId; 96 | } 97 | 98 | // Get the asosciated Art Blocks token if any (might not be an AB token) 99 | let token = Token.load(fullTokenId); 100 | 101 | // Skip if this is not a token associated with Art Blocks 102 | if (!token) { 103 | continue; 104 | } 105 | numValidTokens++; 106 | 107 | // Link both of them (NFT with sale) 108 | let tableEntryId = buildTokenSaleLookupTableId( 109 | token.project, 110 | token.id, 111 | saleId 112 | ); 113 | 114 | let saleLookupTable = new SaleLookupTable(tableEntryId); 115 | 116 | saleLookupTable.token = token.id; 117 | saleLookupTable.project = token.project; 118 | saleLookupTable.sale = saleId; 119 | saleLookupTable.timestamp = event.block.timestamp; 120 | saleLookupTable.blockNumber = event.block.number; 121 | saleLookupTables.push(saleLookupTable); 122 | } 123 | 124 | if (numValidTokens == 0) { 125 | return; 126 | } 127 | 128 | let payments = new Array(); 129 | for (let i = 0; i < event.params.consideration.length; i++) { 130 | const considerationItem = event.params.consideration[i]; 131 | 132 | let p = new Payment(saleId + "-" + i.toString()); 133 | p.sale = saleId; 134 | p.paymentType = itemTypeMapping[considerationItem.itemType]; 135 | p.paymentToken = considerationItem.token; 136 | p.price = considerationItem.amount; 137 | p.recipient = considerationItem.recipient; 138 | payments.push(p); 139 | } 140 | 141 | // Create sale 142 | let sale = new Sale(saleId); 143 | sale.txHash = event.transaction.hash; 144 | sale.exchange = OS_SP; 145 | sale.saleType = event.params.offer.length > 1 ? "Bundle" : "Single"; 146 | sale.blockNumber = event.block.number; 147 | sale.blockTimestamp = event.block.timestamp; 148 | sale.buyer = event.params.recipient; 149 | sale.seller = event.params.offerer; 150 | sale.isPrivate = isPrivateSale( 151 | event.params.offer, 152 | event.params.consideration 153 | ); 154 | sale.summaryTokensSold = summaryTokensSold; 155 | sale.save(); 156 | 157 | for (let i = 0; i < payments.length; i++) { 158 | payments[i].save(); 159 | } 160 | // Lastly, save the lookup tables (must be saved AFTER sale gets saved) 161 | for (let i = 0; i < saleLookupTables.length; i++) { 162 | saleLookupTables[i].save(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/extra-minter-details-helpers.ts: -------------------------------------------------------------------------------- 1 | import { json, JSONValue, TypedMap, log } from "@graphprotocol/graph-ts"; 2 | 3 | import { Minter, ProjectMinterConfiguration } from "../generated/schema"; 4 | 5 | import { 6 | typedMapToJSONString, 7 | createMergedTypedMap, 8 | createUpdatedTypedMapWithEntryAdded, 9 | createUpdatedTypedMapWithEntryRemoved, 10 | createUpdatedTypedMapWithArrayValueRemoved, 11 | createUpdatedTypedMapWithArrayValueAdded 12 | } from "./json"; 13 | 14 | import { 15 | getProjectMinterConfigExtraMinterDetailsTypedMap, 16 | getMinterExtraMinterDetailsTypedMap 17 | } from "./helpers"; 18 | 19 | // Generic Handlers 20 | // Below is all logic pertaining to generic handlers used for maintaining JSON config stores on both the ProjectMinterConfiguration and Minter entities. 21 | // Most logic is shared and bubbled up each respective handler for each action. We utilize ducktype to allow these to work on either a Minter or ProjectMinterConfiguration 22 | // Because AssemblyScript does not support union types, we need to manually type check inside each method, to ensure correct usage. 23 | // Currently supported key-value types (value: T) include boolean, BigInt, ETH address, and bytes values. 24 | // For any questions reach out to @jon or @ryley-o.eth. or see the following document https://docs.google.com/document/d/1XSxl04eJyTxc_rbj6cmq-j00zaYDzApBBLT67JXtaOw/edit?disco=AAAAZa8xp-Q 25 | 26 | export function setProjectMinterConfigExtraMinterDetailsValue( 27 | key: string, 28 | value: ValueType, 29 | config: ProjectMinterConfiguration 30 | ): void { 31 | let minterDetails = getProjectMinterConfigExtraMinterDetailsTypedMap(config); 32 | 33 | minterDetails = createUpdatedTypedMapWithEntryAdded( 34 | minterDetails, 35 | key, 36 | value 37 | ); 38 | 39 | config.extraMinterDetails = typedMapToJSONString(minterDetails); 40 | config.save(); 41 | } 42 | 43 | export function setMinterExtraMinterDetailsValue( 44 | key: string, 45 | value: ValueType, 46 | minter: Minter 47 | ): void { 48 | let minterDetails = getMinterExtraMinterDetailsTypedMap(minter); 49 | 50 | minterDetails = createUpdatedTypedMapWithEntryAdded( 51 | minterDetails, 52 | key, 53 | value 54 | ); 55 | 56 | minter.extraMinterDetails = typedMapToJSONString(minterDetails); 57 | minter.save(); 58 | } 59 | 60 | export function removeProjectMinterConfigExtraMinterDetailsEntry( 61 | key: string, 62 | config: ProjectMinterConfiguration 63 | ): void { 64 | let minterDetails = getProjectMinterConfigExtraMinterDetailsTypedMap(config); 65 | minterDetails = createUpdatedTypedMapWithEntryRemoved(minterDetails, key); 66 | 67 | config.extraMinterDetails = typedMapToJSONString(minterDetails); 68 | config.save(); 69 | } 70 | 71 | export function removeMinterExtraMinterDetailsEntry( 72 | key: string, 73 | minter: Minter 74 | ): void { 75 | let minterDetails = getMinterExtraMinterDetailsTypedMap(minter); 76 | minterDetails = createUpdatedTypedMapWithEntryRemoved(minterDetails, key); 77 | 78 | minter.extraMinterDetails = typedMapToJSONString(minterDetails); 79 | minter.save(); 80 | } 81 | 82 | export function addProjectMinterConfigExtraMinterDetailsManyValue( 83 | config: ProjectMinterConfiguration, 84 | key: string, 85 | value: ValueType 86 | ): void { 87 | let minterDetails = getProjectMinterConfigExtraMinterDetailsTypedMap(config); 88 | minterDetails = createUpdatedTypedMapWithArrayValueAdded( 89 | minterDetails, 90 | key, 91 | value 92 | ); 93 | config.extraMinterDetails = typedMapToJSONString(minterDetails); 94 | config.save(); 95 | } 96 | 97 | export function addMinterExtraMinterDetailsManyValue( 98 | minter: Minter, 99 | key: string, 100 | value: ValueType 101 | ): void { 102 | let minterDetails = getMinterExtraMinterDetailsTypedMap(minter); 103 | minterDetails = createUpdatedTypedMapWithArrayValueAdded( 104 | minterDetails, 105 | key, 106 | value 107 | ); 108 | minter.extraMinterDetails = typedMapToJSONString(minterDetails); 109 | minter.save(); 110 | } 111 | 112 | export function removeProjectMinterConfigExtraMinterDetailsManyValue( 113 | config: ProjectMinterConfiguration, 114 | key: string, 115 | value: ValueType 116 | ): void { 117 | let minterDetails = getProjectMinterConfigExtraMinterDetailsTypedMap(config); 118 | minterDetails = createUpdatedTypedMapWithArrayValueRemoved( 119 | minterDetails, 120 | key, 121 | value 122 | ); 123 | config.extraMinterDetails = typedMapToJSONString(minterDetails); 124 | config.save(); 125 | } 126 | 127 | export function removeMinterExtraMinterDetailsManyValue( 128 | minter: Minter, 129 | key: string, 130 | value: ValueType 131 | ): void { 132 | let minterDetails = getMinterExtraMinterDetailsTypedMap(minter); 133 | minterDetails = createUpdatedTypedMapWithArrayValueRemoved( 134 | minterDetails, 135 | key, 136 | value 137 | ); 138 | minter.extraMinterDetails = typedMapToJSONString(minterDetails); 139 | minter.save(); 140 | } 141 | 142 | export function mergeProjectMinterConfigExtraMinterDetails( 143 | projectMinterConfig: ProjectMinterConfiguration, 144 | extraMinterDetails: TypedMap 145 | ): void { 146 | let currentExtraMinterDetailsResult = json.try_fromString( 147 | projectMinterConfig.extraMinterDetails 148 | ); 149 | 150 | if (currentExtraMinterDetailsResult.isOk) { 151 | const newExtraMinterDetails = createMergedTypedMap( 152 | currentExtraMinterDetailsResult.value.toObject(), 153 | extraMinterDetails 154 | ); 155 | 156 | projectMinterConfig.extraMinterDetails = typedMapToJSONString( 157 | newExtraMinterDetails 158 | ); 159 | } else { 160 | log.warning( 161 | "Failed to parse extraMinterDetails json string for project minter config {}", 162 | [projectMinterConfig.id] 163 | ); 164 | } 165 | 166 | projectMinterConfig.save(); 167 | } 168 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/minter-suite-v2/split-funds-lib.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "@jest/globals"; 2 | import { 3 | getSubgraphConfig, 4 | getAccounts, 5 | createSubgraphClient, 6 | waitUntilSubgraphIsSynced, 7 | getMinterDetails, 8 | getProjectMinterConfigurationDetails, 9 | } from "../utils/helpers"; 10 | import { MinterFilterV2__factory } from "../../contracts/factories/MinterFilterV2__factory"; 11 | import { MinterSetPriceERC20V5__factory } from "../../contracts/factories/MinterSetPriceERC20V5__factory"; 12 | import { ERC20Mock__factory } from "../../contracts/factories/ERC20Mock__factory"; 13 | import { ethers } from "ethers"; 14 | // hide nuisance logs about event overloading 15 | import { Logger } from "@ethersproject/logger"; 16 | import { ERC20MockAltDecimals__factory } from "../../contracts"; 17 | Logger.setLogLevel(Logger.levels.ERROR); 18 | 19 | // waiting for subgraph to sync can take longer than the default 5s timeout 20 | jest.setTimeout(30 * 1000); 21 | 22 | const config = getSubgraphConfig(); 23 | 24 | const client = createSubgraphClient(); 25 | const { deployer, artist } = getAccounts(); 26 | 27 | // set up contract instances and/or addresses 28 | const coreRegistryAddress = config.metadata?.coreRegistryAddress; 29 | if (!coreRegistryAddress) 30 | throw new Error("No core registry address found in config metadata"); 31 | 32 | const sharedMinterFilter = config.sharedMinterFilterContracts?.[0]; 33 | if (!sharedMinterFilter) { 34 | throw new Error("No shared minter filter found in config metadata"); 35 | } 36 | const sharedMinterFilterContract = new MinterFilterV2__factory(deployer).attach( 37 | sharedMinterFilter.address 38 | ); 39 | // get contract from the subgraph config 40 | if (!config.iGenArt721CoreContractV3_BaseContracts) { 41 | throw new Error("No iGenArt721CoreContractV3_BaseContracts in config"); 42 | } 43 | const genArt721CoreAddress = 44 | config.iGenArt721CoreContractV3_BaseContracts[0].address; 45 | 46 | const bytecodeStorageReaderAddress = 47 | config.metadata?.bytecodeStorageReaderAddress; 48 | if (!bytecodeStorageReaderAddress) 49 | throw new Error( 50 | "No bytecode storage reader address found in config metadata" 51 | ); 52 | 53 | // get MinterSetPriceV5 54 | // @dev this is minter at index 0 in the subgraph config 55 | if (!config.genericMinterEventsLibContracts) { 56 | throw new Error("No genericMinterEventsLibContracts in config"); 57 | } 58 | const minterSetPriceV5Address = 59 | config.genericMinterEventsLibContracts[0].address; 60 | 61 | // get MinterSetPriceERC20V5 62 | // @dev this is minter at index 1 in the subgraph config 63 | const minterSetPriceERC20V5Address = 64 | config.genericMinterEventsLibContracts[1].address; 65 | const minterSetPriceERC20V5Contract = new MinterSetPriceERC20V5__factory( 66 | deployer 67 | ).attach(minterSetPriceERC20V5Address); 68 | 69 | describe("SplitFundsLib event handling", () => { 70 | beforeAll(async () => { 71 | await waitUntilSubgraphIsSynced(client); 72 | }); 73 | 74 | describe("Indexed after setup", () => { 75 | test("created new Minter during deployment and allowlisting", async () => { 76 | const targetId = minterSetPriceV5Address.toLowerCase(); 77 | const minterRes = await getMinterDetails(client, targetId); 78 | expect(minterRes.id).toBe(targetId); 79 | }); 80 | }); 81 | 82 | describe("ProjectCurrencyInfoUpdated", () => { 83 | afterEach(async () => { 84 | // clear the minter for project zero 85 | // @dev call success depends on test state, so use a try/catch block 86 | try { 87 | await sharedMinterFilterContract 88 | .connect(artist) 89 | .removeMinterForProject(0, genArt721CoreAddress); 90 | } catch (error) { 91 | // try block will only fail in case of previously failed test where 92 | // project zero never had its minter assigned. 93 | // Thus, swallow error here because the test failure has already been 94 | // reported, and additional error messaging from afterEach is not 95 | // helpful. 96 | } 97 | // @dev we don't clear the currency info for project zero on the ERC20 98 | // minter, because it cannot be set to the zero address. Instead, we 99 | // deploy new ERC20 currencies for each test, and set the minter to use 100 | // that currency 101 | }); 102 | 103 | test("Currency is updated and configured", async () => { 104 | const currencySymbol = "ERC20"; 105 | // deploy new ERC20 currency, sending initial supply to artist 106 | const newCurrency = await new ERC20MockAltDecimals__factory( 107 | artist 108 | ).deploy(ethers.utils.parseEther("100")); 109 | // set minter for project zero to the fixed price ERC20 minter 110 | await sharedMinterFilterContract 111 | .connect(artist) 112 | .setMinterForProject( 113 | 0, 114 | genArt721CoreAddress, 115 | minterSetPriceERC20V5Address 116 | ); 117 | // update currency info 118 | await minterSetPriceERC20V5Contract 119 | .connect(artist) 120 | .updateProjectCurrencyInfo( 121 | 0, 122 | genArt721CoreAddress, 123 | currencySymbol, 124 | newCurrency.address 125 | ); 126 | await waitUntilSubgraphIsSynced(client); 127 | // validate currency info in subgraph 128 | const targetId = `${minterSetPriceERC20V5Address.toLowerCase()}-${genArt721CoreAddress.toLowerCase()}-0`; 129 | const minterConfigRes = await getProjectMinterConfigurationDetails( 130 | client, 131 | targetId 132 | ); 133 | 134 | const decimals = await newCurrency.decimals(); 135 | 136 | expect(minterConfigRes.currencySymbol).toBe(currencySymbol); 137 | expect(minterConfigRes.currencyAddress).toBe( 138 | newCurrency.address.toLowerCase() 139 | ); 140 | expect(minterConfigRes.currencyDecimals).toBe(decimals); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/minter-suite-v2/max-invocations-lib.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "@jest/globals"; 2 | import { 3 | getSubgraphConfig, 4 | getAccounts, 5 | createSubgraphClient, 6 | waitUntilSubgraphIsSynced, 7 | getMinterDetails, 8 | getProjectMinterConfigurationDetails, 9 | } from "../utils/helpers"; 10 | 11 | import { 12 | GenArt721CoreV3__factory, 13 | GenArt721CoreV3LibraryAddresses, 14 | } from "../../contracts/factories/GenArt721CoreV3__factory"; 15 | import { MinterFilterV2__factory } from "../../contracts/factories/MinterFilterV2__factory"; 16 | import { MinterSetPriceV5__factory } from "../../contracts/factories/MinterSetPriceV5__factory"; 17 | // hide nuisance logs about event overloading 18 | import { Logger } from "@ethersproject/logger"; 19 | Logger.setLogLevel(Logger.levels.ERROR); 20 | 21 | // waiting for subgraph to sync can take longer than the default 5s timeout 22 | jest.setTimeout(30 * 1000); 23 | 24 | const config = getSubgraphConfig(); 25 | 26 | const client = createSubgraphClient(); 27 | const { deployer, artist } = getAccounts(); 28 | 29 | // set up contract instances and/or addresses 30 | const coreRegistryAddress = config.metadata?.coreRegistryAddress; 31 | if (!coreRegistryAddress) 32 | throw new Error("No core registry address found in config metadata"); 33 | 34 | const sharedMinterFilter = config.sharedMinterFilterContracts?.[0]; 35 | if (!sharedMinterFilter) { 36 | throw new Error("No shared minter filter found in config metadata"); 37 | } 38 | const sharedMinterFilterContract = new MinterFilterV2__factory(deployer).attach( 39 | sharedMinterFilter.address 40 | ); 41 | // get contract from the subgraph config 42 | if (!config.iGenArt721CoreContractV3_BaseContracts) { 43 | throw new Error("No iGenArt721CoreContractV3_BaseContracts in config"); 44 | } 45 | const genArt721CoreAddress = 46 | config.iGenArt721CoreContractV3_BaseContracts[0].address; 47 | 48 | const bytecodeStorageReaderAddress = 49 | config.metadata?.bytecodeStorageReaderAddress; 50 | if (!bytecodeStorageReaderAddress) 51 | throw new Error( 52 | "No bytecode storage reader address found in config metadata" 53 | ); 54 | const linkLibraryAddresses: GenArt721CoreV3LibraryAddresses = { 55 | "contracts/libs/v0.8.x/BytecodeStorageV1.sol:BytecodeStorageReader": 56 | bytecodeStorageReaderAddress, 57 | }; 58 | const genArt721CoreContract = new GenArt721CoreV3__factory( 59 | linkLibraryAddresses, 60 | deployer 61 | ).attach(genArt721CoreAddress); 62 | 63 | // get MinterSetPriceV5 64 | // @dev this is minter at index 0 in the subgraph config 65 | if (!config.genericMinterEventsLibContracts) { 66 | throw new Error("No genericMinterEventsLibContracts in config"); 67 | } 68 | const minterSetPriceV5Address = 69 | config.genericMinterEventsLibContracts[0].address; 70 | const minterSetPriceV5Contract = new MinterSetPriceV5__factory(deployer).attach( 71 | minterSetPriceV5Address 72 | ); 73 | 74 | describe("MaxInvocationsLib event handling", () => { 75 | beforeAll(async () => { 76 | await waitUntilSubgraphIsSynced(client); 77 | }); 78 | 79 | describe("Indexed after setup", () => { 80 | test("created new Minter during deployment and allowlisting", async () => { 81 | const targetId = minterSetPriceV5Address.toLowerCase(); 82 | const minterRes = await getMinterDetails(client, targetId); 83 | expect(minterRes.id).toBe(targetId); 84 | }); 85 | }); 86 | 87 | describe("ProjectMaxInvocationsUpdated", () => { 88 | afterEach(async () => { 89 | // reset minter max invocations to core max invocations 90 | // @dev does not depend on test state, so can be run in afterEach 91 | // without needing a try/catch block 92 | await minterSetPriceV5Contract 93 | .connect(artist) 94 | .syncProjectMaxInvocationsToCore(0, genArt721CoreAddress); 95 | 96 | // clear the minter for project zero 97 | // @dev call success depends on test state, so use a try/catch block 98 | try { 99 | await sharedMinterFilterContract 100 | .connect(artist) 101 | .removeMinterForProject(0, genArt721CoreAddress); 102 | } catch (error) { 103 | // try block will only fail in case of previously failed test where 104 | // project zero never had its minter assigned. 105 | // Thus, swallow error here because the test failure has already been 106 | // reported, and additional error messaging from afterEach is not 107 | // helpful. 108 | } 109 | }); 110 | 111 | test("Max invocations is updated and configured", async () => { 112 | // set minter for project zero to the target minter 113 | await sharedMinterFilterContract 114 | .connect(artist) 115 | .setMinterForProject(0, genArt721CoreAddress, minterSetPriceV5Address); 116 | // verify initial max invocation state in subgraph 117 | const projectStateData = await genArt721CoreContract.projectStateData(0); 118 | const targetId = `${minterSetPriceV5Address.toLowerCase()}-${genArt721CoreAddress.toLowerCase()}-0`; 119 | const minterConfigRes = await getProjectMinterConfigurationDetails( 120 | client, 121 | targetId 122 | ); 123 | // subgraph max invocations should match core max invocations 124 | expect(minterConfigRes.maxInvocations).toBe( 125 | projectStateData.maxInvocations.toString() 126 | ); 127 | // set max invocations to 99 128 | await minterSetPriceV5Contract 129 | .connect(artist) 130 | .manuallyLimitProjectMaxInvocations(0, genArt721CoreAddress, 99); 131 | await waitUntilSubgraphIsSynced(client); 132 | // validate max invocations in subgraph was updated 133 | const minterConfigRes2 = await getProjectMinterConfigurationDetails( 134 | client, 135 | targetId 136 | ); 137 | // subgraph max invocations should match core max invocations 138 | expect(minterConfigRes2.maxInvocations).toBe("99"); 139 | // state is reset in afterEach 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /src/core-registry.ts: -------------------------------------------------------------------------------- 1 | import { Address, log, dataSource, BigInt } from "@graphprotocol/graph-ts"; 2 | 3 | import { refreshContractAtAddress } from "./mapping-v3-core"; 4 | 5 | import { CoreRegistry, Contract } from "../generated/schema"; 6 | import { 7 | IGenArt721CoreV3_Base_Template, 8 | OwnableGenArt721CoreV3Contract_Template, 9 | IERC721GenArt721CoreV3Contract_Template, 10 | AdminACLV0_Template, 11 | IGenArt721CoreContractV3_Engine_Flex_Template 12 | } from "../generated/templates"; 13 | 14 | import { 15 | ContractRegistered, 16 | ContractUnregistered 17 | } from "../generated/CoreRegistry/ICoreRegistryV1"; 18 | 19 | import { Ownable } from "../generated/OwnableGenArt721CoreV3Contract/Ownable"; 20 | import { 21 | COMPROMISED_ENGINE_REGISTRY_ADDRESS_GOERLI, 22 | COMPROMISED_ENGINE_REGISTRY_ADDRESS_MAINNET, 23 | COMPROMISED_ENGINE_REGISTRY_ADDRESS_ARBITRUM, 24 | COMPROMISED_ENGINE_REGISTRY_CUTOFF_BLOCK_GOERLI, 25 | COMPROMISED_ENGINE_REGISTRY_CUTOFF_BLOCK_MAINNET, 26 | COMPROMISED_ENGINE_REGISTRY_CUTOFF_BLOCK_ARBITRUM 27 | } from "./constants"; 28 | import { booleanToString } from "./helpers"; 29 | 30 | /*** EVENT HANDLERS ***/ 31 | // Registered contracts are tracked dynamically, and the contract's `registeredOn` 32 | // field is set to this core registry. 33 | export function handleContractRegistered(event: ContractRegistered): void { 34 | // ensure an engine registry entity exists 35 | loadOrCreateCoreRegistry(event.address); 36 | 37 | const network = dataSource.network(); 38 | 39 | if ( 40 | (network == "goerli" && 41 | event.address.toHexString() == 42 | COMPROMISED_ENGINE_REGISTRY_ADDRESS_GOERLI && 43 | event.block.number >= COMPROMISED_ENGINE_REGISTRY_CUTOFF_BLOCK_GOERLI) || 44 | (network == "mainnet" && 45 | event.address.toHexString() == 46 | COMPROMISED_ENGINE_REGISTRY_ADDRESS_MAINNET && 47 | event.block.number >= COMPROMISED_ENGINE_REGISTRY_CUTOFF_BLOCK_MAINNET) || 48 | (network == "arbitrum-one" && 49 | event.address.toHexString() == 50 | COMPROMISED_ENGINE_REGISTRY_ADDRESS_ARBITRUM && 51 | event.block.number >= COMPROMISED_ENGINE_REGISTRY_CUTOFF_BLOCK_ARBITRUM) 52 | ) { 53 | log.warning( 54 | "[WARN] Compromised Core Registry at address {} attempted to register new core contract.", 55 | [event.address.toHexString()] 56 | ); 57 | return; 58 | } 59 | 60 | // check if the contract is already registered 61 | const coreAddress = event.params._contractAddress; 62 | // dynamically track the new contract if not already in store, and refresh it 63 | // state to ensure it is up to date 64 | let contractEntity = Contract.load(coreAddress.toHexString()); 65 | if (!contractEntity) { 66 | // dynamically track the new contract via its required templates 67 | IGenArt721CoreV3_Base_Template.create(coreAddress); 68 | OwnableGenArt721CoreV3Contract_Template.create(coreAddress); 69 | IERC721GenArt721CoreV3Contract_Template.create(coreAddress); 70 | // @dev okay to create this template even if the contract is not engine flex as event handlers do not overlap 71 | IGenArt721CoreContractV3_Engine_Flex_Template.create(coreAddress); 72 | // also track the new contract's Admin ACL contract to enable indexing if admin changes 73 | // @dev for V3 core contracts, the admin acl contract is the core contract's owner 74 | const ownableV3Core = Ownable.bind(coreAddress); 75 | const adminACLAddressResult = ownableV3Core.try_owner(); 76 | if (adminACLAddressResult.reverted) { 77 | // @dev this should never happen, but if it does, we should not create the Admin ACL template 78 | log.warning( 79 | "[WARN] V3 Core Contract at address {} does not implement owner() function.", 80 | [ownableV3Core._address.toHexString()] 81 | ); 82 | return; 83 | } else { 84 | const adminACLAddress = adminACLAddressResult.value; 85 | AdminACLV0_Template.create(adminACLAddress); 86 | } 87 | // refresh contract 88 | refreshContractAtAddress(coreAddress, event.block.timestamp); 89 | } 90 | // set this core registry as the contract's registeredOn field 91 | // @dev this will overwrite the previous core registry if the contract 92 | // was previously not in store 93 | let contractEntityReload = Contract.load(coreAddress.toHexString()); 94 | if (contractEntityReload) { 95 | contractEntityReload.registeredOn = event.address.toHexString(); 96 | contractEntityReload.updatedAt = event.block.timestamp; 97 | contractEntityReload.save(); 98 | } 99 | } 100 | 101 | // Unregistered contracts are not removed from the store, but the contract's 102 | // `registeredOn` field will be nulled if this core registry is the current 103 | // core registry. 104 | export function handleContractUnregistered(event: ContractUnregistered): void { 105 | // ensure an core registry entity exists 106 | loadOrCreateCoreRegistry(event.address); 107 | // remove this core registry from the contract's registeredOn field 108 | const coreAddress = event.params._contractAddress; 109 | let contractEntity = Contract.load(coreAddress.toHexString()); 110 | if (contractEntity) { 111 | if (contractEntity.registeredOn == event.address.toHexString()) { 112 | contractEntity.registeredOn = null; 113 | contractEntity.save(); 114 | } 115 | } 116 | // We do not remove the contract entity from the store because it will likely 117 | // be re-added upon handling the contract's next emitted event. 118 | // This is because we cannot remove a data source template from the subgraph. 119 | } 120 | 121 | /*** HELPER FUNCTIONS ***/ 122 | function loadOrCreateCoreRegistry(address: Address): CoreRegistry { 123 | let coreRegistryEntity = CoreRegistry.load(address.toHexString()); 124 | if (!coreRegistryEntity) { 125 | coreRegistryEntity = new CoreRegistry(address.toHexString()); 126 | // initialize the core registry's registered contracts array 127 | // must assume empty, since not enumerable mapping in the contract 128 | // @dev this means we must track core registry contract events 129 | // immendiatly after deployment to ensure we have a complete list 130 | // of registered contracts 131 | coreRegistryEntity.save(); 132 | } 133 | return coreRegistryEntity as CoreRegistry; 134 | } 135 | -------------------------------------------------------------------------------- /posterity/tests/secondary/opensea/seaportHelpers.ts: -------------------------------------------------------------------------------- 1 | import { ethereum, Address, BigInt, Bytes } from "@graphprotocol/graph-ts"; 2 | import { newMockEvent } from "matchstick-as"; 3 | import { OrderFulfilled } from "../../../../generated/SeaportExchange/SeaportExchange"; 4 | import { ItemType } from "../../../../src/secondary/opensea/os-seaport-mapping"; 5 | import { 6 | DEFAULT_COLLECTION, 7 | DEFAULT_CURRENCY, 8 | DEFAULT_ORDER_HASH, 9 | DEFAULT_TAKER, 10 | DEFAULT_MAKER, 11 | DEFAULT_TOKEN_ID, 12 | DEFAULT_ZONE, 13 | DEFAULT_PRICE 14 | } from "../../shared-helpers"; 15 | 16 | export const MOCK_AB_ADDRESS = Address.fromString( 17 | "0x6C093Fe8bc59e1e0cAe2Ec10F0B717D3D182056B" 18 | ); 19 | export const MOCK_OS_ADDRESS = Address.fromString( 20 | "0x5b3256965e7C3cF26E11FCAf296DfC8807C01073" 21 | ); 22 | 23 | function buildOrderTuple( 24 | tokenId: BigInt, 25 | collection: Address = DEFAULT_COLLECTION, 26 | itemType: ItemType = ItemType.ERC721 27 | ): ethereum.Tuple { 28 | let orderTuple = new ethereum.Tuple(); 29 | 30 | orderTuple.push(ethereum.Value.fromI32(itemType)); 31 | orderTuple.push(ethereum.Value.fromAddress(collection)); 32 | orderTuple.push(ethereum.Value.fromUnsignedBigInt(tokenId)); 33 | orderTuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(1))); 34 | return orderTuple; 35 | } 36 | 37 | function buildConsiderationTuple( 38 | recipient: Address, 39 | price: BigInt, 40 | itemType: ItemType = ItemType.NATIVE, 41 | currency: Address = DEFAULT_CURRENCY 42 | ): ethereum.Tuple { 43 | let tuple = new ethereum.Tuple(); 44 | tuple.push(ethereum.Value.fromI32(itemType)); 45 | tuple.push(ethereum.Value.fromAddress(currency)); 46 | tuple.push(ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(0))); 47 | tuple.push(ethereum.Value.fromUnsignedBigInt(price)); 48 | tuple.push(ethereum.Value.fromAddress(recipient)); 49 | return tuple; 50 | } 51 | 52 | export function createOrderFulfilledEvent( 53 | isPrivateSale: boolean, 54 | bundle: boolean = false, 55 | multiplePayments: boolean = false, 56 | orderHashParam: string = DEFAULT_ORDER_HASH, 57 | recipientParam: Address = DEFAULT_TAKER, 58 | offererParam: Address = DEFAULT_MAKER, 59 | currencyParam: Address = DEFAULT_CURRENCY, 60 | collectionParam: Address = DEFAULT_COLLECTION, 61 | tokenIdParam: BigInt = DEFAULT_TOKEN_ID, 62 | zoneParam: Address = DEFAULT_ZONE, 63 | priceParam: BigInt = DEFAULT_PRICE 64 | ): OrderFulfilled { 65 | let mockEvent = newMockEvent(); 66 | 67 | let orderFulfilledEvent = new OrderFulfilled( 68 | mockEvent.address, 69 | mockEvent.logIndex, 70 | mockEvent.transactionLogIndex, 71 | mockEvent.logType, 72 | mockEvent.block, 73 | mockEvent.transaction, 74 | mockEvent.parameters, 75 | mockEvent.receipt 76 | ); 77 | let orderHash = new ethereum.EventParam( 78 | "orderHash", 79 | ethereum.Value.fromFixedBytes( 80 | changetype(Bytes.fromHexString(orderHashParam)) 81 | ) 82 | ); 83 | let offerrer = new ethereum.EventParam( 84 | "offerrer", 85 | ethereum.Value.fromAddress(offererParam) 86 | ); 87 | 88 | let zone = new ethereum.EventParam( 89 | "zone", 90 | ethereum.Value.fromAddress(zoneParam) 91 | ); 92 | 93 | let recipient = new ethereum.EventParam( 94 | "recipient", 95 | ethereum.Value.fromAddress(recipientParam) 96 | ); 97 | 98 | let numOrders = bundle ? 2 : 1; 99 | let orderTupleArray = new Array(); 100 | for (let i = 0; i < numOrders; i++) { 101 | orderTupleArray.push( 102 | buildOrderTuple( 103 | tokenIdParam.plus(BigInt.fromI32(i)), 104 | collectionParam, 105 | ItemType.ERC721 106 | ) 107 | ); 108 | } 109 | 110 | let offer = new ethereum.EventParam( 111 | "offer", 112 | ethereum.Value.fromTupleArray(orderTupleArray) 113 | ); 114 | 115 | let considerationTupleArray = new Array(); 116 | 117 | // Simulate standard OS txn (90% to buyer, 7.5% to AB, 2.5% to OS) 118 | considerationTupleArray.push( 119 | buildConsiderationTuple( 120 | offererParam, 121 | priceParam.div(BigInt.fromI32(10)).times(BigInt.fromI32(9)) 122 | ) 123 | ); 124 | considerationTupleArray.push( 125 | buildConsiderationTuple( 126 | MOCK_AB_ADDRESS, 127 | priceParam.div(BigInt.fromI32(1000)).times(BigInt.fromI32(75)) 128 | ) 129 | ); 130 | considerationTupleArray.push( 131 | buildConsiderationTuple( 132 | MOCK_OS_ADDRESS, 133 | priceParam.div(BigInt.fromI32(1000)).times(BigInt.fromI32(25)) 134 | ) 135 | ); 136 | 137 | if (multiplePayments) { 138 | // Add additional ERC20 payment 139 | considerationTupleArray.push( 140 | buildConsiderationTuple( 141 | offererParam, 142 | BigInt.fromI32(1000), 143 | ItemType.ERC20, 144 | Address.fromString("0x11111139b223fe8d0a0e5c4f27ead9083c756cc2") 145 | ) 146 | ); 147 | 148 | // Add ERC721 payment 149 | considerationTupleArray.push( 150 | buildConsiderationTuple( 151 | offererParam, 152 | BigInt.fromI32(1), 153 | ItemType.ERC721, 154 | Address.fromString("0x22222239b223fe8d0a0e5c4f27ead9083c756cc2") 155 | ) 156 | ); 157 | 158 | // Add ERC1155 payment 159 | considerationTupleArray.push( 160 | buildConsiderationTuple( 161 | offererParam, 162 | BigInt.fromI32(1), 163 | ItemType.ERC1155, 164 | Address.fromString("0x33333339b223fe8d0a0e5c4f27ead9083c756cc2") 165 | ) 166 | ); 167 | } 168 | 169 | if (isPrivateSale) { 170 | // Add ERC1155 payment 171 | considerationTupleArray.push( 172 | buildConsiderationTuple( 173 | recipientParam, 174 | tokenIdParam, 175 | ItemType.ERC721, 176 | collectionParam 177 | ) 178 | ); 179 | } 180 | 181 | let consideration = new ethereum.EventParam( 182 | "consideration", 183 | ethereum.Value.fromTupleArray(considerationTupleArray) 184 | ); 185 | 186 | orderFulfilledEvent.parameters = new Array(); 187 | orderFulfilledEvent.parameters.push(orderHash); 188 | orderFulfilledEvent.parameters.push(offerrer); 189 | orderFulfilledEvent.parameters.push(zone); 190 | orderFulfilledEvent.parameters.push(recipient); 191 | orderFulfilledEvent.parameters.push(offer); 192 | orderFulfilledEvent.parameters.push(consideration); 193 | 194 | return orderFulfilledEvent; 195 | } 196 | -------------------------------------------------------------------------------- /posterity/tests/secondary/looksrare/looksrareHelpers.ts: -------------------------------------------------------------------------------- 1 | import { Address, Bytes, BigInt, ethereum } from "@graphprotocol/graph-ts"; 2 | import { newMockEvent } from "matchstick-as"; 3 | import { 4 | TakerBid, 5 | TakerAsk 6 | } from "../../../../generated/LooksRareExchange/LooksRareExchange"; 7 | import { 8 | DEFAULT_AMOUNT, 9 | DEFAULT_COLLECTION, 10 | DEFAULT_CURRENCY, 11 | DEFAULT_MAKER, 12 | DEFAULT_ORDER_HASH, 13 | DEFAULT_ORDER_NONCE, 14 | DEFAULT_PRICE, 15 | DEFAULT_STRATEGY, 16 | DEFAULT_TAKER, 17 | DEFAULT_TOKEN_ID 18 | } from "../../shared-helpers"; 19 | 20 | export function createTakerBidEvent( 21 | isPrivateSale: boolean, 22 | orderHashParam: string = DEFAULT_ORDER_HASH, 23 | orderNonceParam: BigInt = DEFAULT_ORDER_NONCE, 24 | takerParam: Address = DEFAULT_TAKER, 25 | makerParam: Address = DEFAULT_MAKER, 26 | strategyParam: Address = DEFAULT_STRATEGY, 27 | currencyParam: Address = DEFAULT_CURRENCY, 28 | collectionParam: Address = DEFAULT_COLLECTION, 29 | tokenIdParam: BigInt = DEFAULT_TOKEN_ID, 30 | amountParam: BigInt = DEFAULT_AMOUNT, 31 | priceParam: BigInt = DEFAULT_PRICE 32 | ): TakerBid { 33 | let strategyAddress = strategyParam; 34 | if (isPrivateSale) { 35 | strategyAddress = Address.fromString( 36 | "0x58d83536d3efedb9f7f2a1ec3bdaad2b1a4dd98c" 37 | ); 38 | } 39 | 40 | let mockEvent = newMockEvent(); 41 | let takerBidEvent = new TakerBid( 42 | mockEvent.address, 43 | mockEvent.logIndex, 44 | mockEvent.transactionLogIndex, 45 | mockEvent.logType, 46 | mockEvent.block, 47 | mockEvent.transaction, 48 | mockEvent.parameters, 49 | mockEvent.receipt 50 | ); 51 | let orderHash = new ethereum.EventParam( 52 | "orderHash", 53 | ethereum.Value.fromFixedBytes( 54 | changetype(Bytes.fromHexString(orderHashParam)) 55 | ) 56 | ); 57 | let orderNonce = new ethereum.EventParam( 58 | "orderNonce", 59 | ethereum.Value.fromUnsignedBigInt(orderNonceParam) 60 | ); 61 | let taker = new ethereum.EventParam( 62 | "taker", 63 | ethereum.Value.fromAddress(takerParam) 64 | ); 65 | let maker = new ethereum.EventParam( 66 | "maker", 67 | ethereum.Value.fromAddress(makerParam) 68 | ); 69 | let strategy = new ethereum.EventParam( 70 | "strategy", 71 | ethereum.Value.fromAddress(strategyAddress) 72 | ); 73 | let currency = new ethereum.EventParam( 74 | "currency", 75 | ethereum.Value.fromAddress(currencyParam) 76 | ); 77 | let collection = new ethereum.EventParam( 78 | "collection", 79 | ethereum.Value.fromAddress(collectionParam) 80 | ); 81 | let tokenId = new ethereum.EventParam( 82 | "tokenId", 83 | ethereum.Value.fromUnsignedBigInt(tokenIdParam) 84 | ); 85 | let amount = new ethereum.EventParam( 86 | "amount", 87 | ethereum.Value.fromUnsignedBigInt(amountParam) 88 | ); 89 | let price = new ethereum.EventParam( 90 | "price", 91 | ethereum.Value.fromUnsignedBigInt(priceParam) 92 | ); 93 | 94 | takerBidEvent.parameters = new Array(); 95 | takerBidEvent.parameters.push(orderHash); 96 | takerBidEvent.parameters.push(orderNonce); 97 | takerBidEvent.parameters.push(taker); 98 | takerBidEvent.parameters.push(maker); 99 | takerBidEvent.parameters.push(strategy); 100 | takerBidEvent.parameters.push(currency); 101 | takerBidEvent.parameters.push(collection); 102 | takerBidEvent.parameters.push(tokenId); 103 | takerBidEvent.parameters.push(amount); 104 | takerBidEvent.parameters.push(price); 105 | 106 | return takerBidEvent; 107 | } 108 | 109 | export function createTakerAskEvent( 110 | isPrivateSale: boolean, 111 | orderHashParam: string = DEFAULT_ORDER_HASH, 112 | orderNonceParam: BigInt = DEFAULT_ORDER_NONCE, 113 | takerParam: Address = DEFAULT_TAKER, 114 | makerParam: Address = DEFAULT_MAKER, 115 | strategyParam: Address = DEFAULT_STRATEGY, 116 | currencyParam: Address = DEFAULT_CURRENCY, 117 | collectionParam: Address = DEFAULT_COLLECTION, 118 | tokenIdParam: BigInt = DEFAULT_TOKEN_ID, 119 | amountParam: BigInt = DEFAULT_AMOUNT, 120 | priceParam: BigInt = DEFAULT_PRICE 121 | ): TakerAsk { 122 | let strategyAddress = strategyParam; 123 | if (isPrivateSale) { 124 | strategyAddress = Address.fromString( 125 | "0x58d83536d3efedb9f7f2a1ec3bdaad2b1a4dd98c" 126 | ); 127 | } 128 | 129 | let mockEvent = newMockEvent(); 130 | let takerAsk = new TakerAsk( 131 | mockEvent.address, 132 | mockEvent.logIndex, 133 | mockEvent.transactionLogIndex, 134 | mockEvent.logType, 135 | mockEvent.block, 136 | mockEvent.transaction, 137 | mockEvent.parameters, 138 | mockEvent.receipt 139 | ); 140 | let orderHash = new ethereum.EventParam( 141 | "orderHash", 142 | ethereum.Value.fromFixedBytes( 143 | changetype(Bytes.fromHexString(orderHashParam)) 144 | ) 145 | ); 146 | let orderNonce = new ethereum.EventParam( 147 | "orderNonce", 148 | ethereum.Value.fromUnsignedBigInt(orderNonceParam) 149 | ); 150 | let taker = new ethereum.EventParam( 151 | "taker", 152 | ethereum.Value.fromAddress(takerParam) 153 | ); 154 | let maker = new ethereum.EventParam( 155 | "maker", 156 | ethereum.Value.fromAddress(makerParam) 157 | ); 158 | let strategy = new ethereum.EventParam( 159 | "strategy", 160 | ethereum.Value.fromAddress(strategyAddress) 161 | ); 162 | let currency = new ethereum.EventParam( 163 | "currency", 164 | ethereum.Value.fromAddress(currencyParam) 165 | ); 166 | let collection = new ethereum.EventParam( 167 | "collection", 168 | ethereum.Value.fromAddress(collectionParam) 169 | ); 170 | let tokenId = new ethereum.EventParam( 171 | "tokenId", 172 | ethereum.Value.fromUnsignedBigInt(tokenIdParam) 173 | ); 174 | let amount = new ethereum.EventParam( 175 | "amount", 176 | ethereum.Value.fromUnsignedBigInt(amountParam) 177 | ); 178 | let price = new ethereum.EventParam( 179 | "price", 180 | ethereum.Value.fromUnsignedBigInt(priceParam) 181 | ); 182 | 183 | takerAsk.parameters = new Array(); 184 | takerAsk.parameters.push(orderHash); 185 | takerAsk.parameters.push(orderNonce); 186 | takerAsk.parameters.push(taker); 187 | takerAsk.parameters.push(maker); 188 | takerAsk.parameters.push(strategy); 189 | takerAsk.parameters.push(currency); 190 | takerAsk.parameters.push(collection); 191 | takerAsk.parameters.push(tokenId); 192 | takerAsk.parameters.push(amount); 193 | takerAsk.parameters.push(price); 194 | 195 | return takerAsk; 196 | } 197 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/minter-suite-v2/da-lin-lib.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "@jest/globals"; 2 | import { 3 | getSubgraphConfig, 4 | createSubgraphClient, 5 | getAccounts, 6 | waitUntilSubgraphIsSynced, 7 | getMinterDetails, 8 | getProjectMinterConfigurationDetails, 9 | } from "../utils/helpers"; 10 | 11 | import { MinterFilterV2__factory } from "../../contracts/factories/MinterFilterV2__factory"; 12 | import { MinterDALinV5__factory } from "../../contracts/factories/MinterDALinV5__factory"; 13 | 14 | import { ethers } from "ethers"; 15 | // hide nuisance logs about event overloading 16 | import { Logger } from "@ethersproject/logger"; 17 | Logger.setLogLevel(Logger.levels.ERROR); 18 | 19 | // waiting for subgraph to sync can take longer than the default 5s timeout 20 | jest.setTimeout(30 * 1000); 21 | 22 | const config = getSubgraphConfig(); 23 | 24 | const client = createSubgraphClient(); 25 | const { deployer, artist } = getAccounts(); 26 | 27 | // set up delegation registry address 28 | const delegationRegistryAddress = config.metadata?.delegationRegistryAddress; 29 | if (!delegationRegistryAddress) 30 | throw new Error("No delegation registry address found in config metadata"); 31 | 32 | // set up contract instances and/or addresses 33 | const coreRegistryAddress = config.metadata?.coreRegistryAddress; 34 | if (!coreRegistryAddress) 35 | throw new Error("No core registry address found in config metadata"); 36 | 37 | const sharedMinterFilter = config.sharedMinterFilterContracts?.[0]; 38 | if (!sharedMinterFilter) { 39 | throw new Error("No shared minter filter found in config metadata"); 40 | } 41 | const sharedMinterFilterContract = new MinterFilterV2__factory(deployer).attach( 42 | sharedMinterFilter.address 43 | ); 44 | 45 | // get contract from the subgraph config 46 | if (!config.iGenArt721CoreContractV3_BaseContracts) { 47 | throw new Error("No iGenArt721CoreContractV3_BaseContracts in config"); 48 | } 49 | const genArt721CoreAddress = 50 | config.iGenArt721CoreContractV3_BaseContracts[0].address; 51 | 52 | // get MinterDALin contract from the subgraph config 53 | if (!config.DALinLibContracts) { 54 | throw new Error("No DALinLibContracts in config"); 55 | } 56 | const minterDALinV5Address = config.DALinLibContracts[0].address; 57 | const minterDALinV5Contract = new MinterDALinV5__factory(deployer).attach( 58 | minterDALinV5Address 59 | ); 60 | 61 | describe("DALinLib event handling", () => { 62 | beforeAll(async () => { 63 | await waitUntilSubgraphIsSynced(client); 64 | }); 65 | 66 | describe("Indexed after setup", () => { 67 | test("created new Minter during deployment and allowlisting", async () => { 68 | const targetId = minterDALinV5Address.toLowerCase(); 69 | const minterRes = await getMinterDetails(client, targetId); 70 | expect(minterRes.id).toBe(targetId); 71 | }); 72 | }); 73 | 74 | describe("AuctionMinimumLengthSecondsUpdated", () => { 75 | // @dev no need to reset the affected value after each test 76 | test("updated after admin configures", async () => { 77 | // query public constant for the expected value (>0) 78 | const initialValue = 79 | await minterDALinV5Contract.minimumAuctionLengthSeconds(); 80 | const newTargetValue = initialValue.add(1); 81 | // update the minter value 82 | await minterDALinV5Contract 83 | .connect(deployer) 84 | .setMinimumAuctionLengthSeconds(newTargetValue); 85 | // validate minter's extraMinterDetails in subgraph 86 | await waitUntilSubgraphIsSynced(client); 87 | const targetId = minterDALinV5Address.toLowerCase(); 88 | const minterRes = await getMinterDetails(client, targetId); 89 | const extraMinterDetails = JSON.parse(minterRes.extraMinterDetails); 90 | expect(extraMinterDetails.minimumAuctionLengthInSeconds).toBe( 91 | newTargetValue.toNumber() 92 | ); 93 | }); 94 | }); 95 | 96 | describe("SetAuctionDetailsLin", () => { 97 | afterEach(async () => { 98 | // clear the auction details for the project 99 | await minterDALinV5Contract 100 | .connect(deployer) 101 | .resetAuctionDetails(0, genArt721CoreAddress); 102 | // clear the current minter for the project 103 | // @dev call success depends on test state, so use a try/catch block 104 | try { 105 | await sharedMinterFilterContract 106 | .connect(artist) 107 | .removeMinterForProject(0, genArt721CoreAddress); 108 | } catch (error) { 109 | // try block will only fail in case of previously failed test where 110 | // project zero never had its minter assigned. 111 | // Thus, swallow error here because the test failure has already been 112 | // reported, and additional error messaging from afterEach is not 113 | // helpful. 114 | } 115 | }); 116 | 117 | test("subgraph is updated after event emitted", async () => { 118 | // artist configures auction 119 | await sharedMinterFilterContract.connect(artist).setMinterForProject( 120 | 0, // _projectId 121 | genArt721CoreAddress, // _coreContract 122 | minterDALinV5Address // _minter 123 | ); 124 | const latestBlock = await deployer.provider.getBlock("latest"); 125 | const targetAuctionStart = latestBlock.timestamp + 3600; 126 | const targetAuctionEnd = targetAuctionStart + 3600; 127 | const targetStartPrice = ethers.utils.parseEther("1"); 128 | const targetBasePrice = ethers.utils.parseEther("0.1"); 129 | await minterDALinV5Contract.connect(artist).setAuctionDetails( 130 | 0, // _projectId 131 | genArt721CoreAddress, // _coreContract 132 | targetAuctionStart, // _auctionTimestampStart 133 | targetAuctionEnd, // _auctionTimestampEnd 134 | targetStartPrice, // _startPrice 135 | targetBasePrice // _basePrice 136 | ); 137 | // validate project minter config in subgraph 138 | await waitUntilSubgraphIsSynced(client); 139 | const targetId = `${minterDALinV5Address.toLowerCase()}-${genArt721CoreAddress.toLowerCase()}-0`; 140 | const minterConfigRes = await getProjectMinterConfigurationDetails( 141 | client, 142 | targetId 143 | ); 144 | // validate fields 145 | expect(minterConfigRes.priceIsConfigured).toBe(true); 146 | expect(minterConfigRes.basePrice).toBe(targetBasePrice.toString()); 147 | // validate extraMinterDetails 148 | const extraMinterDetails = JSON.parse(minterConfigRes.extraMinterDetails); 149 | expect(extraMinterDetails.startPrice).toBe(targetStartPrice.toString()); 150 | expect(extraMinterDetails.startTime).toBe(targetAuctionStart); 151 | expect(extraMinterDetails.endTime).toBe(targetAuctionEnd); 152 | }); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /config/deprecated/mainnet-with-secondary.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "mainnet", 3 | "genArt721CoreV3Contracts": [ 4 | { 5 | "address": "0x99a9B7c1116f9ceEB1652de04d5969CcE509B069", 6 | "name": "Art Blocks Flagship (V3)", 7 | "startBlock": 15726705 8 | } 9 | ], 10 | "genArt721CoreContracts": [ 11 | { 12 | "address": "0xa7d8d9ef8D8Ce8992Df33D8b8CF4Aebabd5bD270", 13 | "startBlock": 11437151, 14 | "name": "Art Blocks Flagship (V1)" 15 | } 16 | ], 17 | "genArt721Contracts": [ 18 | { 19 | "address": "0x059EDD72Cd353dF5106D2B9cC5ab83a52287aC3a", 20 | "startBlock": 11338811, 21 | "name": "Art Blocks Flagship (V0)" 22 | } 23 | ], 24 | "pbabContracts": [ 25 | { 26 | "address": "0x28f2D3805652FB5d359486dFfb7D08320D403240", 27 | "name": "Doodle Labs", 28 | "startBlock": 13426126 29 | }, 30 | { 31 | "address": "0xbdde08bd57e5c9fd563ee7ac61618cb2ecdc0ce0", 32 | "name": "CryptoCitizens", 33 | "startBlock": 13711854 34 | }, 35 | { 36 | "address": "0x13aAe6f9599880edbB7d144BB13F1212CeE99533", 37 | "name": "Flutter Gen Art", 38 | "startBlock": 13711972 39 | }, 40 | { 41 | "address": "0x62e37f664b5945629B6549a87F8e10Ed0B6D923b", 42 | "name": "Tboa Club", 43 | "startBlock": 13712223 44 | }, 45 | { 46 | "address": "0xa319C382a702682129fcbF55d514E61a16f97f9c", 47 | "name": "Plottables", 48 | "startBlock": 13712123 49 | }, 50 | { 51 | "address": "0xd10e3DEe203579FcEE90eD7d0bDD8086F7E53beB", 52 | "name": "ArtCode", 53 | "startBlock": 14122786 54 | }, 55 | { 56 | "address": "0x0A1BBD57033F57E7B6743621b79fCB9Eb2CE3676", 57 | "name": "Bright Moments", 58 | "startBlock": 14484184 59 | }, 60 | { 61 | "address": "0x010bE6545e14f1DC50256286d9920e833F809C6A", 62 | "name": "Legends of Metaterra", 63 | "startBlock": 14567671 64 | }, 65 | { 66 | "address": "0x64780CE53f6e966E18a22Af13a2F97369580Ec11", 67 | "name": "Art Blocks x Pace", 68 | "startBlock": 14855005 69 | } 70 | ], 71 | "minterFilterV0Contracts": [ 72 | { 73 | "address": "0x4aafCE293b9B0faD169c78049A81e400f518E199", 74 | "startBlock": 14567785, 75 | "name": "Art Blocks Flagship Minter Fiter for V1 core" 76 | }, 77 | { 78 | "address": "0x267313E435863f15285CF0f8572068aAA62fC40d", 79 | "startBlock": 14855007, 80 | "name": "Art Blocks x Pace" 81 | } 82 | ], 83 | "minterFilterV1Contracts": [ 84 | { 85 | "address": "0x092B8F64e713d66b38522978BCf4649db14b931E", 86 | "name": "Art Blocks Flagship Minter Filter for V3 core", 87 | "startBlock": 15726707 88 | } 89 | ], 90 | "minterSetPriceV0Contracts": [ 91 | { 92 | "address": "0x1DEC9E52f1320F7Deb29cBCd7B7d67f3dF785142", 93 | "startBlock": 14567789, 94 | "name": "Art Blocks Flagship" 95 | } 96 | ], 97 | "minterSetPriceERC20V0Contracts": [ 98 | { 99 | "address": "0x48742D38a0809135EFd643c1150BfC13768C3907", 100 | "startBlock": 14567788, 101 | "name": "Art Blocks Flagship" 102 | } 103 | ], 104 | "minterDALinV0Contracts": [ 105 | { 106 | "address": "0xd219f61Bb5A3ffDeCB4362610977F1dAB3930eE2", 107 | "startBlock": 14567790, 108 | "name": "Art Blocks Flagship" 109 | } 110 | ], 111 | "minterDAExpV0Contracts": [ 112 | { 113 | "address": "0xFc74fD0f2c7EaD04f1E5E9fd82Aef55620710D7C", 114 | "startBlock": 14567792, 115 | "name": "Art Blocks Flagship" 116 | } 117 | ], 118 | "minterSetPriceV1Contracts": [ 119 | { 120 | "address": "0x934cdc04C434b8dBf3E1265F4f198D70566f7355", 121 | "startBlock": "14801771", 122 | "name": "Art Blocks Flagship" 123 | }, 124 | { 125 | "address": "0xE9BdfAee0895bA8134aCfF99B041E6069D719C47", 126 | "startBlock": "14855008", 127 | "name": "Art Blocks x Pace" 128 | } 129 | ], 130 | "minterSetPriceERC20V1Contracts": [ 131 | { 132 | "address": "0x0BbB93c5d118D1dee49e96BCAdc161403f4F8612", 133 | "startBlock": "14801771", 134 | "name": "Art Blocks Flagship" 135 | }, 136 | { 137 | "address": "0x7f13A57D138586dEBb254847D5358ADB57810C85", 138 | "startBlock": "14855008", 139 | "name": "Art Blocks x Pace" 140 | } 141 | ], 142 | "minterDALinV1Contracts": [ 143 | { 144 | "address": "0x32710950B014c2D29EA24f480Dd02c7e4610663b", 145 | "startBlock": "14801771", 146 | "name": "Art Blocks Flagship" 147 | }, 148 | { 149 | "address": "0x211c4EDf29B6758AE3C788B8F9C1802D20ECAAfE", 150 | "startBlock": "14855008", 151 | "name": "Art Blocks x Pace" 152 | } 153 | ], 154 | "minterDAExpV1Contracts": [ 155 | { 156 | "address": "0xD94C7060808f3c876824E57e685702f3834D2e13", 157 | "startBlock": "14801771", 158 | "name": "Art Blocks Flagship" 159 | }, 160 | { 161 | "address": "0x12b7c521a4e4b988cE4CeB241872D620815E3B48", 162 | "startBlock":"14855008", 163 | "name": "Art Blocks x Pace" 164 | } 165 | ], 166 | "minterSetPriceV2Contracts": [ 167 | { 168 | "address": "0x7B7917e083CeA6d9f6a3060a7330c1072fcb4e40", 169 | "name": "Flagship Minter for V3 core", 170 | "startBlock": 15726708 171 | } 172 | ], 173 | "minterSetPriceERC20V2Contracts": [ 174 | { 175 | "address": "0xe4c6EeF13649e9C4Ad8ae8A9C7fA9A7F26B4287a", 176 | "name": "Flagship Minter for V3 core", 177 | "startBlock": 15726709 178 | } 179 | ], 180 | "minterDALinV2Contracts": [ 181 | { 182 | "address": "0xdaa6D1e224f4B9f7c4f1368C362C4333A8e385A6", 183 | "name": "Flagship Minter for V3 core", 184 | "startBlock": 15726710 185 | } 186 | ], 187 | "minterDAExpV2Contracts": [ 188 | { 189 | "address": "0x706d6C6ef700a3c1C3a727f0c46492492E0A72b5", 190 | "name": "Flagship Minter for V3 core", 191 | "startBlock": 15726712 192 | } 193 | ], 194 | "minterMerkleV1Contracts": [ 195 | { 196 | "address": "0xae5A48D22Cd069c4d72dDe204A7fB4B302e614af", 197 | "name": "Flagship Minter for V3 core", 198 | "startBlock": 15726714 199 | } 200 | ], 201 | "minterHolderV1Contracts": [ 202 | { 203 | "address": "0xa198E22C32879f4214a37eB3051525bD9aff9145", 204 | "name": "Flagship Minter for V3 core", 205 | "startBlock": 15726716 206 | } 207 | ], 208 | "adminACLV0Contracts": [ 209 | { 210 | "address": "0x39F0e1e8Df2C135Df35B3FA496e943ac4B90Fe0f", 211 | "name": "Admin ACL V0 for V3 core", 212 | "startBlock": 15726705 213 | } 214 | ], 215 | "openSeaContractV1": { 216 | "address": "0x7Be8076f4EA4A4AD08075C2508e481d6C946D12b", 217 | "startBlock": 11338811 218 | }, 219 | "openSeaContractV2": { 220 | "address": "0x7f268357A8c2552623316e2562D90e642bB538E5", 221 | "startBlock": 14120912 222 | }, 223 | "openSeaSeaport": { 224 | "address": "0x00000000006c3852cbEf3e08E8dF289169EdE581", 225 | "startBlock": 14946473 226 | }, 227 | "looksRare": { 228 | "address": "0x59728544B08AB483533076417FbBB2fD0B17CE3a", 229 | "startBlock": 13885625 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/split-atomic/split-atomic-factory.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "@jest/globals"; 2 | import { 3 | getSubgraphConfig, 4 | createSubgraphClient, 5 | getAccounts, 6 | waitUntilSubgraphIsSynced, 7 | getSplitAtomicFactoryDetails, 8 | getSplitAtomicContractDetails, 9 | } from "../utils/helpers"; 10 | 11 | // splitter contracts 12 | import { SplitAtomicFactoryV0__factory } from "../../contracts/factories/SplitAtomicFactoryV0__factory"; 13 | 14 | // hide nuisance logs about event overloading 15 | import { Logger } from "@ethersproject/logger"; 16 | Logger.setLogLevel(Logger.levels.ERROR); 17 | 18 | // waiting for subgraph to sync can take longer than the default 5s timeout 19 | jest.setTimeout(30 * 1000); 20 | 21 | const config = getSubgraphConfig(); 22 | 23 | const client = createSubgraphClient(); 24 | const { deployer, artist } = getAccounts(); 25 | 26 | // get SplitAtomicFactory contract from the subgraph config 27 | if (!config.iSplitAtomicFactoryV0Contracts) { 28 | throw new Error("No iSplitAtomicFactoryV0Contracts in config"); 29 | } 30 | const splitAtomicFactoryV0Address = 31 | config.iSplitAtomicFactoryV0Contracts[0].address; 32 | const splitAtomicFactoryV0Contract = new SplitAtomicFactoryV0__factory( 33 | deployer 34 | ).attach(splitAtomicFactoryV0Address); 35 | // get SplitAtomic implementation contract from the subgraph config metadata 36 | if (!config.metadata?.splitAtomicImplementationAddress) { 37 | throw new Error("No splitAtomicImplementationAddress metadata in config"); 38 | } 39 | const splitAtomicImplementationAddress = 40 | config.metadata.splitAtomicImplementationAddress; 41 | 42 | describe("SplitAtomicFactoryV0 event handling", () => { 43 | beforeAll(async () => { 44 | await waitUntilSubgraphIsSynced(client); 45 | }); 46 | 47 | describe("Indexed after setup", () => { 48 | test("created new Minter during deployment and allowlisting", async () => { 49 | const targetId = splitAtomicFactoryV0Address.toLowerCase(); 50 | const splitAtomicFactoryRes = await getSplitAtomicFactoryDetails( 51 | client, 52 | targetId 53 | ); 54 | expect(splitAtomicFactoryRes.id).toBe(targetId); 55 | }); 56 | }); 57 | 58 | describe("Deployed", () => { 59 | // @dev since factory was already deployed, confirm state of existing subgraph entity 60 | test("updated after deployment", async () => { 61 | const targetId = splitAtomicFactoryV0Address.toLowerCase(); 62 | const splitAtomicFactoryRes = await getSplitAtomicFactoryDetails( 63 | client, 64 | targetId 65 | ); 66 | // validate fields 67 | expect(splitAtomicFactoryRes.id).toBe(targetId); 68 | expect(splitAtomicFactoryRes.type).toBe("SplitAtomicFactoryV0"); 69 | expect(splitAtomicFactoryRes.implementation).toBe( 70 | splitAtomicImplementationAddress.toLowerCase() 71 | ); 72 | expect(splitAtomicFactoryRes.splitAtomicContracts.length).toBe(0); 73 | expect(splitAtomicFactoryRes.requiredSplitAddress).toBe( 74 | deployer.address.toLowerCase() 75 | ); 76 | expect(splitAtomicFactoryRes.requiredSplitBasisPoints).toBe("2222"); 77 | expect(splitAtomicFactoryRes.abandoned).toBe(false); 78 | expect(splitAtomicFactoryRes.updatedAt).toBeDefined(); 79 | }); 80 | }); 81 | 82 | describe("SplitAtomicCreated", () => { 83 | test("handles split atomic factory creation", async () => { 84 | // validate initial state of split atomic factory in subgraph 85 | await waitUntilSubgraphIsSynced(client); 86 | const targetId = splitAtomicFactoryV0Address.toLowerCase(); 87 | const splitAtomicFactoryRes = await getSplitAtomicFactoryDetails( 88 | client, 89 | targetId 90 | ); 91 | const initialUpdatedAt = splitAtomicFactoryRes.updatedAt; 92 | // deploy the split atomic from the factory 93 | const splitStruct = [ 94 | { 95 | recipient: deployer.address, 96 | basisPoints: 2222, 97 | }, 98 | { 99 | recipient: artist.address, 100 | basisPoints: 7778, 101 | }, 102 | ]; 103 | await splitAtomicFactoryV0Contract.createSplit(splitStruct); 104 | await waitUntilSubgraphIsSynced(client); 105 | // validate split atomic factory in subgraph 106 | const splitAtomicFactoryRes2 = await getSplitAtomicFactoryDetails( 107 | client, 108 | targetId 109 | ); 110 | expect(parseInt(splitAtomicFactoryRes2.updatedAt)).toBeGreaterThan( 111 | parseInt(initialUpdatedAt) 112 | ); 113 | expect( 114 | splitAtomicFactoryRes2.splitAtomicContracts.length 115 | ).toBeGreaterThan(0); 116 | 117 | // validate new split atomic contract in subgraph 118 | const splitAtomicContractId = 119 | splitAtomicFactoryRes2.splitAtomicContracts[0].id; 120 | const splitAtomicContractRes = await getSplitAtomicContractDetails( 121 | client, 122 | splitAtomicContractId 123 | ); 124 | expect(splitAtomicContractRes.id).toBe(splitAtomicContractId); 125 | expect(splitAtomicContractRes.type).toBe("SplitAtomicV0"); 126 | expect(splitAtomicContractRes.implementation).toBe( 127 | splitAtomicImplementationAddress.toLowerCase() 128 | ); 129 | expect(splitAtomicContractRes.splitAtomicFactory.id).toBe(targetId); 130 | expect(splitAtomicContractRes.splits.length).toBe(2); 131 | expect(splitAtomicContractRes.updatedAt).toBe( 132 | splitAtomicFactoryRes2.updatedAt 133 | ); 134 | // validate splits 135 | const split0 = splitAtomicContractRes.splits[0]; 136 | expect(split0.id).toBe(`${splitAtomicContractId}-0`); 137 | expect(split0.splitAtomicContract.id).toBe(splitAtomicContractId); 138 | expect(split0.index).toBe("0"); 139 | expect(split0.recipient).toBe(deployer.address.toLowerCase()); 140 | expect(split0.basisPoints).toBe("2222"); 141 | const split1 = splitAtomicContractRes.splits[1]; 142 | expect(split1.id).toBe(`${splitAtomicContractId}-1`); 143 | expect(split1.splitAtomicContract.id).toBe(splitAtomicContractId); 144 | expect(split1.index).toBe("1"); 145 | expect(split1.recipient).toBe(artist.address.toLowerCase()); 146 | expect(split1.basisPoints).toBe("7778"); 147 | }); 148 | }); 149 | 150 | describe("SplitAtomicAbandoned", () => { 151 | test("handles split atomic factory abandonment", async () => { 152 | // validate initial state of split atomic factory in subgraph 153 | await waitUntilSubgraphIsSynced(client); 154 | const targetId = splitAtomicFactoryV0Address.toLowerCase(); 155 | const splitAtomicFactoryRes = await getSplitAtomicFactoryDetails( 156 | client, 157 | targetId 158 | ); 159 | expect(splitAtomicFactoryRes.abandoned).toBe(false); 160 | // abandon the split atomic factory 161 | await splitAtomicFactoryV0Contract.connect(deployer).abandon(); 162 | await waitUntilSubgraphIsSynced(client); 163 | // validate split atomic factory state update 164 | const splitAtomicFactoryRes2 = await getSplitAtomicFactoryDetails( 165 | client, 166 | targetId 167 | ); 168 | expect(splitAtomicFactoryRes2.abandoned).toBe(true); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /src/settlement-exp-lib-mapping.ts: -------------------------------------------------------------------------------- 1 | import { Address, BigInt, log, ethereum } from "@graphprotocol/graph-ts"; 2 | import { ReceiptUpdated } from "../generated/SettlementExpLib/SettlementExpLib"; 3 | import { ISharedMinterDAExpSettlementV0 } from "../generated/SettlementExpLib/ISharedMinterDAExpSettlementV0"; 4 | 5 | import { loadOrCreateMinterProjectAndConfigIfProject } from "./generic-minter-events-lib-mapping"; 6 | 7 | import { 8 | loadOrCreateMinter, 9 | updateProjectIfMinterConfigIsActive, 10 | getTotalDAExpAuctionTime, 11 | generateContractSpecificId, 12 | loadOrCreateReceipt, 13 | MinterProjectAndConfig 14 | } from "./helpers"; 15 | 16 | import { 17 | setMinterExtraMinterDetailsValue, 18 | setProjectMinterConfigExtraMinterDetailsValue 19 | } from "./extra-minter-details-helpers"; 20 | 21 | import { Receipt } from "../generated/schema"; 22 | 23 | /////////////////////////////////////////////////////////////////////////////// 24 | // EVENT HANDLERS start here 25 | /////////////////////////////////////////////////////////////////////////////// 26 | 27 | // project-level configuration events 28 | 29 | // note: state at the time of updating auctionRevenuesCollected to true is performed 30 | // in an after-update hook in the handler for the generic event 31 | 32 | /** 33 | * Handles the event that updates the Receipt of a collector on a project. 34 | * @param event The event carrying the updated Receipt data. 35 | */ 36 | export function handleReceiptUpdated(event: ReceiptUpdated): void { 37 | const minterProjectAndConfig = loadOrCreateMinterProjectAndConfigIfProject( 38 | event.address, // minter 39 | event.params.coreContract, 40 | event.params.projectId, 41 | event.block.timestamp 42 | ); 43 | if (!minterProjectAndConfig) { 44 | // project wasn't found, warning already logged in helper function 45 | return; 46 | } 47 | 48 | // load or create receipt 49 | let projectId = generateContractSpecificId( 50 | event.params.coreContract, 51 | event.params.projectId 52 | ); 53 | let receipt: Receipt = loadOrCreateReceipt( 54 | minterProjectAndConfig.minter.id, 55 | projectId, 56 | event.params.purchaser, 57 | event.block.timestamp 58 | ); 59 | // update receipt state 60 | receipt.netPosted = event.params.netPosted; 61 | receipt.numPurchased = BigInt.fromI32(event.params.numPurchased); 62 | receipt.updatedAt = event.block.timestamp; 63 | receipt.save(); 64 | 65 | // additionally, sync latest purchase price and number of settleable 66 | // purchases for project on this minter 67 | // @dev this is because this can be the only event emitted from the 68 | // minter during a purchase on a settleable minter 69 | syncLatestPurchasePrice(minterProjectAndConfig, event.block.timestamp); 70 | syncNumSettleableInvocations(minterProjectAndConfig, event.block.timestamp); 71 | 72 | // sync functions already induce sync if the project minter configuration is 73 | // the active one, so no need to induce sync here 74 | } 75 | 76 | /////////////////////////////////////////////////////////////////////////////// 77 | // EVENT HANDLERS end here 78 | /////////////////////////////////////////////////////////////////////////////// 79 | 80 | /////////////////////////////////////////////////////////////////////////////// 81 | // HELPERS start here 82 | /////////////////////////////////////////////////////////////////////////////// 83 | 84 | /** 85 | * @notice Syncs a settleable minter's project latest purchase price in 86 | * extraMinterDetails to the current value on the minter. 87 | * @param minterProjectAndConfig MinterProjectAndConfig object to by synced 88 | * @param timestamp The timestamp of the event that triggered this sync 89 | */ 90 | function syncLatestPurchasePrice( 91 | minterProjectAndConfig: MinterProjectAndConfig, 92 | timestamp: BigInt 93 | ): void { 94 | const settleableMinter = ISharedMinterDAExpSettlementV0.bind( 95 | Address.fromString(minterProjectAndConfig.minter.id) 96 | ); 97 | const coreContractAddress = minterProjectAndConfig.project.contract; 98 | const latestPurchasePriceResult = settleableMinter.try_getProjectLatestPurchasePrice( 99 | minterProjectAndConfig.project.projectId, 100 | Address.fromString(coreContractAddress) 101 | ); 102 | // handle revert (unexpected, so log warning for debugging) 103 | if (latestPurchasePriceResult.reverted) { 104 | log.warning( 105 | "Failed to sync latest purchase price for project {} on core contract {} on minter {}", 106 | [ 107 | minterProjectAndConfig.project.projectId.toString(), 108 | coreContractAddress, 109 | minterProjectAndConfig.minter.id 110 | ] 111 | ); 112 | // return early and abort sync. 113 | return; 114 | } 115 | // update extraMinterDetails key `currentSettledPrice` to be latestPurchasePrice 116 | setProjectMinterConfigExtraMinterDetailsValue( 117 | "currentSettledPrice", 118 | latestPurchasePriceResult.value.toString(), // Price is likely to overflow js Number.MAX_SAFE_INTEGER, so store as string 119 | minterProjectAndConfig.projectMinterConfiguration 120 | ); 121 | 122 | // induce sync if the project minter configuration is the active one 123 | updateProjectIfMinterConfigIsActive( 124 | minterProjectAndConfig.project, 125 | minterProjectAndConfig.projectMinterConfiguration, 126 | timestamp 127 | ); 128 | } 129 | 130 | /** 131 | * @notice Syncs a settleable minter's project num settleable invocations in 132 | * extraMinterDetails to the current value on the minter. 133 | * @param minterProjectAndConfig MinterProjectAndConfig object to by synced 134 | * @param timestamp The timestamp of the event that triggered this sync 135 | */ 136 | function syncNumSettleableInvocations( 137 | minterProjectAndConfig: MinterProjectAndConfig, 138 | timestamp: BigInt 139 | ): void { 140 | const settleableMinter = ISharedMinterDAExpSettlementV0.bind( 141 | Address.fromString(minterProjectAndConfig.minter.id) 142 | ); 143 | const coreContractAddress = minterProjectAndConfig.project.contract; 144 | const numSettleableInvocationsResult = settleableMinter.try_getNumSettleableInvocations( 145 | minterProjectAndConfig.project.projectId, 146 | Address.fromString(coreContractAddress) 147 | ); 148 | // handle revert (unexpected, so log warning for debugging) 149 | if (numSettleableInvocationsResult.reverted) { 150 | log.warning( 151 | "Failed to sync num settleable invocations for project {} on core contract {} on minter {}", 152 | [ 153 | minterProjectAndConfig.project.projectId.toString(), 154 | coreContractAddress, 155 | minterProjectAndConfig.minter.id 156 | ] 157 | ); 158 | // return early and abort sync. 159 | return; 160 | } 161 | // update extraMinterDetails key `currentSettledPrice` to be latestPurchasePrice 162 | setProjectMinterConfigExtraMinterDetailsValue( 163 | "numSettleableInvocations", 164 | numSettleableInvocationsResult.value, 165 | minterProjectAndConfig.projectMinterConfiguration 166 | ); 167 | 168 | // induce sync if the project minter configuration is the active one 169 | updateProjectIfMinterConfigIsActive( 170 | minterProjectAndConfig.project, 171 | minterProjectAndConfig.projectMinterConfiguration, 172 | timestamp 173 | ); 174 | } 175 | 176 | /////////////////////////////////////////////////////////////////////////////// 177 | // HELPERS end here 178 | /////////////////////////////////////////////////////////////////////////////// 179 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/minter-suite-v2/da-exp-lib.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "@jest/globals"; 2 | import { 3 | getSubgraphConfig, 4 | createSubgraphClient, 5 | getAccounts, 6 | waitUntilSubgraphIsSynced, 7 | getMinterDetails, 8 | getProjectMinterConfigurationDetails, 9 | } from "../utils/helpers"; 10 | 11 | import { MinterFilterV2__factory } from "../../contracts/factories/MinterFilterV2__factory"; 12 | import { MinterDAExpV5__factory } from "../../contracts/factories/MinterDAExpV5__factory"; 13 | 14 | import { ethers } from "ethers"; 15 | // hide nuisance logs about event overloading 16 | import { Logger } from "@ethersproject/logger"; 17 | Logger.setLogLevel(Logger.levels.ERROR); 18 | 19 | // waiting for subgraph to sync can take longer than the default 5s timeout 20 | jest.setTimeout(30 * 1000); 21 | 22 | const config = getSubgraphConfig(); 23 | 24 | const client = createSubgraphClient(); 25 | const { deployer, artist } = getAccounts(); 26 | 27 | // set up delegation registry address 28 | const delegationRegistryAddress = config.metadata?.delegationRegistryAddress; 29 | if (!delegationRegistryAddress) 30 | throw new Error("No delegation registry address found in config metadata"); 31 | 32 | // set up contract instances and/or addresses 33 | const coreRegistryAddress = config.metadata?.coreRegistryAddress; 34 | if (!coreRegistryAddress) 35 | throw new Error("No core registry address found in config metadata"); 36 | 37 | const sharedMinterFilter = config.sharedMinterFilterContracts?.[0]; 38 | if (!sharedMinterFilter) { 39 | throw new Error("No shared minter filter found in config metadata"); 40 | } 41 | const sharedMinterFilterContract = new MinterFilterV2__factory(deployer).attach( 42 | sharedMinterFilter.address 43 | ); 44 | 45 | // get contract from the subgraph config 46 | if (!config.iGenArt721CoreContractV3_BaseContracts) { 47 | throw new Error("No iGenArt721CoreContractV3_BaseContracts in config"); 48 | } 49 | const genArt721CoreAddress = 50 | config.iGenArt721CoreContractV3_BaseContracts[0].address; 51 | 52 | // get MinterDAExp contract from the subgraph config 53 | if (!config.DAExpLibContracts) { 54 | throw new Error("No DAExpLibContracts in config"); 55 | } 56 | const minterDAExpV5Address = config.DAExpLibContracts[0].address; 57 | const minterDAExpV5Contract = new MinterDAExpV5__factory(deployer).attach( 58 | minterDAExpV5Address 59 | ); 60 | 61 | // helper function to calculate approximate DAExp length 62 | function getApproxDAExpLength( 63 | startPrice: ethers.BigNumber, 64 | basePrice: ethers.BigNumber, 65 | halfLifeSeconds: number 66 | ): number { 67 | const EXTRA_DECIMALS = 10 ** 5; 68 | // const startPriceFloatingPoint = ethers.utils.parseUnits(startPrice); 69 | // const basePriceFloatingPoint = basePrice.toBigDecimal(); 70 | const priceRatio = 71 | startPrice.mul(EXTRA_DECIMALS).div(basePrice).toNumber() / EXTRA_DECIMALS; 72 | const completedHalfLives = Math.floor(Math.log(priceRatio) / Math.log(2)); 73 | const x1 = completedHalfLives * halfLifeSeconds; 74 | const x2 = x1 + halfLifeSeconds; 75 | const y1 = startPrice.div(2 ** completedHalfLives); 76 | const y2 = y1.div(2); 77 | const totalAuctionTime = 78 | x1 + 79 | (x2 - x1) * 80 | (basePrice.sub(y1).mul(EXTRA_DECIMALS).div(y2.sub(y1)).toNumber() / 81 | EXTRA_DECIMALS); 82 | return totalAuctionTime; 83 | } 84 | 85 | describe("DAExpLib event handling", () => { 86 | beforeAll(async () => { 87 | await waitUntilSubgraphIsSynced(client); 88 | }); 89 | 90 | describe("Indexed after setup", () => { 91 | test("created new Minter during deployment and allowlisting", async () => { 92 | const targetId = minterDAExpV5Address.toLowerCase(); 93 | const minterRes = await getMinterDetails(client, targetId); 94 | expect(minterRes.id).toBe(targetId); 95 | }); 96 | }); 97 | 98 | describe("AuctionMinHalfLifeSecondsUpdated", () => { 99 | // @dev no need to reset the affected value after each test 100 | test("updated after admin configures", async () => { 101 | // query public constant for the expected value (>0) 102 | const initialValue = 103 | await minterDAExpV5Contract.minimumPriceDecayHalfLifeSeconds(); 104 | const newTargetValue = initialValue.add(1); 105 | // update the minter value 106 | await minterDAExpV5Contract 107 | .connect(deployer) 108 | .setMinimumPriceDecayHalfLifeSeconds(newTargetValue); 109 | // validate minter's extraMinterDetails in subgraph 110 | await waitUntilSubgraphIsSynced(client); 111 | const targetId = minterDAExpV5Address.toLowerCase(); 112 | const minterRes = await getMinterDetails(client, targetId); 113 | const extraMinterDetails = JSON.parse(minterRes.extraMinterDetails); 114 | expect(extraMinterDetails.minimumHalfLifeInSeconds).toBe( 115 | newTargetValue.toNumber() 116 | ); 117 | }); 118 | }); 119 | 120 | describe("SetAuctionDetailsExp", () => { 121 | afterEach(async () => { 122 | // clear the auction details for the project 123 | await minterDAExpV5Contract 124 | .connect(deployer) 125 | .resetAuctionDetails(0, genArt721CoreAddress); 126 | // clear the current minter for the project 127 | // @dev call success depends on test state, so use a try/catch block 128 | try { 129 | await sharedMinterFilterContract 130 | .connect(artist) 131 | .removeMinterForProject(0, genArt721CoreAddress); 132 | } catch (error) { 133 | // try block will only fail in case of previously failed test where 134 | // project zero never had its minter assigned. 135 | // Thus, swallow error here because the test failure has already been 136 | // reported, and additional error messaging from afterEach is not 137 | // helpful. 138 | } 139 | }); 140 | 141 | test("subgraph is updated after event emitted", async () => { 142 | // artist configures auction 143 | await sharedMinterFilterContract.connect(artist).setMinterForProject( 144 | 0, // _projectId 145 | genArt721CoreAddress, // _coreContract 146 | minterDAExpV5Address // _minter 147 | ); 148 | const latestBlock = await deployer.provider.getBlock("latest"); 149 | const targetAuctionStart = latestBlock.timestamp + 3600; 150 | const targetStartPrice = ethers.utils.parseEther("1"); 151 | const targetBasePrice = ethers.utils.parseEther("0.1"); 152 | await minterDAExpV5Contract.connect(artist).setAuctionDetails( 153 | 0, // _projectId 154 | genArt721CoreAddress, // _coreContract 155 | targetAuctionStart, // _timestampStart 156 | 600, // _priceDecayHalfLifeSeconds 157 | targetStartPrice, // _startPrice 158 | targetBasePrice // _basePrice 159 | ); 160 | // validate project minter config in subgraph 161 | await waitUntilSubgraphIsSynced(client); 162 | const targetId = `${minterDAExpV5Address.toLowerCase()}-${genArt721CoreAddress.toLowerCase()}-0`; 163 | const minterConfigRes = await getProjectMinterConfigurationDetails( 164 | client, 165 | targetId 166 | ); 167 | // validate fields 168 | expect(minterConfigRes.priceIsConfigured).toBe(true); 169 | expect(minterConfigRes.basePrice).toBe(targetBasePrice.toString()); 170 | // validate extraMinterDetails 171 | const extraMinterDetails = JSON.parse(minterConfigRes.extraMinterDetails); 172 | expect(extraMinterDetails.startPrice).toBe(targetStartPrice.toString()); 173 | expect(extraMinterDetails.startTime).toBe(targetAuctionStart); 174 | expect(extraMinterDetails.halfLifeSeconds).toBe(600); 175 | const approxDALength = getApproxDAExpLength( 176 | targetStartPrice, 177 | targetBasePrice, 178 | 600 179 | ); 180 | expect(extraMinterDetails.approximateDAExpEndTime).toBe( 181 | targetAuctionStart + approxDALength 182 | ); 183 | }); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /tests/subgraph/mapping-v2-core/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Address, BigInt, ethereum } from "@graphprotocol/graph-ts"; 2 | import { 3 | createMockedFunction, 4 | newMockCall 5 | } from "matchstick-as/assembly/index"; 6 | import { AddProjectCall } from "../../../generated/GenArt721Core2PBAB/GenArt721Core2PBAB"; 7 | import { handleAddProject } from "../../../src/mapping-v2-core"; 8 | import { 9 | CURRENT_BLOCK_TIMESTAMP, 10 | TEST_CONTRACT_ADDRESS, 11 | TEST_CONTRACT, 12 | DEFAULT_PROJECT_VALUES 13 | } from "../shared-helpers"; 14 | import { Project } from "../../../generated/schema"; 15 | import { generateContractSpecificId } from "../../../src/helpers"; 16 | 17 | // helper mock function to initialize a Project entity in local in-memory store 18 | export function addNewProjectToStore( 19 | projectId: BigInt, 20 | projectName: string, 21 | artistAddress: Address, 22 | pricePerTokenInWei: BigInt, 23 | mockCallsWithDefaults: boolean, 24 | timestamp: BigInt | null 25 | ): Project { 26 | if (mockCallsWithDefaults) { 27 | mockProjectDetailsCallWithDefaults(projectId, projectName); 28 | mockProjectTokenInfoCallWithDefaults( 29 | projectId, 30 | artistAddress, 31 | pricePerTokenInWei 32 | ); 33 | mockProjectScriptInfoCall(projectId, null); 34 | } 35 | 36 | const newProjectCall = changetype(newMockCall()); 37 | newProjectCall.to = TEST_CONTRACT_ADDRESS; 38 | newProjectCall.block.timestamp = CURRENT_BLOCK_TIMESTAMP; 39 | 40 | newProjectCall.inputValues = [ 41 | new ethereum.EventParam( 42 | "projectName", 43 | ethereum.Value.fromString(projectName) 44 | ), 45 | new ethereum.EventParam( 46 | "artistAddress", 47 | ethereum.Value.fromAddress(artistAddress) 48 | ), 49 | new ethereum.EventParam( 50 | "pricePerTokenInWei", 51 | ethereum.Value.fromUnsignedBigInt(pricePerTokenInWei) 52 | ) 53 | ]; 54 | 55 | handleAddProject(newProjectCall); 56 | 57 | return changetype( 58 | Project.load(generateContractSpecificId(TEST_CONTRACT_ADDRESS, projectId)) 59 | ); 60 | } 61 | 62 | // mocks return values for Soldity contract calls in refreshContract() helper function 63 | export function mockRefreshContractCalls( 64 | nextProjectId: BigInt, 65 | overrides: Map | null 66 | ): void { 67 | createMockedFunction( 68 | TEST_CONTRACT_ADDRESS, 69 | "admin", 70 | "admin():(address)" 71 | ).returns([ethereum.Value.fromAddress(TEST_CONTRACT.admin)]); 72 | 73 | createMockedFunction( 74 | TEST_CONTRACT_ADDRESS, 75 | "renderProviderAddress", 76 | "renderProviderAddress():(address)" 77 | ).returns([ethereum.Value.fromAddress(TEST_CONTRACT.renderProviderAddress)]); 78 | 79 | createMockedFunction( 80 | TEST_CONTRACT_ADDRESS, 81 | "renderProviderPercentage", 82 | "renderProviderPercentage():(uint256)" 83 | ).returns([ 84 | ethereum.Value.fromUnsignedBigInt(TEST_CONTRACT.renderProviderPercentage) 85 | ]); 86 | 87 | createMockedFunction( 88 | TEST_CONTRACT_ADDRESS, 89 | "nextProjectId", 90 | "nextProjectId():(uint256)" 91 | ).returns([ethereum.Value.fromUnsignedBigInt(nextProjectId)]); 92 | 93 | createMockedFunction( 94 | TEST_CONTRACT_ADDRESS, 95 | "randomizerContract", 96 | "randomizerContract():(address)" 97 | ).returns([ethereum.Value.fromAddress(TEST_CONTRACT.randomizerContract)]); 98 | } 99 | 100 | export function mockProjectDetailsCallWithDefaults( 101 | projectId: BigInt, 102 | name: string 103 | ): void { 104 | return mockProjectDetailsCall(projectId, name, null, null, null, null); 105 | } 106 | 107 | export function mockProjectDetailsCall( 108 | projectId: BigInt, 109 | name: string, 110 | artistName: string | null, 111 | description: string | null, 112 | website: string | null, 113 | license: string | null 114 | ): void { 115 | let projectDetailsReturnArray: Array = [ 116 | ethereum.Value.fromString(name), // name 117 | ethereum.Value.fromString( 118 | artistName ? artistName : DEFAULT_PROJECT_VALUES.artistName 119 | ), // artistName 120 | ethereum.Value.fromString( 121 | description ? description : DEFAULT_PROJECT_VALUES.description 122 | ), // description 123 | ethereum.Value.fromString( 124 | website ? website : DEFAULT_PROJECT_VALUES.website 125 | ), // website 126 | ethereum.Value.fromString( 127 | license ? license : DEFAULT_PROJECT_VALUES.license 128 | ) // license 129 | ]; 130 | 131 | createMockedFunction( 132 | TEST_CONTRACT_ADDRESS, 133 | "projectDetails", 134 | "projectDetails(uint256):(string,string,string,string,string)" 135 | ) 136 | .withArgs([ethereum.Value.fromUnsignedBigInt(projectId)]) 137 | .returns(projectDetailsReturnArray); 138 | } 139 | 140 | export function mockProjectTokenInfoCallWithDefaults( 141 | projectId: BigInt, 142 | artistAddress: Address, 143 | pricePerTokenInWei: BigInt 144 | ): void { 145 | return mockProjectTokenInfoCall( 146 | projectId, 147 | artistAddress, 148 | pricePerTokenInWei, 149 | null, 150 | null, 151 | false, 152 | null, 153 | null, 154 | null, 155 | null 156 | ); 157 | } 158 | 159 | export function mockProjectTokenInfoCall( 160 | projectId: BigInt, 161 | artistAddress: Address, 162 | pricePerTokenInWei: BigInt, 163 | invocations: BigInt | null, 164 | maxInvocations: BigInt | null, 165 | active: boolean, 166 | additionalPayeeAddress: Address | null, 167 | additionalPayeePercentage: BigInt | null, 168 | currencySymbol: string | null, 169 | currencyAddress: Address | null 170 | ): void { 171 | let projectTokenInfoReturnArray: Array = [ 172 | ethereum.Value.fromAddress(artistAddress), // artistAddress 173 | ethereum.Value.fromUnsignedBigInt(pricePerTokenInWei), // pricePerTokenInWei 174 | ethereum.Value.fromUnsignedBigInt( 175 | invocations ? invocations : DEFAULT_PROJECT_VALUES.invocations 176 | ), // invocations 177 | ethereum.Value.fromUnsignedBigInt( 178 | maxInvocations ? maxInvocations : DEFAULT_PROJECT_VALUES.maxInvocations 179 | ), // maxInvocations 180 | ethereum.Value.fromBoolean(active), // active 181 | ethereum.Value.fromAddress( 182 | additionalPayeeAddress 183 | ? additionalPayeeAddress 184 | : DEFAULT_PROJECT_VALUES.additionalPayeeAddress 185 | ), // additionalPayee 186 | ethereum.Value.fromUnsignedBigInt( 187 | additionalPayeePercentage 188 | ? additionalPayeePercentage 189 | : DEFAULT_PROJECT_VALUES.additionalPayeePercentage 190 | ), // additionalPayeePercentage 191 | ethereum.Value.fromString( 192 | currencySymbol ? currencySymbol : DEFAULT_PROJECT_VALUES.currencySymbol 193 | ), // currencySymbol 194 | ethereum.Value.fromAddress( 195 | currencyAddress ? currencyAddress : DEFAULT_PROJECT_VALUES.currencyAddress 196 | ) // currencyAddress 197 | ]; 198 | 199 | createMockedFunction( 200 | TEST_CONTRACT_ADDRESS, 201 | "projectTokenInfo", 202 | "projectTokenInfo(uint256):(address,uint256,uint256,uint256,bool,address,uint256,string,address)" 203 | ) 204 | .withArgs([ethereum.Value.fromUnsignedBigInt(projectId)]) 205 | .returns(projectTokenInfoReturnArray); 206 | } 207 | 208 | export function mockTokenURICall(tokenId: BigInt, tokenURI: string): void { 209 | createMockedFunction( 210 | TEST_CONTRACT_ADDRESS, 211 | "tokenURI", 212 | "tokenURI(uint256):(string)" 213 | ) 214 | .withArgs([ethereum.Value.fromUnsignedBigInt(tokenId)]) 215 | .returns([ethereum.Value.fromString(tokenURI)]); 216 | } 217 | 218 | export function mockProjectScriptInfoCall( 219 | projectId: BigInt, 220 | overrides: Map | null 221 | ): void { 222 | let projectScriptInfoReturnArray: Array = [ 223 | ethereum.Value.fromString( 224 | overrides && overrides.has("scriptJSON") 225 | ? changetype>(overrides).get("scriptJSON") 226 | : DEFAULT_PROJECT_VALUES.scriptJSON 227 | ), // scriptJSON 228 | ethereum.Value.fromUnsignedBigInt( 229 | overrides && overrides.has("scriptCount") 230 | ? BigInt.fromString( 231 | changetype>(overrides).get("scriptCount") 232 | ) 233 | : DEFAULT_PROJECT_VALUES.scriptCount 234 | ), // scriptCount 235 | ethereum.Value.fromString( 236 | overrides && overrides.has("ipfsHash") 237 | ? changetype>(overrides).get("ipfsHash") 238 | : DEFAULT_PROJECT_VALUES.ipfsHash 239 | ), // IPFSHash 240 | ethereum.Value.fromBoolean( 241 | overrides && overrides.has("locked") 242 | ? changetype>(overrides).get("locked") === "true" 243 | : DEFAULT_PROJECT_VALUES.locked 244 | ), // locked 245 | ethereum.Value.fromBoolean( 246 | overrides && overrides.has("paused") 247 | ? changetype>(overrides).get("paused") === "true" 248 | : DEFAULT_PROJECT_VALUES.paused 249 | ) // paused 250 | ]; 251 | 252 | createMockedFunction( 253 | TEST_CONTRACT_ADDRESS, 254 | "projectScriptInfo", 255 | "projectScriptInfo(uint256):(string,uint256,string,bool,bool)" 256 | ) 257 | .withArgs([ethereum.Value.fromUnsignedBigInt(projectId)]) 258 | .returns(projectScriptInfoReturnArray); 259 | } 260 | -------------------------------------------------------------------------------- /tests/e2e/runner/__tests__/minter-suite-v2/set-price-lib.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "@jest/globals"; 2 | import { 3 | getSubgraphConfig, 4 | getAccounts, 5 | createSubgraphClient, 6 | waitUntilSubgraphIsSynced, 7 | getMinterDetails, 8 | getProjectMinterConfigurationDetails, 9 | } from "../utils/helpers"; 10 | import { MinterFilterV2__factory } from "../../contracts/factories/MinterFilterV2__factory"; 11 | import { MinterSetPriceV5__factory } from "../../contracts/factories/MinterSetPriceV5__factory"; 12 | import { MinterSetPriceERC20V5__factory } from "../../contracts/factories/MinterSetPriceERC20V5__factory"; 13 | import { ERC20Mock__factory } from "../../contracts/factories/ERC20Mock__factory"; 14 | import { ethers } from "ethers"; 15 | // hide nuisance logs about event overloading 16 | import { Logger } from "@ethersproject/logger"; 17 | Logger.setLogLevel(Logger.levels.ERROR); 18 | 19 | // waiting for subgraph to sync can take longer than the default 5s timeout 20 | jest.setTimeout(30 * 1000); 21 | 22 | const config = getSubgraphConfig(); 23 | 24 | const client = createSubgraphClient(); 25 | const { deployer, artist } = getAccounts(); 26 | 27 | // set up contract instances and/or addresses 28 | const coreRegistryAddress = config.metadata?.coreRegistryAddress; 29 | if (!coreRegistryAddress) 30 | throw new Error("No core registry address found in config metadata"); 31 | 32 | const sharedMinterFilter = config.sharedMinterFilterContracts?.[0]; 33 | if (!sharedMinterFilter) { 34 | throw new Error("No shared minter filter found in config metadata"); 35 | } 36 | const sharedMinterFilterContract = new MinterFilterV2__factory(deployer).attach( 37 | sharedMinterFilter.address 38 | ); 39 | // get contract from the subgraph config 40 | if (!config.iGenArt721CoreContractV3_BaseContracts) { 41 | throw new Error("No iGenArt721CoreContractV3_BaseContracts in config"); 42 | } 43 | const genArt721CoreAddress = 44 | config.iGenArt721CoreContractV3_BaseContracts[0].address; 45 | 46 | const bytecodeStorageReaderAddress = 47 | config.metadata?.bytecodeStorageReaderAddress; 48 | if (!bytecodeStorageReaderAddress) 49 | throw new Error( 50 | "No bytecode storage reader address found in config metadata" 51 | ); 52 | 53 | // get MinterSetPriceV5 54 | // @dev this is minter at index 0 in the subgraph config 55 | if (!config.genericMinterEventsLibContracts) { 56 | throw new Error("No genericMinterEventsLibContracts in config"); 57 | } 58 | const minterSetPriceV5Address = 59 | config.genericMinterEventsLibContracts[0].address; 60 | const minterSetPriceV5Contract = new MinterSetPriceV5__factory(deployer).attach( 61 | minterSetPriceV5Address 62 | ); 63 | 64 | // get MinterSetPriceERC20V5 65 | // @dev this is minter at index 1 in the subgraph config 66 | const minterSetPriceERC20V5Address = 67 | config.genericMinterEventsLibContracts[1].address; 68 | const minterSetPriceERC20V5Contract = new MinterSetPriceERC20V5__factory( 69 | deployer 70 | ).attach(minterSetPriceERC20V5Address); 71 | 72 | describe("SetPriceLib event handling", () => { 73 | beforeAll(async () => { 74 | await waitUntilSubgraphIsSynced(client); 75 | }); 76 | 77 | describe("Indexed after setup", () => { 78 | test("created new Minter during deployment and allowlisting", async () => { 79 | const targetId = minterSetPriceV5Address.toLowerCase(); 80 | const minterRes = await getMinterDetails(client, targetId); 81 | expect(minterRes.id).toBe(targetId); 82 | }); 83 | }); 84 | 85 | describe("PricePerTokenUpdated", () => { 86 | afterEach(async () => { 87 | // clear the minter for project zero 88 | // @dev call success depends on test state, so use a try/catch block 89 | try { 90 | await sharedMinterFilterContract 91 | .connect(artist) 92 | .removeMinterForProject(0, genArt721CoreAddress); 93 | } catch (error) { 94 | // try block will only fail in case of previously failed test where 95 | // project zero never had its minter assigned. 96 | // Thus, swallow error here because the test failure has already been 97 | // reported, and additional error messaging from afterEach is not 98 | // helpful. 99 | } 100 | }); 101 | 102 | test("Price is updated and configured", async () => { 103 | // set minter for project zero to the target minter 104 | await sharedMinterFilterContract 105 | .connect(artist) 106 | .setMinterForProject(0, genArt721CoreAddress, minterSetPriceV5Address); 107 | // update price 108 | const newPrice = ethers.utils.parseEther(Math.random().toString()); 109 | await minterSetPriceV5Contract 110 | .connect(artist) 111 | .updatePricePerTokenInWei(0, genArt721CoreAddress, newPrice); 112 | await waitUntilSubgraphIsSynced(client); 113 | // validate price in subgraph 114 | const targetId = `${minterSetPriceV5Address.toLowerCase()}-${genArt721CoreAddress.toLowerCase()}-0`; 115 | const minterConfigRes = await getProjectMinterConfigurationDetails( 116 | client, 117 | targetId 118 | ); 119 | expect(minterConfigRes.basePrice).toBe(newPrice.toString()); 120 | // @dev this could have been true before the update, but we can't know for sure, and 121 | // at least we validate that it's true after the update 122 | expect(minterConfigRes.priceIsConfigured).toBe(true); 123 | }); 124 | }); 125 | 126 | describe("PricePerTokenReset", () => { 127 | afterEach(async () => { 128 | // clear the minter for project zero 129 | // @dev call success depends on test state, so use a try/catch block 130 | try { 131 | await sharedMinterFilterContract 132 | .connect(artist) 133 | .removeMinterForProject(0, genArt721CoreAddress); 134 | } catch (error) { 135 | // try block will only fail in case of previously failed test where 136 | // project zero never had its minter assigned. 137 | // Thus, swallow error here because the test failure has already been 138 | // reported, and additional error messaging from afterEach is not 139 | // helpful. 140 | } 141 | // @dev we don't clear the currency info for project zero on the ERC20 142 | // minter, because it cannot be set to the zero address. Instead, we 143 | // deploy new ERC20 currencies for each test, and set the minter to use 144 | // that currency 145 | }); 146 | 147 | test("Currency is updated and configured", async () => { 148 | const currencySymbol = "ERC20"; 149 | // deploy new ERC20 currency, sending initial supply to artist 150 | const newCurrency = await new ERC20Mock__factory(artist).deploy( 151 | ethers.utils.parseEther("100") 152 | ); 153 | // set minter for project zero to the fixed price ERC20 minter 154 | await sharedMinterFilterContract 155 | .connect(artist) 156 | .setMinterForProject( 157 | 0, 158 | genArt721CoreAddress, 159 | minterSetPriceERC20V5Address 160 | ); 161 | // update currency info 162 | await minterSetPriceERC20V5Contract 163 | .connect(artist) 164 | .updateProjectCurrencyInfo( 165 | 0, 166 | genArt721CoreAddress, 167 | currencySymbol, 168 | newCurrency.address 169 | ); 170 | // configure price 171 | const newPrice = ethers.utils.parseEther(Math.random().toString()); 172 | await minterSetPriceERC20V5Contract 173 | .connect(artist) 174 | .updatePricePerTokenInWei(0, genArt721CoreAddress, newPrice); 175 | await waitUntilSubgraphIsSynced(client); 176 | // validate currency info and price is configured in subgraph 177 | const targetId = `${minterSetPriceERC20V5Address.toLowerCase()}-${genArt721CoreAddress.toLowerCase()}-0`; 178 | const minterConfigRes = await getProjectMinterConfigurationDetails( 179 | client, 180 | targetId 181 | ); 182 | expect(minterConfigRes.currencySymbol).toBe(currencySymbol); 183 | expect(minterConfigRes.currencyAddress).toBe( 184 | newCurrency.address.toLowerCase() 185 | ); 186 | expect(minterConfigRes.basePrice).toBe(newPrice.toString()); 187 | expect(minterConfigRes.priceIsConfigured).toBe(true); 188 | // invoke price reset by updating to a different currency 189 | const newCurrency2 = await new ERC20Mock__factory(artist).deploy( 190 | ethers.utils.parseEther("100") 191 | ); 192 | const currencySymbol2 = "ERC20"; 193 | await minterSetPriceERC20V5Contract 194 | .connect(artist) 195 | .updateProjectCurrencyInfo( 196 | 0, 197 | genArt721CoreAddress, 198 | currencySymbol2, 199 | newCurrency2.address 200 | ); 201 | await waitUntilSubgraphIsSynced(client); 202 | // validate currency info and price is NOT configured in subgraph 203 | const minterConfigRes2 = await getProjectMinterConfigurationDetails( 204 | client, 205 | targetId 206 | ); 207 | expect(minterConfigRes2.currencySymbol).toBe(currencySymbol2); 208 | expect(minterConfigRes2.currencyAddress).toBe( 209 | newCurrency2.address.toLowerCase() 210 | ); 211 | expect(minterConfigRes2.priceIsConfigured).toBe(false); 212 | expect(minterConfigRes2.basePrice).toBe("0"); 213 | }); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /tests/subgraph/mapping-v0-core/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Address, BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts"; 2 | import { 3 | assert, 4 | createMockedFunction, 5 | newMockCall 6 | } from "matchstick-as/assembly/index"; 7 | import { AddProjectCall } from "../../../generated/GenArt721/GenArt721"; 8 | import { Contract } from "../../../generated/schema"; 9 | import { generateContractSpecificId } from "../../../src/helpers"; 10 | import { handleAddProject } from "../../../src/mapping-v0-core"; 11 | import { 12 | CURRENT_BLOCK_TIMESTAMP, 13 | RANDOMIZER_ADDRESS, 14 | ContractValues, 15 | DEFAULT_PROJECT_VALUES, 16 | PROJECT_ENTITY_TYPE, 17 | booleanToString, 18 | CONTRACT_ENTITY_TYPE, 19 | TEST_CONTRACT_ADDRESS, 20 | TEST_CONTRACT 21 | } from "../shared-helpers"; 22 | 23 | // helper mock function to initialize a Project entity in local in-memory store 24 | export function addNewProjectToStore( 25 | projectId: BigInt, 26 | projectName: string, 27 | artistAddress: Address, 28 | pricePerTokenInWei: BigInt, 29 | mockCallsWithDefaults: boolean, 30 | timestamp: BigInt | null 31 | ): void { 32 | if (mockCallsWithDefaults) { 33 | mockProjectDetailsCallWithDefaults(projectId, projectName); 34 | mockProjectTokenInfoCallWithDefaults( 35 | projectId, 36 | artistAddress, 37 | pricePerTokenInWei 38 | ); 39 | mockProjectScriptInfoCall(projectId, null); 40 | } 41 | 42 | const newProjectCall = changetype(newMockCall()); 43 | newProjectCall.to = TEST_CONTRACT_ADDRESS; 44 | newProjectCall.block.timestamp = CURRENT_BLOCK_TIMESTAMP; 45 | 46 | newProjectCall.inputValues = [ 47 | new ethereum.EventParam( 48 | "projectName", 49 | ethereum.Value.fromString(projectName) 50 | ), 51 | new ethereum.EventParam( 52 | "artistAddress", 53 | ethereum.Value.fromAddress(artistAddress) 54 | ), 55 | new ethereum.EventParam( 56 | "pricePerTokenInWei", 57 | ethereum.Value.fromUnsignedBigInt(pricePerTokenInWei) 58 | ) 59 | ]; 60 | 61 | handleAddProject(newProjectCall); 62 | } 63 | 64 | // mocks return values for Soldity contract calls in refreshContract() helper function 65 | export function mockRefreshContractCalls( 66 | nextProjectId: BigInt, 67 | overrides: Map | null 68 | ): void { 69 | createMockedFunction( 70 | TEST_CONTRACT_ADDRESS, 71 | "admin", 72 | "admin():(address)" 73 | ).returns([ethereum.Value.fromAddress(TEST_CONTRACT.admin)]); 74 | 75 | createMockedFunction( 76 | TEST_CONTRACT_ADDRESS, 77 | "artblocksAddress", 78 | "artblocksAddress():(address)" 79 | ).returns([ethereum.Value.fromAddress(TEST_CONTRACT.renderProviderAddress)]); 80 | 81 | createMockedFunction( 82 | TEST_CONTRACT_ADDRESS, 83 | "artblocksPercentage", 84 | "artblocksPercentage():(uint256)" 85 | ).returns([ 86 | ethereum.Value.fromUnsignedBigInt(TEST_CONTRACT.renderProviderPercentage) 87 | ]); 88 | 89 | createMockedFunction( 90 | TEST_CONTRACT_ADDRESS, 91 | "nextProjectId", 92 | "nextProjectId():(uint256)" 93 | ).returns([ethereum.Value.fromUnsignedBigInt(nextProjectId)]); 94 | 95 | createMockedFunction( 96 | TEST_CONTRACT_ADDRESS, 97 | "randomizerContract", 98 | "randomizerContract():(address)" 99 | ).returns([ethereum.Value.fromAddress(TEST_CONTRACT.randomizerContract)]); 100 | } 101 | 102 | export function mockProjectDetailsCallWithDefaults( 103 | projectId: BigInt, 104 | name: string 105 | ): void { 106 | return mockProjectDetailsCall(projectId, name, null, null, null, null); 107 | } 108 | 109 | export function mockProjectDetailsCall( 110 | projectId: BigInt, 111 | name: string, 112 | artistName: string | null, 113 | description: string | null, 114 | website: string | null, 115 | license: string | null 116 | ): void { 117 | let projectDetailsReturnArray: Array = [ 118 | ethereum.Value.fromString(name), // name 119 | ethereum.Value.fromString( 120 | artistName ? artistName : DEFAULT_PROJECT_VALUES.artistName 121 | ), // artistName 122 | ethereum.Value.fromString( 123 | description ? description : DEFAULT_PROJECT_VALUES.description 124 | ), // description 125 | ethereum.Value.fromString( 126 | website ? website : DEFAULT_PROJECT_VALUES.website 127 | ), // website 128 | ethereum.Value.fromString( 129 | license ? license : DEFAULT_PROJECT_VALUES.license 130 | ), // license 131 | ethereum.Value.fromBoolean(true) // dynamic 132 | ]; 133 | 134 | createMockedFunction( 135 | TEST_CONTRACT_ADDRESS, 136 | "projectDetails", 137 | "projectDetails(uint256):(string,string,string,string,string,bool)" 138 | ) 139 | .withArgs([ethereum.Value.fromUnsignedBigInt(projectId)]) 140 | .returns(projectDetailsReturnArray); 141 | } 142 | 143 | export function mockProjectTokenInfoCallWithDefaults( 144 | projectId: BigInt, 145 | artistAddress: Address, 146 | pricePerTokenInWei: BigInt 147 | ): void { 148 | return mockProjectTokenInfoCall( 149 | projectId, 150 | artistAddress, 151 | pricePerTokenInWei, 152 | null, 153 | null, 154 | false, 155 | null, 156 | null 157 | ); 158 | } 159 | 160 | export function mockProjectTokenInfoCall( 161 | projectId: BigInt, 162 | artistAddress: Address, 163 | pricePerTokenInWei: BigInt, 164 | invocations: BigInt | null, 165 | maxInvocations: BigInt | null, 166 | active: boolean, 167 | additionalPayeeAddress: Address | null, 168 | additionalPayeePercentage: BigInt | null 169 | ): void { 170 | let projectTokenInfoReturnArray: Array = [ 171 | ethereum.Value.fromAddress(artistAddress), // artistAddress 172 | ethereum.Value.fromUnsignedBigInt(pricePerTokenInWei), // pricePerTokenInWei 173 | ethereum.Value.fromUnsignedBigInt( 174 | invocations ? invocations : DEFAULT_PROJECT_VALUES.invocations 175 | ), // invocations 176 | ethereum.Value.fromUnsignedBigInt( 177 | maxInvocations ? maxInvocations : DEFAULT_PROJECT_VALUES.maxInvocations 178 | ), // maxInvocations 179 | ethereum.Value.fromBoolean(active), // active 180 | ethereum.Value.fromAddress( 181 | additionalPayeeAddress 182 | ? additionalPayeeAddress 183 | : DEFAULT_PROJECT_VALUES.additionalPayeeAddress 184 | ), // additionalPayee 185 | ethereum.Value.fromUnsignedBigInt( 186 | additionalPayeePercentage 187 | ? additionalPayeePercentage 188 | : DEFAULT_PROJECT_VALUES.additionalPayeePercentage 189 | ) // additionalPayeePercentage 190 | ]; 191 | 192 | createMockedFunction( 193 | TEST_CONTRACT_ADDRESS, 194 | "projectTokenInfo", 195 | "projectTokenInfo(uint256):(address,uint256,uint256,uint256,bool,address,uint256)" 196 | ) 197 | .withArgs([ethereum.Value.fromUnsignedBigInt(projectId)]) 198 | .returns(projectTokenInfoReturnArray); 199 | } 200 | 201 | export function mockTokenURICall(tokenId: BigInt, tokenURI: string): void { 202 | createMockedFunction( 203 | TEST_CONTRACT_ADDRESS, 204 | "tokenURI", 205 | "tokenURI(uint256):(string)" 206 | ) 207 | .withArgs([ethereum.Value.fromUnsignedBigInt(tokenId)]) 208 | .returns([ethereum.Value.fromString(tokenURI)]); 209 | } 210 | 211 | export function mockProjectScriptInfoCall( 212 | projectId: BigInt, 213 | overrides: Map | null 214 | ): void { 215 | let projectScriptInfoReturnArray: Array = [ 216 | ethereum.Value.fromString( 217 | overrides && overrides.has("scriptJSON") 218 | ? changetype>(overrides).get("scriptJSON") 219 | : DEFAULT_PROJECT_VALUES.scriptJSON 220 | ), // scriptJSON 221 | ethereum.Value.fromUnsignedBigInt( 222 | overrides && overrides.has("scriptCount") 223 | ? BigInt.fromString( 224 | changetype>(overrides).get("scriptCount") 225 | ) 226 | : DEFAULT_PROJECT_VALUES.scriptCount 227 | ), // scriptCount 228 | ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(1)), // hashes 229 | ethereum.Value.fromString( 230 | overrides && overrides.has("ipfsHash") 231 | ? changetype>(overrides).get("ipfsHash") 232 | : DEFAULT_PROJECT_VALUES.ipfsHash 233 | ), // IPFSHash 234 | ethereum.Value.fromBoolean( 235 | overrides && overrides.has("locked") 236 | ? changetype>(overrides).get("locked") === "true" 237 | : DEFAULT_PROJECT_VALUES.locked 238 | ), // locked 239 | ethereum.Value.fromBoolean( 240 | overrides && overrides.has("paused") 241 | ? changetype>(overrides).get("paused") === "true" 242 | : DEFAULT_PROJECT_VALUES.paused 243 | ) // paused 244 | ]; 245 | 246 | createMockedFunction( 247 | TEST_CONTRACT_ADDRESS, 248 | "projectScriptInfo", 249 | "projectScriptInfo(uint256):(string,uint256,uint256,string,bool,bool)" 250 | ) 251 | .withArgs([ethereum.Value.fromUnsignedBigInt(projectId)]) 252 | .returns(projectScriptInfoReturnArray); 253 | } 254 | 255 | export const mockShowTokenHashes = function( 256 | contractAddress: Address, 257 | tokenId: BigInt, 258 | hash: Bytes 259 | ): void { 260 | let showTokenHashesInput: Array = [ 261 | ethereum.Value.fromUnsignedBigInt(tokenId) 262 | ]; 263 | createMockedFunction( 264 | contractAddress, 265 | "showTokenHashes", 266 | "showTokenHashes(uint256):(bytes32[])" 267 | ) 268 | .withArgs(showTokenHashesInput) 269 | .returns([ethereum.Value.fromBytesArray([hash])]); 270 | }; 271 | --------------------------------------------------------------------------------