From fded5a8bb6633df81aa838805b9861ccd22033ef Mon Sep 17 00:00:00 2001 From: mike-metaplex <111764486+mike-metaplex@users.noreply.github.com> Date: Tue, 13 Sep 2022 14:00:24 -0400 Subject: [PATCH] add createNftOnChain and refactor files and folders (#53) * add createNftOnChain and refactor files and folders * use NetworkingRouter implementation for SolanaRouter * Max supply for Master Edition test * fix namespace issue and only retry on nilStatus * update swift packages Co-authored-by: Arturo Jamaica --- Package.resolved | 2 +- Sources/Metaplex/Modules/NFTS/NftClient.swift | 29 ++++ .../CreateNftOnChainOperationHandler.swift | 113 +++++++++++++++ .../Accounts/MasterEditionAccount.swift | 57 -------- .../Accounts/MetadataAccount.swift | 135 ------------------ .../Data/MetaplexCollection.swift | 31 ++++ .../Data/MetaplexCollectionDetails.swift | 32 +++++ .../TokenMetadata/Data/MetaplexCreator.swift | 36 +++++ .../TokenMetadata/Data/MetaplexData.swift | 64 +++++++++ .../TokenMetadata/Data/MetaplexDataV2.swift | 101 +++++++++++++ .../Data/MetaplexTokenStandard.swift | 13 ++ .../TokenMetadata/Data/MetaplexUses.swift | 41 ++++++ .../Edition/MasterEditionV1.swift | 39 +++++ .../Edition/MasterEditionV2.swift | 39 +++++ .../Edition/MasterEditionVersion.swift | 13 ++ Sources/Metaplex/Shared/BytesEncodable.swift | 41 ++++++ .../Metaplex/Shared/OperationHandler.swift | 5 + Sources/Metaplex/Solana/Connection.swift | 50 ++++++- .../InstructionBuilder+CreateNft.swift | 130 +++++++++++++++++ .../Builder/InstructionBuilder.swift | 17 +++ .../Instructions/CreateMasterEditionV3.swift | 59 ++++++++ .../CreateMetadataAccountV3.swift | 94 ++++++++++++ .../Utility/PublicKey+Extensions.swift | 20 +++ .../Metaplex/Utility/String+Extensions.swift | 12 ++ ...reateNftOnChainOperationHandlerTests.swift | 76 ++++++++++ ...ftByMintOnChainOperationHandlerTests.swift | 2 +- .../Accounts/MetadataAccountTests.swift | 2 +- 27 files changed, 1055 insertions(+), 198 deletions(-) create mode 100644 Sources/Metaplex/Modules/NFTS/Operations/CreateNftOnChainOperationHandler.swift create mode 100644 Sources/Metaplex/Programs/TokenMetadata/Data/MetaplexCollection.swift create mode 100644 Sources/Metaplex/Programs/TokenMetadata/Data/MetaplexCollectionDetails.swift create mode 100644 Sources/Metaplex/Programs/TokenMetadata/Data/MetaplexCreator.swift create mode 100644 Sources/Metaplex/Programs/TokenMetadata/Data/MetaplexData.swift create mode 100644 Sources/Metaplex/Programs/TokenMetadata/Data/MetaplexDataV2.swift create mode 100644 Sources/Metaplex/Programs/TokenMetadata/Data/MetaplexTokenStandard.swift create mode 100644 Sources/Metaplex/Programs/TokenMetadata/Data/MetaplexUses.swift create mode 100644 Sources/Metaplex/Programs/TokenMetadata/Edition/MasterEditionV1.swift create mode 100644 Sources/Metaplex/Programs/TokenMetadata/Edition/MasterEditionV2.swift create mode 100644 Sources/Metaplex/Programs/TokenMetadata/Edition/MasterEditionVersion.swift create mode 100644 Sources/Metaplex/Shared/BytesEncodable.swift create mode 100644 Sources/Metaplex/TransactionInstructions/Builder/InstructionBuilder+CreateNft.swift create mode 100644 Sources/Metaplex/TransactionInstructions/Builder/InstructionBuilder.swift create mode 100644 Sources/Metaplex/TransactionInstructions/Instructions/CreateMasterEditionV3.swift create mode 100644 Sources/Metaplex/TransactionInstructions/Instructions/CreateMetadataAccountV3.swift create mode 100644 Sources/Metaplex/Utility/PublicKey+Extensions.swift create mode 100644 Sources/Metaplex/Utility/String+Extensions.swift create mode 100644 Tests/MetaplexTests/Modules/NFTs/Operations/CreateNftOnChainOperationHandlerTests.swift diff --git a/Package.resolved b/Package.resolved index 170138b..ba33c7c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,7 +15,7 @@ "location" : "https://github.com/metaplex-foundation/Solana.Swift.git", "state" : { "branch" : "master", - "revision" : "75df095c2f4e2e75e5e8e6f6b0fcc9ee7388c68d" + "revision" : "9342eb1e96a0a8b78f4a38f255f1fcbfe0797383" } }, { diff --git a/Sources/Metaplex/Modules/NFTS/NftClient.swift b/Sources/Metaplex/Modules/NFTS/NftClient.swift index 723f854..ed8a057 100644 --- a/Sources/Metaplex/Modules/NFTS/NftClient.swift +++ b/Sources/Metaplex/Modules/NFTS/NftClient.swift @@ -76,4 +76,33 @@ public class NftClient { public func findNftsByOwner(publicKey: PublicKey, onComplete: @escaping (Result<[NFT?], OperationError>) -> Void) { findAllByOwner(publicKey: publicKey, onComplete: onComplete) } + + public func createNft( + name: String, + symbol: String?, + uri: String, + sellerFeeBasisPoints: UInt16, + hasCreators: Bool, + addressCount: UInt32, + creators: [MetaplexCreator], + isMutable: Bool, + mintAccountState: AccountState, + account: Account, + onComplete: @escaping (Result) -> Void + ) { + let input = CreateNftInput( + mintAccountState: mintAccountState, + account: account, + name: name, + symbol: symbol, + uri: uri, + sellerFeeBasisPoints: sellerFeeBasisPoints, + hasCreators: hasCreators, + addressCount: addressCount, + creators: creators, + isMutable: isMutable + ) + let operation = CreateNftOnChainOperationHandler(metaplex: self.metaplex) + operation.handle(operation: CreateNftOperation.pure(.success(input))).run { onComplete($0) } + } } diff --git a/Sources/Metaplex/Modules/NFTS/Operations/CreateNftOnChainOperationHandler.swift b/Sources/Metaplex/Modules/NFTS/Operations/CreateNftOnChainOperationHandler.swift new file mode 100644 index 0000000..da9b68b --- /dev/null +++ b/Sources/Metaplex/Modules/NFTS/Operations/CreateNftOnChainOperationHandler.swift @@ -0,0 +1,113 @@ +// +// CreateNftOnChainOperationHandler.swift +// +// +// Created by Michael J. Huber Jr. on 9/2/22. +// + +import Foundation +import Solana + +public enum AccountState { + case new(Account) + case existing(Account) + + var account: Account { + switch self { + case .new(let account): + return account + case .existing(let account): + return account + } + } +} + +struct CreateNftInput { + let mintAccountState: AccountState + let account: Account + let name: String + let symbol: String? + let uri: String + let sellerFeeBasisPoints: UInt16 + let hasCreators: Bool + let addressCount: UInt32 + let creators: [MetaplexCreator] + let collection: MetaplexCollection? = nil + let uses: MetaplexUses? = nil + let isMutable: Bool +} + +typealias CreateNftOperation = OperationResult + +class CreateNftOnChainOperationHandler: OperationHandler { + var metaplex: Metaplex + + typealias I = CreateNftInput + typealias O = NFT + + init(metaplex: Metaplex) { + self.metaplex = metaplex + } + + func handle(operation: CreateNftOperation) -> OperationResult { + let builder = InstructionBuilder(metaplex: metaplex) + return operation.flatMap { input in + OperationResult<[TransactionInstruction], Error>.init { callback in + builder.createNftInstructions(input: input) { + callback($0) + } + }.mapError { + OperationError.buildInstructionsError($0) + }.flatMap { instructions in + OperationResult.init { callback in + self.metaplex.connection.serializeTransaction(instructions: instructions, recentBlockhash: nil, signers: [input.account, input.mintAccountState.account]) { + callback($0) + } + } + .mapError { OperationError.serializeTransactionError($0) } + }.flatMap { serializedTransaction in + OperationResult.init { callback in + self.metaplex.sendTransaction(serializedTransaction: serializedTransaction) { + callback($0) + } + } + .mapError { OperationError.sendTransactionError($0) } + }.flatMap { signature in + let operation: () -> OperationResult> = { + OperationResult.init { callback in + // We are sleeping here in order to wait for the transaction to finalize on the chain. + #warning("This needs to be refactored into something more elegant.") + sleep(3) + self.metaplex.confirmTransaction( + signature: signature, + configs: nil + ) { result in + switch result { + case .success(let signature): + guard let signature = signature, let status = signature.confirmationStatus, status == .finalized else { + callback(.failure(OperationError.nilSignatureStatus)) + return + } + callback(.success(signature)) + case .failure(let error): + callback(.failure(error)) + } + } + } + .mapError { error in + if case OperationError.nilSignatureStatus = error { + return Retry.retry(error) + } + return Retry.doNotRetry(error) + } + } + let retry = OperationResult.retry(attempts: 5, operation: operation) + .mapError { OperationError.confirmTransactionError($0) } + return retry + }.flatMap { (status: SignatureStatus) -> OperationResult in + let findNft = FindNftByMintOnChainOperationHandler(metaplex: self.metaplex) + return findNft.handle(operation: FindNftByMintOperation.pure(.success(input.mintAccountState.account.publicKey))) + } + } + } +} diff --git a/Sources/Metaplex/Programs/TokenMetadata/Accounts/MasterEditionAccount.swift b/Sources/Metaplex/Programs/TokenMetadata/Accounts/MasterEditionAccount.swift index 0714b5c..12f9b86 100644 --- a/Sources/Metaplex/Programs/TokenMetadata/Accounts/MasterEditionAccount.swift +++ b/Sources/Metaplex/Programs/TokenMetadata/Accounts/MasterEditionAccount.swift @@ -51,63 +51,6 @@ public enum MetadataKey { } } -public class MasterEditionV1: BufferLayout { - public let supply: UInt64? - public let maxSupply: UInt64? - public let printingMint: PublicKey - public let oneTimePrintingAuthorizationMint: PublicKey - - public static var BUFFER_LENGTH: UInt64 = 282 - - public init(supply: UInt64?, maxSupply: UInt64?, printingMint: PublicKey, oneTimePrintingAuthorizationMint: PublicKey) { - self.supply = supply - self.maxSupply = maxSupply - self.printingMint = printingMint - self.oneTimePrintingAuthorizationMint = oneTimePrintingAuthorizationMint - } - - required public init(from reader: inout BinaryReader) throws { - self.supply = try? .init(from: &reader) - self.maxSupply = try? .init(from: &reader) - self.printingMint = try .init(from: &reader) - self.oneTimePrintingAuthorizationMint = try .init(from: &reader) - } - - public func serialize(to writer: inout Data) throws { - try supply?.serialize(to: &writer) - try maxSupply?.serialize(to: &writer) - try printingMint.serialize(to: &writer) - try oneTimePrintingAuthorizationMint.serialize(to: &writer) - } -} - -public class MasterEditionV2: BufferLayout { - public let supply: UInt64 - public let maxSupply: UInt64? - - public static var BUFFER_LENGTH: UInt64 = 282 - - public init(supply: UInt64, maxSupply: UInt64?) { - self.supply = supply - self.maxSupply = maxSupply - } - - required public init(from reader: inout BinaryReader) throws { - self.supply = try .init(from: &reader) - self.maxSupply = try? .init(from: &reader) - } - - public func serialize(to writer: inout Data) throws { - try supply.serialize(to: &writer) - try maxSupply?.serialize(to: &writer) - } -} - -public enum MasterEditionVersion: Codable { - case masterEditionV1(MasterEditionV1) - case masterEditionV2(MasterEditionV2) -} - public class MasterEditionAccount: BufferLayout { static func pda(mintKey: PublicKey) -> Result { diff --git a/Sources/Metaplex/Programs/TokenMetadata/Accounts/MetadataAccount.swift b/Sources/Metaplex/Programs/TokenMetadata/Accounts/MetadataAccount.swift index e8c9d12..f873d72 100644 --- a/Sources/Metaplex/Programs/TokenMetadata/Accounts/MetadataAccount.swift +++ b/Sources/Metaplex/Programs/TokenMetadata/Accounts/MetadataAccount.swift @@ -8,27 +8,7 @@ import Foundation import Solana -extension PublicKey { - - static let vaultProgramId = PublicKey(string: "vau1zxA2LbssAUEF7Gpw91zMM1LvXrvpzJtmZ58rPsn")! - - static let auctionProgramId = PublicKey(string: "auctxRXPeJoc4817jDhf4HbjnhEcr1cCXenosMhK5R8")! - - static let metaplexProgramId = PublicKey(string: "p1exdMJcjVao65QdewkaZRUnU6VPSXhus9n2GzWfh98")! - - static let systemProgramID = PublicKey(string: "11111111111111111111111111111111")! -} - -extension String { - static let metadataPrefix = "metadata" -} - -public enum MetaplexTokenStandard: UInt8, Codable { - case NonFungible = 0, FungibleAsset = 1, Fungible = 2, NonFungibleEdition = 3 -} - public struct MetadataAccount: BufferLayout { - static func pda(mintKey: PublicKey) -> Result { let seedMetadata = [String.metadataPrefix.bytes, TokenMetadataProgram.publicKey.bytes, @@ -124,118 +104,3 @@ public struct MetadataAccount: BufferLayout { } } } - -public struct MetaplexCollection: BorshCodable, BufferLayout { - public static var BUFFER_LENGTH: UInt64 = 10 - - public let verified: Bool - public let key: PublicKey - - public init(verified: Bool, key: PublicKey){ - self.verified = verified - self.key = key - } - - public init(from reader: inout BinaryReader) throws { - self.verified = try .init(from: &reader) - self.key = try .init(from: &reader) - } - - public func serialize(to writer: inout Data) throws { - try verified.serialize(to: &writer) - try key.serialize(to: &writer) - } -} - -public struct MetaplexData: BorshCodable, BufferLayout { - public static var BUFFER_LENGTH: UInt64 = 10 - - public init(name: String, symbol: String, uri: String, sellerFeeBasisPoints: UInt16, hasCreators: Bool, addressCount: UInt32, creators: [MetaplexCreator]) { - self._name = name - self._symbol = symbol - self._uri = uri - self.sellerFeeBasisPoints = sellerFeeBasisPoints - self.hasCreators = hasCreators - self.addressCount = addressCount - self.creators = creators - } - - private let _name: String - private let _symbol: String - private let _uri: String - - public var name: String { - _name.trimmingCharacters(in: CharacterSet(charactersIn: "\0").union(.whitespacesAndNewlines)) - } - public var symbol: String { - _symbol.trimmingCharacters(in: CharacterSet(charactersIn: "\0").union(.whitespacesAndNewlines)) - } - public var uri: String { - _uri.trimmingCharacters(in: CharacterSet(charactersIn: "\0").union(.whitespacesAndNewlines)) - } - - public let sellerFeeBasisPoints: UInt16 - public let hasCreators: Bool - public let addressCount: UInt32 - public let creators: [MetaplexCreator] - - public init(from reader: inout BinaryReader) throws { - self._name = try .init(from: &reader) - self._symbol = try .init(from: &reader) - self._uri = try .init(from: &reader) - self.sellerFeeBasisPoints = try .init(from: &reader) - self.hasCreators = try .init(from: &reader) - var creatorsArray: [MetaplexCreator] = [] - if self.hasCreators { - let addressCount: UInt32 = try .init(from: &reader) - for _ in 0..], Error>) -> Void) func getAccountInfo(account: PublicKey, decodedTo: T.Type, onComplete: @escaping (Result, Error>) -> Void) - func getMultipleAccountsInfo(accounts: [PublicKey], decodedTo: T.Type, onComplete: @escaping (Result<[BufferInfo?], Error>) -> Void) - func confirmTransaction(signature: String, configs: RequestConfiguration?, onComplete: @escaping (Result<[SignatureStatus?], Error>) -> Void) + func getMultipleAccountsInfo( + accounts: [PublicKey], + decodedTo: T.Type, + onComplete: @escaping (Result<[BufferInfo?], Error>) -> Void + ) + func getCreatingTokenAccountFee(onComplete: @escaping (Result) -> Void) + func createTokenAccount( + mintAddress: String, + payer: Account, + onComplete: @escaping ((Result<(signature: String, newPubkey: String), Error>) -> Void) + ) + func serializeTransaction( + instructions: [TransactionInstruction], + recentBlockhash: String?, + signers: [Account], + onComplete: @escaping ((Result) -> Void) + ) + func confirmTransaction( + signature: String, + configs: RequestConfiguration?, + onComplete: @escaping (Result<[SignatureStatus?], Error>) -> Void + ) } public class SolanaConnectionDriver: Connection { public let solanaRPC: Api + public let action: Action public init(endpoint: RPCEndpoint) { - self.solanaRPC = Api(router: .init(endpoint: endpoint), supportedTokens: []) + let router = NetworkingRouter(endpoint: endpoint) + self.solanaRPC = Api(router: router, supportedTokens: []) + self.action = Action(api: solanaRPC, router: router, supportedTokens: []) } public func getProgramAccounts(publicKey: PublicKey, decodedTo: T.Type, config: RequestConfiguration, onComplete: @escaping (Result<[ProgramAccount], Error>) -> Void) where T: BufferLayout { @@ -37,6 +60,27 @@ public class SolanaConnectionDriver: Connection { solanaRPC.getMultipleAccounts(pubkeys: accounts.map { $0.base58EncodedString }, decodedTo: T.self, onComplete: onComplete) } + public func getCreatingTokenAccountFee(onComplete: @escaping (Result) -> Void) { + solanaRPC.getMinimumBalanceForRentExemption(dataLength: MINT_SIZE, onComplete: onComplete) + } + + public func createTokenAccount( + mintAddress: String, + payer: Account, + onComplete: @escaping ((Result<(signature: String, newPubkey: String), Error>) -> Void) + ) { + action.createTokenAccount(mintAddress: mintAddress, payer: payer, onComplete: onComplete) + } + + public func serializeTransaction( + instructions: [TransactionInstruction], + recentBlockhash: String? = nil, + signers: [Account], + onComplete: @escaping ((Result) -> Void) + ) { + action.serializeTransaction(instructions: instructions, recentBlockhash: recentBlockhash, signers: signers, onComplete: onComplete) + } + public func confirmTransaction(signature: String, configs: RequestConfiguration?, onComplete: @escaping (Result<[SignatureStatus?], Error>) -> Void) { solanaRPC.getSignatureStatuses(pubkeys: [signature], configs: configs, onComplete: onComplete) } diff --git a/Sources/Metaplex/TransactionInstructions/Builder/InstructionBuilder+CreateNft.swift b/Sources/Metaplex/TransactionInstructions/Builder/InstructionBuilder+CreateNft.swift new file mode 100644 index 0000000..b766540 --- /dev/null +++ b/Sources/Metaplex/TransactionInstructions/Builder/InstructionBuilder+CreateNft.swift @@ -0,0 +1,130 @@ +// +// InstructionBuilder+CreateNft.swift +// +// +// Created by Michael J. Huber Jr. on 9/12/22. +// + +import Foundation +import Solana + +extension InstructionBuilder { + func createNftInstructions( + input: CreateNftInput, + onComplete: @escaping (Result<[TransactionInstruction], Error>) -> Void + ) { + metaplex.connection.getCreatingTokenAccountFee { result in + switch result { + case .success(let lamports): + let transactions = buildCreateNftInstructions(for: input, with: lamports) + onComplete(.success(transactions)) + case .failure(let error): + onComplete(.failure(error)) + } + } + } + + private func buildCreateNftInstructions(for input: CreateNftInput, with lamports: UInt64) -> [TransactionInstruction] { + var instructions = [TransactionInstruction]() + + let mint = input.mintAccountState.account.publicKey + + switch input.mintAccountState { + case .new: + let createInstruction = SystemProgram.createAccountInstruction( + from: metaplex.identity().publicKey, + toNewPubkey: mint, + lamports: lamports, + space: MINT_SIZE + ) + instructions.append(createInstruction) + + let initMintInstruction = SolanaTokenProgram.initializeMintInstruction( + tokenProgramId: PublicKey.tokenProgramId, + mint: mint, + decimals: 0, + authority: metaplex.identity().publicKey, + freezeAuthority: metaplex.identity().publicKey + ) + instructions.append(initMintInstruction) + + case .existing: + break + } + + if case let .success(associatedAccount) = PublicKey.associatedTokenAddress( + walletAddress: metaplex.identity().publicKey, + tokenMintAddress: mint + ) { + let associatedInstruction = AssociatedTokenProgram.createAssociatedTokenAccountInstruction( + mint: mint, + associatedAccount: associatedAccount, + owner: metaplex.identity().publicKey, + payer: metaplex.identity().publicKey + ) + instructions.append(associatedInstruction) + + let toMintInstruction = SolanaTokenProgram.mintToInstruction( + tokenProgramId: TokenProgram.publicKey, + mint: mint, + destination: associatedAccount, + authority: metaplex.identity().publicKey, + amount: 1 + ) + instructions.append(toMintInstruction) + } + + let metadata = try! MetadataAccount.pda(mintKey: mint).get() + let nftData = MetaplexDataV2( + name: input.name, + symbol: input.symbol ?? "", + uri: input.uri, + sellerFeeBasisPoints: input.sellerFeeBasisPoints, + hasCreators: input.hasCreators, + addressCount: input.addressCount, + creators: input.creators, + collection: input.collection, + uses: input.uses + ) + let metadataInstruction = CreateMetadataAccountV3.createMetadataAccountV3Instruction( + accounts: .init( + metadata: metadata, + mint: mint, + mintAuthority: metaplex.identity().publicKey, + payer: metaplex.identity().publicKey, + updateAuthority: metaplex.identity().publicKey, + systemProgram: nil, + rent: nil + ), + arguments: .init( + data: nftData, + isMutable: input.isMutable, + collectionDetails: nil + ) + ) + instructions.append(metadataInstruction) + + // NOTE: Need to verify creators + + let edition = try! MasterEditionAccount.pda(mintKey: mint).get() + let masterEditionInstruction = CreateMasterEditionV3.createMasterEditionV3( + accounts: .init( + edition: edition, + mint: mint, + updateAuthority: metaplex.identity().publicKey, + mintAuthority: metaplex.identity().publicKey, + payer: metaplex.identity().publicKey, + metadata: metadata, + tokenProgram: nil, + systemProgram: nil, + rent: nil + ), + arguments: .init(maxSupply: 1) + ) + instructions.append(masterEditionInstruction) + + // NOTE: Need to verify collection + + return instructions + } +} diff --git a/Sources/Metaplex/TransactionInstructions/Builder/InstructionBuilder.swift b/Sources/Metaplex/TransactionInstructions/Builder/InstructionBuilder.swift new file mode 100644 index 0000000..a716d39 --- /dev/null +++ b/Sources/Metaplex/TransactionInstructions/Builder/InstructionBuilder.swift @@ -0,0 +1,17 @@ +// +// InstructionBuilder.swift +// +// +// Created by Michael J. Huber Jr. on 9/12/22. +// + +import Foundation +import Solana + +struct InstructionBuilder { + let metaplex: Metaplex + + init(metaplex: Metaplex) { + self.metaplex = metaplex + } +} diff --git a/Sources/Metaplex/TransactionInstructions/Instructions/CreateMasterEditionV3.swift b/Sources/Metaplex/TransactionInstructions/Instructions/CreateMasterEditionV3.swift new file mode 100644 index 0000000..af0711a --- /dev/null +++ b/Sources/Metaplex/TransactionInstructions/Instructions/CreateMasterEditionV3.swift @@ -0,0 +1,59 @@ +// +// CreateMasterEditionV3.swift +// +// +// Created by Michael J. Huber Jr. on 9/12/22. +// + +import Foundation +import Solana + +public struct CreateMasterEditionV3InstructionAccounts { + let edition: PublicKey + let mint: PublicKey + let updateAuthority: PublicKey + let mintAuthority: PublicKey + let payer: PublicKey + let metadata: PublicKey + let tokenProgram: PublicKey? + let systemProgram: PublicKey? + let rent: PublicKey? +} + +public struct CreateMasterEditionV3InstructionData { + let maxSupply: UInt64? +} + +public struct CreateMasterEditionV3 { + private struct Index { + static let create: UInt8 = 17 + } + + static func createMasterEditionV3( + accounts: CreateMasterEditionV3InstructionAccounts, + arguments: CreateMasterEditionV3InstructionData, + programId: PublicKey = TokenMetadataProgram.publicKey + ) -> TransactionInstruction { + let keys = [ + Account.Meta(publicKey: accounts.edition, isSigner: false, isWritable: true), + Account.Meta(publicKey: accounts.mint, isSigner: false, isWritable: true), + Account.Meta(publicKey: accounts.updateAuthority, isSigner: true, isWritable: false), + Account.Meta(publicKey: accounts.mintAuthority, isSigner: true, isWritable: false), + Account.Meta(publicKey: accounts.payer, isSigner: true, isWritable: true), + Account.Meta(publicKey: accounts.metadata, isSigner: false, isWritable: true), + Account.Meta(publicKey: accounts.tokenProgram ?? PublicKey.tokenProgramId, isSigner: false, isWritable: false), + Account.Meta(publicKey: accounts.systemProgram ?? PublicKey.systemProgramId, isSigner: false, isWritable: false), + Account.Meta(publicKey: accounts.rent ?? PublicKey.sysvarRent, isSigner: false, isWritable: false) + ] + + var data = [Index.create] + data.append(contentsOf: (arguments.maxSupply != nil).bytes) + data.append(contentsOf: arguments.maxSupply?.bytes ?? UInt64(0).bytes) + + return TransactionInstruction( + keys: keys, + programId: programId, + data: data + ) + } +} diff --git a/Sources/Metaplex/TransactionInstructions/Instructions/CreateMetadataAccountV3.swift b/Sources/Metaplex/TransactionInstructions/Instructions/CreateMetadataAccountV3.swift new file mode 100644 index 0000000..390ab66 --- /dev/null +++ b/Sources/Metaplex/TransactionInstructions/Instructions/CreateMetadataAccountV3.swift @@ -0,0 +1,94 @@ +// +// CreateMetadataAccountV3.swift +// +// +// Created by Michael J. Huber Jr. on 8/30/22. +// + +import Foundation +import Solana + +public struct CreateMetadataAccountV3InstructionAccounts { + let metadata: PublicKey + let mint: PublicKey + let mintAuthority: PublicKey + let payer: PublicKey + let updateAuthority: PublicKey + let systemProgram: PublicKey? + let rent: PublicKey? +} + +public struct CreateMetadataAccountV3InstructionData: BorshCodable, BufferLayout { + public static var BUFFER_LENGTH: UInt64 = 10 + + public let data: MetaplexDataV2 + public let isMutable: Bool + public let collectionDetails: MetaplexCollectionDetails? + + public init(data: MetaplexDataV2, isMutable: Bool, collectionDetails: MetaplexCollectionDetails?) { + self.data = data + self.isMutable = isMutable + self.collectionDetails = collectionDetails + } + + public init(from reader: inout BinaryReader) throws { + self.data = try .init(from: &reader) + self.isMutable = try .init(from: &reader) + self.collectionDetails = try .init(from: &reader) + } + + public func serialize(to writer: inout Data) throws { + try data.serialize(to: &writer) + try isMutable.serialize(to: &writer) + try collectionDetails?.serialize(to: &writer) + } +} + +struct SignMetadataInstructionAccounts { + let metadata: PublicKey + let creator: PublicKey +} + +public struct CreateMetadataAccountV3 { + private struct Index { + static let create: UInt8 = 33 + static let signMetadata: UInt8 = 7 + } + + static func createMetadataAccountV3Instruction( + accounts: CreateMetadataAccountV3InstructionAccounts, + arguments: CreateMetadataAccountV3InstructionData, + programId: PublicKey = TokenMetadataProgram.publicKey + ) -> TransactionInstruction { + let keys = [ + Account.Meta(publicKey: accounts.metadata, isSigner: false, isWritable: true), + Account.Meta(publicKey: accounts.mint, isSigner: false, isWritable: false), + Account.Meta(publicKey: accounts.mintAuthority, isSigner: true, isWritable: false), + Account.Meta(publicKey: accounts.payer, isSigner: true, isWritable: true), + Account.Meta(publicKey: accounts.updateAuthority, isSigner: false, isWritable: false), + Account.Meta(publicKey: accounts.systemProgram ?? PublicKey.systemProgramId, isSigner: false, isWritable: false), + Account.Meta(publicKey: accounts.rent ?? PublicKey.sysvarRent, isSigner: false, isWritable: false) + ] + + var data = [Index.create] + + var writtenMetadata = Data() + try! arguments.data.serialize(to: &writtenMetadata) + data.append(contentsOf: writtenMetadata.bytes) + data.append(contentsOf: arguments.isMutable.bytes) + + var collectionDetailsBytes = [UInt8(0)] + if let collectionDetails = arguments.collectionDetails { + var writtenCollectionDetails = Data() + try! collectionDetails.serialize(to: &writtenCollectionDetails) + collectionDetailsBytes = writtenCollectionDetails.bytes + } + data.append(contentsOf: collectionDetailsBytes) + + return TransactionInstruction( + keys: keys, + programId: programId, + data: data + ) + } +} diff --git a/Sources/Metaplex/Utility/PublicKey+Extensions.swift b/Sources/Metaplex/Utility/PublicKey+Extensions.swift new file mode 100644 index 0000000..2340711 --- /dev/null +++ b/Sources/Metaplex/Utility/PublicKey+Extensions.swift @@ -0,0 +1,20 @@ +// +// PublicKey+Extensions.swift +// +// +// Created by Michael J. Huber Jr. on 9/12/22. +// + +import Foundation +import Solana + +extension PublicKey { + static let vaultProgramId = PublicKey(string: "vau1zxA2LbssAUEF7Gpw91zMM1LvXrvpzJtmZ58rPsn")! + + static let auctionProgramId = PublicKey(string: "auctxRXPeJoc4817jDhf4HbjnhEcr1cCXenosMhK5R8")! + + static let metaplexProgramId = PublicKey(string: "p1exdMJcjVao65QdewkaZRUnU6VPSXhus9n2GzWfh98")! + + static let systemProgramID = PublicKey(string: "11111111111111111111111111111111")! +} + diff --git a/Sources/Metaplex/Utility/String+Extensions.swift b/Sources/Metaplex/Utility/String+Extensions.swift new file mode 100644 index 0000000..437deb6 --- /dev/null +++ b/Sources/Metaplex/Utility/String+Extensions.swift @@ -0,0 +1,12 @@ +// +// String+Extensions.swift +// +// +// Created by Michael J. Huber Jr. on 9/12/22. +// + +import Foundation + +extension String { + static let metadataPrefix = "metadata" +} diff --git a/Tests/MetaplexTests/Modules/NFTs/Operations/CreateNftOnChainOperationHandlerTests.swift b/Tests/MetaplexTests/Modules/NFTs/Operations/CreateNftOnChainOperationHandlerTests.swift new file mode 100644 index 0000000..a2df61a --- /dev/null +++ b/Tests/MetaplexTests/Modules/NFTs/Operations/CreateNftOnChainOperationHandlerTests.swift @@ -0,0 +1,76 @@ +// +// CreateNftOnChainOperationHandlerTests.swift +// +// +// Created by Michael J. Huber Jr. on 9/12/22. +// + +import Foundation +import XCTest +import Solana +@testable import Metaplex + +final class CreateNftOnChainOperationTests: XCTestCase { + var account: Account! + var mintAccount: Account! + var metaplex: Metaplex! + + override func setUpWithError() throws { + let phrase: [String] = "siege amazing camp income refuse struggle feed kingdom lawn champion velvet crystal stomach trend hen uncover roast nasty until hidden crumble city bag minute".components(separatedBy: " ") + account = Account(phrase: phrase, network: .devnet)! + mintAccount = Account(network: .devnet) + + let solanaConnection = SolanaConnectionDriver(endpoint: .devnetSolana) + let solanaIdentityDriver = KeypairIdentityDriver(solanaRPC: solanaConnection.solanaRPC, account: account) + let storageDriver = MemoryStorageDriver() + + metaplex = Metaplex(connection: solanaConnection, identityDriver: solanaIdentityDriver, storageDriver: storageDriver) + } + + func testCreateNftOnChainOperation() { + let input = CreateNftInput( + mintAccountState: .new(mintAccount), + account: account, + name: "Lil Foxy2009", + symbol: "LF", + uri: "https://bafybeig7fvs66jwfszmddy5ojxyjvitgs7sfoxlk3lz3qhclqmvqtfbi4y.ipfs.nftstorage.link/2009.json", + sellerFeeBasisPoints: 660, + hasCreators: true, + addressCount: 1, + creators: [.init(address: PublicKey(string: "Cf6C3xpvYNFx5qwq9Q7BczKcxTL5fRY5r3czg2sNDBfe")!, verified: 0, share: 100)], + isMutable: true + ) + + var result: Result? + + let lock = RunLoopSimpleLock() + lock.dispatch { + let operation = CreateNftOnChainOperationHandler(metaplex: self.metaplex) + operation.handle(operation: CreateNftOperation.pure(.success(input))).run { + result = $0 + lock.stop() + } + } + lock.run() + + guard let nft = try? result?.get() else { + return XCTFail("NFT result is nil.") + } + + XCTAssertNotNil(nft) + XCTAssertEqual(nft.metadataAccount.data.name, "Lil Foxy2009") + XCTAssertEqual(nft.metadataAccount.mint.base58EncodedString, mintAccount.publicKey.base58EncodedString) + XCTAssertEqual(nft.metadataAccount.data.creators.count, 1) + XCTAssertEqual(nft.masterEditionAccount?.type, 6) + + guard let masterEditionAccount = nft.masterEditionAccount else { + return XCTFail("MasterEditionAccount is nil.") + } + switch masterEditionAccount.masterEditionVersion { + case .masterEditionV1(_): + XCTFail() + case .masterEditionV2(let masterEditionV2): + XCTAssertEqual(masterEditionV2.maxSupply, 1) + } + } +} diff --git a/Tests/MetaplexTests/Modules/NFTs/Operations/FindNftByMintOnChainOperationHandlerTests.swift b/Tests/MetaplexTests/Modules/NFTs/Operations/FindNftByMintOnChainOperationHandlerTests.swift index 6d9b074..9fbb514 100644 --- a/Tests/MetaplexTests/Modules/NFTs/Operations/FindNftByMintOnChainOperationHandlerTests.swift +++ b/Tests/MetaplexTests/Modules/NFTs/Operations/FindNftByMintOnChainOperationHandlerTests.swift @@ -43,7 +43,7 @@ final class FindNftByMintOnChainOperationTests: XCTestCase { case .masterEditionV1(_): XCTFail() case .masterEditionV2(let masterEditionV1): - XCTAssertEqual(masterEditionV1.maxSupply, 1) + XCTAssertEqual(masterEditionV1.maxSupply, 0) } } } diff --git a/Tests/MetaplexTests/Programs/TokenMetadata/Accounts/MetadataAccountTests.swift b/Tests/MetaplexTests/Programs/TokenMetadata/Accounts/MetadataAccountTests.swift index 04cf579..e967c1a 100644 --- a/Tests/MetaplexTests/Programs/TokenMetadata/Accounts/MetadataAccountTests.swift +++ b/Tests/MetaplexTests/Programs/TokenMetadata/Accounts/MetadataAccountTests.swift @@ -46,7 +46,7 @@ final class MetadataAccountTests: XCTestCase { } func testMetadataAccountSerialization() { - let connection = Api(router: .init(endpoint: .mainnetBetaSolana), supportedTokens: []) + let connection = Api(router: NetworkingRouter(endpoint: .mainnetBetaSolana), supportedTokens: []) var result: Result? let lock = RunLoopSimpleLock()