From da716480bae0913e6abebfd64ced891ab7d4f0d5 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Mon, 15 Jul 2024 18:46:07 +0100 Subject: [PATCH 01/22] chore: add offchain metadata methods --- src/ingestors/index.ts | 4 +- src/ingestors/transient-base/abi.ts | 510 ++++++++++++++++++ src/ingestors/transient-base/index.ts | 91 ++++ .../transient-base/offchain-metadata.ts | 133 +++++ 4 files changed, 737 insertions(+), 1 deletion(-) create mode 100644 src/ingestors/transient-base/abi.ts create mode 100644 src/ingestors/transient-base/index.ts create mode 100644 src/ingestors/transient-base/offchain-metadata.ts diff --git a/src/ingestors/index.ts b/src/ingestors/index.ts index e726629..dc051fd 100644 --- a/src/ingestors/index.ts +++ b/src/ingestors/index.ts @@ -1,6 +1,7 @@ +import { FxHashIngestor } from './fxhash'; import { MintIngestor } from '../lib/types/mint-ingestor'; import { ProhibitionDailyIngestor } from './prohibition-daily'; -import { FxHashIngestor } from './fxhash'; +import { TransientIngestor } from './transient-base'; export type MintIngestionMap = { [key: string]: MintIngestor; @@ -9,6 +10,7 @@ export type MintIngestionMap = { export const ALL_MINT_INGESTORS: MintIngestionMap = { 'prohibition-daily': new ProhibitionDailyIngestor(), fxhash: new FxHashIngestor(), + transient: new TransientIngestor(), }; export * from './'; diff --git a/src/ingestors/transient-base/abi.ts b/src/ingestors/transient-base/abi.ts new file mode 100644 index 0000000..4e5bc03 --- /dev/null +++ b/src/ingestors/transient-base/abi.ts @@ -0,0 +1,510 @@ +export const TRANSIENT_BASE_ABI = [ + { + inputs: [ + { internalType: 'address', name: 'initOwner', type: 'address' }, + { internalType: 'address', name: 'initSanctionsOracle', type: 'address' }, + { internalType: 'address', name: 'initWethAddress', type: 'address' }, + { internalType: 'address', name: 'initProtocolFeeReceiver', type: 'address' }, + { internalType: 'uint256', name: 'initProtocolFee', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [{ internalType: 'address', name: 'target', type: 'address' }], + name: 'AddressEmptyCode', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'AddressInsufficientBalance', + type: 'error', + }, + { inputs: [], name: 'AlreadyReachedMintAllowance', type: 'error' }, + { inputs: [], name: 'DropAlreadyConfigured', type: 'error' }, + { inputs: [], name: 'DropUpdateNotAllowed', type: 'error' }, + { inputs: [], name: 'ETHTransferFailed', type: 'error' }, + { inputs: [], name: 'EnforcedPause', type: 'error' }, + { inputs: [], name: 'ExpectedPause', type: 'error' }, + { inputs: [], name: 'FailedInnerCall', type: 'error' }, + { inputs: [], name: 'InsufficentERC20Transfer', type: 'error' }, + { inputs: [], name: 'InsufficientFunds', type: 'error' }, + { inputs: [], name: 'InvalidBatchArguments', type: 'error' }, + { inputs: [], name: 'InvalidDropSupply', type: 'error' }, + { inputs: [], name: 'InvalidDropType', type: 'error' }, + { inputs: [], name: 'InvalidPayoutReceiver', type: 'error' }, + { inputs: [], name: 'MintZeroTokens', type: 'error' }, + { inputs: [], name: 'NotAllowedForVelocityDrops', type: 'error' }, + { inputs: [], name: 'NotApprovedMintContract', type: 'error' }, + { inputs: [], name: 'NotDropAdmin', type: 'error' }, + { inputs: [], name: 'NotOnAllowlist', type: 'error' }, + { + inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], + name: 'OwnableInvalidOwner', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'OwnableUnauthorizedAccount', + type: 'error', + }, + { inputs: [], name: 'ReentrancyGuardReentrantCall', type: 'error' }, + { + inputs: [{ internalType: 'address', name: 'token', type: 'address' }], + name: 'SafeERC20FailedOperation', + type: 'error', + }, + { inputs: [], name: 'SanctionedAddress', type: 'error' }, + { inputs: [], name: 'YouShallNotMint', type: 'error' }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'nftAddress', type: 'address' }, + { indexed: true, internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'DropClosed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'nftAddress', type: 'address' }, + { indexed: true, internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { + components: [ + { internalType: 'enum DropType', name: 'dropType', type: 'uint8' }, + { internalType: 'address', name: 'payoutReceiver', type: 'address' }, + { internalType: 'uint256', name: 'initialSupply', type: 'uint256' }, + { internalType: 'uint256', name: 'supply', type: 'uint256' }, + { internalType: 'uint256', name: 'allowance', type: 'uint256' }, + { internalType: 'address', name: 'currencyAddress', type: 'address' }, + { internalType: 'uint256', name: 'startTime', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, + { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, + { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, + { internalType: 'int256', name: 'decayRate', type: 'int256' }, + ], + indexed: false, + internalType: 'struct Drop', + name: 'drop', + type: 'tuple', + }, + ], + name: 'DropConfigured', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'nftAddress', type: 'address' }, + { indexed: true, internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { + components: [ + { internalType: 'enum DropType', name: 'dropType', type: 'uint8' }, + { internalType: 'address', name: 'payoutReceiver', type: 'address' }, + { internalType: 'uint256', name: 'initialSupply', type: 'uint256' }, + { internalType: 'uint256', name: 'supply', type: 'uint256' }, + { internalType: 'uint256', name: 'allowance', type: 'uint256' }, + { internalType: 'address', name: 'currencyAddress', type: 'address' }, + { internalType: 'uint256', name: 'startTime', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, + { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, + { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, + { internalType: 'int256', name: 'decayRate', type: 'int256' }, + ], + indexed: false, + internalType: 'struct Drop', + name: 'drop', + type: 'tuple', + }, + ], + name: 'DropUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' }, + { indexed: true, internalType: 'address', name: 'newOwner', type: 'address' }, + ], + name: 'OwnershipTransferred', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'address', name: 'account', type: 'address' }], + name: 'Paused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'newProtocolFeeReceiver', type: 'address' }, + { indexed: true, internalType: 'uint256', name: 'newProtocolFee', type: 'uint256' }, + ], + name: 'ProtocolFeeUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'nftAddress', type: 'address' }, + { indexed: true, internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { indexed: false, internalType: 'address', name: 'nftReceiver', type: 'address' }, + { indexed: false, internalType: 'address', name: 'currencyAddress', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: 'price', type: 'uint256' }, + { indexed: false, internalType: 'int256', name: 'decayRate', type: 'int256' }, + { indexed: false, internalType: 'bool', name: 'isPresale', type: 'bool' }, + ], + name: 'Purchase', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'prevOracle', type: 'address' }, + { indexed: true, internalType: 'address', name: 'newOracle', type: 'address' }, + ], + name: 'SanctionsOracleUpdated', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'address', name: 'account', type: 'address' }], + name: 'Unpaused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'prevWeth', type: 'address' }, + { indexed: true, internalType: 'address', name: 'newWeth', type: 'address' }, + ], + name: 'WethUpdated', + type: 'event', + }, + { + inputs: [], + name: 'ADMIN_ROLE', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'APPROVED_MINT_CONTRACT', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'VERSION', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'closeDrop', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { + components: [ + { internalType: 'enum DropType', name: 'dropType', type: 'uint8' }, + { internalType: 'address', name: 'payoutReceiver', type: 'address' }, + { internalType: 'uint256', name: 'initialSupply', type: 'uint256' }, + { internalType: 'uint256', name: 'supply', type: 'uint256' }, + { internalType: 'uint256', name: 'allowance', type: 'uint256' }, + { internalType: 'address', name: 'currencyAddress', type: 'address' }, + { internalType: 'uint256', name: 'startTime', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, + { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, + { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, + { internalType: 'int256', name: 'decayRate', type: 'int256' }, + ], + internalType: 'struct Drop', + name: 'drop', + type: 'tuple', + }, + ], + name: 'configureDrop', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'getDrop', + outputs: [ + { + components: [ + { internalType: 'enum DropType', name: 'dropType', type: 'uint8' }, + { internalType: 'address', name: 'payoutReceiver', type: 'address' }, + { internalType: 'uint256', name: 'initialSupply', type: 'uint256' }, + { internalType: 'uint256', name: 'supply', type: 'uint256' }, + { internalType: 'uint256', name: 'allowance', type: 'uint256' }, + { internalType: 'address', name: 'currencyAddress', type: 'address' }, + { internalType: 'uint256', name: 'startTime', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, + { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, + { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, + { internalType: 'int256', name: 'decayRate', type: 'int256' }, + ], + internalType: 'struct Drop', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'getDropPhase', + outputs: [{ internalType: 'enum DropPhase', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'getDropRound', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256[]', name: 'tokenIds', type: 'uint256[]' }, + ], + name: 'getDrops', + outputs: [ + { + components: [ + { internalType: 'enum DropType', name: 'dropType', type: 'uint8' }, + { internalType: 'address', name: 'payoutReceiver', type: 'address' }, + { internalType: 'uint256', name: 'initialSupply', type: 'uint256' }, + { internalType: 'uint256', name: 'supply', type: 'uint256' }, + { internalType: 'uint256', name: 'allowance', type: 'uint256' }, + { internalType: 'address', name: 'currencyAddress', type: 'address' }, + { internalType: 'uint256', name: 'startTime', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, + { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, + { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, + { internalType: 'int256', name: 'decayRate', type: 'int256' }, + ], + internalType: 'struct Drop[]', + name: 'drops', + type: 'tuple[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + ], + name: 'getNumberMinted', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'oracle', + outputs: [{ internalType: 'contract IChainalysisSanctionsOracle', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bool', name: 'status', type: 'bool' }], + name: 'pause', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'paused', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'protocolFee', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'protocolFeeReceiver', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'numberToMint', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleNumberCanMint', type: 'uint256' }, + { internalType: 'bytes32[]', name: 'proof', type: 'bytes32[]' }, + ], + name: 'purchase', + outputs: [{ internalType: 'uint256', name: 'refundAmount', type: 'uint256' }], + stateMutability: 'payable', + type: 'function', + }, + { inputs: [], name: 'renounceOwnership', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [ + { internalType: 'address', name: 'newProtocolFeeReceiver', type: 'address' }, + { internalType: 'uint256', name: 'newProtocolFee', type: 'uint256' }, + ], + name: 'setProtocolFeeSettings', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'newOracle', type: 'address' }], + name: 'setSanctionsOracle', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'newWethAddress', type: 'address' }], + name: 'setWethAddress', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'uint256', name: 'allowance', type: 'uint256' }, + ], + name: 'updateDropAllowance', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'int256', name: 'decayRate', type: 'int256' }, + ], + name: 'updateDropDecayRate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'uint256', name: 'startTime', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, + { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, + ], + name: 'updateDropDuration', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'address', name: 'payoutReceiver', type: 'address' }, + ], + name: 'updateDropPayoutReceiver', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, + ], + name: 'updateDropPresaleMerkleRoot', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'address', name: 'currencyAddress', type: 'address' }, + { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, + { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, + ], + name: 'updateDropPrices', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'weth', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +]; diff --git a/src/ingestors/transient-base/index.ts b/src/ingestors/transient-base/index.ts new file mode 100644 index 0000000..43d88df --- /dev/null +++ b/src/ingestors/transient-base/index.ts @@ -0,0 +1,91 @@ +import { MintContractOptions, MintIngestor, MintIngestorResources } from '../../lib/types/mint-ingestor'; +import { MintIngestionErrorName, MintIngestorError } from '../../lib/types/mint-ingestor-error'; +import { MintInstructionType, MintTemplate } from '../../lib/types/mint-template'; +import { + getTransientBaseMintByAddressAndChain, + getTransientBaseMintByURL, + transientSupports, +} from './offchain-metadata'; + +import { MintTemplateBuilder } from '../../lib/builder/mint-template-builder'; +import { TRANSIENT_BASE_ABI } from './abi'; + +export class TransientIngestor implements MintIngestor { + configuration = { + supportsContractIsExpensive: true, + }; + + async supportsUrl(resources: MintIngestorResources, url: string): Promise { + if (new URL(url).hostname !== 'www.transient.xyz') { + return false; + } + const { chainId, contractAddress } = await getTransientBaseMintByURL(resources, url); + return !!chainId && !!contractAddress; + } + + async supportsContract(resources: MintIngestorResources, contract: MintContractOptions): Promise { + const { chainId, contractAddress } = contract; + if (!chainId || !contractAddress) { + return false; + } + return await transientSupports(contract, resources.fetcher); + } + + async createMintTemplateForUrl(resources: MintIngestorResources, url: string): Promise { + const isCompatible = await this.supportsUrl(resources, url); + if (!isCompatible) { + throw new MintIngestorError(MintIngestionErrorName.IncompatibleUrl, 'Incompatible URL'); + } + + const { chainId, contractAddress } = await getTransientBaseMintByURL(resources, url); + + if (!chainId || !contractAddress) { + throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Missing required data'); + } + + return this.createMintForContract(resources, { chainId, contractAddress, url }); + } + + async createMintForContract(resources: MintIngestorResources, contract: MintContractOptions): Promise { + const { chainId, contractAddress } = contract; + if (!chainId || !contractAddress) { + throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Missing required data'); + } + + const mintBuilder = new MintTemplateBuilder() + .setMintInstructionType(MintInstructionType.EVM_MINT) + .setPartnerName('Transient'); + + if (contract.url) { + mintBuilder.setMarketingUrl(contract.url); + } + + // asides name and image no other metadata we need is onchain so we can just use the offchain metadata + const { name, image, description, priceInWei, mintAddress, public_sale_start_at, public_sale_end_at } = + await getTransientBaseMintByAddressAndChain(resources, contract.chainId, contract.contractAddress); + + mintBuilder.setName(name).setDescription(description).setFeaturedImageUrl(image); + + mintBuilder.setMintInstructions({ + chainId, + contractAddress, + mintAddress, + contractMethod: 'purchase', + contractParams: '["address", 1, 1, address]', + abi: TRANSIENT_BASE_ABI, + priceWei: priceInWei, + }); + + const startDate = public_sale_start_at ? new Date(public_sale_start_at) : new Date(); + const endDate = public_sale_end_at ? new Date(public_sale_end_at) : null; + + const liveDate = new Date() > startDate ? new Date() : startDate; + mintBuilder + .setAvailableForPurchaseEnd(endDate || new Date('2030-01-01')) + .setAvailableForPurchaseStart(startDate || new Date()) + .setLiveDate(liveDate); + + const output = mintBuilder.build(); + return output; + } +} diff --git a/src/ingestors/transient-base/offchain-metadata.ts b/src/ingestors/transient-base/offchain-metadata.ts new file mode 100644 index 0000000..63b2fff --- /dev/null +++ b/src/ingestors/transient-base/offchain-metadata.ts @@ -0,0 +1,133 @@ +import { AxiosInstance, AxiosResponse } from 'axios'; +import { MintContractOptions, MintIngestionErrorName, MintIngestorError, MintIngestorResources } from 'src/lib'; + +/** + * @param resources MintIngestorResources + * @param url string ie 'https://www.transient.xyz/stacks/kansas-smile' + * @returns { chainId: number, contractAddress: string, image: string , name: string} + * @throws MintIngestorError + */ +export const getTransientBaseMintByURL = async ( + resources: MintIngestorResources, + url: string, +): Promise<{ + chainId: number; + contractAddress: string; + image: string; + name: string; + mintAddress: string; + priceInWei: string; + description: string; +}> => { + // https://www.transient.xyz/stacks/kansas-smile + const urlParts = url.split('/'); + const slug = urlParts.pop(); + if (!slug) { + throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Mint not found'); + } + return await getTransientBaseMintBySlug(resources, slug); +}; + +export const getTransientBaseMintBySlug = async ( + resources: MintIngestorResources, + slug: string, +): Promise<{ + chainId: number; + contractAddress: string; + image: string; + name: string; + mintAddress: string; + priceInWei: string; + description: string; + public_sale_start_at: string; + public_sale_end_at: string; +}> => { + // search Transient API for the slug + let response: AxiosResponse; + try { + response = await resources.fetcher.get(`https://api.transient.xyz/v1/catalog/stacks/${slug}?format=json`); + } catch (error) { + throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Mint not found'); + } + + + const data = response.data; + if (!data || !data.nft_token) { + throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Mint not found'); + } + + const { nft_contract, nft_token } = data; + + return { + chainId: nft_contract.address.chain, + contractAddress: nft_contract.address.raw_address, + image: nft_token.image_uri, + name: nft_token.name, + priceInWei: data.current_cost, + mintAddress: data.stacks_address.raw_address, + description: data.description, + public_sale_start_at: data.public_sale_start_at, + public_sale_end_at: data.public_sale_end_at, + }; +}; + +export const getTransientBaseMintByAddressAndChain = async ( + resources: MintIngestorResources, + chainId: number, + contractAddress: string, +) => { + let response: AxiosResponse; + try { + response = await resources.fetcher.get( + `https://api.transient.xyz/v1/catalog/nfts?chain=${chainId}&address=${contractAddress}&format=json`, + ); + } catch (error) { + throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Could not query mint from Transient API'); + } + + const data = response.data; + if (!data || !data.results.length) { + throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Project not found'); + } + + const token = data.results.find((token: any) => token.nft_contract.address.raw_address === contractAddress); + if (!token) { + throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Project not found'); + } + + const slug = token.name.toLocaleLowerCase().split(' ').join('-'); + + return await getTransientBaseMintBySlug(resources, slug); +}; + +export const transientSupports = async (contract: MintContractOptions, fetcher: AxiosInstance): Promise => { + const { chainId, contractAddress } = contract; + let response: AxiosResponse; + try { + response = await fetcher.get(`https://api.transient.xyz/v1/catalog/nfts?chain=${chainId}&format=json`); + } catch (error) { + return false; + } + + const data = response.data; + if (!data || !data.results.length) { + return false; + } + + const tokens = data.results + .filter((token: any) => token.nft_contract.address.raw_address === contractAddress) + // filter by tokenId if set + .filter((token: any) => !contract.tokenId || token.token_id === contract.tokenId) + // now if url was set, check for slug match + .filter( + (token: any) => + !contract.url || + token.name.toLocaleLowerCase() === contract.url.split('/').pop()?.split('-').join(' ').toLocaleLowerCase(), + ); + + if (!tokens.length) { + return false; + } + + return true; +}; From 6dd6b4bc7aabffd260754dd19f61b3ee46f0b2eb Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Mon, 15 Jul 2024 19:13:59 +0100 Subject: [PATCH 02/22] chore: added tests for transient ingestor --- .../transient-base/offchain-metadata.ts | 3 +- test/ingestors/transient.test.ts | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 test/ingestors/transient.test.ts diff --git a/src/ingestors/transient-base/offchain-metadata.ts b/src/ingestors/transient-base/offchain-metadata.ts index 63b2fff..550e034 100644 --- a/src/ingestors/transient-base/offchain-metadata.ts +++ b/src/ingestors/transient-base/offchain-metadata.ts @@ -1,5 +1,5 @@ import { AxiosInstance, AxiosResponse } from 'axios'; -import { MintContractOptions, MintIngestionErrorName, MintIngestorError, MintIngestorResources } from 'src/lib'; +import { MintContractOptions, MintIngestionErrorName, MintIngestorError, MintIngestorResources } from '../../lib'; /** * @param resources MintIngestorResources @@ -50,7 +50,6 @@ export const getTransientBaseMintBySlug = async ( throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Mint not found'); } - const data = response.data; if (!data || !data.nft_token) { throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Mint not found'); diff --git a/test/ingestors/transient.test.ts b/test/ingestors/transient.test.ts new file mode 100644 index 0000000..7c47f7f --- /dev/null +++ b/test/ingestors/transient.test.ts @@ -0,0 +1,87 @@ +import { EVMMintInstructions, SolanaMintInstructions } from '../../src/lib/types/mint-template'; + +import { MintTemplateBuilder } from '../../src/lib/builder/mint-template-builder'; +import { ProhibitionDailyIngestor } from '../../src/ingestors/prohibition-daily'; +import { TransientIngestor } from '../../src/ingestors/transient-base/index'; +import { basicIngestorTests } from '../shared/basic-ingestor-tests'; +import { expect } from 'chai'; +import { mintIngestorResources } from '../../src/lib/resources'; + +const resources = mintIngestorResources(); + +describe('Transient', function () { + basicIngestorTests(new ProhibitionDailyIngestor(), resources, { + successUrls: [ + 'https://www.transient.xyz/stacks/kansas-smile' + ], + failureUrls: [ + 'https://www.transient.xyz/stacks/kansas-smiles', + 'https://www.transient.xyz/stacks' + ], + successContracts: [ + { chainId: 8453, contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8' } + ], + failureContracts: [ + { chainId: 8453, contractAddress: '0x965ef172b303b0bcdc38669df1de3c26bad2db8a' }, + { chainId: 8453, contractAddress: 'derp' } + ] + }); + it('createMintTemplateForUrl: Returns a mint template for a supported URL', async function () { + const ingestor = new ProhibitionDailyIngestor(); + const url = 'https://www.transient.xyz/stacks/kansas-smile'; + + const template = await ingestor.createMintTemplateForUrl(resources, url); + + // Verify that the mint template passed validation + const builder = new MintTemplateBuilder(template); + builder.validateMintTemplate(); + + expect(template.name).to.equal('Kansas Smile'); + expect(template.description).to.equal("'Kansas Smile' explores how our longing for the past connects with modern technology. It shows how digital tools can recreate and reimagine the past, allowing us to experience nostalgia in new ways through AI.\n\nExhibited at 'Digital Dreams' Kansas City 2024, part of the Digital Collage exhibition.\n\n(Analog and digital processes with AI)"); + const mintInstructions = template.mintInstructions as EVMMintInstructions; + + expect(mintInstructions.contractAddress).to.equal('0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8'); + expect(mintInstructions.contractMethod).to.equal('purchase'); + expect(mintInstructions.contractParams).to.equal('["address", 1, 1, address]'); + expect(mintInstructions.priceWei).to.equal('20000000000000000'); + + expect(template.featuredImageUrl?.length).to.be.matches(/https:\/\/ipfs.io\/ipfs\/.+\/media/); + + expect(template.marketingUrl).to.equal(url); + expect(template.availableForPurchaseStart?.getTime()).to.equal(1717777800000); + expect(template.availableForPurchaseEnd?.getTime()).to.equal(3294577800000); + }); + + it('createMintTemplateForContract: Returns a mint template for a supported contract', async function () { + const ingestor = new TransientIngestor(); + const contract = { + chainId: 8453, + contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8', + url: 'https://www.transient.xyz/stacks/kansas-smile' + }; + + const template = await ingestor.createMintForContract(resources, contract); + + // Verify that the mint template passed validation + const builder = new MintTemplateBuilder(template); + builder.validateMintTemplate(); + + expect(template.name).to.equal('Kansas Smile'); + expect(template.description).to.equal("'Kansas Smile' explores how our longing for the past connects with modern technology. It shows how digital tools can recreate and reimagine the past, allowing us to experience nostalgia in new ways through AI.\n\nExhibited at 'Digital Dreams' Kansas City 2024, part of the Digital Collage exhibition.\n\n(Analog and digital processes with AI)"); + const mintInstructions = template.mintInstructions as EVMMintInstructions; + + expect(mintInstructions.contractAddress).to.equal('0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8'); + expect(mintInstructions.contractMethod).to.equal('purchase'); + expect(mintInstructions.contractParams).to.equal('["address", 1, 1, address]'); + expect(mintInstructions.priceWei).to.equal('20000000000000000'); + const mintAddress = template.mintInstructions as SolanaMintInstructions; + + expect(mintAddress).to.equal('0x32953d7ae37b05075b88c34e800ae80c1cb1b794'); + + expect(template.featuredImageUrl?.length).to.be.greaterThan(0); + + expect(template.marketingUrl).to.equal(contract.url); + expect(template.availableForPurchaseStart?.getTime()).to.equal(1717777800000); + expect(template.availableForPurchaseEnd?.getTime()).to.equal(3294577800000); + }); +}); \ No newline at end of file From 30d746b0fd5ea5c01e8e026e24d45aa41af087fc Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Mon, 15 Jul 2024 19:17:08 +0100 Subject: [PATCH 03/22] trim abi --- src/ingestors/transient-base/abi.ts | 487 ---------------------------- 1 file changed, 487 deletions(-) diff --git a/src/ingestors/transient-base/abi.ts b/src/ingestors/transient-base/abi.ts index 4e5bc03..9f68af8 100644 --- a/src/ingestors/transient-base/abi.ts +++ b/src/ingestors/transient-base/abi.ts @@ -1,375 +1,4 @@ export const TRANSIENT_BASE_ABI = [ - { - inputs: [ - { internalType: 'address', name: 'initOwner', type: 'address' }, - { internalType: 'address', name: 'initSanctionsOracle', type: 'address' }, - { internalType: 'address', name: 'initWethAddress', type: 'address' }, - { internalType: 'address', name: 'initProtocolFeeReceiver', type: 'address' }, - { internalType: 'uint256', name: 'initProtocolFee', type: 'uint256' }, - ], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - inputs: [{ internalType: 'address', name: 'target', type: 'address' }], - name: 'AddressEmptyCode', - type: 'error', - }, - { - inputs: [{ internalType: 'address', name: 'account', type: 'address' }], - name: 'AddressInsufficientBalance', - type: 'error', - }, - { inputs: [], name: 'AlreadyReachedMintAllowance', type: 'error' }, - { inputs: [], name: 'DropAlreadyConfigured', type: 'error' }, - { inputs: [], name: 'DropUpdateNotAllowed', type: 'error' }, - { inputs: [], name: 'ETHTransferFailed', type: 'error' }, - { inputs: [], name: 'EnforcedPause', type: 'error' }, - { inputs: [], name: 'ExpectedPause', type: 'error' }, - { inputs: [], name: 'FailedInnerCall', type: 'error' }, - { inputs: [], name: 'InsufficentERC20Transfer', type: 'error' }, - { inputs: [], name: 'InsufficientFunds', type: 'error' }, - { inputs: [], name: 'InvalidBatchArguments', type: 'error' }, - { inputs: [], name: 'InvalidDropSupply', type: 'error' }, - { inputs: [], name: 'InvalidDropType', type: 'error' }, - { inputs: [], name: 'InvalidPayoutReceiver', type: 'error' }, - { inputs: [], name: 'MintZeroTokens', type: 'error' }, - { inputs: [], name: 'NotAllowedForVelocityDrops', type: 'error' }, - { inputs: [], name: 'NotApprovedMintContract', type: 'error' }, - { inputs: [], name: 'NotDropAdmin', type: 'error' }, - { inputs: [], name: 'NotOnAllowlist', type: 'error' }, - { - inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], - name: 'OwnableInvalidOwner', - type: 'error', - }, - { - inputs: [{ internalType: 'address', name: 'account', type: 'address' }], - name: 'OwnableUnauthorizedAccount', - type: 'error', - }, - { inputs: [], name: 'ReentrancyGuardReentrantCall', type: 'error' }, - { - inputs: [{ internalType: 'address', name: 'token', type: 'address' }], - name: 'SafeERC20FailedOperation', - type: 'error', - }, - { inputs: [], name: 'SanctionedAddress', type: 'error' }, - { inputs: [], name: 'YouShallNotMint', type: 'error' }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'nftAddress', type: 'address' }, - { indexed: true, internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - ], - name: 'DropClosed', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'nftAddress', type: 'address' }, - { indexed: true, internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - { - components: [ - { internalType: 'enum DropType', name: 'dropType', type: 'uint8' }, - { internalType: 'address', name: 'payoutReceiver', type: 'address' }, - { internalType: 'uint256', name: 'initialSupply', type: 'uint256' }, - { internalType: 'uint256', name: 'supply', type: 'uint256' }, - { internalType: 'uint256', name: 'allowance', type: 'uint256' }, - { internalType: 'address', name: 'currencyAddress', type: 'address' }, - { internalType: 'uint256', name: 'startTime', type: 'uint256' }, - { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, - { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, - { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, - { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, - { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, - { internalType: 'int256', name: 'decayRate', type: 'int256' }, - ], - indexed: false, - internalType: 'struct Drop', - name: 'drop', - type: 'tuple', - }, - ], - name: 'DropConfigured', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'nftAddress', type: 'address' }, - { indexed: true, internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - { - components: [ - { internalType: 'enum DropType', name: 'dropType', type: 'uint8' }, - { internalType: 'address', name: 'payoutReceiver', type: 'address' }, - { internalType: 'uint256', name: 'initialSupply', type: 'uint256' }, - { internalType: 'uint256', name: 'supply', type: 'uint256' }, - { internalType: 'uint256', name: 'allowance', type: 'uint256' }, - { internalType: 'address', name: 'currencyAddress', type: 'address' }, - { internalType: 'uint256', name: 'startTime', type: 'uint256' }, - { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, - { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, - { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, - { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, - { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, - { internalType: 'int256', name: 'decayRate', type: 'int256' }, - ], - indexed: false, - internalType: 'struct Drop', - name: 'drop', - type: 'tuple', - }, - ], - name: 'DropUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' }, - { indexed: true, internalType: 'address', name: 'newOwner', type: 'address' }, - ], - name: 'OwnershipTransferred', - type: 'event', - }, - { - anonymous: false, - inputs: [{ indexed: false, internalType: 'address', name: 'account', type: 'address' }], - name: 'Paused', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'newProtocolFeeReceiver', type: 'address' }, - { indexed: true, internalType: 'uint256', name: 'newProtocolFee', type: 'uint256' }, - ], - name: 'ProtocolFeeUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'nftAddress', type: 'address' }, - { indexed: true, internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - { indexed: false, internalType: 'address', name: 'nftReceiver', type: 'address' }, - { indexed: false, internalType: 'address', name: 'currencyAddress', type: 'address' }, - { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' }, - { indexed: false, internalType: 'uint256', name: 'price', type: 'uint256' }, - { indexed: false, internalType: 'int256', name: 'decayRate', type: 'int256' }, - { indexed: false, internalType: 'bool', name: 'isPresale', type: 'bool' }, - ], - name: 'Purchase', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'prevOracle', type: 'address' }, - { indexed: true, internalType: 'address', name: 'newOracle', type: 'address' }, - ], - name: 'SanctionsOracleUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [{ indexed: false, internalType: 'address', name: 'account', type: 'address' }], - name: 'Unpaused', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'prevWeth', type: 'address' }, - { indexed: true, internalType: 'address', name: 'newWeth', type: 'address' }, - ], - name: 'WethUpdated', - type: 'event', - }, - { - inputs: [], - name: 'ADMIN_ROLE', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'APPROVED_MINT_CONTRACT', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'VERSION', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - ], - name: 'closeDrop', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - { - components: [ - { internalType: 'enum DropType', name: 'dropType', type: 'uint8' }, - { internalType: 'address', name: 'payoutReceiver', type: 'address' }, - { internalType: 'uint256', name: 'initialSupply', type: 'uint256' }, - { internalType: 'uint256', name: 'supply', type: 'uint256' }, - { internalType: 'uint256', name: 'allowance', type: 'uint256' }, - { internalType: 'address', name: 'currencyAddress', type: 'address' }, - { internalType: 'uint256', name: 'startTime', type: 'uint256' }, - { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, - { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, - { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, - { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, - { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, - { internalType: 'int256', name: 'decayRate', type: 'int256' }, - ], - internalType: 'struct Drop', - name: 'drop', - type: 'tuple', - }, - ], - name: 'configureDrop', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - ], - name: 'getDrop', - outputs: [ - { - components: [ - { internalType: 'enum DropType', name: 'dropType', type: 'uint8' }, - { internalType: 'address', name: 'payoutReceiver', type: 'address' }, - { internalType: 'uint256', name: 'initialSupply', type: 'uint256' }, - { internalType: 'uint256', name: 'supply', type: 'uint256' }, - { internalType: 'uint256', name: 'allowance', type: 'uint256' }, - { internalType: 'address', name: 'currencyAddress', type: 'address' }, - { internalType: 'uint256', name: 'startTime', type: 'uint256' }, - { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, - { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, - { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, - { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, - { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, - { internalType: 'int256', name: 'decayRate', type: 'int256' }, - ], - internalType: 'struct Drop', - name: '', - type: 'tuple', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - ], - name: 'getDropPhase', - outputs: [{ internalType: 'enum DropPhase', name: '', type: 'uint8' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - ], - name: 'getDropRound', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256[]', name: 'tokenIds', type: 'uint256[]' }, - ], - name: 'getDrops', - outputs: [ - { - components: [ - { internalType: 'enum DropType', name: 'dropType', type: 'uint8' }, - { internalType: 'address', name: 'payoutReceiver', type: 'address' }, - { internalType: 'uint256', name: 'initialSupply', type: 'uint256' }, - { internalType: 'uint256', name: 'supply', type: 'uint256' }, - { internalType: 'uint256', name: 'allowance', type: 'uint256' }, - { internalType: 'address', name: 'currencyAddress', type: 'address' }, - { internalType: 'uint256', name: 'startTime', type: 'uint256' }, - { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, - { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, - { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, - { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, - { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, - { internalType: 'int256', name: 'decayRate', type: 'int256' }, - ], - internalType: 'struct Drop[]', - name: 'drops', - type: 'tuple[]', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - { internalType: 'address', name: 'recipient', type: 'address' }, - ], - name: 'getNumberMinted', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'oracle', - outputs: [{ internalType: 'contract IChainalysisSanctionsOracle', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'owner', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ internalType: 'bool', name: 'status', type: 'bool' }], - name: 'pause', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'paused', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, { inputs: [], name: 'protocolFee', @@ -377,13 +6,6 @@ export const TRANSIENT_BASE_ABI = [ stateMutability: 'view', type: 'function', }, - { - inputs: [], - name: 'protocolFeeReceiver', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, { inputs: [ { internalType: 'address', name: 'nftAddress', type: 'address' }, @@ -398,113 +20,4 @@ export const TRANSIENT_BASE_ABI = [ stateMutability: 'payable', type: 'function', }, - { inputs: [], name: 'renounceOwnership', outputs: [], stateMutability: 'nonpayable', type: 'function' }, - { - inputs: [ - { internalType: 'address', name: 'newProtocolFeeReceiver', type: 'address' }, - { internalType: 'uint256', name: 'newProtocolFee', type: 'uint256' }, - ], - name: 'setProtocolFeeSettings', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'newOracle', type: 'address' }], - name: 'setSanctionsOracle', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'newWethAddress', type: 'address' }], - name: 'setWethAddress', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }], - name: 'transferOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - { internalType: 'uint256', name: 'allowance', type: 'uint256' }, - ], - name: 'updateDropAllowance', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - { internalType: 'int256', name: 'decayRate', type: 'int256' }, - ], - name: 'updateDropDecayRate', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - { internalType: 'uint256', name: 'startTime', type: 'uint256' }, - { internalType: 'uint256', name: 'presaleDuration', type: 'uint256' }, - { internalType: 'uint256', name: 'publicDuration', type: 'uint256' }, - ], - name: 'updateDropDuration', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - { internalType: 'address', name: 'payoutReceiver', type: 'address' }, - ], - name: 'updateDropPayoutReceiver', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - { internalType: 'bytes32', name: 'presaleMerkleRoot', type: 'bytes32' }, - ], - name: 'updateDropPresaleMerkleRoot', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'nftAddress', type: 'address' }, - { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, - { internalType: 'address', name: 'currencyAddress', type: 'address' }, - { internalType: 'uint256', name: 'presaleCost', type: 'uint256' }, - { internalType: 'uint256', name: 'publicCost', type: 'uint256' }, - ], - name: 'updateDropPrices', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'weth', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, ]; From cd07ad65d0a44a51a9f81edc59777250b16a6648 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Tue, 16 Jul 2024 21:24:25 +0100 Subject: [PATCH 04/22] chore: set creator data and output contract --- src/ingestors/transient-base/index.ts | 16 +++++++++++++--- .../transient-base/offchain-metadata.ts | 10 ++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/ingestors/transient-base/index.ts b/src/ingestors/transient-base/index.ts index 43d88df..64648a1 100644 --- a/src/ingestors/transient-base/index.ts +++ b/src/ingestors/transient-base/index.ts @@ -61,15 +61,25 @@ export class TransientIngestor implements MintIngestor { } // asides name and image no other metadata we need is onchain so we can just use the offchain metadata - const { name, image, description, priceInWei, mintAddress, public_sale_start_at, public_sale_end_at } = + const { name, image, description, priceInWei, mintAddress, public_sale_start_at, public_sale_end_at, user } = await getTransientBaseMintByAddressAndChain(resources, contract.chainId, contract.contractAddress); mintBuilder.setName(name).setDescription(description).setFeaturedImageUrl(image); + mintBuilder.setMintOutputContract({ + chainId, + address: contractAddress, + }); + + mintBuilder.setCreator({ + name: user.name, + imageUrl: user.image, + websiteUrl: user.website + }); + mintBuilder.setMintInstructions({ chainId, - contractAddress, - mintAddress, + contractAddress: mintAddress, contractMethod: 'purchase', contractParams: '["address", 1, 1, address]', abi: TRANSIENT_BASE_ABI, diff --git a/src/ingestors/transient-base/offchain-metadata.ts b/src/ingestors/transient-base/offchain-metadata.ts index 550e034..85266d0 100644 --- a/src/ingestors/transient-base/offchain-metadata.ts +++ b/src/ingestors/transient-base/offchain-metadata.ts @@ -41,6 +41,11 @@ export const getTransientBaseMintBySlug = async ( description: string; public_sale_start_at: string; public_sale_end_at: string; + user: { + name: string; + image: string; + website: string; + } }> => { // search Transient API for the slug let response: AxiosResponse; @@ -67,6 +72,11 @@ export const getTransientBaseMintBySlug = async ( description: data.description, public_sale_start_at: data.public_sale_start_at, public_sale_end_at: data.public_sale_end_at, + user: { + name: data.nft_contract.user.display_name, + image: data.nft_contract.user.pfp, + website: `https://www.transient.xyz${data.nft_contract.user.uri}`, + }, }; }; From 21113166bd29dd668f4767398c033c5ad3d68968 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Tue, 16 Jul 2024 21:27:53 +0100 Subject: [PATCH 05/22] chose: set contract address parameter correctly --- src/ingestors/transient-base/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ingestors/transient-base/index.ts b/src/ingestors/transient-base/index.ts index 64648a1..8168430 100644 --- a/src/ingestors/transient-base/index.ts +++ b/src/ingestors/transient-base/index.ts @@ -81,7 +81,7 @@ export class TransientIngestor implements MintIngestor { chainId, contractAddress: mintAddress, contractMethod: 'purchase', - contractParams: '["address", 1, 1, address]', + contractParams: `["${contract.contractAddress}", 1, address, 1]`, abi: TRANSIENT_BASE_ABI, priceWei: priceInWei, }); From d94919fc46c6a39e8479a73a6dfbb111388a3cc2 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Tue, 16 Jul 2024 21:33:39 +0100 Subject: [PATCH 06/22] chore: add token id to contract parameters --- src/ingestors/transient-base/index.ts | 17 +++++++++++++---- .../transient-base/offchain-metadata.ts | 2 ++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/ingestors/transient-base/index.ts b/src/ingestors/transient-base/index.ts index 8168430..7f493e3 100644 --- a/src/ingestors/transient-base/index.ts +++ b/src/ingestors/transient-base/index.ts @@ -61,8 +61,17 @@ export class TransientIngestor implements MintIngestor { } // asides name and image no other metadata we need is onchain so we can just use the offchain metadata - const { name, image, description, priceInWei, mintAddress, public_sale_start_at, public_sale_end_at, user } = - await getTransientBaseMintByAddressAndChain(resources, contract.chainId, contract.contractAddress); + const { + name, + image, + description, + priceInWei, + mintAddress, + public_sale_start_at, + public_sale_end_at, + token_id, + user, + } = await getTransientBaseMintByAddressAndChain(resources, contract.chainId, contract.contractAddress); mintBuilder.setName(name).setDescription(description).setFeaturedImageUrl(image); @@ -74,14 +83,14 @@ export class TransientIngestor implements MintIngestor { mintBuilder.setCreator({ name: user.name, imageUrl: user.image, - websiteUrl: user.website + websiteUrl: user.website, }); mintBuilder.setMintInstructions({ chainId, contractAddress: mintAddress, contractMethod: 'purchase', - contractParams: `["${contract.contractAddress}", 1, address, 1]`, + contractParams: `["${contract.contractAddress}", ${token_id}, address, 1]`, abi: TRANSIENT_BASE_ABI, priceWei: priceInWei, }); diff --git a/src/ingestors/transient-base/offchain-metadata.ts b/src/ingestors/transient-base/offchain-metadata.ts index 85266d0..95d69a8 100644 --- a/src/ingestors/transient-base/offchain-metadata.ts +++ b/src/ingestors/transient-base/offchain-metadata.ts @@ -41,6 +41,7 @@ export const getTransientBaseMintBySlug = async ( description: string; public_sale_start_at: string; public_sale_end_at: string; + token_id: number; user: { name: string; image: string; @@ -72,6 +73,7 @@ export const getTransientBaseMintBySlug = async ( description: data.description, public_sale_start_at: data.public_sale_start_at, public_sale_end_at: data.public_sale_end_at, + token_id: data.nft_token.token_id, user: { name: data.nft_contract.user.display_name, image: data.nft_contract.user.pfp, From f77411b69ac7b0546ada536b7366b44601425a3f Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Thu, 18 Jul 2024 13:11:50 -0400 Subject: [PATCH 07/22] Default to 0 --- src/ingestors/fxhash/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ingestors/fxhash/index.ts b/src/ingestors/fxhash/index.ts index c10c0e4..e4e526c 100644 --- a/src/ingestors/fxhash/index.ts +++ b/src/ingestors/fxhash/index.ts @@ -64,7 +64,7 @@ export class FxHashIngestor implements MintIngestor { chainId: contract.chainId, contractAddress, contractMethod: 'buy', - contractParams: `["${contract.contractAddress}", 1, 1, address]`, + contractParams: `["${contract.contractAddress}", 0, 1, address]`, abi: abi, priceWei: `${priceWei}`, }); From 916cd7a8d7cb524c59ce2dea608e61015e45b8f8 Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Thu, 18 Jul 2024 13:13:04 -0400 Subject: [PATCH 08/22] Fix test --- test/ingestors/fxhash.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/ingestors/fxhash.test.ts b/test/ingestors/fxhash.test.ts index 0a567b6..371c500 100644 --- a/test/ingestors/fxhash.test.ts +++ b/test/ingestors/fxhash.test.ts @@ -39,7 +39,7 @@ describe('fxhash', function () { expect(mintInstructions.contractAddress).to.equal('0x6e625892C739bFD960671Db5544E260757480725'); expect(mintInstructions.contractMethod).to.equal('buy'); - expect(mintInstructions.contractParams).to.equal('["0x914cf2d92b087C9C01a062111392163c3B35B60e", 1, 1, address]'); + expect(mintInstructions.contractParams).to.equal('["0x914cf2d92b087C9C01a062111392163c3B35B60e", 0, 1, address]'); expect(mintInstructions.priceWei).to.equal('4200000000000000'); expect(template.featuredImageUrl).to.equal('ipfs://Qmc9eKhAkQvt1mXq1pD5FP9ZnprBNuU2USq5rELKVdb9uf'); @@ -65,7 +65,7 @@ describe('fxhash', function () { expect(mintInstructions.contractAddress).to.equal('0x4bDcaC532143d8d35ed759189EE22E3704580b9D'); expect(mintInstructions.contractMethod).to.equal('buy'); - expect(mintInstructions.contractParams).to.equal('["0x755625dEfD0f1Bb90850d533f30176aa7a425f6E", 1, 1, address]'); + expect(mintInstructions.contractParams).to.equal('["0x755625dEfD0f1Bb90850d533f30176aa7a425f6E", 0, 1, address]'); expect(mintInstructions.priceWei).to.equal('500000000000000'); expect(template.featuredImageUrl).to.equal('ipfs://QmYV4LXoz18youcW7zREFFFVpPf6Tn1j4QRzmTi1cSPinb'); @@ -152,7 +152,7 @@ describe('fxhash', function () { expect(mintInstructions.contractAddress).to.equal('0x6e625892C739bFD960671Db5544E260757480725'); expect(mintInstructions.contractMethod).to.equal('buy'); - expect(mintInstructions.contractParams).to.equal('["0x914cf2d92b087C9C01a062111392163c3B35B60e", 1, 1, address]'); + expect(mintInstructions.contractParams).to.equal('["0x914cf2d92b087C9C01a062111392163c3B35B60e", 0, 1, address]'); expect(mintInstructions.priceWei).to.equal('4200000000000000'); expect(template.featuredImageUrl).to.equal('ipfs://Qmc9eKhAkQvt1mXq1pD5FP9ZnprBNuU2USq5rELKVdb9uf'); From de7dd799f495211d22bbc353f88c1a7f78793728 Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Thu, 18 Jul 2024 13:13:36 -0400 Subject: [PATCH 09/22] v1.0.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a48278..da7d827 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@floornfts/mobile-minting", - "version": "1.0.7", + "version": "1.0.8", "repository": "git://github.com/floornfts/mobile-minting.git", "main": "./dist/index.js", "module": "./dist/index.mjs", From fbb0e0ce40c25db72af598680f7d505079312811 Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Thu, 18 Jul 2024 20:14:03 -0400 Subject: [PATCH 10/22] Add simulations to tests & dry runs --- package.json | 1 + src/dry-run/index.ts | 12 ++++ src/ingestors/fxhash/index.ts | 6 +- src/ingestors/fxhash/offchain-metadata.ts | 10 +++- src/ingestors/prohibition-daily/abi.ts | 3 +- src/lib/simulation/simulation.ts | 73 +++++++++++++++++++++++ test/ingestors/fxhash.test.ts | 34 ++++++++++- test/ingestors/prohibition-daily.test.ts | 2 + test/shared/basic-ingestor-tests.ts | 17 +++++- yarn.lock | 50 ++++++++++++++++ 10 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 src/lib/simulation/simulation.ts diff --git a/package.json b/package.json index da7d827..6461e98 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "alchemy-sdk": "^3.3.1", "axios": "^1.7.2", "dotenv": "^16.4.5", + "ethers": "^6.13.1", "np": "^10.0.6", "ts-node": "10.9.1", "tslib": "^2.6.3" diff --git a/src/dry-run/index.ts b/src/dry-run/index.ts index 2e4740f..b5f0c62 100644 --- a/src/dry-run/index.ts +++ b/src/dry-run/index.ts @@ -1,6 +1,8 @@ +import { simulateEVMTransactionWithAlchemy } from "../lib/simulation/simulation"; import { ALL_MINT_INGESTORS } from "../ingestors"; import { mintIngestorResources } from "../lib/resources"; import dotenv from 'dotenv'; +import { EVMMintInstructions } from "src/lib"; dotenv.config(); const args = process.argv.slice(2); @@ -49,6 +51,16 @@ const resources = mintIngestorResources(); process.exit(1); } console.log(JSON.stringify(result, null, 2)); + + console.log('Simulating transaction....'); + const mintInstructions = result.mintInstructions as EVMMintInstructions; + const simulationResult = await simulateEVMTransactionWithAlchemy(mintInstructions); + if (simulationResult.success) { + console.log('✅ Simulation Success'); + } else { + console.log('❌ Simulation Failed'); + console.log(JSON.stringify(simulationResult, null, 2)); + } } catch (error) { console.error(error); process.exit(1); diff --git a/src/ingestors/fxhash/index.ts b/src/ingestors/fxhash/index.ts index e4e526c..43d1ad0 100644 --- a/src/ingestors/fxhash/index.ts +++ b/src/ingestors/fxhash/index.ts @@ -36,7 +36,7 @@ export class FxHashIngestor implements MintIngestor { throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Project not found'); } - const { priceWei, type } = fxHashGetPricingFromParams(token); + const { priceWei, type, reserveId } = fxHashGetPricingFromParams(token); if (!type) { throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Could not resolve mint type'); @@ -58,13 +58,13 @@ export class FxHashIngestor implements MintIngestor { websiteUrl: token.author.account.profile.website, description: token.author.account.profile.description, twitterUsername: token.author.account.profile.twitter?.split('/').pop(), - }) + }); mintBuilder.setMintInstructions({ chainId: contract.chainId, contractAddress, contractMethod: 'buy', - contractParams: `["${contract.contractAddress}", 0, 1, address]`, + contractParams: `["${contract.contractAddress}", ${reserveId}, 1, address]`, abi: abi, priceWei: `${priceWei}`, }); diff --git a/src/ingestors/fxhash/offchain-metadata.ts b/src/ingestors/fxhash/offchain-metadata.ts index 430c19a..071bb0f 100644 --- a/src/ingestors/fxhash/offchain-metadata.ts +++ b/src/ingestors/fxhash/offchain-metadata.ts @@ -157,6 +157,11 @@ export function fxHashGetPricingFromParams( const { pricingFixed, pricingDutchAuction, isFrame } = generativeToken; let price = 0; let type: FxHashPricingType | undefined = isFrame ? FxHashPricingType.FRAME : undefined; + let reserveId: number = 1; + + if (!!generativeToken.openEditionsEndsAt) { + reserveId = 0; + } if (pricingFixed) { type = type || FxHashPricingType.FIXED; price = pricingFixed.price; @@ -166,7 +171,10 @@ export function fxHashGetPricingFromParams( price = pricingDutchAuction.restingPrice; } - return { priceWei: price, type: type } + if (type === FxHashPricingType.DUTCH_AUCTION) { + } + + return { priceWei: price, type: type, reserveId: reserveId } } const BASE_FRAME_CONTRACT_ADDRESS = '0x6e625892C739bFD960671Db5544E260757480725'; diff --git a/src/ingestors/prohibition-daily/abi.ts b/src/ingestors/prohibition-daily/abi.ts index 106b322..91e7819 100644 --- a/src/ingestors/prohibition-daily/abi.ts +++ b/src/ingestors/prohibition-daily/abi.ts @@ -12,7 +12,6 @@ export const PROHIBITION_DAILY_ABI = [ stateMutability: 'view', type: 'function', }, - { inputs: [], name: 'saleStart', @@ -88,5 +87,5 @@ export const PROHIBITION_DAILY_ABI = [ outputs: [], stateMutability: 'payable', type: 'function', - }, + } ]; diff --git a/src/lib/simulation/simulation.ts b/src/lib/simulation/simulation.ts new file mode 100644 index 0000000..896a1a0 --- /dev/null +++ b/src/lib/simulation/simulation.ts @@ -0,0 +1,73 @@ +import { Alchemy, BigNumber, Network, Utils } from "alchemy-sdk"; +import { EVMMintInstructions } from "../types"; +export const SIGNER1_WALLET = '0x965EF172b303B0BcdC38669DF1De3c26BAD2dB8a'; +export const TEST_RECIPIENT = '0x0cc9601298361e844451a7e35e1d7fcd72750e47'; + +export const NETWORKS: Record = { + 1: Network.ETH_MAINNET, + 5: Network.ETH_GOERLI, + 137: Network.MATIC_MAINNET, + 42161: Network.ARB_MAINNET, + 80001: Network.MATIC_MUMBAI, + 8453: Network.BASE_MAINNET, + 84531: Network.BASE_GOERLI, + 11155111: Network.ETH_SEPOLIA +}; + +export const simulateEVMTransactionWithAlchemy = async (mintInstructions: EVMMintInstructions, blockNumber?: string): Promise<{ message: string, success: boolean, rawSimulationResult: any }> => { + const alchemy = new Alchemy({ + apiKey: process.env.ALCHEMY_API_KEY, + network: NETWORKS[mintInstructions.chainId], + }); + + const abi = mintInstructions.abi; + const iface = new Utils.Interface(abi); + + const paramsArray = prepareContractParams(mintInstructions.contractParams); + + const data = iface.encodeFunctionData(mintInstructions.contractMethod, paramsArray); + + const tx = { + to: mintInstructions.contractAddress, + from: SIGNER1_WALLET, + data: data, + value: BigNumber.from(mintInstructions.priceWei || '0').toHexString().replace('0x0', '0x'), + }; + + const simResult = await alchemy.transact.simulateExecution(tx, blockNumber ? blockNumber : undefined); + + const failedCalls: any = simResult.calls.find((call) => call.error) || []; + + const message = failedCalls?.revertReason; + const success = failedCalls.length === 0; + + if (!success) { + console.log(`Error Simulating: ${message}, ${JSON.stringify(simResult)}`); + } + + return { message, success, rawSimulationResult: simResult }; +} + +export const prepareContractParams = (template: string): any[] => { + const regex = /{{(\w+)}}|tokenId|address|encodedAddress/g; + + const replacedTemplate = template.replace(regex, (match) => { + switch (match) { + case 'address': + return `"${TEST_RECIPIENT}"`; + case 'encodedAddress': + return `"${encodeAddress(TEST_RECIPIENT)}"`; + default: + return '""'; + } + }); + + return JSON.parse(replacedTemplate); + }; + + export const encodeAddress = (address: string) => { + const rawAddress = address.replace('0x', ''); + + return '0x' + rawAddress.padStart(64, '0'); + }; + \ No newline at end of file diff --git a/test/ingestors/fxhash.test.ts b/test/ingestors/fxhash.test.ts index 371c500..5ef04e9 100644 --- a/test/ingestors/fxhash.test.ts +++ b/test/ingestors/fxhash.test.ts @@ -3,8 +3,36 @@ import { FxHashIngestor } from '../../src/ingestors/fxhash'; import { mintIngestorResources } from '../../src/lib/resources'; import { EVMMintInstructions } from '../../src/lib/types/mint-template'; import { MintTemplateBuilder } from '../../src/lib/builder/mint-template-builder'; +import { basicIngestorTests } from '../shared/basic-ingestor-tests'; describe('fxhash', function () { + + basicIngestorTests( + new FxHashIngestor(), + mintIngestorResources(), + { + successUrls: [ + 'https://fxhash.xyz/generative/slug/allegro', + 'https://fxhash.xyz/generative/slug/graphomania', + 'https://www.fxhash.xyz/generative/slug/the-space-in-between' + ], + failureUrls: ['https://example.com'], + successContracts: [{ + chainId: 8453, + contractAddress: '0x914cf2d92b087C9C01a062111392163c3B35B60e' + }], + failureContracts: [ + { + chainId: 8453, + contractAddress: '0x6140F00e4Ff3936702E68744f2b5978885464cbB' + } + ] + }, + { + // 8453: '0x107A60C' + } + ); + it('supportsUrl: Returns false for an unsupported URL', async function () { const ingestor = new FxHashIngestor(); const url = 'https://example.com'; @@ -39,7 +67,7 @@ describe('fxhash', function () { expect(mintInstructions.contractAddress).to.equal('0x6e625892C739bFD960671Db5544E260757480725'); expect(mintInstructions.contractMethod).to.equal('buy'); - expect(mintInstructions.contractParams).to.equal('["0x914cf2d92b087C9C01a062111392163c3B35B60e", 0, 1, address]'); + expect(mintInstructions.contractParams).to.equal('["0x914cf2d92b087C9C01a062111392163c3B35B60e", 1, 1, address]'); expect(mintInstructions.priceWei).to.equal('4200000000000000'); expect(template.featuredImageUrl).to.equal('ipfs://Qmc9eKhAkQvt1mXq1pD5FP9ZnprBNuU2USq5rELKVdb9uf'); @@ -65,7 +93,7 @@ describe('fxhash', function () { expect(mintInstructions.contractAddress).to.equal('0x4bDcaC532143d8d35ed759189EE22E3704580b9D'); expect(mintInstructions.contractMethod).to.equal('buy'); - expect(mintInstructions.contractParams).to.equal('["0x755625dEfD0f1Bb90850d533f30176aa7a425f6E", 0, 1, address]'); + expect(mintInstructions.contractParams).to.equal('["0x755625dEfD0f1Bb90850d533f30176aa7a425f6E", 1, 1, address]'); expect(mintInstructions.priceWei).to.equal('500000000000000'); expect(template.featuredImageUrl).to.equal('ipfs://QmYV4LXoz18youcW7zREFFFVpPf6Tn1j4QRzmTi1cSPinb'); @@ -152,7 +180,7 @@ describe('fxhash', function () { expect(mintInstructions.contractAddress).to.equal('0x6e625892C739bFD960671Db5544E260757480725'); expect(mintInstructions.contractMethod).to.equal('buy'); - expect(mintInstructions.contractParams).to.equal('["0x914cf2d92b087C9C01a062111392163c3B35B60e", 0, 1, address]'); + expect(mintInstructions.contractParams).to.equal('["0x914cf2d92b087C9C01a062111392163c3B35B60e", 1, 1, address]'); expect(mintInstructions.priceWei).to.equal('4200000000000000'); expect(template.featuredImageUrl).to.equal('ipfs://Qmc9eKhAkQvt1mXq1pD5FP9ZnprBNuU2USq5rELKVdb9uf'); diff --git a/test/ingestors/prohibition-daily.test.ts b/test/ingestors/prohibition-daily.test.ts index eb6b92a..38c1588 100644 --- a/test/ingestors/prohibition-daily.test.ts +++ b/test/ingestors/prohibition-daily.test.ts @@ -23,6 +23,8 @@ describe('prohibition-daily', function () { { chainId: 8453, contractAddress: '0x965ef172b303b0bcdc38669df1de3c26bad2db8a' }, { chainId: 8453, contractAddress: 'derp' } ] + }, { + 8453: '0xF469C6' }); it('createMintTemplateForUrl: Returns a mint template for a supported URL', async function () { const ingestor = new ProhibitionDailyIngestor(); diff --git a/test/shared/basic-ingestor-tests.ts b/test/shared/basic-ingestor-tests.ts index 78aa2c6..71897fc 100644 --- a/test/shared/basic-ingestor-tests.ts +++ b/test/shared/basic-ingestor-tests.ts @@ -1,6 +1,8 @@ import { expect } from "chai"; import { MintContractOptions, MintIngestor, MintIngestorResources } from "../../src/lib/types/mint-ingestor"; import { MintTemplateBuilder } from "../../src/lib/builder/mint-template-builder"; +import { simulateEVMTransactionWithAlchemy } from "../../src/lib/simulation/simulation"; +import { EVMMintInstructions } from "../../src/lib/types"; export const basicIngestorTests = ( ingestor: MintIngestor, @@ -9,9 +11,12 @@ export const basicIngestorTests = ( successUrls: string[], failureUrls: string[], successContracts: MintContractOptions[], - failureContracts: MintContractOptions[] - } + failureContracts: MintContractOptions[], + }, + simulationBlocks: { [key: number]: string } = {} ) => { + const shouldSimulate = process.env.SIMULATE_DURING_TESTS === "true"; + describe(`${ingestor.constructor.name}-auto`, function () { const { successUrls, failureUrls, successContracts, failureContracts } = cases; @@ -46,6 +51,12 @@ export const basicIngestorTests = ( // Verify that the mint template passed validation const builder = new MintTemplateBuilder(template); builder.validateMintTemplate(); + + if (shouldSimulate) { + const mintInstructions = template.mintInstructions as EVMMintInstructions; + const result = await simulateEVMTransactionWithAlchemy(mintInstructions, simulationBlocks[mintInstructions.chainId]); + expect(result.success).to.be.true; + } } }); it('createMintForContract: Returns a mint template for a supported contract', async function () { @@ -58,4 +69,4 @@ export const basicIngestorTests = ( } }); }); -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index ca4f4e2..8e0fc50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adraffy/ens-normalize@1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" + integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.13": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" @@ -597,6 +602,18 @@ dependencies: call-bind "^1.0.7" +"@noble/curves@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + +"@noble/hashes@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -708,6 +725,11 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b" integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg== +"@types/node@18.15.13": + version "18.15.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" + integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== + "@types/node@^20.14.6": version "20.14.6" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.6.tgz#f3c19ffc98c2220e18de259bb172dd4d892a6075" @@ -837,6 +859,11 @@ aes-js@3.0.0: resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== +aes-js@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" + integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== + aggregate-error@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-4.0.1.tgz#25091fe1573b9e0be892aeda15c7c66a545f758e" @@ -1854,6 +1881,19 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +ethers@^6.13.1: + version "6.13.1" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.1.tgz#2b9f9c7455cde9d38b30fe6589972eb083652961" + integrity sha512-hdJ2HOxg/xx97Lm9HdCWk949BfYqYWpyw4//78SiwOLgASyfrNszfMUNB2joKjvGUdwhHfaiMMFFwacVVoLR9A== + dependencies: + "@adraffy/ens-normalize" "1.10.1" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "18.15.13" + aes-js "4.0.0-beta.5" + tslib "2.4.0" + ws "8.17.1" + event-emitter@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" @@ -4202,6 +4242,11 @@ tsc-alias@^1.8.10: normalize-path "^3.0.0" plimit-lit "^1.2.6" +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -4493,6 +4538,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + xdg-basedir@^5.0.1, xdg-basedir@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz#1efba19425e73be1bc6f2a6ceb52a3d2c884c0c9" From c82fd0099684f10c2d5ef50a5f9c9c05039ec3d0 Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Fri, 19 Jul 2024 08:21:59 -0400 Subject: [PATCH 11/22] SIMULATE_DURING_TESTS=true --- .env.sample | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.sample b/.env.sample index 15db10f..0062a7e 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,2 @@ ALCHEMY_API_KEY= +SIMULATE_DURING_TESTS=true From 13ca8ee04bd5c30e98577af2d5b75e4354d6a71a Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 19 Jul 2024 13:47:27 +0100 Subject: [PATCH 12/22] remove prohibition constructor --- test/ingestors/transient.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/ingestors/transient.test.ts b/test/ingestors/transient.test.ts index 7c47f7f..815c161 100644 --- a/test/ingestors/transient.test.ts +++ b/test/ingestors/transient.test.ts @@ -1,7 +1,6 @@ import { EVMMintInstructions, SolanaMintInstructions } from '../../src/lib/types/mint-template'; import { MintTemplateBuilder } from '../../src/lib/builder/mint-template-builder'; -import { ProhibitionDailyIngestor } from '../../src/ingestors/prohibition-daily'; import { TransientIngestor } from '../../src/ingestors/transient-base/index'; import { basicIngestorTests } from '../shared/basic-ingestor-tests'; import { expect } from 'chai'; @@ -10,7 +9,7 @@ import { mintIngestorResources } from '../../src/lib/resources'; const resources = mintIngestorResources(); describe('Transient', function () { - basicIngestorTests(new ProhibitionDailyIngestor(), resources, { + basicIngestorTests(new TransientIngestor(), resources, { successUrls: [ 'https://www.transient.xyz/stacks/kansas-smile' ], @@ -27,7 +26,7 @@ describe('Transient', function () { ] }); it('createMintTemplateForUrl: Returns a mint template for a supported URL', async function () { - const ingestor = new ProhibitionDailyIngestor(); + const ingestor = new TransientIngestor(); const url = 'https://www.transient.xyz/stacks/kansas-smile'; const template = await ingestor.createMintTemplateForUrl(resources, url); From d870b7cb942082b2a2b73e477debd908d4c69a97 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 19 Jul 2024 17:24:58 +0100 Subject: [PATCH 13/22] fix issues with contract params --- src/ingestors/transient-base/index.ts | 10 +- .../transient-base/offchain-metadata.ts | 2 +- test/ingestors/transient.test.ts | 146 +++++++++--------- 3 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/ingestors/transient-base/index.ts b/src/ingestors/transient-base/index.ts index 7f493e3..cb910ff 100644 --- a/src/ingestors/transient-base/index.ts +++ b/src/ingestors/transient-base/index.ts @@ -19,8 +19,12 @@ export class TransientIngestor implements MintIngestor { if (new URL(url).hostname !== 'www.transient.xyz') { return false; } - const { chainId, contractAddress } = await getTransientBaseMintByURL(resources, url); - return !!chainId && !!contractAddress; + try { + const { chainId, contractAddress } = await getTransientBaseMintByURL(resources, url); + return !!chainId && !!contractAddress; + } catch (error) { + return false; + } } async supportsContract(resources: MintIngestorResources, contract: MintContractOptions): Promise { @@ -90,7 +94,7 @@ export class TransientIngestor implements MintIngestor { chainId, contractAddress: mintAddress, contractMethod: 'purchase', - contractParams: `["${contract.contractAddress}", ${token_id}, address, 1]`, + contractParams: `[address, ${token_id}, address, 1, 0, []]`, abi: TRANSIENT_BASE_ABI, priceWei: priceInWei, }); diff --git a/src/ingestors/transient-base/offchain-metadata.ts b/src/ingestors/transient-base/offchain-metadata.ts index 95d69a8..98be518 100644 --- a/src/ingestors/transient-base/offchain-metadata.ts +++ b/src/ingestors/transient-base/offchain-metadata.ts @@ -115,7 +115,7 @@ export const transientSupports = async (contract: MintContractOptions, fetcher: const { chainId, contractAddress } = contract; let response: AxiosResponse; try { - response = await fetcher.get(`https://api.transient.xyz/v1/catalog/nfts?chain=${chainId}&format=json`); + response = await fetcher.get(`https://api.transient.xyz/v1/catalog/nfts?chainId=${chainId}&address=${contractAddress}`); } catch (error) { return false; } diff --git a/test/ingestors/transient.test.ts b/test/ingestors/transient.test.ts index 815c161..2005f79 100644 --- a/test/ingestors/transient.test.ts +++ b/test/ingestors/transient.test.ts @@ -9,78 +9,74 @@ import { mintIngestorResources } from '../../src/lib/resources'; const resources = mintIngestorResources(); describe('Transient', function () { - basicIngestorTests(new TransientIngestor(), resources, { - successUrls: [ - 'https://www.transient.xyz/stacks/kansas-smile' - ], - failureUrls: [ - 'https://www.transient.xyz/stacks/kansas-smiles', - 'https://www.transient.xyz/stacks' - ], - successContracts: [ - { chainId: 8453, contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8' } - ], - failureContracts: [ - { chainId: 8453, contractAddress: '0x965ef172b303b0bcdc38669df1de3c26bad2db8a' }, - { chainId: 8453, contractAddress: 'derp' } - ] - }); - it('createMintTemplateForUrl: Returns a mint template for a supported URL', async function () { - const ingestor = new TransientIngestor(); - const url = 'https://www.transient.xyz/stacks/kansas-smile'; - - const template = await ingestor.createMintTemplateForUrl(resources, url); - - // Verify that the mint template passed validation - const builder = new MintTemplateBuilder(template); - builder.validateMintTemplate(); - - expect(template.name).to.equal('Kansas Smile'); - expect(template.description).to.equal("'Kansas Smile' explores how our longing for the past connects with modern technology. It shows how digital tools can recreate and reimagine the past, allowing us to experience nostalgia in new ways through AI.\n\nExhibited at 'Digital Dreams' Kansas City 2024, part of the Digital Collage exhibition.\n\n(Analog and digital processes with AI)"); - const mintInstructions = template.mintInstructions as EVMMintInstructions; - - expect(mintInstructions.contractAddress).to.equal('0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8'); - expect(mintInstructions.contractMethod).to.equal('purchase'); - expect(mintInstructions.contractParams).to.equal('["address", 1, 1, address]'); - expect(mintInstructions.priceWei).to.equal('20000000000000000'); - - expect(template.featuredImageUrl?.length).to.be.matches(/https:\/\/ipfs.io\/ipfs\/.+\/media/); - - expect(template.marketingUrl).to.equal(url); - expect(template.availableForPurchaseStart?.getTime()).to.equal(1717777800000); - expect(template.availableForPurchaseEnd?.getTime()).to.equal(3294577800000); - }); - - it('createMintTemplateForContract: Returns a mint template for a supported contract', async function () { - const ingestor = new TransientIngestor(); - const contract = { - chainId: 8453, - contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8', - url: 'https://www.transient.xyz/stacks/kansas-smile' - }; - - const template = await ingestor.createMintForContract(resources, contract); - - // Verify that the mint template passed validation - const builder = new MintTemplateBuilder(template); - builder.validateMintTemplate(); - - expect(template.name).to.equal('Kansas Smile'); - expect(template.description).to.equal("'Kansas Smile' explores how our longing for the past connects with modern technology. It shows how digital tools can recreate and reimagine the past, allowing us to experience nostalgia in new ways through AI.\n\nExhibited at 'Digital Dreams' Kansas City 2024, part of the Digital Collage exhibition.\n\n(Analog and digital processes with AI)"); - const mintInstructions = template.mintInstructions as EVMMintInstructions; - - expect(mintInstructions.contractAddress).to.equal('0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8'); - expect(mintInstructions.contractMethod).to.equal('purchase'); - expect(mintInstructions.contractParams).to.equal('["address", 1, 1, address]'); - expect(mintInstructions.priceWei).to.equal('20000000000000000'); - const mintAddress = template.mintInstructions as SolanaMintInstructions; - - expect(mintAddress).to.equal('0x32953d7ae37b05075b88c34e800ae80c1cb1b794'); - - expect(template.featuredImageUrl?.length).to.be.greaterThan(0); - - expect(template.marketingUrl).to.equal(contract.url); - expect(template.availableForPurchaseStart?.getTime()).to.equal(1717777800000); - expect(template.availableForPurchaseEnd?.getTime()).to.equal(3294577800000); - }); -}); \ No newline at end of file + basicIngestorTests(new TransientIngestor(), resources, { + successUrls: ['https://www.transient.xyz/stacks/kansas-smile'], + failureUrls: ['https://www.transient.xyz/stacks/kansas-smiles', 'https://www.transient.xyz/stacks'], + successContracts: [{ chainId: 8453, contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8' }], + failureContracts: [ + { chainId: 8453, contractAddress: '0x965ef172b303b0bcdc38669df1de3c26bad2db8a' }, + { chainId: 8453, contractAddress: 'derp' }, + ], + }); + it('createMintTemplateForUrl: Returns a mint template for a supported URL', async function () { + const ingestor = new TransientIngestor(); + const url = 'https://www.transient.xyz/stacks/kansas-smile'; + + const template = await ingestor.createMintTemplateForUrl(resources, url); + + // Verify that the mint template passed validation + const builder = new MintTemplateBuilder(template); + builder.validateMintTemplate(); + + expect(template.name).to.equal('Kansas Smile'); + expect(template.description).to.equal( + "'Kansas Smile' explores how our longing for the past connects with modern technology. It shows how digital tools can recreate and reimagine the past, allowing us to experience nostalgia in new ways through AI.\n\nExhibited at 'Digital Dreams' Kansas City 2024, part of the Digital Collage exhibition.\n\n(Analog and digital processes with AI)", + ); + const mintInstructions = template.mintInstructions as EVMMintInstructions; + + expect(mintInstructions.contractAddress).to.equal('0x32953d7ae37b05075b88c34e800ae80c1cb1b794'); + expect(mintInstructions.contractMethod).to.equal('purchase'); + expect(mintInstructions.contractParams).to.equal('[address, 1, address, 1, 0, []]'); + expect(mintInstructions.priceWei).to.equal('20000000000000000'); + + expect(template.featuredImageUrl).to.be.matches(/https:\/\/ipfs.io\/ipfs\/.+\/media/); + + expect(template.marketingUrl).to.equal(url); + expect(template.availableForPurchaseStart?.getTime()).to.equal(1717777800000); + expect(template.availableForPurchaseEnd?.getTime()).to.equal(3294577800000); + }); + + it('createMintTemplateForContract: Returns a mint template for a supported contract', async function () { + const ingestor = new TransientIngestor(); + const contract = { + chainId: 8453, + contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8', + url: 'https://www.transient.xyz/stacks/kansas-smile', + }; + + const template = await ingestor.createMintForContract(resources, contract); + + // Verify that the mint template passed validation + const builder = new MintTemplateBuilder(template); + builder.validateMintTemplate(); + + expect(template.name).to.equal('Kansas Smile'); + expect(template.description).to.equal( + "'Kansas Smile' explores how our longing for the past connects with modern technology. It shows how digital tools can recreate and reimagine the past, allowing us to experience nostalgia in new ways through AI.\n\nExhibited at 'Digital Dreams' Kansas City 2024, part of the Digital Collage exhibition.\n\n(Analog and digital processes with AI)", + ); + const mintInstructions = template.mintInstructions as EVMMintInstructions; + + expect(mintInstructions.contractAddress).to.equal('0x32953d7ae37b05075b88c34e800ae80c1cb1b794'); + expect(mintInstructions.contractMethod).to.equal('purchase'); + expect(mintInstructions.contractParams).to.equal('[address, 1, address, 1, 0, []]'); + expect(mintInstructions.priceWei).to.equal('20000000000000000'); + + expect(template.mintOutputContract?.address).to.equal('0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8'); + + expect(template.featuredImageUrl?.length).to.be.greaterThan(0); + + expect(template.marketingUrl).to.equal(contract.url); + expect(template.availableForPurchaseStart?.getTime()).to.equal(1717777800000); + expect(template.availableForPurchaseEnd?.getTime()).to.equal(3294577800000); + }); +}); From ebda82e0790191f2eece96509bc46e47751a23a4 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Sat, 20 Jul 2024 22:00:48 +0100 Subject: [PATCH 14/22] add protocol fee support from onchain metadata --- src/ingestors/transient-base/index.ts | 8 ++++++-- src/ingestors/transient-base/onchain-metadata.ts | 15 +++++++++++++++ test/ingestors/transient.test.ts | 13 ++++++++----- 3 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 src/ingestors/transient-base/onchain-metadata.ts diff --git a/src/ingestors/transient-base/index.ts b/src/ingestors/transient-base/index.ts index cb910ff..7a216e2 100644 --- a/src/ingestors/transient-base/index.ts +++ b/src/ingestors/transient-base/index.ts @@ -7,8 +7,10 @@ import { transientSupports, } from './offchain-metadata'; +import { BigNumber } from 'alchemy-sdk'; import { MintTemplateBuilder } from '../../lib/builder/mint-template-builder'; import { TRANSIENT_BASE_ABI } from './abi'; +import { getTransientProtocolFeeInEth } from './onchain-metadata'; export class TransientIngestor implements MintIngestor { configuration = { @@ -78,6 +80,8 @@ export class TransientIngestor implements MintIngestor { } = await getTransientBaseMintByAddressAndChain(resources, contract.chainId, contract.contractAddress); mintBuilder.setName(name).setDescription(description).setFeaturedImageUrl(image); + const protocolFee = await getTransientProtocolFeeInEth(chainId, mintAddress, resources.alchemy); + const totalPrice = BigNumber.from(priceInWei).add(BigNumber.from(protocolFee)).toString(); mintBuilder.setMintOutputContract({ chainId, @@ -94,9 +98,9 @@ export class TransientIngestor implements MintIngestor { chainId, contractAddress: mintAddress, contractMethod: 'purchase', - contractParams: `[address, ${token_id}, address, 1, 0, []]`, + contractParams: `["${contractAddress}", ${token_id}, address, 1, 0, []]`, abi: TRANSIENT_BASE_ABI, - priceWei: priceInWei, + priceWei: totalPrice, }); const startDate = public_sale_start_at ? new Date(public_sale_start_at) : new Date(); diff --git a/src/ingestors/transient-base/onchain-metadata.ts b/src/ingestors/transient-base/onchain-metadata.ts new file mode 100644 index 0000000..811c8ff --- /dev/null +++ b/src/ingestors/transient-base/onchain-metadata.ts @@ -0,0 +1,15 @@ +import { Alchemy, Contract } from 'alchemy-sdk'; + +import { TRANSIENT_BASE_ABI } from './abi'; + +const getContract = async (contractAddress: string, alchemy: Alchemy): Promise => { + const ethersProvider = await alchemy.config.getProvider(); + const contract = new Contract(contractAddress, TRANSIENT_BASE_ABI, ethersProvider); + return contract; +}; + +export const getTransientProtocolFeeInEth = async (chainId: number, mintContractAddress: string, alchemy: Alchemy) => { + const contract = await getContract(mintContractAddress, alchemy); + const protocolFee = await contract.functions.protocolFee(); + return `${protocolFee}`; +}; diff --git a/test/ingestors/transient.test.ts b/test/ingestors/transient.test.ts index 2005f79..c9a2efc 100644 --- a/test/ingestors/transient.test.ts +++ b/test/ingestors/transient.test.ts @@ -12,7 +12,10 @@ describe('Transient', function () { basicIngestorTests(new TransientIngestor(), resources, { successUrls: ['https://www.transient.xyz/stacks/kansas-smile'], failureUrls: ['https://www.transient.xyz/stacks/kansas-smiles', 'https://www.transient.xyz/stacks'], - successContracts: [{ chainId: 8453, contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8' }], + successContracts: [ + { chainId: 8453, contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8' } + // { chainId: 8453, contractAddress: '0x8a2e6797d5930527272642a41f36cedc84a3935e' }, + ], failureContracts: [ { chainId: 8453, contractAddress: '0x965ef172b303b0bcdc38669df1de3c26bad2db8a' }, { chainId: 8453, contractAddress: 'derp' }, @@ -36,8 +39,8 @@ describe('Transient', function () { expect(mintInstructions.contractAddress).to.equal('0x32953d7ae37b05075b88c34e800ae80c1cb1b794'); expect(mintInstructions.contractMethod).to.equal('purchase'); - expect(mintInstructions.contractParams).to.equal('[address, 1, address, 1, 0, []]'); - expect(mintInstructions.priceWei).to.equal('20000000000000000'); + expect(mintInstructions.contractParams).to.equal('["0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8", 1, address, 1, 0, []]'); + expect(mintInstructions.priceWei).to.equal('20900000000000000'); expect(template.featuredImageUrl).to.be.matches(/https:\/\/ipfs.io\/ipfs\/.+\/media/); @@ -68,8 +71,8 @@ describe('Transient', function () { expect(mintInstructions.contractAddress).to.equal('0x32953d7ae37b05075b88c34e800ae80c1cb1b794'); expect(mintInstructions.contractMethod).to.equal('purchase'); - expect(mintInstructions.contractParams).to.equal('[address, 1, address, 1, 0, []]'); - expect(mintInstructions.priceWei).to.equal('20000000000000000'); + expect(mintInstructions.contractParams).to.equal('["0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8", 1, address, 1, 0, []]'); + expect(mintInstructions.priceWei).to.equal('20900000000000000'); expect(template.mintOutputContract?.address).to.equal('0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8'); From 2d5bf39d230c25959a1176260f4680bce180ff8a Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Sun, 21 Jul 2024 21:00:44 +0100 Subject: [PATCH 15/22] chore: added tests for ERC721TL and ERC7160TL contracts --- test/ingestors/transient.test.ts | 173 ++++++++++++++++++++++--------- 1 file changed, 122 insertions(+), 51 deletions(-) diff --git a/test/ingestors/transient.test.ts b/test/ingestors/transient.test.ts index c9a2efc..8c13410 100644 --- a/test/ingestors/transient.test.ts +++ b/test/ingestors/transient.test.ts @@ -13,7 +13,7 @@ describe('Transient', function () { successUrls: ['https://www.transient.xyz/stacks/kansas-smile'], failureUrls: ['https://www.transient.xyz/stacks/kansas-smiles', 'https://www.transient.xyz/stacks'], successContracts: [ - { chainId: 8453, contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8' } + { chainId: 8453, contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8' }, // { chainId: 8453, contractAddress: '0x8a2e6797d5930527272642a41f36cedc84a3935e' }, ], failureContracts: [ @@ -21,65 +21,136 @@ describe('Transient', function () { { chainId: 8453, contractAddress: 'derp' }, ], }); - it('createMintTemplateForUrl: Returns a mint template for a supported URL', async function () { + const testCases = [ + { + name: 'ERC1155TL', + input: { + url: 'https://www.transient.xyz/stacks/kansas-smile', + resources, + }, + chainId: 8453, + expected: { + name: 'Kansas Smile', + description: + "'Kansas Smile' explores how our longing for the past connects with modern technology. It shows how digital tools can recreate and reimagine the past, allowing us to experience nostalgia in new ways through AI.\n\nExhibited at 'Digital Dreams' Kansas City 2024, part of the Digital Collage exhibition.\n\n(Analog and digital processes with AI)", + contractAddress: '0x32953d7ae37b05075b88c34e800ae80c1cb1b794', + contractMethod: 'purchase', + contractParams: '["0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8", 1, address, 1, 0, []]', + priceWei: '20900000000000000', + featuredImageUrlPattern: /https:\/\/ipfs.io\/ipfs\/.+\/media/, + availableForPurchaseStart: 1717777800000, + availableForPurchaseEnd: 3294577800000, + outputContractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8', + }, + }, + { + name: 'ERC7160TL', + input: { + url: 'https://www.transient.xyz/stacks/16384', + resources, + }, + chainId: 8453, + expected: { + name: '16384 Squares', + description: + 'By exposing a 1px 8x8 grid to simulated physics turbulence, the grid interferes with the diodes in a ever changing moiré patterns, revealing a subtle breeze of random generated palm leaves behind the grid. \n\nThe colors of the leaves are then transferred back into the squares of the grid as large pixels.\n\n16384 are the amount of emitting diodes on the 128px LED artworks from Spøgelsesmaskinen. \n\nEach token holds both a 4K MP4 version and the original 128x128px GIF artwork for low res LED screens.\nOn mint a new generative variation of the art piece will be added by the artist to the token, also as both MP4 and GIF.\n\nEach piece is 60 seconds loop, of 2161 frames\n\nCollectors are able to require a physical custom assembled LED display made specifically for this artwork on https://spogel.xyz/led', + contractAddress: '0x384092784cfaa91efaa77870c04d958e20840242', + contractMethod: 'purchase', + contractParams: '["0xd2f9c0ef092d7ffd1a5de43b6ee546065461887d", 0, address, 1, 0, []]', + priceWei: '50900000000000000', + featuredImageUrlPattern: /^https:\/\/dv0xp0uwyoh8r\.cloudfront\.net\/stacks\/[0-9a-fA-F\-]+\/media/, + availableForPurchaseStart: 1720465200000, + availableForPurchaseEnd: 3297265200000, + outputContractAddress: '0xd2f9c0ef092d7ffd1a5de43b6ee546065461887d', + }, + }, + { + name: 'ERC721TL', + input: { + url: 'https://www.transient.xyz/stacks/volumina-8', + resources, + }, + chainId: 8453, + expected: { + name: 'Volumina 8', + description: 'Volumina 8 is one of a series of manipulated image sequences derived from photography of Australian landscapes taken in Spring 2022. Various phone and laptop apps have been utilised to construct mosaics, kaleidoscopic patterns and other structures from the original photos, in an exploration of expanding and contracting symmetries implied and derived from organic forms and processes of the natural world.', + contractAddress: '0x384092784cfaa91efaa77870c04d958e20840242', + contractMethod: 'purchase', + contractParams: '["0x999f084f06ee49a3deef0c9d1511d2f040da4034", 0, address, 1, 0, []]', + priceWei: '150900000000000000', + featuredImageUrlPattern: /^https:\/\/dv0xp0uwyoh8r\.cloudfront\.net\/stacks\/[0-9a-fA-F\-]+\/media/, + availableForPurchaseStart: 1720600200000, + availableForPurchaseEnd: 3297400200000, + outputContractAddress: '0x999f084f06ee49a3deef0c9d1511d2f040da4034', + }, + }, + ]; + describe(`createMintTemplateForUrl: Returns a mint template for`, async function () { const ingestor = new TransientIngestor(); - const url = 'https://www.transient.xyz/stacks/kansas-smile'; + testCases.forEach(({ name, input, expected }) => { + it(`${name} stack`, async function () { + const template = await ingestor.createMintTemplateForUrl(input.resources, input.url); - const template = await ingestor.createMintTemplateForUrl(resources, url); + // Verify that the mint template passed validation + const builder = new MintTemplateBuilder(template); + builder.validateMintTemplate(); - // Verify that the mint template passed validation - const builder = new MintTemplateBuilder(template); - builder.validateMintTemplate(); + expect(template.name).to.equal(expected.name); + expect(template.description).to.equal(expected.description); - expect(template.name).to.equal('Kansas Smile'); - expect(template.description).to.equal( - "'Kansas Smile' explores how our longing for the past connects with modern technology. It shows how digital tools can recreate and reimagine the past, allowing us to experience nostalgia in new ways through AI.\n\nExhibited at 'Digital Dreams' Kansas City 2024, part of the Digital Collage exhibition.\n\n(Analog and digital processes with AI)", - ); - const mintInstructions = template.mintInstructions as EVMMintInstructions; + const mintInstructions = template.mintInstructions as EVMMintInstructions; - expect(mintInstructions.contractAddress).to.equal('0x32953d7ae37b05075b88c34e800ae80c1cb1b794'); - expect(mintInstructions.contractMethod).to.equal('purchase'); - expect(mintInstructions.contractParams).to.equal('["0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8", 1, address, 1, 0, []]'); - expect(mintInstructions.priceWei).to.equal('20900000000000000'); + expect(mintInstructions.contractAddress).to.equal(expected.contractAddress); + expect(mintInstructions.contractMethod).to.equal(expected.contractMethod); + expect(mintInstructions.contractParams).to.equal(expected.contractParams); + expect(mintInstructions.priceWei).to.equal(expected.priceWei); + expect(template.mintOutputContract?.address).to.equal(expected.outputContractAddress); - expect(template.featuredImageUrl).to.be.matches(/https:\/\/ipfs.io\/ipfs\/.+\/media/); + expect(template.featuredImageUrl).to.match(expected.featuredImageUrlPattern); - expect(template.marketingUrl).to.equal(url); - expect(template.availableForPurchaseStart?.getTime()).to.equal(1717777800000); - expect(template.availableForPurchaseEnd?.getTime()).to.equal(3294577800000); + expect(template.marketingUrl).to.equal(input.url); + expect(template.availableForPurchaseStart?.getTime()).to.equal(expected.availableForPurchaseStart); + expect(template.availableForPurchaseEnd?.getTime()).to.equal(expected.availableForPurchaseEnd); + }); + }); }); - it('createMintTemplateForContract: Returns a mint template for a supported contract', async function () { + describe('createMintTemplateForContract: Returns a mint template for a supported contract', async function () { const ingestor = new TransientIngestor(); - const contract = { - chainId: 8453, - contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8', - url: 'https://www.transient.xyz/stacks/kansas-smile', - }; - - const template = await ingestor.createMintForContract(resources, contract); - - // Verify that the mint template passed validation - const builder = new MintTemplateBuilder(template); - builder.validateMintTemplate(); - - expect(template.name).to.equal('Kansas Smile'); - expect(template.description).to.equal( - "'Kansas Smile' explores how our longing for the past connects with modern technology. It shows how digital tools can recreate and reimagine the past, allowing us to experience nostalgia in new ways through AI.\n\nExhibited at 'Digital Dreams' Kansas City 2024, part of the Digital Collage exhibition.\n\n(Analog and digital processes with AI)", - ); - const mintInstructions = template.mintInstructions as EVMMintInstructions; - - expect(mintInstructions.contractAddress).to.equal('0x32953d7ae37b05075b88c34e800ae80c1cb1b794'); - expect(mintInstructions.contractMethod).to.equal('purchase'); - expect(mintInstructions.contractParams).to.equal('["0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8", 1, address, 1, 0, []]'); - expect(mintInstructions.priceWei).to.equal('20900000000000000'); - - expect(template.mintOutputContract?.address).to.equal('0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8'); - - expect(template.featuredImageUrl?.length).to.be.greaterThan(0); - - expect(template.marketingUrl).to.equal(contract.url); - expect(template.availableForPurchaseStart?.getTime()).to.equal(1717777800000); - expect(template.availableForPurchaseEnd?.getTime()).to.equal(3294577800000); + for (const testCase of testCases) { + it(`${testCase.name} contract at ${testCase.expected.outputContractAddress}`, async function () { + const contract = { + chainId: testCase.chainId, + contractAddress: testCase.expected.outputContractAddress, + url: testCase.input.url, + } + const template = await ingestor.createMintForContract(resources, contract); + + // Verify that the mint template passed validation + const builder = new MintTemplateBuilder(template); + builder.validateMintTemplate(); + + expect(template.name).to.equal(testCase.expected.name); + expect(template.description).to.equal( + testCase.expected.description + ); + const mintInstructions = template.mintInstructions as EVMMintInstructions; + + expect(mintInstructions.contractAddress).to.equal(testCase.expected.contractAddress); + expect(mintInstructions.contractMethod).to.equal('purchase'); + expect(mintInstructions.contractParams).to.equal( + testCase.expected.contractParams + ); + expect(mintInstructions.priceWei).to.equal(testCase.expected.priceWei); + + expect(template.mintOutputContract?.address).to.equal(testCase.expected.outputContractAddress); + + expect(template.featuredImageUrl?.length).to.be.greaterThan(0); + + expect(template.marketingUrl).to.equal(contract.url); + expect(template.availableForPurchaseStart?.getTime()).to.equal(testCase.expected.availableForPurchaseStart); + expect(template.availableForPurchaseEnd?.getTime()).to.equal(testCase.expected.availableForPurchaseEnd); + }); + } }); }); From d2dced6b8b9a8d37daae6655f26faf3e20e87d8b Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Sun, 21 Jul 2024 21:01:21 +0100 Subject: [PATCH 16/22] chore: Refactor TransientIngestor to use updated Transient API --- src/ingestors/transient-base/index.ts | 2 +- .../transient-base/offchain-metadata.ts | 118 ++++++++++++++---- 2 files changed, 95 insertions(+), 25 deletions(-) diff --git a/src/ingestors/transient-base/index.ts b/src/ingestors/transient-base/index.ts index 7a216e2..26f5782 100644 --- a/src/ingestors/transient-base/index.ts +++ b/src/ingestors/transient-base/index.ts @@ -34,7 +34,7 @@ export class TransientIngestor implements MintIngestor { if (!chainId || !contractAddress) { return false; } - return await transientSupports(contract, resources.fetcher); + return await transientSupports(contract, resources); } async createMintTemplateForUrl(resources: MintIngestorResources, url: string): Promise { diff --git a/src/ingestors/transient-base/offchain-metadata.ts b/src/ingestors/transient-base/offchain-metadata.ts index 98be518..128925e 100644 --- a/src/ingestors/transient-base/offchain-metadata.ts +++ b/src/ingestors/transient-base/offchain-metadata.ts @@ -1,5 +1,50 @@ import { AxiosInstance, AxiosResponse } from 'axios'; import { MintContractOptions, MintIngestionErrorName, MintIngestorError, MintIngestorResources } from '../../lib'; +interface NFTContractVersion { + major: number; + minor: number; + patch: number; +} + +interface NFTContractAddress { + raw_address: string; + chain: string; + ecosystem: string; +} + +interface NFTContractUser { + id: number; + username: string; + display_name: string; + pfp: string; + uri: string; + joined_at: string; + updated_at: string; +} + +interface NFTContract { + name: string; + symbol: string; + contract_type: string; + version: NFTContractVersion; + uri: string; + address: NFTContractAddress; + user: NFTContractUser; + created_at: string; + updated_at: string; +} + +interface NFT { + id: number; + token_id: number; + name: string; + uri: string; + image_uri: string; + is_valid: boolean; + nft_contract: NFTContract; + created_at: string; + updated_at: string; +} /** * @param resources MintIngestorResources @@ -19,7 +64,6 @@ export const getTransientBaseMintByURL = async ( priceInWei: string; description: string; }> => { - // https://www.transient.xyz/stacks/kansas-smile const urlParts = url.split('/'); const slug = urlParts.pop(); if (!slug) { @@ -46,7 +90,7 @@ export const getTransientBaseMintBySlug = async ( name: string; image: string; website: string; - } + }; }> => { // search Transient API for the slug let response: AxiosResponse; @@ -57,23 +101,23 @@ export const getTransientBaseMintBySlug = async ( } const data = response.data; - if (!data || !data.nft_token) { + if (!data || !data.nft_contract) { throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Mint not found'); } - const { nft_contract, nft_token } = data; + const { nft_contract, media_url, name } = data; return { chainId: nft_contract.address.chain, contractAddress: nft_contract.address.raw_address, - image: nft_token.image_uri, - name: nft_token.name, + image: media_url, + name, priceInWei: data.current_cost, mintAddress: data.stacks_address.raw_address, description: data.description, public_sale_start_at: data.public_sale_start_at, public_sale_end_at: data.public_sale_end_at, - token_id: data.nft_token.token_id, + token_id: data.nft_token?.token_id || 0, user: { name: data.nft_contract.user.display_name, image: data.nft_contract.user.pfp, @@ -90,7 +134,7 @@ export const getTransientBaseMintByAddressAndChain = async ( let response: AxiosResponse; try { response = await resources.fetcher.get( - `https://api.transient.xyz/v1/catalog/nfts?chain=${chainId}&address=${contractAddress}&format=json`, + `https://api.transient.xyz/v1/catalog/stacks?chain=${chainId}&address=${contractAddress}&format=json`, ); } catch (error) { throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Could not query mint from Transient API'); @@ -101,34 +145,42 @@ export const getTransientBaseMintByAddressAndChain = async ( throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Project not found'); } - const token = data.results.find((token: any) => token.nft_contract.address.raw_address === contractAddress); + const token = data.results.find( + (token: NFT) => + token.nft_contract.address.raw_address === contractAddress && + token.nft_contract.address.chain == chainId.toString(), + ); + if (!token) { throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Project not found'); } - const slug = token.name.toLocaleLowerCase().split(' ').join('-'); + const slug = token.slug; return await getTransientBaseMintBySlug(resources, slug); }; -export const transientSupports = async (contract: MintContractOptions, fetcher: AxiosInstance): Promise => { - const { chainId, contractAddress } = contract; - let response: AxiosResponse; - try { - response = await fetcher.get(`https://api.transient.xyz/v1/catalog/nfts?chainId=${chainId}&address=${contractAddress}`); - } catch (error) { - return false; +export const transientSupports = async ( + contract: MintContractOptions, + resources: MintIngestorResources, +): Promise => { + const { chainId, contractAddress, tokenId } = contract; + const { fetcher } = resources; + if (!tokenId) { + try { + const exists = await getTransientBaseMintByAddressAndChain(resources, chainId, contractAddress); + return !!exists; + } catch (error) { + return false; + } } - const data = response.data; - if (!data || !data.results.length) { - return false; - } + const _tokens = await getTransientTokensForAddress(fetcher, chainId, contractAddress); - const tokens = data.results - .filter((token: any) => token.nft_contract.address.raw_address === contractAddress) + const tokens = _tokens + .filter((token) => token.nft_contract.address.raw_address === contractAddress) // filter by tokenId if set - .filter((token: any) => !contract.tokenId || token.token_id === contract.tokenId) + .filter((token) => !contract.tokenId || token.token_id.toString() == contract.tokenId) // now if url was set, check for slug match .filter( (token: any) => @@ -142,3 +194,21 @@ export const transientSupports = async (contract: MintContractOptions, fetcher: return true; }; + +const getTransientTokensForAddress = async (fetcher: AxiosInstance, chainId: number, contractAddress: string) => { + let response: AxiosResponse; + try { + response = await fetcher.get( + `https://api.transient.xyz/v1/catalog/nfts?chainId=${chainId}&address=${contractAddress}`, + ); + } catch (error) { + return []; + } + + const data = response.data; + if (!data || !data.results.length) { + return []; + } + + return data.results as NFT[]; +}; From 9437dc54b8c52de16561ebd1ebf9e65361d83a78 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Mon, 22 Jul 2024 15:11:38 +0100 Subject: [PATCH 17/22] feat: edit abi for ERC721TL and ERC7160TL contracts --- src/ingestors/transient-base/abi.ts | 19 ++++++++++++++ src/ingestors/transient-base/index.ts | 10 ++++--- .../transient-base/offchain-metadata.ts | 6 +++-- test/ingestors/transient.test.ts | 26 ++++++++++--------- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/ingestors/transient-base/abi.ts b/src/ingestors/transient-base/abi.ts index 9f68af8..1abf14c 100644 --- a/src/ingestors/transient-base/abi.ts +++ b/src/ingestors/transient-base/abi.ts @@ -21,3 +21,22 @@ export const TRANSIENT_BASE_ABI = [ type: 'function', }, ]; + +/** + * This ABI also works for ERC721TL contracts + */ +export const TRANSIENT_ERC7160TL_ABI = [ + { + inputs: [ + { internalType: 'address', name: 'nftAddress', type: 'address' }, + { internalType: 'address', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'numberToMint', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleNumberCanMint', type: 'uint256' }, + { internalType: 'bytes32[]', name: 'proof', type: 'bytes32[]' }, + ], + name: 'purchase', + outputs: [{ internalType: 'uint256', name: 'refundAmount', type: 'uint256' }], + stateMutability: 'payable', + type: 'function', + }, +]; diff --git a/src/ingestors/transient-base/index.ts b/src/ingestors/transient-base/index.ts index 26f5782..03a95a3 100644 --- a/src/ingestors/transient-base/index.ts +++ b/src/ingestors/transient-base/index.ts @@ -1,6 +1,7 @@ import { MintContractOptions, MintIngestor, MintIngestorResources } from '../../lib/types/mint-ingestor'; import { MintIngestionErrorName, MintIngestorError } from '../../lib/types/mint-ingestor-error'; import { MintInstructionType, MintTemplate } from '../../lib/types/mint-template'; +import { TRANSIENT_BASE_ABI, TRANSIENT_ERC7160TL_ABI } from './abi'; import { getTransientBaseMintByAddressAndChain, getTransientBaseMintByURL, @@ -9,7 +10,6 @@ import { import { BigNumber } from 'alchemy-sdk'; import { MintTemplateBuilder } from '../../lib/builder/mint-template-builder'; -import { TRANSIENT_BASE_ABI } from './abi'; import { getTransientProtocolFeeInEth } from './onchain-metadata'; export class TransientIngestor implements MintIngestor { @@ -76,6 +76,7 @@ export class TransientIngestor implements MintIngestor { public_sale_start_at, public_sale_end_at, token_id, + contract_type, user, } = await getTransientBaseMintByAddressAndChain(resources, contract.chainId, contract.contractAddress); @@ -98,8 +99,11 @@ export class TransientIngestor implements MintIngestor { chainId, contractAddress: mintAddress, contractMethod: 'purchase', - contractParams: `["${contractAddress}", ${token_id}, address, 1, 0, []]`, - abi: TRANSIENT_BASE_ABI, + contractParams: + contract_type == 'ERC1155TL' + ? `["${contractAddress}", ${token_id}, address, 1, 0, []]` + : `["${contractAddress}", address, 1, 0, []]`, + abi: contract_type == 'ERC1155TL' ? TRANSIENT_BASE_ABI : TRANSIENT_ERC7160TL_ABI, priceWei: totalPrice, }); diff --git a/src/ingestors/transient-base/offchain-metadata.ts b/src/ingestors/transient-base/offchain-metadata.ts index 128925e..d6660ae 100644 --- a/src/ingestors/transient-base/offchain-metadata.ts +++ b/src/ingestors/transient-base/offchain-metadata.ts @@ -85,7 +85,8 @@ export const getTransientBaseMintBySlug = async ( description: string; public_sale_start_at: string; public_sale_end_at: string; - token_id: number; + token_id?: number; + contract_type: string; user: { name: string; image: string; @@ -117,7 +118,8 @@ export const getTransientBaseMintBySlug = async ( description: data.description, public_sale_start_at: data.public_sale_start_at, public_sale_end_at: data.public_sale_end_at, - token_id: data.nft_token?.token_id || 0, + token_id: data.nft_token?.token_id as number | undefined, + contract_type: nft_contract.contract_type, user: { name: data.nft_contract.user.display_name, image: data.nft_contract.user.pfp, diff --git a/test/ingestors/transient.test.ts b/test/ingestors/transient.test.ts index 8c13410..45626f8 100644 --- a/test/ingestors/transient.test.ts +++ b/test/ingestors/transient.test.ts @@ -10,11 +10,16 @@ const resources = mintIngestorResources(); describe('Transient', function () { basicIngestorTests(new TransientIngestor(), resources, { - successUrls: ['https://www.transient.xyz/stacks/kansas-smile'], + successUrls: [ + 'https://www.transient.xyz/stacks/kansas-smile', + 'https://www.transient.xyz/stacks/16384', + 'https://www.transient.xyz/stacks/volumina-8', + ], failureUrls: ['https://www.transient.xyz/stacks/kansas-smiles', 'https://www.transient.xyz/stacks'], successContracts: [ { chainId: 8453, contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8' }, - // { chainId: 8453, contractAddress: '0x8a2e6797d5930527272642a41f36cedc84a3935e' }, + { chainId: 8453, contractAddress: '0xd2f9c0ef092d7ffd1a5de43b6ee546065461887d' }, + { chainId: 8453, contractAddress: '0x999f084f06ee49a3deef0c9d1511d2f040da4034' }, ], failureContracts: [ { chainId: 8453, contractAddress: '0x965ef172b303b0bcdc38669df1de3c26bad2db8a' }, @@ -56,7 +61,7 @@ describe('Transient', function () { 'By exposing a 1px 8x8 grid to simulated physics turbulence, the grid interferes with the diodes in a ever changing moiré patterns, revealing a subtle breeze of random generated palm leaves behind the grid. \n\nThe colors of the leaves are then transferred back into the squares of the grid as large pixels.\n\n16384 are the amount of emitting diodes on the 128px LED artworks from Spøgelsesmaskinen. \n\nEach token holds both a 4K MP4 version and the original 128x128px GIF artwork for low res LED screens.\nOn mint a new generative variation of the art piece will be added by the artist to the token, also as both MP4 and GIF.\n\nEach piece is 60 seconds loop, of 2161 frames\n\nCollectors are able to require a physical custom assembled LED display made specifically for this artwork on https://spogel.xyz/led', contractAddress: '0x384092784cfaa91efaa77870c04d958e20840242', contractMethod: 'purchase', - contractParams: '["0xd2f9c0ef092d7ffd1a5de43b6ee546065461887d", 0, address, 1, 0, []]', + contractParams: '["0xd2f9c0ef092d7ffd1a5de43b6ee546065461887d", address, 1, 0, []]', priceWei: '50900000000000000', featuredImageUrlPattern: /^https:\/\/dv0xp0uwyoh8r\.cloudfront\.net\/stacks\/[0-9a-fA-F\-]+\/media/, availableForPurchaseStart: 1720465200000, @@ -73,10 +78,11 @@ describe('Transient', function () { chainId: 8453, expected: { name: 'Volumina 8', - description: 'Volumina 8 is one of a series of manipulated image sequences derived from photography of Australian landscapes taken in Spring 2022. Various phone and laptop apps have been utilised to construct mosaics, kaleidoscopic patterns and other structures from the original photos, in an exploration of expanding and contracting symmetries implied and derived from organic forms and processes of the natural world.', + description: + 'Volumina 8 is one of a series of manipulated image sequences derived from photography of Australian landscapes taken in Spring 2022. Various phone and laptop apps have been utilised to construct mosaics, kaleidoscopic patterns and other structures from the original photos, in an exploration of expanding and contracting symmetries implied and derived from organic forms and processes of the natural world.', contractAddress: '0x384092784cfaa91efaa77870c04d958e20840242', contractMethod: 'purchase', - contractParams: '["0x999f084f06ee49a3deef0c9d1511d2f040da4034", 0, address, 1, 0, []]', + contractParams: '["0x999f084f06ee49a3deef0c9d1511d2f040da4034", address, 1, 0, []]', priceWei: '150900000000000000', featuredImageUrlPattern: /^https:\/\/dv0xp0uwyoh8r\.cloudfront\.net\/stacks\/[0-9a-fA-F\-]+\/media/, availableForPurchaseStart: 1720600200000, @@ -123,7 +129,7 @@ describe('Transient', function () { chainId: testCase.chainId, contractAddress: testCase.expected.outputContractAddress, url: testCase.input.url, - } + }; const template = await ingestor.createMintForContract(resources, contract); // Verify that the mint template passed validation @@ -131,16 +137,12 @@ describe('Transient', function () { builder.validateMintTemplate(); expect(template.name).to.equal(testCase.expected.name); - expect(template.description).to.equal( - testCase.expected.description - ); + expect(template.description).to.equal(testCase.expected.description); const mintInstructions = template.mintInstructions as EVMMintInstructions; expect(mintInstructions.contractAddress).to.equal(testCase.expected.contractAddress); expect(mintInstructions.contractMethod).to.equal('purchase'); - expect(mintInstructions.contractParams).to.equal( - testCase.expected.contractParams - ); + expect(mintInstructions.contractParams).to.equal(testCase.expected.contractParams); expect(mintInstructions.priceWei).to.equal(testCase.expected.priceWei); expect(template.mintOutputContract?.address).to.equal(testCase.expected.outputContractAddress); From bcf97678479fa6327314989570513d8a9ef12525 Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Wed, 24 Jul 2024 19:08:40 -0400 Subject: [PATCH 18/22] Update tests.yaml --- .github/workflows/tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index aae5376..946973f 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -14,4 +14,5 @@ jobs: - name: Run Tests env: ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} + SIMULATE_DURING_TESTS: ${{ var.SIMULATE_DURING_TESTS }} run: yarn install --userconfig=/dev/null && yarn test From 81ef5f40d67f25a847e4d475c7e90481ac43f144 Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Wed, 24 Jul 2024 19:09:32 -0400 Subject: [PATCH 19/22] Empty commit for tests From d6bcc15926d7d603d577ffa21044c9f575479209 Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Wed, 24 Jul 2024 19:10:20 -0400 Subject: [PATCH 20/22] =?UTF-8?q?var=20=E2=86=92=20vars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 946973f..e75ff33 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -14,5 +14,5 @@ jobs: - name: Run Tests env: ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} - SIMULATE_DURING_TESTS: ${{ var.SIMULATE_DURING_TESTS }} + SIMULATE_DURING_TESTS: ${{ vars.SIMULATE_DURING_TESTS }} run: yarn install --userconfig=/dev/null && yarn test From d9db8a1203c5ce1c13c32efd815834dee4993403 Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Wed, 24 Jul 2024 19:55:09 -0400 Subject: [PATCH 21/22] Update tests to pas --- package.json | 4 +-- {test/forks => scripts}/.gitignore | 0 {test/forks => scripts}/test-fork.sh | 0 test/ingestors/fxhash.test.ts | 23 ++++++++-------- test/ingestors/transient.test.ts | 41 ++++++++++++++++------------ 5 files changed, 38 insertions(+), 30 deletions(-) rename {test/forks => scripts}/.gitignore (100%) rename {test/forks => scripts}/test-fork.sh (100%) diff --git a/package.json b/package.json index 6d8a7e1..051bf15 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "test": "NODE_OPTIONS=\"--loader ts-node/esm\" c8 mocha --require ts-node/register", "release": "npm run build && np", "dry-run": "ts-node src/dry-run/index.ts", - "test-fork": "cd ./test/forks/ && sh ./test-fork.sh" + "test-fork": "cd ./scripts/ && sh ./test-fork.sh" }, "dependencies": { "alchemy-sdk": "^3.3.1", @@ -62,4 +62,4 @@ "tsup": "^6.1.2", "typescript": "^5.0.0" } -} +} \ No newline at end of file diff --git a/test/forks/.gitignore b/scripts/.gitignore similarity index 100% rename from test/forks/.gitignore rename to scripts/.gitignore diff --git a/test/forks/test-fork.sh b/scripts/test-fork.sh similarity index 100% rename from test/forks/test-fork.sh rename to scripts/test-fork.sh diff --git a/test/ingestors/fxhash.test.ts b/test/ingestors/fxhash.test.ts index 5ef04e9..f74143a 100644 --- a/test/ingestors/fxhash.test.ts +++ b/test/ingestors/fxhash.test.ts @@ -6,7 +6,6 @@ import { MintTemplateBuilder } from '../../src/lib/builder/mint-template-builder import { basicIngestorTests } from '../shared/basic-ingestor-tests'; describe('fxhash', function () { - basicIngestorTests( new FxHashIngestor(), mintIngestorResources(), @@ -14,23 +13,25 @@ describe('fxhash', function () { successUrls: [ 'https://fxhash.xyz/generative/slug/allegro', 'https://fxhash.xyz/generative/slug/graphomania', - 'https://www.fxhash.xyz/generative/slug/the-space-in-between' + 'https://www.fxhash.xyz/generative/slug/the-space-in-between', ], failureUrls: ['https://example.com'], - successContracts: [{ - chainId: 8453, - contractAddress: '0x914cf2d92b087C9C01a062111392163c3B35B60e' - }], + successContracts: [ + { + chainId: 8453, + contractAddress: '0x914cf2d92b087C9C01a062111392163c3B35B60e', + }, + ], failureContracts: [ { chainId: 8453, - contractAddress: '0x6140F00e4Ff3936702E68744f2b5978885464cbB' - } - ] + contractAddress: '0x6140F00e4Ff3936702E68744f2b5978885464cbB', + }, + ], }, { - // 8453: '0x107A60C' - } + '8453': '0x1081832', + }, ); it('supportsUrl: Returns false for an unsupported URL', async function () { diff --git a/test/ingestors/transient.test.ts b/test/ingestors/transient.test.ts index 45626f8..7f290ec 100644 --- a/test/ingestors/transient.test.ts +++ b/test/ingestors/transient.test.ts @@ -9,23 +9,30 @@ import { mintIngestorResources } from '../../src/lib/resources'; const resources = mintIngestorResources(); describe('Transient', function () { - basicIngestorTests(new TransientIngestor(), resources, { - successUrls: [ - 'https://www.transient.xyz/stacks/kansas-smile', - 'https://www.transient.xyz/stacks/16384', - 'https://www.transient.xyz/stacks/volumina-8', - ], - failureUrls: ['https://www.transient.xyz/stacks/kansas-smiles', 'https://www.transient.xyz/stacks'], - successContracts: [ - { chainId: 8453, contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8' }, - { chainId: 8453, contractAddress: '0xd2f9c0ef092d7ffd1a5de43b6ee546065461887d' }, - { chainId: 8453, contractAddress: '0x999f084f06ee49a3deef0c9d1511d2f040da4034' }, - ], - failureContracts: [ - { chainId: 8453, contractAddress: '0x965ef172b303b0bcdc38669df1de3c26bad2db8a' }, - { chainId: 8453, contractAddress: 'derp' }, - ], - }); + basicIngestorTests( + new TransientIngestor(), + resources, + { + successUrls: [ + 'https://www.transient.xyz/stacks/kansas-smile', + 'https://www.transient.xyz/stacks/16384', + 'https://www.transient.xyz/stacks/volumina-8', + ], + failureUrls: ['https://www.transient.xyz/stacks/kansas-smiles', 'https://www.transient.xyz/stacks'], + successContracts: [ + { chainId: 8453, contractAddress: '0x7c3a99d4a7adddc04ad05d7ca87f4949c1a62fa8' }, + { chainId: 8453, contractAddress: '0xd2f9c0ef092d7ffd1a5de43b6ee546065461887d' }, + { chainId: 8453, contractAddress: '0x999f084f06ee49a3deef0c9d1511d2f040da4034' }, + ], + failureContracts: [ + { chainId: 8453, contractAddress: '0x965ef172b303b0bcdc38669df1de3c26bad2db8a' }, + { chainId: 8453, contractAddress: 'derp' }, + ], + }, + { + '8453': '0x1081832', + }, + ); const testCases = [ { name: 'ERC1155TL', From 23730e5469a884ec87eab94cc1921098ae5c81ed Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Wed, 24 Jul 2024 20:03:48 -0400 Subject: [PATCH 22/22] Update test-fork command --- scripts/test-fork.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test-fork.sh b/scripts/test-fork.sh index ad475a9..35e1cdf 100755 --- a/scripts/test-fork.sh +++ b/scripts/test-fork.sh @@ -52,7 +52,7 @@ git fetch floor git merge floor/main --no-edit # copy the .env.example file to .env -cp ../../../../.env .env +cp ../../../.env .env # Run tests yarn test