-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
code from this guide: https://www.quicknode.com/guides/quicknode-products/functions/create-functions-with-typescript
- Loading branch information
Showing
3 changed files
with
263 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}; | ||
} | ||
} |