Skip to content

Commit

Permalink
Merge pull request #173 from solana-labs/nft-example
Browse files Browse the repository at this point in the history
Add an NFT create example
  • Loading branch information
mcintyre94 committed Nov 28, 2022
2 parents 93b76c7 + 314c314 commit 6091e25
Show file tree
Hide file tree
Showing 127 changed files with 25,911 additions and 7 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
node_modules
build
.env
.next
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The Solana blockchain confirms transactions in less than a second and costs on a
Use the [`@solana/pay` JavaScript SDK](https://github.com/solana-labs/solana-pay/tree/master/core) to start accepting payments in your app today.

### Accept payments in person
Run the open-source [Solana Pay Point of Sale app](https://github.com/solana-labs/solana-pay/tree/master/point-of-sale) to start accepting payments in-person.
Run the open-source [Solana Pay Point of Sale app](https://github.com/solana-labs/solana-pay/tree/master/examples/point-of-sale) to start accepting payments in-person.

## Getting Involved

Expand Down
2 changes: 1 addition & 1 deletion docs/src/INTRODUCTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The Solana blockchain confirms transactions in less than a second and costs on a
Use the [`@solana/pay` JavaScript SDK](https://github.com/solana-labs/solana-pay/tree/master/core) to start accepting payments in your app today.

### Accept payments in person
Run the open-source [Solana Pay Point of Sale app](https://github.com/solana-labs/solana-pay/tree/master/point-of-sale) to start accepting payments in-person.
Run the open-source [Solana Pay Point of Sale app](https://github.com/solana-labs/solana-pay/tree/master/examples/point-of-sale) to start accepting payments in-person.

## Getting Involved

Expand Down
2 changes: 1 addition & 1 deletion docs/src/core/transfer-request/MERCHANT_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ The steps outlined above prevents:
[1]: https://github.com/solana-labs/qr-code-styling
[2]: https://spl.solana.com/memo
[3]: https://github.com/solana-labs/solana/issues/19535
[4]: https://github.com/solana-labs/solana-pay/tree/master/point-of-sale
[4]: https://github.com/solana-labs/solana-pay/tree/master/examples/point-of-sale
[5]: https://github.com/solana-labs/solana-pay/tree/master/core/example/payment-flow-merchant
[6]: https://github.com/solana-labs/solana-pay/blob/master/core/example/payment-flow-merchant/simulateCheckout.ts
[7]: https://github.com/solana-labs/solana-pay/blob/master/core/example/payment-flow-merchant/main.ts#L61
Expand Down
1 change: 1 addition & 0 deletions examples/nft-create/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SHOP_PRIVATE_KEY=...
120 changes: 120 additions & 0 deletions examples/nft-create/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Send an NFT as part of a Solana Pay payment

This repo demonstrates an example of the following transaction using Solana Pay:

- Buyer pays X USDC
- They're airdropped an NFT as part of the transaction

It uses the new `@metaplex-foundation/js` library which makes NFT instructions much easier to work with!


## Environment variable

One environment variable is required, `SHOP_PRIVATE_KEY`. This is the base58 encoded private key for an account that will pay the network and storage fees.

Copy `.env.example` to `.env` and set the private key there.

This is required for uploading NFT metadata and for the checkout API.


## Install dependencies

Dependencies are managed with npm. Run `npm install` in this directory to install them.

## Upload NFT metadata

Before an NFT can be created we need to upload its metadata. The easiest way is using Bundlr storage, which Metaplex has a nice plugin for.

Uploading is done using the script [upload.js](./nft-upload/upload.js)

The NFT image file is [pay-logo.svg](./nft-upload/pay-logo.svg) in the same directory. Feel free to change it!

The rest of the NFT metadata is set by variables in `upload.js`.

Once you've set these you can run the script:

```shell
$ node nft-upload/upload.js
Uploaded metadata: https://arweave.net/mAbxQsdFYQNRFPqWHNzZbkwVw6LFp3k9LvRTxuZpVXk
Done!
```

## Running the web app

This example is a NextJS app. Run `npm run dev` to start it on http://localhost:3000.


## Checkout API

The checkout API takes a public key as input and returns a partially signed transaction, which must be signed by the input public key before it is broadcast.

The checkout API code is at [checkout.ts](./pages/api/checkout.ts)

- You'll need to set the metadata URL to your own, as returned by `nft-upload/upload.js`:
```ts
const METADATA_URI = "..."
```

- You can also set the USDC address (or change the SPL token), the RPC endpoint, the created NFT name and the USDC price in variables at the top of this file.



It first creates a Metaplex `TransactionBuilder` to create an NFT:

```ts
const transactionBuilder = await nfts.builders().create({
uri: METADATA_URI, // use our metadata
name: NFT_NAME,
tokenOwner: account, // NFT is minted to the wallet submitting the transaction (buyer)
updateAuthority: shopKeypair, // we retain update authority
sellerFeeBasisPoints: 100, // 1% royalty
useNewMint: mintKeypair, // we pass our mint in as the new mint to use
})
```

By using a transaction builder we can control the conversion to a Solana `Transaction` and then return it.

It then creates an SPL token transaction to send USDC from the buyer to the shop:

```ts
const usdcTransferInstruction = createTransferCheckedInstruction(
fromUsdcAddress.address, // from USDC address
USDC_ADDRESS, // USDC mint address
toUsdcAddress.address, // to USDC address
account, // owner of the from USDC address (the buyer)
PRICE_USDC * (10 ** decimals), // multiply by 10^decimals
decimals
)
```

This instruction is prepended to the NFT transaction, so that it's part of the same atomic transaction.

We then convert it to a `Transaction`, and sign it as:

- The shop keypair, which is our Metaplex identity and pays the fees (so the buyer pays no SOL, just USDC)
- The mint keypair, which we generate in the API and pass to the NFT create function.

This transaction is only **partially signed**, the USDC instruction additionally requires the user's signature.

We return this transaction, and the user's wallet will be able to sign it as them and then submit it to the network.


## Submitting the transaction

The home page is at [index.tsx](./pages/index.tsx). It has code to connect a wallet (using wallet-adapter) and fetch/send the transaction. It also has code to display a QR code that can be scanned by wallets that support Solana Pay, which encodes a call to the checkout API.

Both are an identical transaction. The browser wallets tend to have better error messaging if anything goes wrong, and you'll have access to the browser console too.

### Making localhost:3000 internet accessible

When you scan the QR code it encodes the full URL of the checkout API, eg. `http://localhost:3000/api/checkout`. Without fiddling with networking on the phone, this can't be resolved by a mobile wallet.

One easy way to handle this is to use [ngrok](https://ngrok.com). Once you sign up (free) and download their CLI you can run `ngrok http 3000`.

You'll see an output with a message like:

```
Forwarding https://6fba-2a02-c7c-50a3-a200-1402-5c1a-a7d2-174d.eu.ngrok.io -> http://localhost:3000
```

This `ngrok.io` domain will forward to your `localhost:3000` and be accessible anywhere. In other words it'll show the home page, with a QR code that encodes eg. `https://6fba-2a02-c7c-50a3-a200-1402-5c1a-a7d2-174d.eu.ngrok.io/api/checkout`. This will work correctly with mobile wallets!
File renamed without changes.
3 changes: 3 additions & 0 deletions examples/nft-create/nft-upload/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
16 changes: 16 additions & 0 deletions examples/nft-create/nft-upload/pay-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions examples/nft-create/nft-upload/upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { bundlrStorage, keypairIdentity, Metaplex, toMetaplexFile } from "@metaplex-foundation/js"
import { clusterApiUrl, Connection, Keypair } from "@solana/web3.js"
import base58 from "bs58"
import * as dotenv from "dotenv"
import * as fs from "fs"
dotenv.config()

// update these variables!
// Connection endpoint, switch to a mainnet RPC if using mainnet
const ENDPOINT = clusterApiUrl('devnet')

// Devnet Bundlr address
const BUNDLR_ADDRESS = "https://devnet.bundlr.network"

// Mainnet Bundlr address, uncomment if using mainnet
// const BUNDLR_ADDRESS = "https://node1.bundlr.network"

// NFT metadata
const NFT_NAME = "Golden Ticket"
const NFT_SYMBOL = "GOLD"
const NFT_DESCRIPTION = "A golden ticket that grants access to loyalty rewards"
// Set this relative to the root directory
const NFT_IMAGE_PATH = "nft-upload/pay-logo.svg"
const NFT_FILE_NAME = "pay-logo.svg"


async function main() {
// Get the shop keypair from the environment variable
const shopPrivateKey = process.env.SHOP_PRIVATE_KEY
if (!shopPrivateKey) throw new Error('SHOP_PRIVATE_KEY not found')
const shopKeypair = Keypair.fromSecretKey(base58.decode(shopPrivateKey))

const connection = new Connection(ENDPOINT)

const nfts = Metaplex
.make(connection, { cluster: 'devnet' })
.use(keypairIdentity(shopKeypair))
.use(bundlrStorage({
address: BUNDLR_ADDRESS,
providerUrl: ENDPOINT,
timeout: 60000
}))
.nfts();

const imageBuffer = fs.readFileSync(NFT_IMAGE_PATH)
const file = toMetaplexFile(imageBuffer, NFT_FILE_NAME)

const uploadedMetadata = await nfts.uploadMetadata({
name: NFT_NAME,
symbol: NFT_SYMBOL,
description: NFT_DESCRIPTION,
image: file,
})

console.log(`Uploaded metadata: ${uploadedMetadata.uri}`)
}

main()
.then(() => {
console.log("Done!")
})
.catch((err) => {
console.error(err)
process.exit(1)
})
Loading

0 comments on commit 6091e25

Please sign in to comment.