From 2a98ca3fc65effeea35b1e72c64e52ad01664a52 Mon Sep 17 00:00:00 2001 From: Dan Oved Date: Tue, 14 Nov 2023 16:39:38 -0800 Subject: [PATCH] wip on premint v2 sdk --- .../src/premint/premint-client.ts | 11 +- .../src/premint/preminter.test.ts | 20 +- .../protocol-sdk/src/premint/preminter.ts | 236 ++++++++++++++---- 3 files changed, 203 insertions(+), 64 deletions(-) diff --git a/packages/protocol-sdk/src/premint/premint-client.ts b/packages/protocol-sdk/src/premint/premint-client.ts index 790e58a9e..006c63927 100644 --- a/packages/protocol-sdk/src/premint/premint-client.ts +++ b/packages/protocol-sdk/src/premint/premint-client.ts @@ -14,7 +14,10 @@ import { zoraCreator1155PremintExecutorImplAddress, zoraCreatorFixedPriceSaleStrategyAddress, } from "@zoralabs/protocol-deployments"; -import { PremintConfig, preminterTypedDataDefinition } from "./preminter"; +import { + PremintConfigV1, + premintConfigV1TypedDataDefinition, +} from "./preminter"; import type { PremintSignatureGetResponse, PremintSignatureResponse, @@ -127,7 +130,7 @@ export const convertCollection = ( export const encodePremintForAPI = ({ tokenConfig, ...premint -}: PremintConfig) => ({ +}: PremintConfigV1) => ({ ...premint, tokenConfig: { ...tokenConfig, @@ -327,7 +330,7 @@ class PremintClient extends ClientBase { verifyingContract: Address; checkSignature: boolean; account?: Address | Account; - premintConfig: PremintConfig; + premintConfig: PremintConfigV1; collection: PremintSignatureGetResponse["collection"]; }) { if (!account) { @@ -339,7 +342,7 @@ class PremintClient extends ClientBase { const signature = await walletClient.signTypedData({ account, - ...preminterTypedDataDefinition({ + ...premintConfigV1TypedDataDefinition({ verifyingContract, premintConfig, chainId: this.chain.id, diff --git a/packages/protocol-sdk/src/premint/preminter.test.ts b/packages/protocol-sdk/src/premint/preminter.test.ts index 6f32ea5ed..42857dfda 100644 --- a/packages/protocol-sdk/src/premint/preminter.test.ts +++ b/packages/protocol-sdk/src/premint/preminter.test.ts @@ -19,9 +19,9 @@ import { import { ContractCreationConfig, - PremintConfig, - TokenCreationConfig, - preminterTypedDataDefinition, + PremintConfigV1, + TokenCreationConfigV1, + premintConfigV1TypedDataDefinition, isValidSignatureV1, } from "./preminter"; import { @@ -45,7 +45,7 @@ const defaultContractConfig = ({ const defaultTokenConfig = ( fixedPriceMinterAddress: Address, creatorAccount: Address, -): TokenCreationConfig => ({ +): TokenCreationConfigV1 => ({ tokenURI: "ipfs://tokenIpfsId0", maxSupply: 100n, maxTokensPerAddress: 10n, @@ -61,7 +61,7 @@ const defaultTokenConfig = ( const defaultPremintConfig = ( fixedPriceMinter: Address, creatorAccount: Address, -): PremintConfig => ({ +): PremintConfigV1 => ({ tokenConfig: defaultTokenConfig(fixedPriceMinter, creatorAccount), deleted: false, uid: 105, @@ -128,7 +128,7 @@ describe("ZoraCreator1155Preminter", () => { }); const signedMessage = await viemClients.walletClient.signTypedData({ - ...preminterTypedDataDefinition({ + ...premintConfigV1TypedDataDefinition({ verifyingContract: contractAddress, chainId: 999, premintConfig, @@ -174,7 +174,7 @@ describe("ZoraCreator1155Preminter", () => { // sign message containing contract and token creation config and uid const signedMessage = await viemClients.walletClient.signTypedData({ - ...preminterTypedDataDefinition({ + ...premintConfigV1TypedDataDefinition({ verifyingContract: contractAddress, // we need to sign here for the anvil chain, cause thats where it is run on chainId: foundry.id, @@ -230,7 +230,7 @@ describe("ZoraCreator1155Preminter", () => { // have creator sign the message to create the contract // and the token const signedMessage = await viemClients.walletClient.signTypedData({ - ...preminterTypedDataDefinition({ + ...premintConfigV1TypedDataDefinition({ verifyingContract: contractAddress, // we need to sign here for the anvil chain, cause thats where it is run on chainId: foundry.id, @@ -326,7 +326,7 @@ describe("ZoraCreator1155Preminter", () => { // sign the message to create the second token const signedMessage2 = await viemClients.walletClient.signTypedData({ - ...preminterTypedDataDefinition({ + ...premintConfigV1TypedDataDefinition({ verifyingContract: contractAddress, chainId: foundry.id, premintConfig: premintConfig2, @@ -418,7 +418,7 @@ describe("ZoraCreator1155Preminter", () => { // have creator sign the message to create the contract // and the token const signedMessage = await viemClients.walletClient.signTypedData({ - ...preminterTypedDataDefinition({ + ...premintConfigV1TypedDataDefinition({ verifyingContract: contractAddress, // we need to sign here for the anvil chain, cause thats where it is run on chainId: foundry.id, diff --git a/packages/protocol-sdk/src/premint/preminter.ts b/packages/protocol-sdk/src/premint/preminter.ts index 1f26b36dc..245f85c27 100644 --- a/packages/protocol-sdk/src/premint/preminter.ts +++ b/packages/protocol-sdk/src/premint/preminter.ts @@ -9,76 +9,129 @@ import { recoverTypedDataAddress, Hex, PublicClient, + zeroAddress, } from "viem"; -type PremintInputs = ExtractAbiFunction< +type PremintV1Inputs = ExtractAbiFunction< typeof preminterAbi, - "premint" + "premintV1" >["inputs"]; -type PreminterHashDataTypes = AbiParametersToPrimitiveTypes; +type PremintV1HashDataTypes = AbiParametersToPrimitiveTypes; -export type ContractCreationConfig = PreminterHashDataTypes[0]; -export type PremintConfig = PreminterHashDataTypes[1]; -export type TokenCreationConfig = PremintConfig["tokenConfig"]; +export type ContractCreationConfig = PremintV1HashDataTypes[0]; + +export type PremintConfigV1 = PremintV1HashDataTypes[1]; +export type TokenCreationConfigV1 = PremintConfigV1["tokenConfig"]; + +type PremintV2Inputs = ExtractAbiFunction< + typeof preminterAbi, + "premintV2" +>["inputs"]; + +type PremintV2HashDataTypes = AbiParametersToPrimitiveTypes; + +export type PremintConfigV2 = PremintV2HashDataTypes[1]; +export type TokenCreationConfigV2 = PremintConfigV2["tokenConfig"]; + +const v1Types = { + CreatorAttribution: [ + { name: "tokenConfig", type: "TokenCreationConfig" }, + // unique id scoped to the contract and token to create. + // ensure that a signature can be replaced, as long as the replacement + // has the same uid, and a newer version. + { name: "uid", type: "uint32" }, + { name: "version", type: "uint32" }, + // if this update should result in the signature being deleted. + { name: "deleted", type: "bool" }, + ], + TokenCreationConfig: [ + { name: "tokenURI", type: "string" }, + { name: "maxSupply", type: "uint256" }, + { name: "maxTokensPerAddress", type: "uint64" }, + { name: "pricePerToken", type: "uint96" }, + { name: "mintStart", type: "uint64" }, + { name: "mintDuration", type: "uint64" }, + { name: "royaltyMintSchedule", type: "uint32" }, + { name: "royaltyBPS", type: "uint32" }, + { name: "royaltyRecipient", type: "address" }, + { name: "fixedPriceMinter", type: "address" }, + ], +} as const; + +const v2Types = { + CreatorAttribution: [ + { name: "tokenConfig", type: "TokenCreationConfig" }, + // unique id scoped to the contract and token to create. + // ensure that a signature can be replaced, as long as the replacement + // has the same uid, and a newer version. + { name: "uid", type: "uint32" }, + { name: "version", type: "uint32" }, + // if this update should result in the signature being deleted. + { name: "deleted", type: "bool" }, + ], + TokenCreationConfig: [ + { name: "tokenURI", type: "string" }, + { name: "maxSupply", type: "uint256" }, + { name: "maxTokensPerAddress", type: "uint64" }, + { name: "pricePerToken", type: "uint96" }, + { name: "mintStart", type: "uint64" }, + { name: "mintDuration", type: "uint64" }, + { name: "royaltyBPS", type: "uint32" }, + { name: "payoutRecipient", type: "address" }, + { name: "fixedPriceMinter", type: "address" }, + { name: "createReferral", type: "address" }, + ], +} as const; + +const PreminterDomain = "Preminter"; // Convenience method to create the structured typed data // needed to sign for a premint contract and token -export const preminterTypedDataDefinition = ({ +export const premintConfigV1TypedDataDefinition = ({ verifyingContract, premintConfig, chainId, }: { verifyingContract: Address; - premintConfig: PremintConfig; + premintConfig: PremintConfigV1; chainId: number; -}) => { - const { tokenConfig, uid, version, deleted } = premintConfig; - const types = { - CreatorAttribution: [ - { name: "tokenConfig", type: "TokenCreationConfig" }, - // unique id scoped to the contract and token to create. - // ensure that a signature can be replaced, as long as the replacement - // has the same uid, and a newer version. - { name: "uid", type: "uint32" }, - { name: "version", type: "uint32" }, - // if this update should result in the signature being deleted. - { name: "deleted", type: "bool" }, - ], - TokenCreationConfig: [ - { name: "tokenURI", type: "string" }, - { name: "maxSupply", type: "uint256" }, - { name: "maxTokensPerAddress", type: "uint64" }, - { name: "pricePerToken", type: "uint96" }, - { name: "mintStart", type: "uint64" }, - { name: "mintDuration", type: "uint64" }, - { name: "royaltyMintSchedule", type: "uint32" }, - { name: "royaltyBPS", type: "uint32" }, - { name: "royaltyRecipient", type: "address" }, - { name: "fixedPriceMinter", type: "address" }, - ], - }; - - const result: TypedDataDefinition = { +}): TypedDataDefinition => { + return { domain: { chainId, - name: "Preminter", + name: PreminterDomain, version: "1", verifyingContract: verifyingContract, }, - types, - message: { - tokenConfig, - uid, - version, - deleted, - }, + types: v1Types, + message: premintConfig, primaryType: "CreatorAttribution", }; +}; - // console.log({ result, deleted }); - - return result; +// Convenience method to create the structured typed data +// needed to sign for a premint contract and token +export const premintConfigV2TypedDataDefinition = ({ + verifyingContract, + premintConfig, + chainId, +}: { + verifyingContract: Address; + premintConfig: PremintConfigV2; + chainId: number; +}): TypedDataDefinition => { + return { + domain: { + chainId, + name: PreminterDomain, + version: "2", + verifyingContract: verifyingContract, + }, + types: v2Types, + message: premintConfig, + primaryType: "CreatorAttribution", + }; }; export async function isValidSignatureV1({ @@ -91,7 +144,41 @@ export async function isValidSignatureV1({ }: { contractAddress: Address; originalContractAdmin: Address; - premintConfig: PremintConfig; + premintConfig: PremintConfigV1; + signature: Hex; + chainId: number; + publicClient: PublicClient; +}): Promise<{ + isAuthorized: boolean; + recoveredAddress?: Address; +}> { + const typedData = premintConfigV1TypedDataDefinition({ + verifyingContract: contractAddress, + premintConfig, + chainId, + }); + + return await isValidSignature({ + typedData, + signature, + publicClient, + originalContractAdmin, + contractAddress, + chainId, + }); +} + +export async function isValidSignatureV2({ + contractAddress, + originalContractAdmin, + premintConfig, + signature, + chainId, + publicClient, +}: { + contractAddress: Address; + originalContractAdmin: Address; + premintConfig: PremintConfigV2; signature: Hex; chainId: number; publicClient: PublicClient; @@ -99,13 +186,36 @@ export async function isValidSignatureV1({ isAuthorized: boolean; recoveredAddress?: Address; }> { - const typedData = preminterTypedDataDefinition({ + const typedData = premintConfigV2TypedDataDefinition({ verifyingContract: contractAddress, premintConfig, chainId, }); - // recover the address from the signature + return await isValidSignature({ + typedData, + signature, + publicClient, + originalContractAdmin, + contractAddress, + chainId, + }); +} + +export async function isValidSignature({ + typedData, + signature, + publicClient, + originalContractAdmin, + contractAddress, +}: { + contractAddress: Address; + originalContractAdmin: Address; + typedData: TypedDataDefinition; + signature: Hex; + chainId: number; + publicClient: PublicClient; +}) { let recoveredAddress: Address; try { @@ -136,3 +246,29 @@ export async function isValidSignatureV1({ recoveredAddress, }; } + +// Takes a premint config v1 and migrates it to +// version 2, adding the createReferral field +export function migratePremintConfigToV2({ + premintConfig, + createReferral = zeroAddress, +}: { + premintConfig: PremintConfigV1; + createReferral: Address; +}): PremintConfigV2 { + return { + ...premintConfig, + tokenConfig: { + tokenURI: premintConfig.tokenConfig.tokenURI, + maxSupply: premintConfig.tokenConfig.maxSupply, + maxTokensPerAddress: premintConfig.tokenConfig.maxTokensPerAddress, + pricePerToken: premintConfig.tokenConfig.pricePerToken, + mintStart: premintConfig.tokenConfig.mintStart, + mintDuration: premintConfig.tokenConfig.mintDuration, + payoutRecipient: premintConfig.tokenConfig.royaltyRecipient, + royaltyBPS: premintConfig.tokenConfig.royaltyBPS, + fixedPriceMinter: premintConfig.tokenConfig.fixedPriceMinter, + createReferral, + }, + }; +}