diff --git a/.changeset/lovely-spoons-attack.md b/.changeset/lovely-spoons-attack.md new file mode 100644 index 00000000000..8331f53ca22 --- /dev/null +++ b/.changeset/lovely-spoons-attack.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Allow signature minting for LoyaltyCard contracts by passing the contractType diff --git a/packages/thirdweb/scripts/generate/abis/erc721/ISignatureMintERC721_v2.json b/packages/thirdweb/scripts/generate/abis/erc721/ISignatureMintERC721_v2.json new file mode 100644 index 00000000000..76c02d9ad40 --- /dev/null +++ b/packages/thirdweb/scripts/generate/abis/erc721/ISignatureMintERC721_v2.json @@ -0,0 +1,5 @@ +[ + "function mintWithSignature((address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) payload, bytes signature) payable returns (address signer)", + "function verify((address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) req, bytes signature) view returns (bool success, address signer)", + "event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, (address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) mintRequest)" +] \ No newline at end of file diff --git a/packages/thirdweb/src/extensions/erc721/__generated__/ISignatureMintERC721_v2/events/TokensMintedWithSignature.ts b/packages/thirdweb/src/extensions/erc721/__generated__/ISignatureMintERC721_v2/events/TokensMintedWithSignature.ts new file mode 100644 index 00000000000..e51025efeaf --- /dev/null +++ b/packages/thirdweb/src/extensions/erc721/__generated__/ISignatureMintERC721_v2/events/TokensMintedWithSignature.ts @@ -0,0 +1,55 @@ +import { prepareEvent } from "../../../../../event/prepare-event.js"; +import type { AbiParameterToPrimitiveType } from "abitype"; + +/** + * Represents the filters for the "TokensMintedWithSignature" event. + */ +export type TokensMintedWithSignatureEventFilters = Partial<{ + signer: AbiParameterToPrimitiveType<{ + type: "address"; + name: "signer"; + indexed: true; + }>; + mintedTo: AbiParameterToPrimitiveType<{ + type: "address"; + name: "mintedTo"; + indexed: true; + }>; + tokenIdMinted: AbiParameterToPrimitiveType<{ + type: "uint256"; + name: "tokenIdMinted"; + indexed: true; + }>; +}>; + +/** + * Creates an event object for the TokensMintedWithSignature event. + * @param filters - Optional filters to apply to the event. + * @returns The prepared event object. + * @extension ERC721 + * @example + * ```ts + * import { getContractEvents } from "thirdweb"; + * import { tokensMintedWithSignatureEvent } from "thirdweb/extensions/erc721"; + * + * const events = await getContractEvents({ + * contract, + * events: [ + * tokensMintedWithSignatureEvent({ + * signer: ..., + * mintedTo: ..., + * tokenIdMinted: ..., + * }) + * ], + * }); + * ``` + */ +export function tokensMintedWithSignatureEvent( + filters: TokensMintedWithSignatureEventFilters = {}, +) { + return prepareEvent({ + signature: + "event TokensMintedWithSignature(address indexed signer, address indexed mintedTo, uint256 indexed tokenIdMinted, (address to, address royaltyRecipient, uint256 royaltyBps, address primarySaleRecipient, string uri, uint256 quantity, uint256 pricePerToken, address currency, uint128 validityStartTimestamp, uint128 validityEndTimestamp, bytes32 uid) mintRequest)", + filters, + }); +} diff --git a/packages/thirdweb/src/extensions/erc721/__generated__/ISignatureMintERC721_v2/read/verify.ts b/packages/thirdweb/src/extensions/erc721/__generated__/ISignatureMintERC721_v2/read/verify.ts new file mode 100644 index 00000000000..271dd56a03a --- /dev/null +++ b/packages/thirdweb/src/extensions/erc721/__generated__/ISignatureMintERC721_v2/read/verify.ts @@ -0,0 +1,196 @@ +import type { AbiParameterToPrimitiveType } from "abitype"; +import { readContract } from "../../../../../transaction/read-contract.js"; +import type { BaseTransactionOptions } from "../../../../../transaction/types.js"; +import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js"; +import { decodeAbiParameters } from "viem"; +import type { Hex } from "../../../../../utils/encoding/hex.js"; +import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js"; + +/** + * Represents the parameters for the "verify" function. + */ +export type VerifyParams = { + req: AbiParameterToPrimitiveType<{ + type: "tuple"; + name: "req"; + components: [ + { type: "address"; name: "to" }, + { type: "address"; name: "royaltyRecipient" }, + { type: "uint256"; name: "royaltyBps" }, + { type: "address"; name: "primarySaleRecipient" }, + { type: "string"; name: "uri" }, + { type: "uint256"; name: "quantity" }, + { type: "uint256"; name: "pricePerToken" }, + { type: "address"; name: "currency" }, + { type: "uint128"; name: "validityStartTimestamp" }, + { type: "uint128"; name: "validityEndTimestamp" }, + { type: "bytes32"; name: "uid" }, + ]; + }>; + signature: AbiParameterToPrimitiveType<{ type: "bytes"; name: "signature" }>; +}; + +export const FN_SELECTOR = "0x252e82e8" as const; +const FN_INPUTS = [ + { + type: "tuple", + name: "req", + components: [ + { + type: "address", + name: "to", + }, + { + type: "address", + name: "royaltyRecipient", + }, + { + type: "uint256", + name: "royaltyBps", + }, + { + type: "address", + name: "primarySaleRecipient", + }, + { + type: "string", + name: "uri", + }, + { + type: "uint256", + name: "quantity", + }, + { + type: "uint256", + name: "pricePerToken", + }, + { + type: "address", + name: "currency", + }, + { + type: "uint128", + name: "validityStartTimestamp", + }, + { + type: "uint128", + name: "validityEndTimestamp", + }, + { + type: "bytes32", + name: "uid", + }, + ], + }, + { + type: "bytes", + name: "signature", + }, +] as const; +const FN_OUTPUTS = [ + { + type: "bool", + name: "success", + }, + { + type: "address", + name: "signer", + }, +] as const; + +/** + * Checks if the `verify` method is supported by the given contract. + * @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors. + * @returns A boolean indicating if the `verify` method is supported. + * @extension ERC721 + * @example + * ```ts + * import { isVerifySupported } from "thirdweb/extensions/erc721"; + * const supported = isVerifySupported(["0x..."]); + * ``` + */ +export function isVerifySupported(availableSelectors: string[]) { + return detectMethod({ + availableSelectors, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + }); +} + +/** + * Encodes the parameters for the "verify" function. + * @param options - The options for the verify function. + * @returns The encoded ABI parameters. + * @extension ERC721 + * @example + * ```ts + * import { encodeVerifyParams } from "thirdweb/extensions/erc721"; + * const result = encodeVerifyParams({ + * req: ..., + * signature: ..., + * }); + * ``` + */ +export function encodeVerifyParams(options: VerifyParams) { + return encodeAbiParameters(FN_INPUTS, [options.req, options.signature]); +} + +/** + * Encodes the "verify" function into a Hex string with its parameters. + * @param options - The options for the verify function. + * @returns The encoded hexadecimal string. + * @extension ERC721 + * @example + * ```ts + * import { encodeVerify } from "thirdweb/extensions/erc721"; + * const result = encodeVerify({ + * req: ..., + * signature: ..., + * }); + * ``` + */ +export function encodeVerify(options: VerifyParams) { + // we do a "manual" concat here to avoid the overhead of the "concatHex" function + // we can do this because we know the specific formats of the values + return (FN_SELECTOR + + encodeVerifyParams(options).slice(2)) as `${typeof FN_SELECTOR}${string}`; +} + +/** + * Decodes the result of the verify function call. + * @param result - The hexadecimal result to decode. + * @returns The decoded result as per the FN_OUTPUTS definition. + * @extension ERC721 + * @example + * ```ts + * import { decodeVerifyResult } from "thirdweb/extensions/erc721"; + * const result = decodeVerifyResultResult("..."); + * ``` + */ +export function decodeVerifyResult(result: Hex) { + return decodeAbiParameters(FN_OUTPUTS, result); +} + +/** + * Calls the "verify" function on the contract. + * @param options - The options for the verify function. + * @returns The parsed result of the function call. + * @extension ERC721 + * @example + * ```ts + * import { verify } from "thirdweb/extensions/erc721"; + * + * const result = await verify({ + * contract, + * req: ..., + * signature: ..., + * }); + * + * ``` + */ +export async function verify(options: BaseTransactionOptions) { + return readContract({ + contract: options.contract, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + params: [options.req, options.signature], + }); +} diff --git a/packages/thirdweb/src/extensions/erc721/__generated__/ISignatureMintERC721_v2/write/mintWithSignature.ts b/packages/thirdweb/src/extensions/erc721/__generated__/ISignatureMintERC721_v2/write/mintWithSignature.ts new file mode 100644 index 00000000000..f7dd0e83154 --- /dev/null +++ b/packages/thirdweb/src/extensions/erc721/__generated__/ISignatureMintERC721_v2/write/mintWithSignature.ts @@ -0,0 +1,214 @@ +import type { AbiParameterToPrimitiveType } from "abitype"; +import type { + BaseTransactionOptions, + WithOverrides, +} from "../../../../../transaction/types.js"; +import { prepareContractCall } from "../../../../../transaction/prepare-contract-call.js"; +import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js"; +import { once } from "../../../../../utils/promise/once.js"; +import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js"; + +/** + * Represents the parameters for the "mintWithSignature" function. + */ +export type MintWithSignatureParams = WithOverrides<{ + payload: AbiParameterToPrimitiveType<{ + type: "tuple"; + name: "payload"; + components: [ + { type: "address"; name: "to" }, + { type: "address"; name: "royaltyRecipient" }, + { type: "uint256"; name: "royaltyBps" }, + { type: "address"; name: "primarySaleRecipient" }, + { type: "string"; name: "uri" }, + { type: "uint256"; name: "quantity" }, + { type: "uint256"; name: "pricePerToken" }, + { type: "address"; name: "currency" }, + { type: "uint128"; name: "validityStartTimestamp" }, + { type: "uint128"; name: "validityEndTimestamp" }, + { type: "bytes32"; name: "uid" }, + ]; + }>; + signature: AbiParameterToPrimitiveType<{ type: "bytes"; name: "signature" }>; +}>; + +export const FN_SELECTOR = "0x439c7be5" as const; +const FN_INPUTS = [ + { + type: "tuple", + name: "payload", + components: [ + { + type: "address", + name: "to", + }, + { + type: "address", + name: "royaltyRecipient", + }, + { + type: "uint256", + name: "royaltyBps", + }, + { + type: "address", + name: "primarySaleRecipient", + }, + { + type: "string", + name: "uri", + }, + { + type: "uint256", + name: "quantity", + }, + { + type: "uint256", + name: "pricePerToken", + }, + { + type: "address", + name: "currency", + }, + { + type: "uint128", + name: "validityStartTimestamp", + }, + { + type: "uint128", + name: "validityEndTimestamp", + }, + { + type: "bytes32", + name: "uid", + }, + ], + }, + { + type: "bytes", + name: "signature", + }, +] as const; +const FN_OUTPUTS = [ + { + type: "address", + name: "signer", + }, +] as const; + +/** + * Checks if the `mintWithSignature` method is supported by the given contract. + * @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors. + * @returns A boolean indicating if the `mintWithSignature` method is supported. + * @extension ERC721 + * @example + * ```ts + * import { isMintWithSignatureSupported } from "thirdweb/extensions/erc721"; + * + * const supported = isMintWithSignatureSupported(["0x..."]); + * ``` + */ +export function isMintWithSignatureSupported(availableSelectors: string[]) { + return detectMethod({ + availableSelectors, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + }); +} + +/** + * Encodes the parameters for the "mintWithSignature" function. + * @param options - The options for the mintWithSignature function. + * @returns The encoded ABI parameters. + * @extension ERC721 + * @example + * ```ts + * import { encodeMintWithSignatureParams } from "thirdweb/extensions/erc721"; + * const result = encodeMintWithSignatureParams({ + * payload: ..., + * signature: ..., + * }); + * ``` + */ +export function encodeMintWithSignatureParams( + options: MintWithSignatureParams, +) { + return encodeAbiParameters(FN_INPUTS, [options.payload, options.signature]); +} + +/** + * Encodes the "mintWithSignature" function into a Hex string with its parameters. + * @param options - The options for the mintWithSignature function. + * @returns The encoded hexadecimal string. + * @extension ERC721 + * @example + * ```ts + * import { encodeMintWithSignature } from "thirdweb/extensions/erc721"; + * const result = encodeMintWithSignature({ + * payload: ..., + * signature: ..., + * }); + * ``` + */ +export function encodeMintWithSignature(options: MintWithSignatureParams) { + // we do a "manual" concat here to avoid the overhead of the "concatHex" function + // we can do this because we know the specific formats of the values + return (FN_SELECTOR + + encodeMintWithSignatureParams(options).slice( + 2, + )) as `${typeof FN_SELECTOR}${string}`; +} + +/** + * Prepares a transaction to call the "mintWithSignature" function on the contract. + * @param options - The options for the "mintWithSignature" function. + * @returns A prepared transaction object. + * @extension ERC721 + * @example + * ```ts + * import { sendTransaction } from "thirdweb"; + * import { mintWithSignature } from "thirdweb/extensions/erc721"; + * + * const transaction = mintWithSignature({ + * contract, + * payload: ..., + * signature: ..., + * overrides: { + * ... + * } + * }); + * + * // Send the transaction + * await sendTransaction({ transaction, account }); + * ``` + */ +export function mintWithSignature( + options: BaseTransactionOptions< + | MintWithSignatureParams + | { + asyncParams: () => Promise; + } + >, +) { + const asyncOptions = once(async () => { + return "asyncParams" in options ? await options.asyncParams() : options; + }); + + return prepareContractCall({ + contract: options.contract, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + params: async () => { + const resolvedOptions = await asyncOptions(); + return [resolvedOptions.payload, resolvedOptions.signature] as const; + }, + value: async () => (await asyncOptions()).overrides?.value, + accessList: async () => (await asyncOptions()).overrides?.accessList, + gas: async () => (await asyncOptions()).overrides?.gas, + gasPrice: async () => (await asyncOptions()).overrides?.gasPrice, + maxFeePerGas: async () => (await asyncOptions()).overrides?.maxFeePerGas, + maxPriorityFeePerGas: async () => + (await asyncOptions()).overrides?.maxPriorityFeePerGas, + nonce: async () => (await asyncOptions()).overrides?.nonce, + extraGas: async () => (await asyncOptions()).overrides?.extraGas, + erc20Value: async () => (await asyncOptions()).overrides?.erc20Value, + }); +} diff --git a/packages/thirdweb/src/extensions/erc721/write/sigMint.ts b/packages/thirdweb/src/extensions/erc721/write/sigMint.ts index 5c8ce8416fc..c4be3747b47 100644 --- a/packages/thirdweb/src/extensions/erc721/write/sigMint.ts +++ b/packages/thirdweb/src/extensions/erc721/write/sigMint.ts @@ -15,6 +15,10 @@ import { type MintWithSignatureParams, mintWithSignature as generatedMintWithSignature, } from "../__generated__/ISignatureMintERC721/write/mintWithSignature.js"; +import { + type MintWithSignatureParams as MintWithSignatureParamsV2, + mintWithSignature as generatedMintWithSignatureV2, +} from "../__generated__/ISignatureMintERC721_v2/write/mintWithSignature.js"; /** * Mints a new ERC721 token with the given minter signature @@ -38,6 +42,23 @@ import { * @returns A promise that resolves to the transaction result. */ export function mintWithSignature( + options: BaseTransactionOptions< + | { payload: PayloadTypeV2; signature: Hex } + | { payload: PayloadType; signature: Hex } + >, +) { + const { payload } = options; + if ("quantity" in payload) { + return mintWithSignatureV2( + options as BaseTransactionOptions, + ); + } + return mintWithSignatureV1( + options as BaseTransactionOptions, + ); +} + +function mintWithSignatureV1( options: BaseTransactionOptions, ) { const value = isNativeTokenAddress(options.payload.currency) @@ -51,10 +72,27 @@ export function mintWithSignature( }); } -export type GenerateMintSignatureOptions = { +function mintWithSignatureV2( + options: BaseTransactionOptions, +) { + const value = isNativeTokenAddress(options.payload.currency) + ? options.payload.pricePerToken * options.payload.quantity + : 0n; + return generatedMintWithSignatureV2({ + ...options, + overrides: { + value, + }, + }); +} + +export type GenerateMintSignatureOptions< + T extends "LoyaltyCard" | "TokenERC721" = "TokenERC721", +> = { account: Account; contract: ThirdwebContract; mintRequest: GeneratePayloadInput; + contractType?: T; }; /** @@ -87,10 +125,10 @@ export type GenerateMintSignatureOptions = { * @extension ERC721 * @returns A promise that resolves to the payload and signature. */ -export async function generateMintSignature( - options: GenerateMintSignatureOptions, -) { - const { mintRequest, account, contract } = options; +export async function generateMintSignature< + T extends "LoyaltyCard" | "TokenERC721" = "TokenERC721", +>(options: GenerateMintSignatureOptions): Promise> { + const { mintRequest, account, contract, contractType } = options; const currency = mintRequest.currency || NATIVE_TOKEN_ADDRESS; const [price, uri, uid] = await Promise.all([ @@ -175,15 +213,70 @@ export async function generateMintSignature( royaltyRecipient = mintRequest.royaltyRecipient; } + if (contractType === "LoyaltyCard") { + return signPayloadV2({ + mintRequest, + account, + contract, + uri, + currency, + uid, + price, + royaltyRecipient, + primarySaleRecipient: saleRecipient, + startTime, + endTime, + }) as Promise>; + } + return signPayloadV1({ + mintRequest, + account, + contract, + uri, + currency, + uid, + price, + royaltyRecipient, + primarySaleRecipient: saleRecipient, + startTime, + endTime, + }) as Promise>; +} + +async function signPayloadV1({ + mintRequest, + account, + contract, + uri, + currency, + uid, + price, + royaltyRecipient, + primarySaleRecipient, + startTime, + endTime, +}: { + mintRequest: GeneratePayloadInput; + account: Account; + contract: ThirdwebContract; + uri: string; + currency: Address; + uid: Hex; + price: bigint; + royaltyRecipient: Address; + primarySaleRecipient: Address; + startTime: Date; + endTime: Date; +}): Promise<{ payload: PayloadType; signature: Hex }> { const payload: PayloadType = { uri, currency, uid, price, to: mintRequest.to, - royaltyRecipient: royaltyRecipient, + royaltyRecipient, royaltyBps: toBigInt(mintRequest.royaltyBps || 0), - primarySaleRecipient: saleRecipient, + primarySaleRecipient, validityStartTimestamp: dateToSeconds(startTime), validityEndTimestamp: dateToSeconds(endTime), }; @@ -195,19 +288,86 @@ export async function generateMintSignature( chainId: contract.chain.id, verifyingContract: contract.address, }, - types: { MintRequest: MintRequest721 }, + types: { + MintRequest: MintRequest721, + }, + primaryType: "MintRequest", + message: payload, + }); + return { payload, signature }; +} + +async function signPayloadV2({ + mintRequest, + account, + contract, + uri, + currency, + uid, + price, + royaltyRecipient, + primarySaleRecipient, + startTime, + endTime, +}: { + mintRequest: GeneratePayloadInput; + account: Account; + contract: ThirdwebContract; + uri: string; + currency: Address; + uid: Hex; + price: bigint; + royaltyRecipient: Address; + primarySaleRecipient: Address; + startTime: Date; + endTime: Date; +}): Promise<{ payload: PayloadTypeV2; signature: Hex }> { + const payload: PayloadTypeV2 = { + uri, + currency, + uid, + quantity: toBigInt(1), // always 1 for 721 NFTs + pricePerToken: price, + to: mintRequest.to, + royaltyRecipient, + royaltyBps: toBigInt(mintRequest.royaltyBps || 0), + primarySaleRecipient, + validityStartTimestamp: dateToSeconds(startTime), + validityEndTimestamp: dateToSeconds(endTime), + }; + + const signature = await account.signTypedData({ + domain: { + name: "SignatureMintERC721", + version: "1", + chainId: contract.chain.id, + verifyingContract: contract.address, + }, + types: { + MintRequest: MintRequest721_V2, + }, primaryType: "MintRequest", message: payload, }); return { payload, signature }; } +type SignPayloadResult = T extends "LoyaltyCard" + ? Awaited> + : Awaited>; + type PayloadType = AbiParameterToPrimitiveType<{ type: "tuple"; name: "payload"; components: typeof MintRequest721; }>; +type PayloadTypeV2 = AbiParameterToPrimitiveType<{ + type: "tuple"; + name: "payload"; + components: typeof MintRequest721_V2; +}>; + type GeneratePayloadInput = { to: string; metadata: NFTInput | string; @@ -234,3 +394,19 @@ const MintRequest721 = [ { name: "validityEndTimestamp", type: "uint128" }, { name: "uid", type: "bytes32" }, ] as const; + +// used for LoyaltyCard contract and base sigmint contracts +// adds quantity to the payload so its the same as 1155 +const MintRequest721_V2 = [ + { name: "to", type: "address" }, + { name: "royaltyRecipient", type: "address" }, + { name: "royaltyBps", type: "uint256" }, + { name: "primarySaleRecipient", type: "address" }, + { name: "uri", type: "string" }, + { name: "quantity", type: "uint256" }, + { name: "pricePerToken", type: "uint256" }, + { name: "currency", type: "address" }, + { name: "validityStartTimestamp", type: "uint128" }, + { name: "validityEndTimestamp", type: "uint128" }, + { name: "uid", type: "bytes32" }, +] as const; diff --git a/packages/thirdweb/src/extensions/erc721/write/sigMint721.test.ts b/packages/thirdweb/src/extensions/erc721/write/sigMint721.test.ts index afdd5296ca8..091b8735326 100644 --- a/packages/thirdweb/src/extensions/erc721/write/sigMint721.test.ts +++ b/packages/thirdweb/src/extensions/erc721/write/sigMint721.test.ts @@ -60,6 +60,48 @@ describe.runIf(process.env.TW_SECRET_KEY)( }); }, 60000); + it("should generate a mint signature and mint an NFT for LoyaltyContract", async () => { + const loyaltyContract = getContract({ + address: await deployERC721Contract({ + account: TEST_ACCOUNT_A, + chain: ANVIL_CHAIN, + client: TEST_CLIENT, + params: { + name: "Test", + symbol: "TST", + royaltyRecipient: TEST_ACCOUNT_C.address, + saleRecipient: TEST_ACCOUNT_B.address, + }, + type: "LoyaltyCard", + }), + chain: ANVIL_CHAIN, + client: TEST_CLIENT, + }); + const { payload, signature } = await generateMintSignature({ + mintRequest: { + to: TEST_ACCOUNT_B.address, + metadata: { + name: "My NFT", + description: "This is my NFT", + image: "https://example.com/image.png", + }, + }, + account: TEST_ACCOUNT_A, + contract: loyaltyContract, + contractType: "LoyaltyCard", + }); + const transaction = mintWithSignature({ + contract: loyaltyContract, + payload, + signature, + }); + const { transactionHash } = await sendTransaction({ + transaction, + account: TEST_ACCOUNT_A, + }); + expect(transactionHash.length).toBe(66); + }); + it("should generate a mint signature and mint an NFT", async () => { const { payload, signature } = await generateMintSignature({ mintRequest: {