Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/new-islands-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@thirdweb-dev/sdk": patch
---

Add fiat checkout to nft-drop, edition-drop and signature-drop
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
TransactionResult,
TransactionResultWithId,
} from "../../core/types";
import { PaperCheckout } from "../../integrations/paper-xyz";
import { EditionMetadata, EditionMetadataOwner } from "../../schema";
import { DropErc1155ContractSchema } from "../../schema/contracts/drop-erc1155";
import { SDKOptions } from "../../schema/sdk-options";
Expand Down Expand Up @@ -106,6 +107,13 @@ export class EditionDropImpl extends StandardErc1155<DropERC1155> {
* ```
*/
public claimConditions: DropErc1155ClaimConditions<DropERC1155>;

/**
* Checkout
* @remarks Create a FIAT currency checkout for your NFT drop.
*/
public checkout: PaperCheckout<DropERC1155>;

public history: DropErc1155History;
public interceptor: ContractInterceptor<DropERC1155>;
public erc1155: Erc1155<DropERC1155>;
Expand Down Expand Up @@ -148,6 +156,7 @@ export class EditionDropImpl extends StandardErc1155<DropERC1155> {
this.platformFees = new ContractPlatformFee(this.contractWrapper);
this.interceptor = new ContractInterceptor(this.contractWrapper);
this.erc1155 = new Erc1155(this.contractWrapper, this.storage);
this.checkout = new PaperCheckout(this.contractWrapper);
}

/**
Expand Down
10 changes: 10 additions & 0 deletions packages/sdk/src/contracts/prebuilt-implementations/nft-drop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
TransactionResult,
TransactionResultWithId,
} from "../../core/types";
import { PaperCheckout } from "../../integrations/paper-xyz";
import { DropErc721ContractSchema } from "../../schema/contracts/drop-erc721";
import { SDKOptions } from "../../schema/sdk-options";
import {
Expand Down Expand Up @@ -161,6 +162,13 @@ export class NFTDropImpl extends StandardErc721<DropERC721> {
* ```
*/
public revealer: DelayedReveal<DropERC721>;

/**
* Checkout
* @remarks Create a FIAT currency checkout for your NFT drop.
*/
public checkout: PaperCheckout<DropERC721>;

public erc721: Erc721<DropERC721>;

constructor(
Expand Down Expand Up @@ -206,6 +214,8 @@ export class NFTDropImpl extends StandardErc721<DropERC721> {
() => this.erc721.nextTokenIdToMint(),
);
this.interceptor = new ContractInterceptor(this.contractWrapper);

this.checkout = new PaperCheckout(this.contractWrapper);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
TransactionResult,
TransactionResultWithId,
} from "../../core/types";
import { PaperCheckout } from "../../integrations/paper-xyz";
import { DropErc721ContractSchema } from "../../schema/contracts/drop-erc721";
import { SDKOptions } from "../../schema/sdk-options";
import {
Expand Down Expand Up @@ -161,6 +162,12 @@ export class SignatureDropImpl extends StandardErc721<SignatureDropContract> {
*/
public signature: Erc721WithQuantitySignatureMintable;

/**
* Checkout
* @remarks Create a FIAT currency checkout for your NFT drop.
*/
public checkout: PaperCheckout<SignatureDropContract>;

constructor(
network: NetworkOrSignerOrProvider,
address: string,
Expand Down Expand Up @@ -212,6 +219,8 @@ export class SignatureDropImpl extends StandardErc721<SignatureDropContract> {
this.contractWrapper,
this.storage,
);

this.checkout = new PaperCheckout(this.contractWrapper);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export * from "./common";
export * from "./constants";
export * from "./contracts";

// export integration things
export * from "./integrations/paper-xyz";

// explcitly export the *TYPES* of prebuilt contracts
export type { EditionImpl } from "./contracts/prebuilt-implementations/edition";
export type { EditionDropImpl } from "./contracts/prebuilt-implementations/edition-drop";
Expand Down
238 changes: 238 additions & 0 deletions packages/sdk/src/integrations/paper-xyz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { ChainId } from "../constants";
import { ContractWrapper } from "../core/classes/contract-wrapper";
import { SignedPayload721WithQuantitySignature } from "../schema/contracts/common/signature";
import { DropERC721 } from "@thirdweb-dev/contracts-js/dist/declarations/src/DropERC721";
import { DropERC1155 } from "@thirdweb-dev/contracts-js/dist/declarations/src/DropERC1155";
import { SignatureDrop } from "@thirdweb-dev/contracts-js/dist/declarations/src/SignatureDrop";
import fetch from "cross-fetch";
import invariant from "tiny-invariant";

const PAPER_API_BASE = `https://paper.xyz/api` as const;
const PAPER_API_VERSION = `2022-08-12` as const;

/**
* @internal
*/
export const PAPER_API_URL =
`${PAPER_API_BASE}/${PAPER_API_VERSION}/platform/thirdweb` as const;

const PAPER_CHAIN_ID_MAP = {
[ChainId.Mainnet]: "Ethereum",
[ChainId.Rinkeby]: "Rinkeby",
[ChainId.Goerli]: "Goerli",
[ChainId.Polygon]: "Polygon",
[ChainId.Mumbai]: "Mumbai",
[ChainId.Avalanche]: "Avalanche",
} as const;

/**
* @internal
*/
export function parseChainIdToPaperChain(chainId: number) {
invariant(
chainId in PAPER_CHAIN_ID_MAP,
`chainId not supported by paper: ${chainId}`,
);
return PAPER_CHAIN_ID_MAP[chainId as keyof typeof PAPER_CHAIN_ID_MAP];
}

type RegisterContractSuccessResponse = {
result: {
id: string;
};
};

/**
*
* @param contractAddress
* @param chainId
* @internal
* @returns the paper xyz contract id
* @throws if the contract is not registered on paper xyz
*/
export async function fetchRegisteredCheckoutId(
contractAddress: string,
chainId: number,
): Promise<string> {
const paperChain = parseChainIdToPaperChain(chainId);
const res = await fetch(
`${PAPER_API_URL}/register-contract?contractAddress=${contractAddress}&chain=${paperChain}`,
);
const json = (await res.json()) as RegisterContractSuccessResponse;
invariant(json.result.id, "Contract is not registered with paper");
return json.result.id;
}

/**
* The parameters for creating a paper.xyz checkout link.
* @public
*/
export type PaperCreateCheckoutLinkShardParams = {
/**
* The title of the checkout.
*/
title: string;
/**
* The number of NFTs that will be purchased through the checkout flow.
*/
quantity?: number;
/**
* The wallet address that the NFT will be sent to.
*/
walletAddress?: string;
/**
* The email address of the recipient.
*/
email?: string;
/**
* The description of the checkout.
*/
description?: string;
/**
* The image that will be displayed on the checkout page.
*/
imageUrl?: string;
/**
* Override the seller's Twitter handle for this checkout.
*/
twitterHandleOverride?: string;
/**
* The time in minutes that the intent will be valid for.
*/
expiresInMinutes?: number;
/**
* Determines whether the buyer or seller pays the network and service fees for this purchase. The seller will be billed if set to SELLER. (default: `BUYER`)
*/
feeBearer?: "BUYER" | "SELLER";
/**
* Arbitrary data that will be included in webhooks and when viewing purchases in the paper.xyz dashboard.
*/
metadata?: Record<string, string | number | null>;
/**
* If true, Paper will send buyers an email when their purchase is transferred to their wallet. (default: true)
*/
sendEmailOnSuccess?: boolean;
/**
* The URL to prompt the user to navigate after they complete their purchase.
*/
successCallbackUrl?: string;
/**
* The URL to redirect (or prompt the user to navigate) if the checkout link is expired or the buyer is not eligible to purchase (sold out, not allowlisted, sale not started, etc.).
*/
cancelCallbackurl?: string;
/**
* If true, the checkout flow will redirect the user to the successCallbackUrl immediately after successful payment and bypass the final receipt page.
*/
redirectAfterPayment?: boolean;
/**
* The maximum quantity the buyer is allowed to purchase in one transaction.
*/
limitPerTransaction?: number;
};

/**
* @internal
*/
export type PaperCreateCheckoutLinkIntentParams<
TContract extends DropERC721 | DropERC1155 | SignatureDrop,
> = PaperCreateCheckoutLinkShardParams &
(TContract extends DropERC1155
? {
contractArgs: { tokenId: string };
}
: TContract extends SignatureDrop
? {
contractArgs: SignedPayload721WithQuantitySignature;
}
: TContract extends DropERC721
? {}
: never);

/**
* @internal
*/
export type PaperCreateCheckoutLinkIntentResult = {
checkoutLinkIntentUrl: string;
};

const DEFAULT_PARAMS: Partial<PaperCreateCheckoutLinkShardParams> = {
expiresInMinutes: 15,
feeBearer: "BUYER",
sendEmailOnSuccess: true,
redirectAfterPayment: false,
};

/**
* @internal
*/
export async function createCheckoutLinkIntent<
TContract extends DropERC721 | DropERC1155 | SignatureDrop,
>(
contractId: string,
params: PaperCreateCheckoutLinkIntentParams<TContract>,
): Promise<string> {
const res = await fetch(`${PAPER_API_URL}/checkout-link-intent`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
contractId,
...DEFAULT_PARAMS,
...params,
metadata: {
...params.metadata,
via_platform: "thirdweb",
},
// overrides that are hard coded
hideNativeMint: true,
hidePaperWallet: !!params.walletAddress,
hideExternalWallet: true,
hidePayWithCrypto: true,
usePaperKey: false,
}),
});
const json = (await res.json()) as PaperCreateCheckoutLinkIntentResult;
invariant(
json.checkoutLinkIntentUrl,
"Failed to create checkout link intent",
);
return json.checkoutLinkIntentUrl;
}

/**
* @internal
*/
export class PaperCheckout<
TContract extends DropERC721 | DropERC1155 | SignatureDrop,
> {
private contractWrapper;

constructor(contractWrapper: ContractWrapper<TContract>) {
this.contractWrapper = contractWrapper;
}

private async getCheckoutId(): Promise<string> {
return fetchRegisteredCheckoutId(
this.contractWrapper.readContract.address,
await this.contractWrapper.getChainID(),
);
}

public async isEnabled(): Promise<boolean> {
try {
return !!(await this.getCheckoutId());
} catch (err) {
return false;
}
}

public async createLinkIntent(
params: PaperCreateCheckoutLinkIntentParams<TContract>,
): Promise<string> {
return await createCheckoutLinkIntent<TContract>(
await this.getCheckoutId(),
params,
);
}
}