Skip to content

Commit

Permalink
Merge pull request #279 from ArtBlocks/index-SEA-bids
Browse files Browse the repository at this point in the history
Index SEA Bids
  • Loading branch information
ryley-o authored Jan 26, 2024
2 parents 1cef32f + 70be453 commit a4b9210
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 5 deletions.
28 changes: 28 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,34 @@ type ProjectMinterConfiguration @entity {
maxInvocations: BigInt
}

type Bid @entity {
"Unique identifier made up of for SEA - {minter contract address}-{bidder}-{value}-{token} [TODO - do something different from RAM]"
id: ID!

"The associated project"
project: Project!

"The associated minter"
minter: Minter!

"The associated token"
token: Token

"The associated account"
bidder: Account!

"The value of the bid"
value: BigInt!

"Highest bid for token"
winningBid: Boolean!

"The timestamp of the bid"
timestamp: BigInt!

updatedAt: BigInt!
}

type Receipt @entity {
"Unique identifier made up of {minter contract address}-{core contract address}-{project number}-{account address}"
id: ID!
Expand Down
9 changes: 9 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ export function generateProjectIdNumberFromTokenIdNumber(
return tokenId.div(BigInt.fromI32(1000000));
}

export function generateSEAMinterBidId(
minterAddress: string,
bidderAddress: string,
bidAmount: string,
tokenId: string
): string {
return minterAddress + "-" + bidderAddress + "-" + bidAmount + "-" + tokenId;
}

export function generateContractSpecificId(
contractAddress: Address,
entityId: BigInt
Expand Down
75 changes: 72 additions & 3 deletions src/sea-lib-mapping.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigInt, JSONValue, TypedMap } from "@graphprotocol/graph-ts";
import { BigInt, log, JSONValue, TypedMap } from "@graphprotocol/graph-ts";

import {
MinAuctionDurationSecondsUpdated,
Expand All @@ -13,14 +13,18 @@ import {
ProjectNextTokenEjected
} from "../generated/SEALib/SEALib";

import { Account, Bid } from "../generated/schema";

import { loadOrCreateMinterProjectAndConfigIfProject } from "./generic-minter-events-lib-mapping";

import {
loadOrCreateMinter,
generateProjectIdNumberFromTokenIdNumber,
getMinterExtraMinterDetailsTypedMap,
getProjectMinterConfigExtraMinterDetailsTypedMap,
updateProjectIfMinterConfigIsActive
updateProjectIfMinterConfigIsActive,
generateContractSpecificId,
generateSEAMinterBidId
} from "./helpers";

import {
Expand Down Expand Up @@ -161,7 +165,10 @@ export function handleAuctionInitialized(event: AuctionInitialized): void {
key: "auctionCurrentBid",
value: toJSONValue(event.params.bidAmount.toString()) // Bid is likely to overflow js Number.MAX_SAFE_INTEGER so store as string
},
{ key: "auctionCurrentBidder", value: toJSONValue(event.params.bidder) },
{
key: "auctionCurrentBidder",
value: toJSONValue(event.params.bidder)
},
{ key: "auctionEndTime", value: toJSONValue(event.params.endTime) },
{ key: "auctionInitialized", value: toJSONValue(true) },
{ key: "auctionSettled", value: toJSONValue(false) },
Expand Down Expand Up @@ -208,6 +215,35 @@ export function handleAuctionBid(event: AuctionBid): void {

const projectMinterConfig = minterProjectAndConfig.projectMinterConfiguration;

// Get the previous highest bid
let currentProjectMinterConfigDetails = getProjectMinterConfigExtraMinterDetailsTypedMap(
projectMinterConfig
);

const previousHighestBidValueJSON = currentProjectMinterConfigDetails.get(
"auctionCurrentBid"
);
const previousHighestBidderJSON = currentProjectMinterConfigDetails.get(
"auctionCurrentBidder"
);

// Update winningBid on the previous highest bid, as it is now outbid
if (previousHighestBidderJSON && previousHighestBidValueJSON) {
const previousHighestBidId = generateSEAMinterBidId(
event.address.toHexString(),
previousHighestBidderJSON.toString(),
previousHighestBidValueJSON.toString(),
event.params.tokenId.toString()
);

const previousWinningBid = Bid.load(previousHighestBidId);
if (previousWinningBid) {
previousWinningBid.winningBid = false;
previousWinningBid.updatedAt = event.block.timestamp;
previousWinningBid.save();
}
}

// update relevant auction details in project minter configuration extraMinterDetails json field
setProjectMinterConfigExtraMinterDetailsValue(
"auctionCurrentBid",
Expand All @@ -219,6 +255,12 @@ export function handleAuctionBid(event: AuctionBid): void {
event.params.bidder,
projectMinterConfig
);
setProjectMinterConfigExtraMinterDetailsValue(
"auctionCurrentBidTimestamp",
event.block.timestamp.toString(),
projectMinterConfig
);

// determine if auction end time needs to be updated
let minterDetails = getMinterExtraMinterDetailsTypedMap(
minterProjectAndConfig.minter
Expand Down Expand Up @@ -253,6 +295,33 @@ export function handleAuctionBid(event: AuctionBid): void {
projectMinterConfig,
event.block.timestamp
);

// Update Bids entity
const bidId = generateSEAMinterBidId(
event.address.toHexString(),
event.params.bidder.toHexString(),
event.params.bidAmount.toString(),
event.params.tokenId.toString()
);

// @dev: a bid with this ID should not already exist for the SEA Minter
const bid = new Bid(bidId);
// Create new account entity if one for the bidder doesn't exist
const bidderAccount = new Account(event.params.bidder.toHexString());
bidderAccount.save();

bid.project = minterProjectAndConfig.project.id;
bid.minter = event.address.toHexString();
bid.token = generateContractSpecificId(
event.params.coreContract,
event.params.tokenId
);
bid.bidder = bidderAccount.id;
bid.value = event.params.bidAmount;
bid.winningBid = true;
bid.timestamp = event.block.timestamp;
bid.updatedAt = event.block.timestamp;
bid.save();
}

export function handleAuctionSettled(event: AuctionSettled): void {
Expand Down
31 changes: 31 additions & 0 deletions tests/e2e/runner/__tests__/graphql/get-bids.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
fragment BidDetails on Bid {
id
project {
id
}
minter {
id
}
token {
id
}
bidder {
id
}
value
winningBid
timestamp
updatedAt
}

query GetBids {
bids {
...BidDetails
}
}

query GetTargetBids($targetId: ID!) {
bids(where: { id: $targetId }) {
...BidDetails
}
}
43 changes: 41 additions & 2 deletions tests/e2e/runner/__tests__/minter-suite-v2/sea-lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getAccounts,
waitUntilSubgraphIsSynced,
getMinterDetails,
getBidDetails,
getProjectMinterConfigurationDetails,
} from "../utils/helpers";

Expand Down Expand Up @@ -363,10 +364,11 @@ describe("SEALib event handling", () => {
.connect(deployer)
.updateMinterTimeBufferSeconds(600);
// deployer bids
const auctionBidValue = ethers.utils.parseEther("1.20");
const tx2 = await minterSEAV1Contract.connect(deployer).createBid(
targetTokenId, // _tokenId
genArt721CoreAddress, // _coreContract
{ value: ethers.utils.parseEther("1.20") }
{ value: auctionBidValue }
);
const receipt2 = await tx2.wait();
const auctionBidTimestamp = (
Expand All @@ -386,7 +388,7 @@ describe("SEALib event handling", () => {
minterConfigRes2.extraMinterDetails
);
expect(extraMinterDetails2.auctionCurrentBid).toBe(
ethers.utils.parseEther("1.20").toString()
auctionBidValue.toString()
);
expect(extraMinterDetails2.auctionCurrentBidder).toBe(
deployer.address.toLowerCase()
Expand All @@ -395,6 +397,43 @@ describe("SEALib event handling", () => {
expect(extraMinterDetails2.auctionEndTime).toBe(
auctionBidTimestamp + 600
);

// PART 4: Bid indexing
// Validate that the Bid entity was created
const bidId = `${minterSEAV1Address.toLowerCase()}-${deployer.address.toLowerCase()}-${auctionBidValue.toString()}-${targetTokenId}`;
const bidRes = await getBidDetails(client, bidId);
expect(bidRes.id).toBe(bidId);
expect(bidRes.bidder.id).toBe(deployer.address.toLowerCase());
expect(bidRes.value).toBe(auctionBidValue.toString());
expect(bidRes.winningBid).toBe(true);
expect(bidRes.timestamp).toBe(auctionBidTimestamp.toString());
expect(bidRes.updatedAt).toBe(auctionBidTimestamp.toString());
expect(bidRes.project.id).toBe(`${genArt721CoreAddress.toLowerCase()}-1`);
expect(bidRes.minter.id).toBe(minterSEAV1Address.toLowerCase());
expect(bidRes.token?.id).toBe(
`${genArt721CoreAddress.toLowerCase()}-${targetTokenId.toString()}`
);

// Create another bid
const tx3 = await minterSEAV1Contract.connect(artist).createBid(
targetTokenId, // _tokenId
genArt721CoreAddress, // _coreContract
{ value: ethers.utils.parseEther("1.50") }
);
const receipt3 = await tx3.wait();
const auctionBid2Timestamp = (
await artist.provider.getBlock(receipt3.blockNumber)
)?.timestamp;
if (!auctionBid2Timestamp) {
throw new Error("No auctionBid2Timestamp found");
}
await waitUntilSubgraphIsSynced(client);
// Validate that the previous winning bid has been updated
const previousWinningBidRes = await getBidDetails(client, bidId);
expect(previousWinningBidRes.winningBid).toBe(false);
expect(previousWinningBidRes.updatedAt).toBe(
auctionBid2Timestamp.toString()
);
});
});

Expand Down
28 changes: 28 additions & 0 deletions tests/e2e/runner/__tests__/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ import {
MinterDetailsFragment,
ProjectMinterConfigurationDetailsFragment,
ProjectDetailsFragment,
BidDetailsFragment,
CoreRegistryDetailsFragment,
ReceiptDetailsFragment,
GetTargetReceiptsQuery,
GetTargetReceiptsQueryVariables,
GetTargetReceiptsDocument,
GetTargetBidsQuery,
GetTargetBidsQueryVariables,
GetTargetBidsDocument,
GetTargetSplitAtomicFactoriesQuery,
GetTargetSplitAtomicFactoriesQueryVariables,
GetTargetSplitAtomicFactoriesDocument,
Expand Down Expand Up @@ -234,6 +238,30 @@ export const getReceiptDetails = async (
return receiptRes;
};

/**
* Gets a Bid detail fragment from the subgraph, at specified id.
* Reverts if no entity is found.
* @param client the subgraph client
* @param bidId the id of the Bid entity
*/
export const getBidDetails = async (
client: Client,
bidId: string
): Promise<BidDetailsFragment> => {
const bidRes = (
await client
.query<GetTargetBidsQuery, GetTargetBidsQueryVariables>(
GetTargetBidsDocument,
{
targetId: bidId,
}
)
.toPromise()
).data?.bids[0];
if (!bidRes) throw new Error("No Bid entity found");
return bidRes;
};

/**
* Gets a CoreRegistry detail fragment from the subgraph, at specified id.
* Reverts if no entity is found.
Expand Down

0 comments on commit a4b9210

Please sign in to comment.