Solana Blinks with Go

brymes

Brymes

Posted on August 23, 2024

Solana Blinks with Go

Blinks are metadata-rich links that represent and enable on-chain activities throughout the Solana ecosystem without needing to navigate to a different app or webpage.

Blinks supports a wide range of activities enabled by Solana Actions
and primarily allow users to interact with the blockchain through social media and other off-chain platforms.

Use-Cases include:

  • NFT trading & minting,
  • Donations,
  • Crowd funding,
  • Token swaps,
  • Lottery/Casino apps and much more

In this article, we shall explore a simple Blink app focused on minting NFTs using Go. Whilst the article is Go focused, the core concepts apply to any Blink app. You can find the complete code on GitHub.

We'll begin by setting up a basic web server using the Gin framework, along with the necessary CORS configuration as defined by the specification. Also we’ll define some endpoints that will be discussed in detail below.

func main() {
    var (
        corsConfig = cors.DefaultConfig()
        router     = gin.Default()
        port       = os.Getenv("PORT")
    )

    corsConfig.AllowAllOrigins = true
    corsConfig.AddAllowHeaders([]string{"Content-Length", "Content-Type", "Access-Control-Allow-Origin"}...)
    corsConfig.AddAllowMethods([]string{"GET", "POST", "OPTIONS"}...)

    router.Use(cors.New(corsConfig))

    router.GET("/actions.json", app.ActionsRulesHandler)
    router.GET("/api/actions/mint_nft", app.GetActionsHandler)
    router.OPTIONS("/api/actions/mint_nft", app.OptionsHandler)
    router.POST("/api/actions/mint_nft", app.PostHandler)

    log.Println("StickyLabs Blink Active 🚀")

    if port == "" {
        port = "8081"
    }

    log.Println("Server is running")
    err := router.Run(fmt.Sprintf(":%v", port))
    if err != nil {
        log.Fatal(err)
        return
    }
}

Enter fullscreen mode Exit fullscreen mode

The core of any Blinks application lies in replicating the Solana Actions API Spec. Below is a visual representation of how Blinks work.

Blinks Action LifeCycle

Action Handlers

Blinks on Solana uses an Action URL scheme to provide a metadata-rich link, enabling various on-chain activities. This section outlines the primary handlers responsible for processing the mint NFT action on the /api/actions/mint_nft

  • GET Handler : Returns Metadata, supported actions and required parameters.
type ActionGetResponse struct {
    Title       string `json:"title"`
    Icon        string `json:"icon"`
    Description string `json:"description"`
    Label       string `json:"label"`
    Links       struct {
        Actions []Actions `json:"actions"`
    } `json:"links"`
}

type Actions struct {
    Label      string             `json:"label"`
    Href       string             `json:"href"`
    Parameters []ActionParameters `json:"parameters,omitempty"`
}

type ActionParameters struct {
    Name     string `json:"name"`
    Label    string `json:"label"`
    Required bool   `json:"required"`
}

func GetActionsHandler(c *gin.Context) {
    payload := ActionGetResponse{
        Title: "Actions Example - Mint NFT",
        Icon:        c.Request.URL.Scheme + "://" + c.Request.URL.Host + "/solana_devs.jpg",
        Description: "Transfer SOL to another Solana wallet",
        Label:       "Transfer",
    }
    payload.Links.Actions = []Actions{
        {"Mint NFT", "/api/actions/mint_nft", []ActionParameters{
            {"name", "Enter the Name of the NFT", true},
            {"symbol", "Enter the Symbol of the NFT", true},
            {"uri", "Enter the Uri of the NFT", true},
        }},
    }

    c.JSON(http.StatusOK, payload)
}
Enter fullscreen mode Exit fullscreen mode
  • OPTIONS : The OPTIONS handler handles CORS requirements, ensuring compatibility with browsers and other client request mechanisms.
var ACTIONS_CORS_HEADERS = map[string]string{
    "Access-Control-Allow-Origin":  "*",
    "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
}

func OptionsHandler(c *gin.Context) {
    for key, value := range ACTIONS_CORS_HEADERS {
        c.Header(key, value)
    }
    c.Status(http.StatusOK)
}
Enter fullscreen mode Exit fullscreen mode
  • POST Handler :The POST handler accepts query parameters, parses the account in base58 provided as JSON, and returns a base64-encoded serialized transaction along with a message for the user to sign and execute.
type MintNFTParams struct {
    Name   string `form:"name"   binding:"required"`
    Symbol string `form:"symbol" binding:"required"`
    URI    string `form:"uri"    binding:"required"`
}

// {  "account": "<account>" } //JSON
type ActionPostRequest struct {
    Account string `json:"account"`
}

type ActionPostResponse struct {
    Fields ActionPostResponseFields `json:"fields"`
}
type ActionPostResponseFields struct {
    Transaction string `json:"transaction"`
    Message     string `json:"message"`
}

func PostHandler(c *gin.Context) {
    var (
        qPayload MintNFTParams
        request  ActionPostRequest
        response ActionPostResponse
    )

    if err := c.ShouldBindQuery(&qPayload); err != nil {
        c.JSON(http.StatusBadRequest, ActionError{Message: "Invalid Query Params"})
        return
    }

    if err := c.ShouldBindJSON(&request); err != nil {
        log.Println(err)
        c.JSON(http.StatusBadRequest, ActionError{Message: "Invalid request"})
        return
    }

    account, err := types.AccountFromBase58(request.Account)
    if err != nil {
        log.Println(err)
        c.JSON(http.StatusBadRequest, ActionError{Message: "Invalid request; Error validating account"})
        return
    }
    response.Fields.Transaction, response.Fields.Message = mintNFT(qPayload, account)

    c.JSON(http.StatusOK, response)
}


Enter fullscreen mode Exit fullscreen mode
  • Minting NFTs

The mintNFT function leverages Solana-Go-SDK for minting NFTs, with few adjustments.

func mintNFT(metadata MintNFTParams, feePayer types.Account) (transaction, message string) {

    message = fmt.Sprintf("Mint NFT %s", metadata.Name)

    c := client.NewClient(rpc.DevnetRPCEndpoint)
    log.Println(metadata)

    mint := types.NewAccount()
    fmt.Printf("NFT: %v\n", mint.PublicKey.ToBase58())

    collection := types.NewAccount()
    fmt.Printf("collection: %v\n", collection.PublicKey.ToBase58())

    ata, _, err := common.FindAssociatedTokenAddress(feePayer.PublicKey, mint.PublicKey)
    if err != nil {
        log.Fatalf("failed to find a valid ata, err: %v", err)
    }

    tokenMetadataPubkey, err := token_metadata.GetTokenMetaPubkey(mint.PublicKey)
    if err != nil {
        log.Fatalf("failed to find a valid token metadata, err: %v", err)

    }
    tokenMasterEditionPubkey, err := token_metadata.GetMasterEdition(mint.PublicKey)
    if err != nil {
        log.Fatalf("failed to find a valid master edition, err: %v", err)
    }

    mintAccountRent, err := c.GetMinimumBalanceForRentExemption(context.Background(), token.MintAccountSize)
    if err != nil {
        log.Fatalf("failed to get mint account rent, err: %v", err)
    }

    recentBlockhashResponse, err := c.GetLatestBlockhash(context.Background())
    if err != nil {
        log.Fatalf("failed to get recent blockhash, err: %v", err)
    }

    tx, err := types.NewTransaction(types.NewTransactionParam{
        Signers: []types.Account{mint, feePayer},
        Message: types.NewMessage(types.NewMessageParam{
            FeePayer:        feePayer.PublicKey,
            RecentBlockhash: recentBlockhashResponse.Blockhash,
            Instructions: []types.Instruction{
                system.CreateAccount(system.CreateAccountParam{
                    From:     feePayer.PublicKey,
                    New:      mint.PublicKey,
                    Owner:    common.TokenProgramID,
                    Lamports: mintAccountRent,
                    Space:    token.MintAccountSize,
                }),
                token.InitializeMint(token.InitializeMintParam{
                    Decimals:   0,
                    Mint:       mint.PublicKey,
                    MintAuth:   feePayer.PublicKey,
                    FreezeAuth: &feePayer.PublicKey,
                }),
                token_metadata.CreateMetadataAccountV3(token_metadata.CreateMetadataAccountV3Param{
                    Metadata:                tokenMetadataPubkey,
                    Mint:                    mint.PublicKey,
                    MintAuthority:           feePayer.PublicKey,
                    Payer:                   feePayer.PublicKey,
                    UpdateAuthority:         feePayer.PublicKey,
                    UpdateAuthorityIsSigner: true,
                    IsMutable:               true,
                    Data: token_metadata.DataV2{
                        Name:                 metadata.Name,
                        Symbol:               metadata.Symbol,
                        Uri:                  metadata.URI,
                        SellerFeeBasisPoints: 100,
                        Creators: &[]token_metadata.Creator{
                            // tODO rede && Minter
                            {
                                Address:  feePayer.PublicKey,
                                Verified: true,
                                Share:    100,
                            },
                        },
                        Collection: &token_metadata.Collection{
                            Verified: false,
                            Key:      collection.PublicKey,
                        },
                        Uses: nil,
                    },
                    CollectionDetails: nil,
                }),
                associated_token_account.Create(associated_token_account.CreateParam{
                    Funder:                 feePayer.PublicKey,
                    Owner:                  feePayer.PublicKey,
                    Mint:                   mint.PublicKey,
                    AssociatedTokenAccount: ata,
                }),
                token.MintTo(token.MintToParam{
                    Mint:   mint.PublicKey,
                    To:     ata,
                    Auth:   feePayer.PublicKey,
                    Amount: 1,
                }),
                token_metadata.CreateMasterEditionV3(token_metadata.CreateMasterEditionParam{
                    Edition:         tokenMasterEditionPubkey,
                    Mint:            mint.PublicKey,
                    UpdateAuthority: feePayer.PublicKey,
                    MintAuthority:   feePayer.PublicKey,
                    Metadata:        tokenMetadataPubkey,
                    Payer:           feePayer.PublicKey,
                    MaxSupply:       pointer.Get[uint64](0),
                }),
            },
        }),
    })
    if err != nil {
        log.Fatalf("failed to new a tx, err: %v", err)
    }

    serialized, err := tx.Serialize()
    if err != nil {
        log.Fatal(err)
    }

    transaction = base64.StdEncoding.EncodeToString(serialized)
    return
}

Enter fullscreen mode Exit fullscreen mode
  • Error Handling: Actions should return user friendly errors in the format below.
// { "message" : "Insert Error Message" } //JSON
type ActionError struct {
    Message string `json:"message"`
}

Enter fullscreen mode Exit fullscreen mode
  • actions.json: The actions.json file should be stored at the root of the domain. It provides instructions to clients on what URLs support Solana Actions and provide mappings that can be used to perform GET requests to the Blink app. For simplicity we would be returning a JSON response from the url path
func ActionsRulesHandler(c *gin.Context) {
    payload := gin.H{
        "rules": []gin.H{
            {
                "pathPattern": "/*",
                "apiPath":     "/api/actions/*",
            },
            {
                "pathPattern": "/api/actions/**",
                "apiPath":     "/api/actions/**",
            },
        },
    }

    c.JSON(http.StatusOK, payload)
}
Enter fullscreen mode Exit fullscreen mode

Testing your Blink

After deploying your app, you could use the Blinks Inspector app to test.

Blinks Inspector Example

Conclusion

I hope this article provides a practical introduction to building Blinks applications on Solana using Go. Entire code can be found here.

For a deeper dive into the Solana Actions framework, and detailed documentation, check out Solana’s official resources

💖 💪 🙅 🚩
brymes
Brymes

Posted on August 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Solana Blinks with Go
web3 Solana Blinks with Go

August 23, 2024