From 4bee73e659a737d87c8aa51b46dc3a91081a20cb Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Mon, 19 Sep 2022 18:58:21 -0700 Subject: [PATCH 1/2] init paper checkout in v3 --- .changeset/new-islands-shave.md | 5 + .../prebuilt-implementations/edition-drop.ts | 9 + .../prebuilt-implementations/nft-drop.ts | 10 + .../signature-drop.ts | 9 + packages/sdk/src/index.ts | 3 + packages/sdk/src/integrations/paper-xyz.ts | 238 ++++++++++++++++++ yarn.lock | 6 +- 7 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 .changeset/new-islands-shave.md create mode 100644 packages/sdk/src/integrations/paper-xyz.ts diff --git a/.changeset/new-islands-shave.md b/.changeset/new-islands-shave.md new file mode 100644 index 00000000000..27a47af37b6 --- /dev/null +++ b/.changeset/new-islands-shave.md @@ -0,0 +1,5 @@ +--- +"@thirdweb-dev/sdk": patch +--- + +Add fiat checkout to nft-drop, edition-drop and signature-drop diff --git a/packages/sdk/src/contracts/prebuilt-implementations/edition-drop.ts b/packages/sdk/src/contracts/prebuilt-implementations/edition-drop.ts index 8f8c1fbe516..7d302230253 100644 --- a/packages/sdk/src/contracts/prebuilt-implementations/edition-drop.ts +++ b/packages/sdk/src/contracts/prebuilt-implementations/edition-drop.ts @@ -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"; @@ -106,6 +107,13 @@ export class EditionDropImpl extends StandardErc1155 { * ``` */ public claimConditions: DropErc1155ClaimConditions; + + /** + * Checkout + * @remarks Create a FIAT currency checkout for your NFT drop. + */ + public checkout: PaperCheckout; + public history: DropErc1155History; public interceptor: ContractInterceptor; public erc1155: Erc1155; @@ -148,6 +156,7 @@ export class EditionDropImpl extends StandardErc1155 { 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); } /** diff --git a/packages/sdk/src/contracts/prebuilt-implementations/nft-drop.ts b/packages/sdk/src/contracts/prebuilt-implementations/nft-drop.ts index 34edb9fe680..596125b4d16 100644 --- a/packages/sdk/src/contracts/prebuilt-implementations/nft-drop.ts +++ b/packages/sdk/src/contracts/prebuilt-implementations/nft-drop.ts @@ -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 { @@ -161,6 +162,13 @@ export class NFTDropImpl extends StandardErc721 { * ``` */ public revealer: DelayedReveal; + + /** + * Checkout + * @remarks Create a FIAT currency checkout for your NFT drop. + */ + public checkout: PaperCheckout; + public erc721: Erc721; constructor( @@ -206,6 +214,8 @@ export class NFTDropImpl extends StandardErc721 { () => this.erc721.nextTokenIdToMint(), ); this.interceptor = new ContractInterceptor(this.contractWrapper); + + this.checkout = new PaperCheckout(this.contractWrapper); } /** diff --git a/packages/sdk/src/contracts/prebuilt-implementations/signature-drop.ts b/packages/sdk/src/contracts/prebuilt-implementations/signature-drop.ts index 3bf4eadbded..8d099198b1d 100644 --- a/packages/sdk/src/contracts/prebuilt-implementations/signature-drop.ts +++ b/packages/sdk/src/contracts/prebuilt-implementations/signature-drop.ts @@ -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 { @@ -161,6 +162,12 @@ export class SignatureDropImpl extends StandardErc721 { */ public signature: Erc721WithQuantitySignatureMintable; + /** + * Checkout + * @remarks Create a FIAT currency checkout for your NFT drop. + */ + public checkout: PaperCheckout; + constructor( network: NetworkOrSignerOrProvider, address: string, @@ -212,6 +219,8 @@ export class SignatureDropImpl extends StandardErc721 { this.contractWrapper, this.storage, ); + + this.checkout = new PaperCheckout(this.contractWrapper); } /** diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index fa19a385fb8..d6b8f9197d4 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -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"; diff --git a/packages/sdk/src/integrations/paper-xyz.ts b/packages/sdk/src/integrations/paper-xyz.ts new file mode 100644 index 00000000000..838757899bd --- /dev/null +++ b/packages/sdk/src/integrations/paper-xyz.ts @@ -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 type { BigNumberish } from "ethers"; +import invariant from "tiny-invariant"; + +const PAPER_API_BASE = `https://paper.xyz/api`; +const PAPER_API_VERSION = `2022-08-12`; + +/** + * @internal + */ +export const PAPER_API_URL = `${PAPER_API_BASE}/${PAPER_API_VERSION}/platform/thirdweb`; + +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 fetchRegisteredContractId( + contractAddress: string, + chainId: number, +): Promise { + 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 wallet address that the NFT will be sent to. + */ + walletAddress: string; + /** + * The email address of the recipient. + */ + email: string; + /** + * The number of NFTs that will be purchased through the checkout flow. + */ + quantity: number; + /** + * The title of the checkout. + */ + title: 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; + /** + * 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: BigNumberish }; + } + : TContract extends SignatureDrop + ? { + contractArgs: SignedPayload721WithQuantitySignature; + } + : TContract extends DropERC721 + ? {} + : never); + +/** + * @internal + */ +export type PaperCreateCheckoutLinkIntentResult = { + checkoutLinkIntentUrl: string; +}; + +const DEFAULT_PARAMS: Partial = { + expiresInMinutes: 15, + feeBearer: "BUYER", + sendEmailOnSuccess: true, + redirectAfterPayment: false, +}; + +/** + * @internal + */ +export async function createCheckoutLinkIntent< + TContract extends DropERC721 | DropERC1155 | SignatureDrop, +>( + contractId: string, + params: PaperCreateCheckoutLinkIntentParams, +): Promise { + 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: true, + hideExternalWallet: true, + hidePayWithCrypto: true, + usePaperKey: true, + }), + }); + 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) { + this.contractWrapper = contractWrapper; + } + + private async getContractId(): Promise { + return fetchRegisteredContractId( + this.contractWrapper.readContract.address, + await this.contractWrapper.getChainID(), + ); + } + + public async isEnabled(): Promise { + try { + return !!(await this.getContractId()); + } catch (err) { + return false; + } + } + + public async createLinkIntent( + params: PaperCreateCheckoutLinkIntentParams, + ): Promise { + return await createCheckoutLinkIntent( + await this.getContractId(), + params, + ); + } +} diff --git a/yarn.lock b/yarn.lock index d036212a5ae..f397e17917e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2537,7 +2537,7 @@ bn.js "^5.2.0" debug "^4.3.3" -"@microsoft/api-documenter@^7.19.4": +"@microsoft/api-documenter@^7.19.13", "@microsoft/api-documenter@^7.19.4": version "7.19.13" resolved "https://registry.yarnpkg.com/@microsoft/api-documenter/-/api-documenter-7.19.13.tgz#a3fba1bdfacec6c1caf1f9181a365395b2cf655e" integrity sha512-wlvbPz1/WC0ZlBYz5oHCYOO7JB1Eh2QTObDdaTeIXRyCGkq2y7DF+0/rXNCtuhR1kM9elvl+IZV1VJzm8z4i1Q== @@ -2559,7 +2559,7 @@ "@microsoft/tsdoc-config" "~0.16.1" "@rushstack/node-core-library" "3.51.2" -"@microsoft/api-extractor@^7.29.2": +"@microsoft/api-extractor@^7.29.2", "@microsoft/api-extractor@^7.31.1": version "7.31.1" resolved "https://registry.yarnpkg.com/@microsoft/api-extractor/-/api-extractor-7.31.1.tgz#defe791e4f1acc3a10202590fd39e7c487edb7e8" integrity sha512-rWEE+S1to8B2X8E8fVttwmCNS7yfvTNzlFGdla/OT8bJeS94L7Lw1Wkynwsl59gb46yvMZrQDXiRkXWzxgvc8g== @@ -2592,7 +2592,7 @@ resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz#155ef21065427901994e765da8a0ba0eaae8b8bd" integrity sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw== -"@microsoft/tsdoc@0.14.2", "@microsoft/tsdoc@^0.14.1": +"@microsoft/tsdoc@0.14.2", "@microsoft/tsdoc@^0.14.1", "@microsoft/tsdoc@^0.14.2": version "0.14.2" resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz#c3ec604a0b54b9a9b87e9735dfc59e1a5da6a5fb" integrity sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== From cb4828fe0e78bc7a10400485d7465144d9620847 Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Mon, 19 Sep 2022 22:06:29 -0700 Subject: [PATCH 2/2] update paper integration --- packages/sdk/src/integrations/paper-xyz.ts | 40 +++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/sdk/src/integrations/paper-xyz.ts b/packages/sdk/src/integrations/paper-xyz.ts index 838757899bd..cdc50a9a944 100644 --- a/packages/sdk/src/integrations/paper-xyz.ts +++ b/packages/sdk/src/integrations/paper-xyz.ts @@ -5,16 +5,16 @@ import { DropERC721 } from "@thirdweb-dev/contracts-js/dist/declarations/src/Dro 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 type { BigNumberish } from "ethers"; import invariant from "tiny-invariant"; -const PAPER_API_BASE = `https://paper.xyz/api`; -const PAPER_API_VERSION = `2022-08-12`; +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`; +export const PAPER_API_URL = + `${PAPER_API_BASE}/${PAPER_API_VERSION}/platform/thirdweb` as const; const PAPER_CHAIN_ID_MAP = { [ChainId.Mainnet]: "Ethereum", @@ -50,7 +50,7 @@ type RegisterContractSuccessResponse = { * @returns the paper xyz contract id * @throws if the contract is not registered on paper xyz */ -export async function fetchRegisteredContractId( +export async function fetchRegisteredCheckoutId( contractAddress: string, chainId: number, ): Promise { @@ -69,21 +69,21 @@ export async function fetchRegisteredContractId( */ export type PaperCreateCheckoutLinkShardParams = { /** - * The wallet address that the NFT will be sent to. + * The title of the checkout. */ - walletAddress: string; + title: string; /** - * The email address of the recipient. + * The number of NFTs that will be purchased through the checkout flow. */ - email: string; + quantity?: number; /** - * The number of NFTs that will be purchased through the checkout flow. + * The wallet address that the NFT will be sent to. */ - quantity: number; + walletAddress?: string; /** - * The title of the checkout. + * The email address of the recipient. */ - title: string; + email?: string; /** * The description of the checkout. */ @@ -138,7 +138,7 @@ export type PaperCreateCheckoutLinkIntentParams< > = PaperCreateCheckoutLinkShardParams & (TContract extends DropERC1155 ? { - contractArgs: { tokenId: BigNumberish }; + contractArgs: { tokenId: string }; } : TContract extends SignatureDrop ? { @@ -186,10 +186,10 @@ export async function createCheckoutLinkIntent< }, // overrides that are hard coded hideNativeMint: true, - hidePaperWallet: true, + hidePaperWallet: !!params.walletAddress, hideExternalWallet: true, hidePayWithCrypto: true, - usePaperKey: true, + usePaperKey: false, }), }); const json = (await res.json()) as PaperCreateCheckoutLinkIntentResult; @@ -212,8 +212,8 @@ export class PaperCheckout< this.contractWrapper = contractWrapper; } - private async getContractId(): Promise { - return fetchRegisteredContractId( + private async getCheckoutId(): Promise { + return fetchRegisteredCheckoutId( this.contractWrapper.readContract.address, await this.contractWrapper.getChainID(), ); @@ -221,7 +221,7 @@ export class PaperCheckout< public async isEnabled(): Promise { try { - return !!(await this.getContractId()); + return !!(await this.getCheckoutId()); } catch (err) { return false; } @@ -231,7 +231,7 @@ export class PaperCheckout< params: PaperCreateCheckoutLinkIntentParams, ): Promise { return await createCheckoutLinkIntent( - await this.getContractId(), + await this.getCheckoutId(), params, ); }