Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
namespace NextGenSoftware.OASIS.API.Providers.SOLANAOASIS.Infrastructure.Services.Solana;

public sealed class SolanaService(Account oasisAccount, IRpcClient rpcClient) : ISolanaService
{
private const uint SellerFeeBasisPoints = 500;
private const byte CreatorShare = 100;
private const string Solana = "Solana";

private readonly List<Creator> _creators =
[
new(oasisAccount.PublicKey, share: CreatorShare, verified: true)
];


public async Task<OASISResult<MintNftResult>> MintNftAsync(MintNFTTransactionRequest mintNftRequest)
{
try
{
MetadataClient metadataClient = new(rpcClient);
Account mintAccount = new();

Metadata tokenMetadata = new()
{
name = mintNftRequest.Title,
symbol = mintNftRequest.Symbol,
sellerFeeBasisPoints = SellerFeeBasisPoints,
uri = mintNftRequest.JSONMetaDataURL,
creators = _creators
};

RequestResult<string> createNftResult = await metadataClient.CreateNFT(
ownerAccount: oasisAccount,
mintAccount: mintAccount,
TokenStandard.NonFungible,
tokenMetadata,
isMasterEdition: true,
isMutable: true);

if (!createNftResult.WasSuccessful)
{
bool isBalanceError =
createNftResult.ErrorData?.Error.Type is TransactionErrorType.InsufficientFundsForFee
or TransactionErrorType.InvalidRentPayingAccount;

bool isLamportError = createNftResult.ErrorData?.Logs?.Any(log =>
log.Contains("insufficient lamports", StringComparison.OrdinalIgnoreCase)) == true;

if (isBalanceError || isLamportError)
{
return HandleError<MintNftResult>(
$"{createNftResult.Reason}.\n Insufficient SOL to cover the transaction fee or rent.");
}

return HandleError<MintNftResult>(createNftResult.Reason);
}

// Log successful NFT creation
Console.WriteLine($"✅ Successfully minted NFT: {mintNftRequest.Title} (Address: {mintAccount.PublicKey.Key})");

return SuccessResult(
new(mintAccount.PublicKey.Key,
Solana,
createNftResult.Result));
}
catch (Exception ex)
{
return HandleError<MintNftResult>(ex.Message);
}
}

public async Task<OASISResult<SendTransactionResult>> SendTransaction(SendTransactionRequest sendTransactionRequest)
{
var response = new OASISResult<SendTransactionResult>();
try
{
(bool success, string res) = sendTransactionRequest.IsRequestValid();
if (!success)
{
response.Message = res;
response.IsError = true;
OASISErrorHandling.HandleError(ref response, res);
return response;
}

PublicKey fromAccount = new(sendTransactionRequest.FromAccount.PublicKey);
PublicKey toAccount = new(sendTransactionRequest.ToAccount.PublicKey);
RequestResult<ResponseValue<LatestBlockHash>> blockHash =
await rpcClient.GetLatestBlockHashAsync();

byte[] tx = new TransactionBuilder().SetRecentBlockHash(blockHash.Result.Value.Blockhash)
.SetFeePayer(fromAccount)
.AddInstruction(MemoProgram.NewMemo(fromAccount, sendTransactionRequest.MemoText))
.AddInstruction(SystemProgram.Transfer(fromAccount, toAccount, sendTransactionRequest.Lampposts))
.Build(oasisAccount);

RequestResult<string> sendTransactionResult = await rpcClient.SendTransactionAsync(tx);
if (!sendTransactionResult.WasSuccessful)
{
response.IsError = true;
response.Message = sendTransactionResult.Reason;
OASISErrorHandling.HandleError(ref response, response.Message);
return response;
}

response.Result = new SendTransactionResult(sendTransactionResult.Result);
}
catch (Exception e)
{
response.Exception = e;
response.Message = e.Message;
response.IsError = true;
OASISErrorHandling.HandleError(ref response, e.Message);
}

return response;
}

public async Task<OASISResult<GetNftResult>> LoadNftAsync(
string address)
{
OASISResult<GetNftResult> response = new();
try
{
PublicKey nftAccount = new(address);
MetadataAccount metadataAccount = await MetadataAccount.GetAccount(rpcClient, nftAccount);

response.IsError = false;
response.IsLoaded = true;
response.Result = new(metadataAccount);

// Log successful metadata retrieval
Console.WriteLine($"✅ Successfully loaded NFT metadata for {address}");
}
catch (ArgumentNullException)
{
// This is often expected during NFT creation process - metadata may not be immediately available
Console.WriteLine($"ℹ️ NFT metadata not yet available for {address} (this is normal during creation)");
response.IsError = true;
response.Message = "NFT metadata lookup failed - account may not exist yet or metadata not available";
OASISErrorHandling.HandleError(ref response, response.Message);
}
catch (NullReferenceException)
{
// This is often expected during NFT creation process - metadata may not be immediately available
Console.WriteLine($"ℹ️ NFT metadata not yet available for {address} (this is normal during creation)");
response.IsError = true;
response.Message = "NFT metadata lookup failed - account may not exist yet or metadata not available";
OASISErrorHandling.HandleError(ref response, response.Message);
}
catch (Exception e)
{
response.IsError = true;
response.Message = e.Message;
OASISErrorHandling.HandleError(ref response, e.Message);
}

return response;
}

public async Task<OASISResult<SendTransactionResult>> SendNftAsync(NFTWalletTransactionRequest mintNftRequest)
{
OASISResult<SendTransactionResult> response = new OASISResult<SendTransactionResult>();

try
{
RequestResult<ResponseValue<AccountInfo>> accountInfoResult = await rpcClient.GetAccountInfoAsync(
AssociatedTokenAccountProgram.DeriveAssociatedTokenAccount(
new PublicKey(mintNftRequest.ToWalletAddress),
new PublicKey(mintNftRequest.TokenAddress)));

bool needsCreateTokenAccount = false;

if (!accountInfoResult.WasSuccessful || accountInfoResult.Result == null ||
accountInfoResult.Result.Value == null)
{
needsCreateTokenAccount = true;
}
else
{
List<string> data = accountInfoResult.Result.Value.Data;
if (data == null || data.Count == 0)
{
needsCreateTokenAccount = true;
}
}

if (needsCreateTokenAccount)
{
RequestResult<ResponseValue<LatestBlockHash>> createAccountBlockHashResult =
await rpcClient.GetLatestBlockHashAsync();
if (!createAccountBlockHashResult.WasSuccessful)
{
return new OASISResult<SendTransactionResult>
{
IsError = true,
Message = "Failed to get latest block hash for account creation: " +
createAccountBlockHashResult.Reason
};
}

TransactionInstruction createAccountTransaction =
AssociatedTokenAccountProgram.CreateAssociatedTokenAccount(
new PublicKey(mintNftRequest.FromWalletAddress),
new PublicKey(mintNftRequest.ToWalletAddress),
new PublicKey(mintNftRequest.TokenAddress));

byte[] createAccountTxBytes = new TransactionBuilder()
.SetRecentBlockHash(createAccountBlockHashResult.Result.Value.Blockhash)
.SetFeePayer(new PublicKey(mintNftRequest.FromWalletAddress))
.AddInstruction(createAccountTransaction)
.Build(oasisAccount);

RequestResult<string> sendCreateAccountResult = await rpcClient.SendTransactionAsync(
createAccountTxBytes,
skipPreflight: false,
commitment: Commitment.Confirmed);

if (!sendCreateAccountResult.WasSuccessful)
{
return new OASISResult<SendTransactionResult>
{
IsError = true,
Message = "Token account creation failed (may already exist or insufficient funds): " + sendCreateAccountResult.Reason
};
}
}

RequestResult<ResponseValue<LatestBlockHash>> transferBlockHashResult =
await rpcClient.GetLatestBlockHashAsync();
if (!transferBlockHashResult.WasSuccessful)
{
return new OASISResult<SendTransactionResult>
{
IsError = true,
Message = "Failed to get latest block hash for transfer: " + transferBlockHashResult.Reason
};
}

TransactionInstruction transferTransaction = TokenProgram.Transfer(
AssociatedTokenAccountProgram.DeriveAssociatedTokenAccount(
new PublicKey(mintNftRequest.FromWalletAddress),
new PublicKey(mintNftRequest.TokenAddress)),
AssociatedTokenAccountProgram.DeriveAssociatedTokenAccount(
new PublicKey(mintNftRequest.ToWalletAddress),
new PublicKey(mintNftRequest.TokenAddress)),
(ulong)mintNftRequest.Amount,
new PublicKey(mintNftRequest.FromWalletAddress));

byte[] transferTxBytes = new TransactionBuilder()
.SetRecentBlockHash(transferBlockHashResult.Result.Value.Blockhash)
.SetFeePayer(new PublicKey(mintNftRequest.FromWalletAddress))
.AddInstruction(transferTransaction)
.Build(oasisAccount);

RequestResult<string> sendTransferResult = await rpcClient.SendTransactionAsync(
transferTxBytes,
skipPreflight: false,
commitment: Commitment.Confirmed);

if (!sendTransferResult.WasSuccessful)
{
response.IsError = true;
response.Message = sendTransferResult.Reason;
return response;
}

// Log successful NFT transfer
Console.WriteLine($"✅ Successfully transferred NFT to {mintNftRequest.ToWalletAddress} (Tx: {sendTransferResult.Result})");

response.IsError = false;
response.Result = new SendTransactionResult
{
TransactionHash = sendTransferResult.Result
};
}
catch (Exception ex)
{
response.IsError = true;
response.Message = ex.Message;
}

return response;
}


private OASISResult<MintNftResult> SuccessResult(MintNftResult result)
{
OASISResult<MintNftResult> response = new()
{
IsSaved = true,
IsError = false,
Result = result
};

return response;
}

private OASISResult<T> HandleError<T>(string message)
{
OASISResult<T> response = new()
{
IsError = true,
Message = message
};

OASISErrorHandling.HandleError(ref response, message);
return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ public static Dictionary<string, object> ManageMetaData(Dictionary<string, objec
break;
}

int editIndex = CLIEngine.GetValidInputForInt("Enter the number of the metadata entry to edit:", true, 1, metaData.Count);
int editIndex = CLIEngine.GetValidInputForInt($"Enter the number of the metadata entry to edit (1-{metaData.Count}):");
if (editIndex < 1 || editIndex > metaData.Count)
{
CLIEngine.ShowErrorMessage("Invalid index.");
break;
}
string editKey = metaData.Keys.ElementAt(editIndex - 1);
object currentValue = metaData[editKey];

Expand All @@ -152,7 +157,7 @@ public static Dictionary<string, object> ManageMetaData(Dictionary<string, objec
}
else
{
string newValue = CLIEngine.GetValidInput("Enter the new text value (or type 'clear' to remove):", addLineBefore: true);
string newValue = CLIEngine.GetValidInput("Enter the new text value (or type 'clear' to remove):");
if (newValue.ToLower() == "clear")
metaData.Remove(editKey);
else
Expand Down Expand Up @@ -185,13 +190,18 @@ public static Dictionary<string, object> ManageMetaData(Dictionary<string, objec
break;
}

int delIndex = CLIEngine.GetValidInputForInt("Enter the number of the metadata entry to delete:", true, 1, metaData.Count);
int delIndex = CLIEngine.GetValidInputForInt($"Enter the number of the metadata entry to delete (1-{metaData.Count}):");
if (delIndex < 1 || delIndex > metaData.Count)
{
CLIEngine.ShowErrorMessage("Invalid index.");
break;
}
string delKey = metaData.Keys.ElementAt(delIndex - 1);

if (CLIEngine.GetConfirmation($"Are you sure you want to delete metadata '{delKey}'?"))
{
metaData.Remove(delKey);
CLIEngine.ShowSuccessMessage($"Metadata '{delKey}' deleted.", addLineBefore: true);
CLIEngine.ShowSuccessMessage($"Metadata '{delKey}' deleted.", lineSpace: true);
}
else
Console.WriteLine("");
Expand Down
Loading