Skip to content

Commit

Permalink
Merge pull request #1766 from ArtBlocks/ben/pro-1005-update-purchase-…
Browse files Browse the repository at this point in the history
…page-with-updated-designs-for-non-ram-flows

Update project sale machines to fetch user eligibility data while paused
  • Loading branch information
yoshiwarab authored Feb 26, 2025
2 parents 13cf875 + b4f436a commit 2738e13
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 33 deletions.
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@artblocks/sdk",
"version": "0.1.30-16",
"version": "0.1.30-17",
"description": "JavaScript SDK for configuring and using Art Blocks minters.",
"main": "dist/index.js",
"repository": "git@github.com:ArtBlocks/artblocks-sdk.git",
Expand Down
15 changes: 0 additions & 15 deletions packages/sdk/src/machines/project-sale-manager-machine/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,21 +199,6 @@ export function isProjectPurchasable(
return false;
}

// If the project is paused, only allow the artist to purchase
if (
liveSaleData.paused &&
project.artist_address !== walletClient.account.address.toLowerCase()
) {
return false;
}

if (project.auction_start_time) {
const startDate = new Date(project.auction_start_time);
if (startDate > new Date()) {
return false;
}
}

if (liveSaleData.ramMinterAuctionDetails) {
const { maxHasBeenInvoked, projectMinterState } =
liveSaleData.ramMinterAuctionDetails;
Expand Down
66 changes: 58 additions & 8 deletions packages/sdk/src/machines/purchase-initiation-machine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
isRAMMinterType,
isUserRejectedError,
} from "../utils";
import { ProjectDetails } from "../project-sale-manager-machine/utils";
import {
LiveSaleData,
ProjectDetails,
} from "../project-sale-manager-machine/utils";
import {
checkERC20Allowance,
getERC20Decimals,
Expand All @@ -29,6 +32,7 @@ import {
initiateHolderMinterPurchase,
initiateMerkleMinterPurchase,
isERC20AllowanceSufficient,
UserIneligibilityReason,
} from "./utils";
import { ArtBlocksClient } from "../..";
import { BidDetailsFragment } from "../../generated/graphql";
Expand All @@ -53,6 +57,7 @@ type AdditionalPurchaseData = {
vaultAddress?: Hex;
erc20Allowance?: bigint;
userBids?: Array<BidDetailsFragment>;
remainingMints?: bigint | null;
};

export type PurchaseInitiationMachineContext = {
Expand All @@ -62,7 +67,7 @@ export type PurchaseInitiationMachineContext = {
purchaseToAddress?: Hex;
errorMessage?: string;
additionalPurchaseData?: AdditionalPurchaseData;
userIneligibilityReason?: string;
userIneligibilityReason?: UserIneligibilityReason;
initiatedTxHash?: Hex;
erc20ApprovalAmount?: bigint;
erc20ApprovalTxHash?: Hex;
Expand All @@ -86,7 +91,8 @@ export type UserPurchaseContext =
}
| {
isEligible: false;
ineligibilityReason: string;
ineligibilityReason: UserIneligibilityReason;
additionalPurchaseData?: AdditionalPurchaseData;
};

/**
Expand All @@ -102,9 +108,12 @@ export type UserPurchaseContext =
* necessary data, such as the user's token balance and allowance.
*
* If the user is eligible for the purchase, the machine transitions to the
* 'readyForPurchase' state, indicating that it is prepared to initiate the purchase
* process. If the user is ineligible, the machine moves to the
* 'userIneligibleForPurchase' state.
* 'waitingForStart' state, where it polls until the sale has officially started.
* Once the sale begins, it automatically transitions to 'readyForPurchase'. If the
* user is ineligible, the machine moves to the 'userIneligibleForPurchase' state.
* This approach prepares and makes available all necessary purchase context before
* the sale actually begins, allowing users to visit a sale page and see if they're
* eligible (for example, if they're on an allowlist) even before the sale starts.
*
* When the 'INITIATE_PURCHASE' event is received in the 'readyForPurchase' state, the
* machine transitions to the 'initiatingPurchase' state. In this state, it invokes the
Expand Down Expand Up @@ -344,7 +353,7 @@ export const purchaseInitiationMachine = setup({
assignUserIneligibilityReason: assign({
userIneligibilityReason: (
_,
params: { userIneligibilityReason?: string }
params: { userIneligibilityReason?: UserIneligibilityReason }
) => params.userIneligibilityReason,
}),
assignInitiatedTxHash: assign({
Expand Down Expand Up @@ -389,6 +398,35 @@ export const purchaseInitiationMachine = setup({
) => {
return !isERC20AllowanceSufficient(context.project, allowance);
},
isSaleStarted: ({ context }) => {
const project = context.project;
const liveSaleData: LiveSaleData =
context.projectSaleManagerMachine.getSnapshot().context.liveSaleData;
const walletClient = context.artblocksClient.getWalletClient();

// TypeScript can't infer that this machine only runs when liveSaleData and walletClient
// are available (they're guaranteed by parent machine preconditions), so we need an explicit
// null check to satisfy the type checker
if (!liveSaleData || !walletClient?.account) {
return false;
}

if (
liveSaleData.paused &&
project.artist_address !== walletClient.account.address.toLowerCase()
) {
return false;
}

if (project.auction_start_time) {
const startDate = new Date(project.auction_start_time);
if (startDate > new Date()) {
return false;
}
}

return true;
},
},
}).createMachine({
/** @xstate-layout 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 */
Expand All @@ -410,7 +448,7 @@ export const purchaseInitiationMachine = setup({
}),
onDone: [
{
target: "readyForPurchase",
target: "waitingForStart",
actions: {
type: "assignAdditionalPurchaseData",
params: ({ event }) => {
Expand Down Expand Up @@ -451,6 +489,18 @@ export const purchaseInitiationMachine = setup({
},
},
},
waitingForStart: {
always: {
target: "readyForPurchase",
guard: "isSaleStarted",
},
after: {
1000: {
target: "waitingForStart",
reenter: true,
},
},
},
readyForPurchase: {
on: {
INITIATE_PURCHASE: [
Expand Down
68 changes: 59 additions & 9 deletions packages/sdk/src/machines/purchase-initiation-machine/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ import {
MinterDetailsFragment,
ProjectDetailsFragment,
} from "../../generated/graphql";
import { LiveSaleData } from "../project-sale-manager-machine/utils";
import {
LiveSaleData,
ProjectDetails,
} from "../project-sale-manager-machine/utils";
import { iDelegationRegistryAbi } from "../../../abis/iDelegationRegistryAbi";
import { DELEGATION_REGISTRY_ADDRESS } from "../../utils/addresses";
import { minterSetPriceERC20V5Abi } from "../../../abis/minterSetPriceERC20V5Abi";
import { ArtBlocksClient } from "../..";

/** Shared Helpers **/
type WalletClientWithAccount = WalletClient & {
Expand All @@ -47,6 +51,35 @@ type ProjectWithValidMinterConfiguration = ProjectDetailsFragment & {
};
};

type UserEligibilityCheckInput = {
project: NonNullable<ProjectDetails>;
artblocksClient: ArtBlocksClient;
};

export const USER_INELIGIBLE_REASONS = {
NOT_A_TOKEN_HOLDER: "NOT_A_TOKEN_HOLDER",
NOT_ALLOWLISTED: "NOT_ALLOWLISTED",
NO_MINTS_REMAINING: "MINT_LIMIT_REACHED",
} as const;

export type UserIneligibilityReason =
(typeof USER_INELIGIBLE_REASONS)[keyof typeof USER_INELIGIBLE_REASONS];

export function getUserIneligibilityReasonMessage(
reason: UserIneligibilityReason
) {
const reasonMap = {
[USER_INELIGIBLE_REASONS.NOT_A_TOKEN_HOLDER]:
"This project is currently available only to owners of specific tokens. If you believe you should have access to purchase this project, please double-check the wallet address you are using and ensure it holds a valid token.",
[USER_INELIGIBLE_REASONS.NOT_ALLOWLISTED]:
"This project is currently available only to users who have been pre-approved and added to the allowlist. If you believe you should have access to purchase this project, please double-check the wallet address you are using and ensure it matches the one expected for the allowlist.",
[USER_INELIGIBLE_REASONS.NO_MINTS_REMAINING]:
"You have no remaining mints available for this project.",
} as const;

return reasonMap[reason];
}

function assertPublicClientAvailable(
publicClient?: PublicClient
): asserts publicClient is PublicClient {
Expand Down Expand Up @@ -98,7 +131,7 @@ const getUserTokensInAllowlistDocument = graphql(/* GraphQL */ `
`);

export async function getHolderMinterUserPurchaseContext(
input: Pick<PurchaseInitiationMachineContext, "project" | "artblocksClient">
input: UserEligibilityCheckInput
): Promise<UserPurchaseContext> {
const { project, artblocksClient } = input;
const walletClient = artblocksClient.getWalletClient();
Expand Down Expand Up @@ -142,8 +175,7 @@ export async function getHolderMinterUserPurchaseContext(
if (userTokensRes.tokens_metadata.length === 0) {
return {
isEligible: false,
ineligibilityReason:
"This project is currently available only to owners of specific tokens. If you believe you should have access to purchase this project, please double-check the wallet address you are using and ensure it holds a valid token.",
ineligibilityReason: USER_INELIGIBLE_REASONS.NOT_A_TOKEN_HOLDER,
};
}

Expand Down Expand Up @@ -300,7 +332,7 @@ function generateUserMerkleProof(addresses: Hex[], userAddress: Hex): Hex[] {
}

export async function getMerkleMinterUserPurchaseContext(
input: Pick<PurchaseInitiationMachineContext, "project" | "artblocksClient">
input: UserEligibilityCheckInput
): Promise<UserPurchaseContext> {
const { project, artblocksClient } = input;
const walletClient = artblocksClient.getWalletClient();
Expand Down Expand Up @@ -354,8 +386,7 @@ export async function getMerkleMinterUserPurchaseContext(
) {
return {
isEligible: false,
ineligibilityReason:
"This project is currently available only to users who have been pre-approved and added to the allowlist. If you believe you should have access to purchase this project, please double-check the wallet address you are using and ensure it matches the one expected for the allowlist.",
ineligibilityReason: USER_INELIGIBLE_REASONS.NOT_ALLOWLISTED,
};
}

Expand Down Expand Up @@ -389,11 +420,27 @@ export async function getMerkleMinterUserPurchaseContext(
([, projectLimitsMintInvocationsPerAddress, remaining]) =>
!projectLimitsMintInvocationsPerAddress || remaining > BigInt(0)
);

const projectLimitsMintInvocationsPerAddress =
remainingInvocationsResults.some(
([, projectLimitsMintInvocationsPerAddress]) =>
projectLimitsMintInvocationsPerAddress
);

const remainingMints = !projectLimitsMintInvocationsPerAddress
? null
: remainingInvocationsResults.reduce((acc, [, , remaining]) => {
return acc + remaining;
}, BigInt(0));

if (!hasRemainingMints) {
return {
isEligible: false,
ineligibilityReason:
"You have no remaining mints available for this project.",
ineligibilityReason: USER_INELIGIBLE_REASONS.NO_MINTS_REMAINING,
additionalPurchaseData: {
allowlist: allowlistedAddresses,
remainingMints,
},
};
}

Expand All @@ -408,11 +455,13 @@ export async function getMerkleMinterUserPurchaseContext(
);
}
);

if (userHasRemainingMints) {
return {
isEligible: true,
additionalPurchaseData: {
allowlist: allowlistedAddresses,
remainingMints,
},
};
}
Expand All @@ -429,6 +478,7 @@ export async function getMerkleMinterUserPurchaseContext(
additionalPurchaseData: {
allowlist: allowlistedAddresses,
vaultAddress: firstVaultWithRemainingMints,
remainingMints,
},
};
} catch (e) {
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/machines/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ export {
type ProjectMinterState,
} from "./project-sale-manager-machine/utils";

export { getUserIneligibilityReasonMessage } from "./purchase-initiation-machine/utils";

// Re-export xstate utility types and createEmptyActor function for use in consuming apps
export {
type ActorRef,
Expand Down

0 comments on commit 2738e13

Please sign in to comment.