From 58b9887b3d1511bcc4e920bf49b285e0e730e2a9 Mon Sep 17 00:00:00 2001 From: adamjeffries <896669+adamjeffries@users.noreply.github.com> Date: Sun, 19 Sep 2021 01:35:38 -0500 Subject: [PATCH 1/2] Replace uploadFile2 with 4 --- js/packages/web/src/actions/nft.tsx | 37 ++++++----- js/packages/web/src/utils/assets.ts | 62 ------------------- js/packages/web/src/views/artCreate/index.tsx | 39 ++++-------- 3 files changed, 32 insertions(+), 106 deletions(-) diff --git a/js/packages/web/src/actions/nft.tsx b/js/packages/web/src/actions/nft.tsx index 7d03423..d4bfd1e 100644 --- a/js/packages/web/src/actions/nft.tsx +++ b/js/packages/web/src/actions/nft.tsx @@ -25,10 +25,11 @@ import { TransactionInstruction, } from '@solana/web3.js'; import crypto from 'crypto'; -import { getAssetCostToStore } from '../utils/assets'; import { AR_SOL_HOLDER_ID } from '../utils/ids'; import BN from 'bn.js'; + const RESERVED_TXN_MANIFEST = 'manifest.json'; +const RESERVED_METADATA = 'metadata.json'; interface IArweaveResult { error?: string; @@ -40,6 +41,18 @@ interface IArweaveResult { }>; } +const uploadToArweave = async (data: FormData): Promise => + ( + await fetch( + 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFile4', + { + method: 'POST', + // @ts-ignore + body: data, + }, + ) + ).json(); + export const mintNFT = async ( connection: Connection, wallet: WalletSigner | undefined, @@ -85,7 +98,7 @@ export const mintNFT = async ( const realFiles: File[] = [ ...files, - new File([JSON.stringify(metadataContent)], 'metadata.json'), + new File([JSON.stringify(metadataContent)], RESERVED_METADATA), ]; const { instructions: pushInstructions, signers: pushSigners } = @@ -170,6 +183,7 @@ export const mintNFT = async ( wallet, instructions, signers, + 'single', ); try { @@ -184,6 +198,8 @@ export const mintNFT = async ( // this means we're done getting AR txn setup. Ship it off to ARWeave! const data = new FormData(); + data.append('transaction', txid); + data.append('env', env); const tags = realFiles.reduce( (acc: Record>, f) => { @@ -193,23 +209,10 @@ export const mintNFT = async ( {}, ); data.append('tags', JSON.stringify(tags)); - data.append('transaction', txid); realFiles.map(f => data.append('file[]', f)); // TODO: convert to absolute file name for image - - const result: IArweaveResult = await ( - await fetch( - // TODO: add CNAME - env.startsWith('mainnet-beta') - ? 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFileProd2' - : 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFile2', - { - method: 'POST', - body: data, - }, - ) - ).json(); + const result: IArweaveResult = await uploadToArweave(data); const metadataFile = result.messages?.find( m => m.filename === RESERVED_TXN_MANIFEST, @@ -323,7 +326,7 @@ export const prepPayForFilesTxn = async ( SystemProgram.transfer({ fromPubkey: wallet.publicKey, toPubkey: AR_SOL_HOLDER_ID, - lamports: await getAssetCostToStore(files), + lamports: 10, }), ); diff --git a/js/packages/web/src/utils/assets.ts b/js/packages/web/src/utils/assets.ts index 5e20f5d..1574254 100644 --- a/js/packages/web/src/utils/assets.ts +++ b/js/packages/web/src/utils/assets.ts @@ -1,69 +1,7 @@ -import { useLocalStorage } from '@oyster/common'; import { TokenInfo } from '@solana/spl-token-registry'; export const LAMPORT_MULTIPLIER = 10 ** 9; -const WINSTON_MULTIPLIER = 10 ** 12; export const filterModalSolTokens = (tokens: TokenInfo[]) => { return tokens; }; - -export async function getAssetCostToStore(files: File[]) { - const localStorage = useLocalStorage(); - const totalBytes = files.reduce((sum, f) => (sum += f.size), 0); - console.log('Total bytes', totalBytes); - const txnFeeInWinstons = parseInt( - await (await fetch('https://arweave.net/price/0')).text(), - ); - console.log('txn fee', txnFeeInWinstons); - const byteCostInWinstons = parseInt( - await ( - await fetch('https://arweave.net/price/' + totalBytes.toString()) - ).text(), - ); - console.log('byte cost', byteCostInWinstons); - const totalArCost = - (txnFeeInWinstons * files.length + byteCostInWinstons) / WINSTON_MULTIPLIER; - - console.log('total ar', totalArCost); - - let conversionRates = JSON.parse( - localStorage.getItem('conversionRates') || '{}', - ); - - if ( - !conversionRates || - !conversionRates.expiry || - conversionRates.expiry < Date.now() - ) { - console.log('Calling conversion rate'); - conversionRates = { - value: JSON.parse( - await ( - await fetch( - 'https://api.coingecko.com/api/v3/simple/price?ids=solana,arweave&vs_currencies=usd', - ) - ).text(), - ), - expiry: Date.now() + 5 * 60 * 1000, - }; - - if (conversionRates.value.solana) { - try { - localStorage.setItem( - 'conversionRates', - JSON.stringify(conversionRates), - ); - } catch { - // ignore - } - } - } - - // To figure out how many lamports are required, multiply ar byte cost by this number - const arMultiplier = - conversionRates.value.arweave.usd / conversionRates.value.solana.usd; - console.log('Ar mult', arMultiplier); - // We also always make a manifest file, which, though tiny, needs payment. - return LAMPORT_MULTIPLIER * totalArCost * arMultiplier * 1.1; -} diff --git a/js/packages/web/src/views/artCreate/index.tsx b/js/packages/web/src/views/artCreate/index.tsx index 06e685a..5d66905 100644 --- a/js/packages/web/src/views/artCreate/index.tsx +++ b/js/packages/web/src/views/artCreate/index.tsx @@ -34,11 +34,11 @@ import { StringPublicKey, } from '@oyster/common'; import { useWallet } from '@solana/wallet-adapter-react'; -import { getAssetCostToStore, LAMPORT_MULTIPLIER } from '../../utils/assets'; +import { LAMPORT_MULTIPLIER } from '../../utils/assets'; import { Connection } from '@solana/web3.js'; import { MintLayout } from '@solana/spl-token'; import { useHistory, useParams } from 'react-router-dom'; -import { cleanName, getLast } from '../../utils/utils'; +import { getLast } from '../../utils/utils'; import { AmountLabel } from '../../components/AmountLabel'; import useWindowDimensions from '../../utils/layout'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; @@ -493,7 +493,11 @@ const UploadStep = (props: { }), }, image: coverFile?.name || '', - animation_url: (props.attributes.properties?.category !== MetadataCategory.Image && customURL) ? customURL : mainFile && mainFile.name, + animation_url: + props.attributes.properties?.category !== + MetadataCategory.Image && customURL + ? customURL + : mainFile && mainFile.name, }); props.setFiles([coverFile, mainFile].filter(f => f) as File[]); props.confirm(); @@ -1023,31 +1027,12 @@ const LaunchStep = (props: { const files = props.files; const metadata = props.attributes; useEffect(() => { - const rentCall = Promise.all([ + Promise.all([ props.connection.getMinimumBalanceForRentExemption(MintLayout.span), props.connection.getMinimumBalanceForRentExemption(MAX_METADATA_LEN), - ]); - if (files.length) - getAssetCostToStore([ - ...files, - new File([JSON.stringify(metadata)], 'metadata.json'), - ]).then(async lamports => { - const sol = lamports / LAMPORT_MULTIPLIER; - - // TODO: cache this and batch in one call - const [mintRent, metadataRent] = await rentCall; - - // const uriStr = 'x'; - // let uriBuilder = ''; - // for (let i = 0; i < MAX_URI_LENGTH; i++) { - // uriBuilder += uriStr; - // } - - const additionalSol = (metadataRent + mintRent) / LAMPORT_MULTIPLIER; - - // TODO: add fees based on number of transactions and signers - setCost(sol + additionalSol); - }); + ]).then(([mintRent, metadataRent]) => { + setCost((metadataRent + mintRent) / LAMPORT_MULTIPLIER); + }); }, [files, metadata, setCost]); return ( @@ -1152,7 +1137,7 @@ const Congrats = (props: { text: "I've created a new NFT artwork on Metaplex, check it out!", url: `${ window.location.origin - }/#/art/${props.nft?.metadataAccount.toString()}`, + }/#/art/${props.nft?.metadataAccount.toString()}`, hashtags: 'NFT,Crypto,Metaplex', // via: "Metaplex", related: 'Metaplex,Solana', From 8b80166f02b1cfe6484cc668f619310674e445db Mon Sep 17 00:00:00 2001 From: adamjeffries <896669+adamjeffries@users.noreply.github.com> Date: Sun, 19 Sep 2021 11:24:31 -0500 Subject: [PATCH 2/2] Restore getAssetCostToStore --- js/packages/web/src/actions/nft.tsx | 3 +- js/packages/web/src/utils/assets.ts | 62 +++++++++++++++++++ js/packages/web/src/views/artCreate/index.tsx | 39 ++++++++---- 3 files changed, 91 insertions(+), 13 deletions(-) diff --git a/js/packages/web/src/actions/nft.tsx b/js/packages/web/src/actions/nft.tsx index d4bfd1e..ee099da 100644 --- a/js/packages/web/src/actions/nft.tsx +++ b/js/packages/web/src/actions/nft.tsx @@ -25,6 +25,7 @@ import { TransactionInstruction, } from '@solana/web3.js'; import crypto from 'crypto'; +import { getAssetCostToStore } from '../utils/assets'; import { AR_SOL_HOLDER_ID } from '../utils/ids'; import BN from 'bn.js'; @@ -326,7 +327,7 @@ export const prepPayForFilesTxn = async ( SystemProgram.transfer({ fromPubkey: wallet.publicKey, toPubkey: AR_SOL_HOLDER_ID, - lamports: 10, + lamports: await getAssetCostToStore(files), }), ); diff --git a/js/packages/web/src/utils/assets.ts b/js/packages/web/src/utils/assets.ts index 1574254..5e20f5d 100644 --- a/js/packages/web/src/utils/assets.ts +++ b/js/packages/web/src/utils/assets.ts @@ -1,7 +1,69 @@ +import { useLocalStorage } from '@oyster/common'; import { TokenInfo } from '@solana/spl-token-registry'; export const LAMPORT_MULTIPLIER = 10 ** 9; +const WINSTON_MULTIPLIER = 10 ** 12; export const filterModalSolTokens = (tokens: TokenInfo[]) => { return tokens; }; + +export async function getAssetCostToStore(files: File[]) { + const localStorage = useLocalStorage(); + const totalBytes = files.reduce((sum, f) => (sum += f.size), 0); + console.log('Total bytes', totalBytes); + const txnFeeInWinstons = parseInt( + await (await fetch('https://arweave.net/price/0')).text(), + ); + console.log('txn fee', txnFeeInWinstons); + const byteCostInWinstons = parseInt( + await ( + await fetch('https://arweave.net/price/' + totalBytes.toString()) + ).text(), + ); + console.log('byte cost', byteCostInWinstons); + const totalArCost = + (txnFeeInWinstons * files.length + byteCostInWinstons) / WINSTON_MULTIPLIER; + + console.log('total ar', totalArCost); + + let conversionRates = JSON.parse( + localStorage.getItem('conversionRates') || '{}', + ); + + if ( + !conversionRates || + !conversionRates.expiry || + conversionRates.expiry < Date.now() + ) { + console.log('Calling conversion rate'); + conversionRates = { + value: JSON.parse( + await ( + await fetch( + 'https://api.coingecko.com/api/v3/simple/price?ids=solana,arweave&vs_currencies=usd', + ) + ).text(), + ), + expiry: Date.now() + 5 * 60 * 1000, + }; + + if (conversionRates.value.solana) { + try { + localStorage.setItem( + 'conversionRates', + JSON.stringify(conversionRates), + ); + } catch { + // ignore + } + } + } + + // To figure out how many lamports are required, multiply ar byte cost by this number + const arMultiplier = + conversionRates.value.arweave.usd / conversionRates.value.solana.usd; + console.log('Ar mult', arMultiplier); + // We also always make a manifest file, which, though tiny, needs payment. + return LAMPORT_MULTIPLIER * totalArCost * arMultiplier * 1.1; +} diff --git a/js/packages/web/src/views/artCreate/index.tsx b/js/packages/web/src/views/artCreate/index.tsx index 5d66905..06e685a 100644 --- a/js/packages/web/src/views/artCreate/index.tsx +++ b/js/packages/web/src/views/artCreate/index.tsx @@ -34,11 +34,11 @@ import { StringPublicKey, } from '@oyster/common'; import { useWallet } from '@solana/wallet-adapter-react'; -import { LAMPORT_MULTIPLIER } from '../../utils/assets'; +import { getAssetCostToStore, LAMPORT_MULTIPLIER } from '../../utils/assets'; import { Connection } from '@solana/web3.js'; import { MintLayout } from '@solana/spl-token'; import { useHistory, useParams } from 'react-router-dom'; -import { getLast } from '../../utils/utils'; +import { cleanName, getLast } from '../../utils/utils'; import { AmountLabel } from '../../components/AmountLabel'; import useWindowDimensions from '../../utils/layout'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; @@ -493,11 +493,7 @@ const UploadStep = (props: { }), }, image: coverFile?.name || '', - animation_url: - props.attributes.properties?.category !== - MetadataCategory.Image && customURL - ? customURL - : mainFile && mainFile.name, + animation_url: (props.attributes.properties?.category !== MetadataCategory.Image && customURL) ? customURL : mainFile && mainFile.name, }); props.setFiles([coverFile, mainFile].filter(f => f) as File[]); props.confirm(); @@ -1027,12 +1023,31 @@ const LaunchStep = (props: { const files = props.files; const metadata = props.attributes; useEffect(() => { - Promise.all([ + const rentCall = Promise.all([ props.connection.getMinimumBalanceForRentExemption(MintLayout.span), props.connection.getMinimumBalanceForRentExemption(MAX_METADATA_LEN), - ]).then(([mintRent, metadataRent]) => { - setCost((metadataRent + mintRent) / LAMPORT_MULTIPLIER); - }); + ]); + if (files.length) + getAssetCostToStore([ + ...files, + new File([JSON.stringify(metadata)], 'metadata.json'), + ]).then(async lamports => { + const sol = lamports / LAMPORT_MULTIPLIER; + + // TODO: cache this and batch in one call + const [mintRent, metadataRent] = await rentCall; + + // const uriStr = 'x'; + // let uriBuilder = ''; + // for (let i = 0; i < MAX_URI_LENGTH; i++) { + // uriBuilder += uriStr; + // } + + const additionalSol = (metadataRent + mintRent) / LAMPORT_MULTIPLIER; + + // TODO: add fees based on number of transactions and signers + setCost(sol + additionalSol); + }); }, [files, metadata, setCost]); return ( @@ -1137,7 +1152,7 @@ const Congrats = (props: { text: "I've created a new NFT artwork on Metaplex, check it out!", url: `${ window.location.origin - }/#/art/${props.nft?.metadataAccount.toString()}`, + }/#/art/${props.nft?.metadataAccount.toString()}`, hashtags: 'NFT,Crypto,Metaplex', // via: "Metaplex", related: 'Metaplex,Solana',