1.0.0 • Published 12 months ago

vision-solana-sdk v1.0.0

Weekly downloads
-
License
-
Repository
github
Last release
12 months ago

SolAi - Solana Program SDK

A SDK built for use in correlation with the Sol Factory Solana program.

Below you can find instructions on using the SDK locally followed by a deeper dive into the SDK's structure and details.

!WARNING
SolAi-SDK is still in development and is unstable, there can be minor and major changes at any time.

Program ID

ClusterProgram Id
Localnet4Fj9kuGYLye3pwCBYaXbuzocEy22gPWT5TcJVJ6JauUt
Devnet6rHuJFF9XCxi9eDHtgJPcBKNpMWyBHhQhrFSkUD5XMYo
Mainnet

Description

The Sol Factory program is structured for Artists to create on-chain Collections that a user can mint a Token-2022 NFT from on the Solana blockchain.

The Solana Factory program was built for use with the Sol Ai SDK.

You can view a baremetal combination of the Program and SDK live on Devnet here.

Getting Started

Prerequisites

For testing purposes it is advised to use at least three developer wallets (Admin, Artist, User)

Installation

npm install @maweiche/react-sdk

Documentation

For more detailed documentation, please visit the SolAI documentation.

Usage

🚨 Important Notes Because SDK instances require Keypair usage all examples are for backend use

To utilize the sdk you will need to import it's type and create an instance with the following to start:

  • Wallet
  • RPC Connection
  • Confirm Options
  • Cluster
const keypair = Keypair.fromSecretKey(base58.decode(YOUR_BS58_SECRET_KEY));
const wallet = new NodeWallet(keypair);
const connection = new Connection('https://api.devnet.solana.com/', 'confirmed')

const sdk = new SDK(
  wallet,
  connection,
  { skipPreflight: true},
  "devnet",
)

Collections

An Artist's Collection consists of the following data:

{
  name: string,
  symbol: string,
  owner: PublicKey,
  saleStartTime: bigint,
  maxSupply: bigint,
  totalSupply: bigint,
  price: bigint,
  stableId: string,
  reference: string,
  whitelist: {
    wallets: PublicKey[]
  },
  whitelistStartTime: bigint,
  whitelistPrice: bigint
}

Collections can be fetched two ways:

  • All - returns every collection
  • Collection Owner - Artist's Wallet Address
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { Keypair, Connection } from '@solana/web3.js';
import base58 from "bs58";
import { SDK } from '@maweiche/react-sdk';

export async function GET(request: Request) {
  try{
    const keypair = Keypair.fromSecretKey(base58.decode(YOUR_BS58_SECRET_KEY));

    const wallet = new NodeWallet(keypair);

    const connection = new Connection('https://api.devnet.solana.com/', 'confirmed')

    const sdk = new SDK(
      wallet,
      connection,
      { skipPreflight: true},
      "devnet",
    )

    // ALL COLLECTIONS******************************************
    const collections = await sdk.collection.getAllCollections(
      connection, // connection
    )
    

    // BY OWNER*************************************************
    const collection = await sdk.collection.getCollectionByOwner(
      connection, // connection
      owner // owner
    )
    
    if(!collections) {
      return new Response('error', { status: 500 });
    }

    return new Response(JSON.stringify(collections), { status: 200 });
  } catch (error) {
    console.log('error', error)
    return new Response('error', { status: 500 });
  }
}

To Create a Collection the signer of the transaction will be the owner of the Collection as well as the payer for the transaction fees.

The example below demonstrates a POST API route that returns a transaction to be signed by the user's wallet on the Front-End.

import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { Keypair, Connection, PublicKey, Transaction } from '@solana/web3.js';
import { SDK } from '@maweiche/react-sdk';
import * as anchor from '@project-serum/anchor';
import base58, * as bs58 from "bs58";

export async function POST(request: Request) {
  const body = await request.json();

  const owner = new PublicKey(body.owner);  // Collection Owner
  const name = body.name; /// Test Collection
  const symbol = body.symbol; // TST
  const sale_start_time = new anchor.BN(body.sale_start_time); // new Date()
  const max_supply = new anchor.BN(body.max_supply); //100
  const price = new anchor.BN(body.price);   // 1.5

  const stable_id = body.stable_id;

  const keypair = Keypair.fromSecretKey(base58.decode(YOUR_BS58_SECRET_KEY));
  const wallet = new NodeWallet(keypair);
  const connection = new Connection('https://api.devnet.solana.com/', 'confirmed')
  const sdk = new SDK(
    wallet,
    connection,
    { skipPreflight: true},
    "devnet",
  )

  const { instructions } = await sdk.collection.createCollection(
    admin.publicKey,
    collectionOwner, // publickey
    name, //string
    symbol, // string
    url, // string
    sale_start_time, // Big Number
    sale_end_time, // Big Number
    max_supply, // Big Number
    price, // Big Number
    stable_id, //string
  );

  const tx = Transaction.from(Buffer.from(instructions, "base64"));
  const serializedTransaction = tx.serialize({
      requireAllSignatures: false,
    });
  const base64 = serializedTransaction.toString("base64");
  const base64JSON = JSON.stringify(base64);

  return new Response(base64JSON, { status: 200 });
}

Collections - FRONT END

import { useWallet } from '@solana/wallet-adapter-react';
import { Connection, PublicKey, Transaction } from '@solana/web3.js';

const { publicKey, sendTransaction } = useWallet();

const confirm = async (signature: string): Promise<string> => {
  const block = await connection.getLatestBlockhash();
  await connection.confirmTransaction({
      signature,
      ...block
  })
  return signature
}

async function createCollection(){
  try {
    const _collection = await fetch('/api/collections/create', {
      method: 'POST',
      body: JSON.stringify({ 
        owner: publicKey?.toBase58(),
        name: name,
        symbol: symbol,
        sale_start_time: sale_start_time,
        max_supply: max_supply,
        price: price,
        stable_id: stable_id,
      }),
      headers: {
        'Content-Type': 'application/json',
      },
    });

    const _txJson = await _collection.json();
    const tx = Transaction.from(Buffer.from(_txJson, "base64"));
    const signature = await sendTransaction(tx, connection, {skipPreflight: true});
    console.log('signature', signature);
  } catch (error) {
    console.log(error)
  }
}

Minting NFTs

After a user selects an available Collection they are able to Mint a NFT from that Collection. Because the AI Image Generation will not return instanaeously, we have built the following flow for the minting process:

  • User Selects a Collection and clicks 'Mint'
  • User is then prompted with a Transaction to pay the Mint price and receive a Placeholder NFT (Token 2022)
  • Once the transaction is approved and confirmed the AI Image Generation begins
  • Upon completion of the AI Image Generation a new NFT (Token 2022) is created and sent directly to the User while simultaneously Burning their Placeholder NFT.

For this API route example we broke this process down into two routes:

  • mint - returns { instructions: TransactionInstruction[], placeholder_mint: PublicKey}
  • finalize - returns { tx_signature: string, nft_mint: string }

The mint route returns a transaction to be signed by the user while the finalize returns a transaction signature since the admin wallet is paying for the nft transfer/creation after the AI Image Generation is complete.

🚨 Important Notes to prevent unauthorized calls to these functions, they require an admin signature

Standard Flow

Mint -- API ROUTE

import { Keypair, Connection, PublicKey, Transaction } from '@solana/web3.js';
import { SDK } from '@maweiche/react-sdk';
import base58, * as bs58 from "bs58";
import dotenv from 'dotenv';
dotenv.config()
export async function POST(request: Request) {
  const body = await request.json();
  const id = body.id;
  const collectionOwner = new PublicKey(body.collectionOwner);
  const publicKey = new PublicKey(body.publicKey);
  const keypair = Keypair.fromSecretKey(base58.decode(YOUR_BS58_SECRET_KEY));
  const wallet = new NodeWallet(keypair);
  const connection = new Connection("https://api.devnet.solana.com", "finalized");

  const sdk = new SDK(
    wallet,
    connection, // rpc connection
    { skipPreflight: true }, // confirmation options
    "devnet", // rpc cluster -- mainnet / devnet / localnet
  )

  const { instructions, placeholder_mint } = await sdk.placeholder.createPlaceholder(
    wallet.feePayer, // admin keypair
    collectionOwner, // collection owner publickey
    buyer.publicKey, // buyer's publickey
    id, // id used for seeds
    'https://arweave.net/-mpn67FnEePrsoKez4f6Dvjb1aMcH1CqCdZX0NCyHK8' // placeholder uri
  );


  const tx = new Transaction().add(...instructions);
  tx.partialSign(wallet.feePayer);

  const serializedTransaction = tx.serialize({
    requireAllSignatures: false,
  });

  const base64 = serializedTransaction.toString("base64");
  
  return new Response(
    JSON.stringify({
      transaction: base64,
      placeholder_mint: placeholder_mint
    }), 
    { status: 200 }
  );
}

MINT - FRONT END

import { useWallet } from '@solana/wallet-adapter-react';
import { Connection, PublicKey, Transaction } from '@solana/web3.js';

const { publicKey, sendTransaction } = useWallet();

const confirm = async (signature: string): Promise<string> => {
  const block = await connection.getLatestBlockhash();
  await connection.confirmTransaction({
      signature,
      ...block
  })
  return signature
}

async function mintNft(){
  try {
    const id = Math.floor(Math.random() * 100000); //random number used to create seeds for buyers placeholder/nft

    // INITIATE MINTING TXN -- REQUIRES USER WALLET SIGNATURE
    const _tx = await fetch('/api/mint', {
      method: 'POST',
      body: JSON.stringify({ 
        id: id, 
        collectionOwner: collectionOwner.publicKey, 
        publicKey: buyer.publicKey?.toBase58()})
      headers: {
        'Content-Type': 'application/json',
      },
    });

    const { transaction, placeholder_mint } = await _tx.json();

    const tx = Transaction.from(Buffer.from(transaction, "base64"));
    const signature = await sendTransaction(tx, connection, {skipPreflight: true});

    // ONCE THIS CONFIRMS, WE KNOW THE USER HAS PAID AND THE PLACEHOLDER SUCCESFULLY MINTED
    const confirmation = await confirm(sig);

    // GET THE IMAGE URL TO POLL
    const placeholder_metadata = await getTokenMetadata(connection, placeholder_mint);
    console.log('placeholder_metadata', placeholder_metadata)
    
    const additional_metadata = placeholder_metadata!.additionalMetadata;
    const token_id = additional_metadata[1][1];
    console.log('placeholder mint', placeholder_mint.toBase58());
    console.log('placeholder token id', token_id);

    const getCollectionUrl = async(collection: PublicKey) => {
        const collection_data = await connection.getAccountInfo(collection);
        const collection_decode = program.coder.accounts.decode("Collection", collection_data!.data);
        // console.log('collection_decode', collection_decode)
        return {
            url: collection_decode.url,
            count: collection_decode.mintCount.toNumber(),
        }
    }
    const { url } = await getCollectionUrl(collection);
    console.log('URL TO POLL: ',`${url}/${token_id}/${buyer.toBase58()}`)


    
    // BEGIN MINTING THE NFT -- SEE NEXT SNIPPET FOR API ROUTE
    if(confirmation) {
      const _response = await fetch('/api/finalize', {
        method: 'POST',
        body: JSON.stringify({ 
          id: id, 
          collectionOwner: collectionOwner.toBase58(), 
          publicKey: publicKey?.toBase58(),
          placeholderMint: placeholder_mint
        }),
        headers: {
          'Content-Type': 'application/json',
        },
      })

      const { tx_signature, nft_mint } = await _response.json();

      console.log(`View NFT: https://solscan.io/token/${response.nft_mint}?cluster=${sdk.cluster}`)
      console.log(`View txn: https://explorer.solana.com/tx/${tx_signature}?cluster=${sdk.cluster}`)
    }
  } catch (error) {
    console.log('error', error)
  }
};

Finalize -- API ENDPOINT

// ex. response
nft tx {
  tx_signature: 'fcYTRbSpDRiu4HbvGPj2FXgdzZej3kPQc12HkZjBRV5VSfNbMBrfoeQYhMiH1Gss3HVy7V7vFz9cE2H5NrYNQ8c',
  nft_mint: 'FjM7C6Xb2pVaaBQ4jhcWeBj8H7ca99mpoMDbvpeuyA17'
}
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { Keypair, Connection, PublicKey } from '@solana/web3.js';
import base58, * as bs58 from "bs58";
import { SDK } from '@maweiche/react-sdk';

export async function POST(request: Request) {
  try{
    const body = await request.json();
    const id = body.id;
    const publicKey = new PublicKey(body.publicKey);
    const placeholderMint = new PublicKey(body.placeholderMint)
    const collectionOwner = new PublicKey(body.collectionOwner);

    const bearerToken = process.env.BEARER as string;

    const keypair = Keypair.fromSecretKey(base58.decode(YOUR_BS58_SECRET_KEY));
    const wallet = new NodeWallet(keypair);
    const connection = new Connection('https://api.devnet.solana.com/', 'confirmed')
    const sdk = new SDK(
      wallet,
      connection,
      { skipPreflight: true},
      "devnet",
    )

    const {tx_signature, nft_mint} = await sdk.nft.createNft(
      sdk.rpcConnection,  // rpc connection
      process.env.BEARER, // bearer token for ai image auth
      wallet.feePayer, // admin keypair
      collectionOwner, // collection owner publickey
      publicKey, // buyer publickey   
      placeholderMint // placeholder mint address as publickey
    ); // returns txn signature and nft mint address


    return new Response(
      JSON.stringify({
        tx_signature: tx_signature,
        nft_mint: nft_mint
      }), 
      { status: 200 }
    );
  } catch (error) {
    console.log('error', error)
    return new Response('error', { status: 500 });
  }
}

Airdrop Placeholder as Collection Owner

import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { Keypair, Connection, PublicKey, Transaction } from '@solana/web3.js';
import { SDK } from '@maweiche/react-sdk';
import base58, * as bs58 from "bs58";
import dotenv from 'dotenv';
dotenv.config()
export async function POST(request: Request) {
  const body = await request.json();
  const id = body.id;
  const collectionOwner = new PublicKey(body.collectionOwner);
  const publicKey = new PublicKey(body.publicKey);

  const program = this.sdk.program;
  const uri = "https://gateway.irys.xyz/-mpn67FnEePrsoKez4f6Dvjb1aMcH1CqCdZX0NCyHK8";
  const protocol = PublicKey.findProgramAddressSync([Buffer.from('protocol')], program.programId)[0];
  const collection = PublicKey.findProgramAddressSync([Buffer.from('collection'), collectionOwner.toBuffer()], program.programId)[0];

  const keypair = Keypair.fromSecretKey(base58.decode(YOUR_BS58_SECRET_KEY));
  const wallet = new NodeWallet(keypair);
  const sdk = new SDK(
    wallet,
    connection,
    { skipPreflight: true},
    "devnet",
  )

  // airdrop placeholder to buyer special instructions
    const ed25519Ix = Ed25519Program.createInstructionWithPrivateKey({
      privateKey: collection_wallet.secretKey,
      message: buyer.publicKey.toBuffer(),
    });

  const airdropPlaceholderIx = await program.methods
    .airdropPlaceholder()
    .accounts({
        buyer: buyer,
        payer: admin,
        collection,
        collectionOwner: collectionOwner,
        buyerMintAta: buyerPlaceholderAta,
        placeholder,
        mint: placeholder_mint,
        auth,
        associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
        tokenProgram: TOKEN_PROGRAM_ID,
        token2022Program: TOKEN_2022_PROGRAM_ID,
        protocol: protocol,
        systemProgram: SystemProgram.programId,
    })
    .instruction()

  const instructions = [ed25519Ix, airdropPlaceholderIx]

  return new Response(JSON.stringify({
    instructions: instructions
    token_id: id
  }), { status: 200 });
}

Claim Airdrop as User

When a user already has a Placeholder NFT, their mint process is different because we just need to finalize the mint process with the AI image generation. For this use case we will assume the user knows which collection they have received an Airdrop for and have selected it.

API ENDPOINT

import { SDK } from "../src";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import {
  PublicKey,
  Keypair,
  Connection,
  GetProgramAccountsFilter,
} from "@solana/web3.js";
import { TOKEN_2022_PROGRAM_ID, getTokenMetadata } from "@solana/spl-token";

export async function POST(request: Request) {
  const body = await request.json();
  const collection = body.collection;
  const buyer = new PublicKey(body.publicKey);

  sdk = new SDK(
      userWallet as NodeWallet,
      new Connection("https://api.devnet.solana.com", "confirmed"),
      { skipPreflight: true},
      "devnet",
  );

  const program = sdk.program;

  const filters:GetProgramAccountsFilter[] = [
    {
      dataSize: 170,    //size of account (bytes)
    },
    {
      memcmp: {
        offset: 32,     //location of our query in the account (bytes)
        bytes: buyer.toBase58(),  //our search criteria, a base58 encoded string
      },         
    }
  ];
  const accounts = await sdk.rpcConnection.getParsedProgramAccounts(
      TOKEN_2022_PROGRAM_ID, 
      {filters: filters}
  );

  const completedTxns = [];

  // THIS WILL MINT A NFT FOR EVERY PLACEHOLDER
  for( let i = 0; i < accounts.length; i++ ) {
    //Parse the account data
    const parsedAccountInfo:any = accounts[i].account.data;
    const mintAddress:string = parsedAccountInfo["parsed"]["info"]["mint"];
    const tokenBalance: number = parsedAccountInfo["parsed"]["info"]["tokenAmount"]["uiAmount"];

    const _token_metadata = await getTokenMetadata(sdk.rpcConnection, new PublicKey(mintAddress));

    if (_token_metadata!.additionalMetadata.length < 6  || tokenBalance == 0  ) {
      continue;
    }

    const collection_key = _token_metadata!.additionalMetadata[5][1]

    if(collection_key === collection) {

      const placeholder_mint = new PublicKey(mintAddress);
      const placeholder_metadata = await getTokenMetadata(connection, placeholder_mint);
      
      const additional_metadata = _token_metadata!.additionalMetadata;
      const token_id = additional_metadata[1][1];

      const getCollectionUrl = async(collection: PublicKey) => {
          const collection_data = await connection.getAccountInfo(collection);
          const collection_decode = program.coder.accounts.decode("Collection", collection_data!.data);

          return {
            url: collection_decode.url,
            count: collection_decode.mintCount.toNumber(),
            owner: collection_decode.owner,
          }
      }
      const { url, owner } = await getCollectionUrl(collection);
      console.log('URL TO POLL: ',`${url}/${token_id}/${buyer.toBase58()}`)
      
      const {tx_signature, nft_mint} = await sdk.nft.createNft(
        connection,  // connection: Connection,
        process.env.bearer, // bearer
        userKeypair, // admin
        collection_owner, // collection owner
        buyer, // buyer    
        placeholder_mint // placeholder mint address
      ); // returns txn signature and nft mint address


      console.log(`nft mint: ${nft_mint}`);

      console.log(`nft tx url: https://explorer.solana.com/tx/${tx_signature}?cluster=${sdk.cluster}`);

      const _tx_obj = {
        nft_mint: nft_mint
        signature: tx_signature
      }

      completedTxns.push(_tx_obj);
    }
  }

  return new Response(JSON.stringify({
    transactions: completedTxns
  }), { status: 200 });
}

FRONT-END

async function mintNft(){
  try {
    const id = Math.floor(Math.random() * 100000); //random number used to create seeds for buyers placeholder/nft

    // INITIATE MINTING TXN -- REQUIRES USER WALLET SIGNATURE
    const { transactions } : <nft_mint: string, signature: string>[] = await fetch('/api/claim', {
      method: 'POST',
      body: JSON.stringify({ 
        collection: collection.publicKey, 
        publicKey: buyer.publicKey?.toBase58()
      })
      headers: {
        'Content-Type': 'application/json',
      },
    });

    console.log(`View your NFT: https://solscan.io/address/${transactions[0].nft_mint}?cluster=${sdk.cluster}`)
    console.log(`View the mint txn: https://explorer.solana.com/tx/${transactions[0].signature}?cluster=${sdk.cluster}`)
  } catch (error) {
    console.log('error', error)
  }
};

Building a Blink Url API Route

To enable Blink Style URLS you need to set up 3 API Routes:

  • /api/actions.json
  • /api/blink
  • /api/blink/key
/api/actions.json

// This route just tells the Blink readers your routing

import { ACTIONS_CORS_HEADERS, ActionsJson } from "@solana/actions";

export const GET = async () => {
  const payload: ActionsJson = {
    rules: [
      {
        pathPattern: "/blink",
        apiPath: "/api/blink",
      },
    ],
  };

  return Response.json(payload, {
    headers: ACTIONS_CORS_HEADERS,
  });
};

export const OPTIONS = GET;
/api/blink

// Main two routes here are GET and OPTIONS

const MINT_AMOUNT_OPTIONS = [1];
const DEFAULT_MINT_AMOUNT_SOL = 1;


export async function GET( request: Request ) {
    try {
        console.log('route pinged')
        function getDonateInfo() {
            const icon = 'https://devnet.irys.xyz/9gdv51JL7p1dtf8Y79og8pJ5s_4tkKYvjMpzcE_moYU'; // IMAGE DISPLAYED IN BLINK
            const title = 'SolAI Test Blink';
            const description = 'Get a NFT from the SolAI Test Collection. All proceeds go to the SolAI Test Collection.';
            return { icon, title, description };
        }
        
        const { icon, title, description } = getDonateInfo();


        const response = {
            icon,
            label: `${DEFAULT_MINT_AMOUNT_SOL} SOL`,
            title,
            description,
            links: {
            actions: [
                ...MINT_AMOUNT_OPTIONS.map((amount) => ({
                label: `${amount} SOL`,
                href: `/api/blink/1`,
                })),
            ],
            },
        };

        console.log('response', response);
        const res = new Response(
            JSON.stringify(response), {
                status: 200,
                headers: {
                    'access-control-allow-origin': '*',
                    'content-type': 'application/json; charset=UTF-8'
                }
            }
        );
        console.log('res', res);
        return res
    } catch (e) {
        console.log(e);
        throw e;
    }
}

export async function OPTIONS( request: Request ) {
    return new Response(null, {
        headers: {
            'access-control-allow-origin': '*',
            'content-type': 'application/json; charset=UTF-8'
        }
    });
};
/api/blink/[key]

// This is the Post route that will generate the txn instructions that will begin the minting process

import { prepareTransaction } from '../../../../helpers/transaction-utils';
import {
    PublicKey,
    Keypair,
    Connection,
} from "@solana/web3.js";
import * as b58 from "bs58";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { SDK } from '@maweiche/react-sdk';

export async function POST( request: Request ) {
  try{
      const sdk = new SDK(
      userWallet as NodeWallet,
      new Connection("https://api.devnet.solana.com", "confirmed"),
      { skipPreflight: true},
      "devnet",
  );

  //  PROGRAM AND ADDRESSES
  const program = sdk.program;
  
  const collection_owner = admin2Wallet.publicKey;
  
  const id = Math.floor(Math.random() * 100000);
  const { account } = await request.json();
  const buyer = new PublicKey(account);
  console.log('buyer', buyer.toBase58());


  const placeholder_tx = await sdk.placeholder.createPlaceholder(
      sdk.rpcConnection,
      userKeypair.publicKey,
      collection_owner,
      buyer,
      id,
  );

  console.log('placeholder_tx', placeholder_tx);

  const _tx = await sdk.nft.createNft(
      sdk.rpcConnection,  // connection: Connection,
      process.env.BEARER!, // bearer
      userKeypair.publicKey, // admin
      collection_owner, // collection owner
      buyer, // buyer   
      id 
  ); 

      
  const instructions = [
      ...placeholder_tx.instructions, 
      ..._tx.instructions
  ];
  const transaction = await prepareTransaction(instructions, buyer);
  transaction.sign([admin2Keypair])

  const base64 = Buffer.from(transaction.serialize()).toString('base64');
  const response = {
      transaction: base64,
  };

  return new Response(JSON.stringify(response), {
      status: 200,
      headers: {
          'access-control-allow-origin': '*',
          'content-type': 'application/json; charset=UTF-8'
      }
  });
  
  } catch (e) {
      console.log(e);
      throw e;
  }
};

App

Check out the example app

Contributing

We welcome contributions to improve the SDK. Please raise an issue or submit a pull request with any suggestions or bug fixes.

License

The SolAI SDK is licensed under the GNU General Public License v3.0.