From f9bfa0fd36622801c07eec3623587a7c8e1d06b7 Mon Sep 17 00:00:00 2001 From: Jordan Prince Date: Mon, 5 Apr 2021 14:02:45 -0500 Subject: [PATCH] Store metadata json in update call --- packages/common/src/actions/account.ts | 88 ++++++++++++++++++- packages/metavinci/src/contexts/accounts.tsx | 58 ++++++++---- packages/metavinci/src/models/nft.ts | 49 +++++++---- .../metavinci/src/views/artCreate/index.tsx | 18 ++-- 4 files changed, 170 insertions(+), 43 deletions(-) diff --git a/packages/common/src/actions/account.ts b/packages/common/src/actions/account.ts index d2234b6..0ef25b1 100644 --- a/packages/common/src/actions/account.ts +++ b/packages/common/src/actions/account.ts @@ -194,6 +194,17 @@ class CreateMetadataArgs { this.uri = args.uri; } } +class UpdateMetadataArgs { + instruction: number = 1; + uri: string; + // Not used by this app, just required for instruction + non_unique_specific_update_authority: number; + + constructor(args: { uri: string }) { + this.uri = args.uri; + this.non_unique_specific_update_authority = 0; + } +} export class Metadata { updateAuthority?: PublicKey; @@ -233,6 +244,17 @@ export const SCHEMA = new Map([ ], }, ], + [ + UpdateMetadataArgs, + { + kind: 'struct', + fields: [ + ['instruction', 'u8'], + ['uri', 'string'], + ['non_unique_specific_update_authority', 'u8'], + ], + }, + ], [ Metadata, { @@ -280,6 +302,68 @@ export function createMint( return account; } +export async function updateMetadata( + symbol: string, + name: string, + uri: string, + mintKey: PublicKey, + updateAuthority: PublicKey, + instructions: TransactionInstruction[], + signers: Account[], +) { + const metadataProgramId = programIds().metadata; + + const metadataAccount = ( + await PublicKey.findProgramAddress( + [ + Buffer.from('metadata'), + metadataProgramId.toBuffer(), + mintKey.toBuffer(), + ], + metadataProgramId, + ) + )[0]; + + const metadataOwnerAccount = ( + await PublicKey.findProgramAddress( + [ + Buffer.from('metadata'), + metadataProgramId.toBuffer(), + Buffer.from(name), + Buffer.from(symbol), + ], + metadataProgramId, + ) + )[0]; + + const value = new UpdateMetadataArgs({ uri }); + const data = Buffer.from(serialize(SCHEMA, value)); + + const keys = [ + { + pubkey: metadataAccount, + isSigner: false, + isWritable: true, + }, + { + pubkey: updateAuthority, + isSigner: true, + isWritable: false, + }, + { + pubkey: metadataOwnerAccount, + isSigner: false, + isWritable: false, + }, + ]; + instructions.push( + new TransactionInstruction({ + keys, + programId: metadataProgramId, + data, + }), + ); +} export async function createMetadata( symbol: string, @@ -289,7 +373,7 @@ export async function createMetadata( mintAuthorityKey: PublicKey, instructions: TransactionInstruction[], payer: PublicKey, - owner: PublicKey, + updateAuthority: PublicKey, signers: Account[], ) { const metadataProgramId = programIds().metadata; @@ -347,7 +431,7 @@ export async function createMetadata( isWritable: false, }, { - pubkey: payer, + pubkey: updateAuthority, isSigner: true, isWritable: false, }, diff --git a/packages/metavinci/src/contexts/accounts.tsx b/packages/metavinci/src/contexts/accounts.tsx index 9beb5cc..7aa48e4 100644 --- a/packages/metavinci/src/contexts/accounts.tsx +++ b/packages/metavinci/src/contexts/accounts.tsx @@ -1,4 +1,14 @@ -import { EventEmitter, programIds, useConnection, decodeMetadata, Metadata, getMultipleAccounts, cache, MintParser, ParsedAccount } from '@oyster/common'; +import { + EventEmitter, + programIds, + useConnection, + decodeMetadata, + Metadata, + getMultipleAccounts, + cache, + MintParser, + ParsedAccount, +} from '@oyster/common'; import { MintInfo } from '@solana/spl-token'; import BN from 'bn.js'; import React, { useContext, useEffect, useState } from 'react'; @@ -18,15 +28,18 @@ export function VinciAccountsProvider({ children = null as any }) { useEffect(() => { (async () => { - const metadataAccounts = await connection.getProgramAccounts(programIds().metadata); + const metadataAccounts = await connection.getProgramAccounts( + programIds().metadata, + ); const mintToMetadata = new Map(); const extendedMetadataFetch = new Map>(); metadataAccounts.forEach(meta => { - try{ + try { const metadata = decodeMetadata(meta.account.data); - if(isValidHttpUrl(metadata.uri)) { + if (isValidHttpUrl(metadata.uri)) { + console.log('Metadata uri', metadata.uri); mintToMetadata.set(metadata.mint.toBase58(), metadata); } } catch { @@ -35,22 +48,35 @@ export function VinciAccountsProvider({ children = null as any }) { } }); - const mints = await getMultipleAccounts(connection, [...mintToMetadata.keys()], 'single'); + const mints = await getMultipleAccounts( + connection, + [...mintToMetadata.keys()], + 'single', + ); mints.keys.forEach((key, index) => { const mintAccount = mints.array[index]; - const mint = cache.add(key, mintAccount, MintParser) as ParsedAccount; - if(mint.info.supply.gt(new BN(1)) || mint.info.decimals !== 0) { + const mint = cache.add( + key, + mintAccount, + MintParser, + ) as ParsedAccount; + if (mint.info.supply.gt(new BN(1)) || mint.info.decimals !== 0) { // naive not NFT check mintToMetadata.delete(key); } else { const metadata = mintToMetadata.get(key); - if(metadata && metadata.uri) { - extendedMetadataFetch.set(key, fetch(metadata.uri).catch(() => { - mintToMetadata.delete(key); - return undefined; - }).then(_ => { - metadata.extended = _; - })); + if (metadata && metadata.uri) { + extendedMetadataFetch.set( + key, + fetch(metadata.uri) + .catch(() => { + mintToMetadata.delete(key); + return undefined; + }) + .then(_ => { + metadata.extended = _; + }), + ); } } }); @@ -61,7 +87,7 @@ export function VinciAccountsProvider({ children = null as any }) { console.log([...mintToMetadata.values()]); })(); - }, [connection, setMetaAccounts]) + }, [connection, setMetaAccounts]); return ( @@ -84,5 +110,5 @@ function isValidHttpUrl(text: string) { return false; } - return url.protocol === "http:" || url.protocol === "https:"; + return url.protocol === 'http:' || url.protocol === 'https:'; } diff --git a/packages/metavinci/src/models/nft.ts b/packages/metavinci/src/models/nft.ts index d6abf33..11f50a7 100644 --- a/packages/metavinci/src/models/nft.ts +++ b/packages/metavinci/src/models/nft.ts @@ -2,8 +2,10 @@ import { createAssociatedTokenAccountInstruction, createMint, createMetadata, + updateMetadata, programIds, sendTransactions, + sendTransaction, } from '@oyster/common'; import { MintLayout, Token } from '@solana/spl-token'; import { WalletAdapter } from '@solana/wallet-base'; @@ -12,12 +14,12 @@ import { Connection, PublicKey, SystemProgram, - SYSVAR_RENT_PUBKEY, TransactionInstruction, } from '@solana/web3.js'; import crypto from 'crypto'; import { getAssetCostToStore } from '../utils/assets'; import { AR_SOL_HOLDER_ID } from '../utils/ids'; +import { IMetadata } from '../views/artCreate'; interface IArweaveResult { error?: string; @@ -32,7 +34,7 @@ export const mintNFT = async ( connection: Connection, wallet: WalletAdapter | undefined, files: File[], - metadata: any, + metadata: { name: string; symbol: string }, ): Promise => { if (!wallet?.publicKey) { return { error: 'No wallet' }; @@ -99,8 +101,8 @@ export const mintNFT = async ( ); await createMetadata( - `🥭🧢#`, - `name: jjjdsfsk🥭🧢#`, + metadata.symbol, + metadata.name, `https://google.com`, mintKey, owner.publicKey, @@ -109,18 +111,6 @@ export const mintNFT = async ( wallet.publicKey, signers, ); - - // For Jordan -> Transfer SOL - console.log(files.length); - // TODO: - // instructions.push( - // Token.createSetAuthorityInstruction( - // TOKEN_PROGRAM_ID, - // mintKey, - // owner.publicKey, - // owner.publicKey, - // [])); - return new Promise(async res => { const txId = await sendTransactions( connection, @@ -157,6 +147,33 @@ export const mintNFT = async ( }, ) ).json(); + + const metadataFile = result.messages?.find( + m => m.filename == 'metadata.json', + ); + if (metadataFile?.transactionId && wallet.publicKey) { + const updateInstructions: TransactionInstruction[] = []; + const updateSigners: Account[] = []; + + await updateMetadata( + metadata.symbol, + metadata.name, + `https://arweave.net/${metadataFile.transactionId}`, + mintKey, + wallet.publicKey, + updateInstructions, + updateSigners, + ); + + await sendTransaction( + connection, + wallet, + updateInstructions, + updateSigners, + true, + 'singleGossip', + ); + } console.log('Result', result); res(result); } diff --git a/packages/metavinci/src/views/artCreate/index.tsx b/packages/metavinci/src/views/artCreate/index.tsx index c9605ac..5b156d8 100644 --- a/packages/metavinci/src/views/artCreate/index.tsx +++ b/packages/metavinci/src/views/artCreate/index.tsx @@ -27,7 +27,7 @@ enum Category { Video = 'video', Image = 'image', } -interface IProps { +export interface IMetadata { name: string; symbol: string; description: string; @@ -45,7 +45,7 @@ export const ArtCreateView = () => { const { wallet, connected } = useWallet(); const [step, setStep] = useState(0); const [saving, setSaving] = useState(false); - const [attributes, setAttributes] = useState({ + const [attributes, setAttributes] = useState({ name: '', symbol: '', description: '', @@ -178,8 +178,8 @@ const CategoryStep = (props: { confirm: (category: Category) => void }) => { }; const UploadStep = (props: { - attributes: IProps; - setAttributes: (attr: IProps) => void; + attributes: IMetadata; + setAttributes: (attr: IMetadata) => void; confirm: () => void; }) => { return ( @@ -232,8 +232,8 @@ const UploadStep = (props: { }; const InfoStep = (props: { - attributes: IProps; - setAttributes: (attr: IProps) => void; + attributes: IMetadata; + setAttributes: (attr: IMetadata) => void; confirm: () => void; }) => { const file = props.attributes.files[0]; @@ -319,8 +319,8 @@ const InfoStep = (props: { }; const RoyaltiesStep = (props: { - attributes: IProps; - setAttributes: (attr: IProps) => void; + attributes: IMetadata; + setAttributes: (attr: IMetadata) => void; confirm: () => void; }) => { const file = props.attributes.files[0]; @@ -371,7 +371,7 @@ const RoyaltiesStep = (props: { ); }; -const LaunchStep = (props: { confirm: () => void; attributes: IProps }) => { +const LaunchStep = (props: { confirm: () => void; attributes: IMetadata }) => { const file = props.attributes.files[0]; const metadata = { ...(props.attributes as any),