Skip to content

Commit

Permalink
add solana function example (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
amilz authored Dec 17, 2024
1 parent 439899d commit 7f5fb5c
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 0 deletions.
10 changes: 10 additions & 0 deletions solana/functions/wallet-portfolio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Solana Wallet Portfolio
A serverless function that allows you to create, update, and query a portfolio of multiple Solana wallets:

- [QuickNode Functions](https://www.quicknode.com/functions) - serverless processing adding, updating, and querying wallets
- [QuickNode Key-Value Store](https://www.quicknode.com/docs/key-value-store/getting-started) - persistent storage of wallet list
- [Solana Web3.js v1.x](https://github.com/solana-labs/solana-web3.js) - can be installed with `npm i @solana/web3.js@1`

_Code explained in this [Guide](https://www.quicknode.com/guides/quicknode-products/functions/create-functions-with-typescript)_

Project is written in TypeScript (./example.ts) and the compiled JavaScript version (./example.js) is used in the QuickNode Function.
107 changes: 107 additions & 0 deletions solana/functions/wallet-portfolio/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.main = void 0;
const web3_js_1 = require("@solana/web3.js");
const ENDPOINT = 'https://example.solana-mainnet.quiknode.pro/123/'; // 👈 Replace with your endpoint
// Helper functions
function isValidSolanaAddress(address) {
try {
new web3_js_1.PublicKey(address);
return true;
}
catch (error) {
return false;
}
}
function validateInput(params) {
const validInstructions = ['createPortfolio', 'updatePortfolio', 'getPortfolio', 'getPortfolioBalances'];
if (!validInstructions.includes(params.user_data.instruction)) {
throw new Error(`Invalid instruction: ${params.user_data.instruction}. Must be one of: ${validInstructions.join(', ')}`);
}
if (!params.user_data.portfolioName) {
throw new Error('Portfolio name is required');
}
if (params.user_data.instruction === 'updatePortfolio') {
if (!params.user_data.addAddresses && !params.user_data.removeAddresses) {
throw new Error('At least one of addAddresses or removeAddresses is required for updatePortfolio instruction');
}
if (params.user_data.addAddresses) {
const invalidAddAddresses = params.user_data.addAddresses.filter(addr => !isValidSolanaAddress(addr));
if (invalidAddAddresses.length > 0) {
throw new Error(`Invalid Solana addresses: ${invalidAddAddresses.join(', ')}`);
}
}
}
}
// Instruction handlers
async function createPortfolio(portfolioName) {
await qnLib.qnUpsertList(portfolioName, { add_items: [] });
return {
message: `Portfolio ${portfolioName} created successfully.`,
portfolioName
};
}
async function updatePortfolio(portfolioName, addAddresses = [], removeAddresses = []) {
await qnLib.qnUpsertList(portfolioName, { add_items: addAddresses, remove_items: removeAddresses });
const updatedPortfolio = await qnLib.qnGetList(portfolioName);
return {
message: `Updated portfolio ${portfolioName}. Added ${addAddresses.length} addresses, removed ${removeAddresses.length} addresses.`,
portfolioName,
addresses: updatedPortfolio
};
}
async function getPortfolio(portfolioName) {
const addresses = await qnLib.qnGetList(portfolioName);
return {
message: `Retrieved portfolio ${portfolioName}.`,
portfolioName,
addresses
};
}
async function getPortfolioBalances(portfolioName) {
const addresses = await qnLib.qnGetList(portfolioName);
// @ts-ignore - Already validated in validateInput
const connection = new web3_js_1.Connection(ENDPOINT);
const balances = await Promise.all(addresses.map(async (address) => {
const publicKey = new web3_js_1.PublicKey(address);
const balance = await connection.getBalance(publicKey);
return {
address,
balance: balance / web3_js_1.LAMPORTS_PER_SOL
};
}));
return {
message: `Retrieved balances for portfolio ${portfolioName}.`,
portfolioName,
balances
};
}
// Main function
async function main(params) {
try {
console.log('Received params:', JSON.stringify(params));
validateInput(params);
const { instruction, portfolioName, addAddresses, removeAddresses } = params.user_data;
switch (instruction) {
case 'createPortfolio':
return await createPortfolio(portfolioName);
case 'updatePortfolio':
return await updatePortfolio(portfolioName, addAddresses, removeAddresses);
case 'getPortfolio':
return await getPortfolio(portfolioName);
case 'getPortfolioBalances':
return await getPortfolioBalances(portfolioName);
default:
throw new Error('Invalid instruction');
}
}
catch (error) {
console.error('Error:', error);
return {
message: 'An error occurred',
portfolioName: params.user_data.portfolioName,
error: error instanceof Error ? error.message : String(error)
};
}
}
exports.main = main;
146 changes: 146 additions & 0 deletions solana/functions/wallet-portfolio/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Connection, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; // (v.1.X - npm i @solana/web3.js@1)

interface QNLib {
qnUpsertList: (key: string, options: { add_items?: string[], remove_items?: string[] }) => Promise<any>;
qnGetList: (key: string) => Promise<string[]>;
}
declare const qnLib: QNLib;
const ENDPOINT = 'https://example.solana-mainnet.quiknode.pro/123/'; // 👈 Replace with your endpoint

// Types & Interfaces
type Instruction = 'createPortfolio' | 'updatePortfolio' | 'getPortfolio' | 'getPortfolioBalances';

interface UserData {
instruction: Instruction;
portfolioName: string;
addAddresses?: string[];
removeAddresses?: string[];
}

interface FunctionParams {
user_data: UserData;
}

interface FunctionResult {
message: string;
portfolioName: string;
addresses?: string[];
balances?: { address: string; balance: number }[];
error?: string;
}

// Helper functions
function isValidSolanaAddress(address: string): boolean {
try {
new PublicKey(address);
return true;
} catch (error) {
return false;
}
}

function validateInput(params: FunctionParams): void {
const validInstructions: Instruction[] = ['createPortfolio', 'updatePortfolio', 'getPortfolio', 'getPortfolioBalances'];
if (!validInstructions.includes(params.user_data.instruction)) {
throw new Error(`Invalid instruction: ${params.user_data.instruction}. Must be one of: ${validInstructions.join(', ')}`);
}

if (!params.user_data.portfolioName) {
throw new Error('Portfolio name is required');
}

if (params.user_data.instruction === 'updatePortfolio') {
if (!params.user_data.addAddresses && !params.user_data.removeAddresses) {
throw new Error('At least one of addAddresses or removeAddresses is required for updatePortfolio instruction');
}

if (params.user_data.addAddresses) {
const invalidAddAddresses = params.user_data.addAddresses.filter(addr => !isValidSolanaAddress(addr));
if (invalidAddAddresses.length > 0) {
throw new Error(`Invalid Solana addresses: ${invalidAddAddresses.join(', ')}`);
}
}
}
}

// Instruction handlers
async function createPortfolio(portfolioName: string): Promise<FunctionResult> {
await qnLib.qnUpsertList(portfolioName, { add_items: [] });
return {
message: `Portfolio ${portfolioName} created successfully.`,
portfolioName
};
}

async function updatePortfolio(portfolioName: string, addAddresses: string[] = [], removeAddresses: string[] = []): Promise<FunctionResult> {
await qnLib.qnUpsertList(portfolioName, { add_items: addAddresses, remove_items: removeAddresses });
const updatedPortfolio = await qnLib.qnGetList(portfolioName);
return {
message: `Updated portfolio ${portfolioName}. Added ${addAddresses.length} addresses, removed ${removeAddresses.length} addresses.`,
portfolioName,
addresses: updatedPortfolio
};
}

async function getPortfolio(portfolioName: string): Promise<FunctionResult> {
const addresses = await qnLib.qnGetList(portfolioName);
return {
message: `Retrieved portfolio ${portfolioName}.`,
portfolioName,
addresses
};
}

async function getPortfolioBalances(portfolioName: string): Promise<FunctionResult> {
const addresses = await qnLib.qnGetList(portfolioName);

// @ts-ignore - Already validated in validateInput
const connection = new Connection(ENDPOINT);

const balances = await Promise.all(
addresses.map(async (address) => {
const publicKey = new PublicKey(address);
const balance = await connection.getBalance(publicKey);
return {
address,
balance: balance / LAMPORTS_PER_SOL
};
})
);

return {
message: `Retrieved balances for portfolio ${portfolioName}.`,
portfolioName,
balances
};
}

// Main function
export async function main(params: FunctionParams): Promise<FunctionResult> {
try {
console.log('Received params:', JSON.stringify(params));
validateInput(params);

const { instruction, portfolioName, addAddresses, removeAddresses } = params.user_data;

switch (instruction) {
case 'createPortfolio':
return await createPortfolio(portfolioName);
case 'updatePortfolio':
return await updatePortfolio(portfolioName, addAddresses, removeAddresses);
case 'getPortfolio':
return await getPortfolio(portfolioName);
case 'getPortfolioBalances':
return await getPortfolioBalances(portfolioName);
default:
throw new Error('Invalid instruction');
}
} catch (error) {
console.error('Error:', error);
return {
message: 'An error occurred',
portfolioName: params.user_data.portfolioName,
error: error instanceof Error ? error.message : String(error)
};
}
}

0 comments on commit 7f5fb5c

Please sign in to comment.