Files
managing-apps/src/Managing.Web3Proxy/src/plugins/custom/privy.ts

1080 lines
33 KiB
TypeScript

import fp from 'fastify-plugin'
import {FastifyInstance, FastifyReply, FastifyRequest} from 'fastify'
import {z} from 'zod'
import canonicalize from 'canonicalize'
import crypto from 'crypto'
import {PrivyClient} from '@privy-io/server-auth'
import {ethers} from 'ethers'
import dotenv from 'dotenv'
import Token from '../../generated/gmxsdk/abis/Token.json' with {type: 'json'}
import {ARBITRUM} from '../../generated/gmxsdk/configs/chains.js'
import {TOKENS} from '../../generated/gmxsdk/configs/tokens.js'
import {CONTRACTS} from '../../generated/gmxsdk/configs/contracts.js'
import {getClientForAddress, getTokenDataFromTicker} from './gmx.js'
import {Balance, Chain, Ticker} from '../../generated/ManagingApiTypes.js'
import {Address} from 'viem'
// Load environment variables
dotenv.config()
/**
* Privy Plugin
*
* This plugin adds functionality for interacting with the Privy API,
* including signing messages and token approvals.
*
* Token Approval Process:
* 1. Client sends a request to /approve-token with:
* - walletId: The user's wallet ID
* - address: The user's wallet address
* - ticker: The token ticker or enum value (as string)
* - amount: The amount to approve (optional, defaults to max amount)
* - chainId: The chain ID where the approval will take place
*
* 2. The server handles the request by:
* - Validating the input parameters
* - Getting the token data based on the ticker
* - Creating and sending an ERC20 approval transaction using ethers.js and Privy
* - Returning the transaction hash to the client
*/
declare module 'fastify' {
export interface FastifyRequest {
signPrivyMessage: typeof signPrivyMessage;
approveToken: typeof approveToken;
initAddress: typeof initAddress;
sendToken: typeof sendToken;
getWalletBalance: typeof getWalletBalance;
}
}
/**
* Returns an initialized PrivyClient instance with configuration from environment variables
* @returns The configured PrivyClient instance
*/
export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
const appId = fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID;
const appSecret = fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET;
const authKey = fastify?.config?.PRIVY_AUTHORIZATION_KEY || process.env.PRIVY_AUTHORIZATION_KEY;
if (!appId || !appSecret || !authKey) {
console.error('Missing Privy environment variables:');
console.error('PRIVY_APP_ID:', appId ? 'present' : 'missing');
console.error('PRIVY_APP_SECRET:', appSecret ? 'present' : 'missing');
console.error('PRIVY_AUTHORIZATION_KEY:', authKey ? 'present' : 'missing');
throw new Error('Missing required Privy environment variables');
}
console.log('appId', appId)
console.log('appSecret', appSecret)
console.log('authKey', authKey)
return new PrivyClient(
appId,
appSecret,
{
walletApi: {
authorizationPrivateKey: authKey
}
},
);
};
/**
* Authentication function for Privy API calls
* @param url The URL for the API endpoint
* @param body The request body
* @returns The authorization signature
*/
export function getAuthorizationSignature({url, body}: {url: string; body: object}) {
const payload = {
version: 1,
method: 'POST',
url,
body,
headers: {
'privy-app-id': process.env.PRIVY_APP_ID
}
};
// JSON-canonicalize the payload and convert it to a buffer
// @ts-ignore - canonicalize is a callable function despite TypeScript error
const serializedPayload = canonicalize(payload) as string;
const serializedPayloadBuffer = Buffer.from(serializedPayload);
// Get the authorization key from environment variables
const privateKeyAsString = (process.env.PRIVY_AUTHORIZATION_KEY ?? "").replace('wallet-auth:', '');
// Convert private key to PEM format and create a key object
const privateKeyAsPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyAsString}\n-----END PRIVATE KEY-----`;
const privateKey = crypto.createPrivateKey({
key: privateKeyAsPem,
format: 'pem',
});
// Sign the payload buffer with the private key
const signatureBuffer = crypto.sign('sha256', serializedPayloadBuffer, privateKey);
const signature = signatureBuffer.toString('base64');
return signature;
}
/**
* Makes a request to the Privy API with proper authentication and headers.
* @param url - The full URL for the API endpoint.
* @param body - The request body (optional for GET requests).
* @param requiresAuth - Whether the request requires authentication.
* @param method - The HTTP method (defaults to POST).
* @returns The response data.
*/
export const makePrivyRequest = async <T>(
url: string,
body: object = {},
requiresAuth = true,
method: 'GET' | 'POST' = 'POST'
): Promise<T> => {
try {
let headers: Record<string, string> = {
'Content-Type': 'application/json',
'privy-app-id': process.env.PRIVY_APP_ID ?? "",
};
if (requiresAuth) {
// Create Basic Authentication header
const appId = process.env.PRIVY_APP_ID ?? "";
const appSecret = process.env.PRIVY_APP_SECRET ?? "";
const basicAuthString = `${appId}:${appSecret}`;
const base64Auth = Buffer.from(basicAuthString).toString('base64');
headers.Authorization = `Basic ${base64Auth}`;
}
const requestInit: RequestInit = {
method: method,
headers,
};
// Only add body for non-GET requests
if (method !== 'GET') {
requestInit.body = JSON.stringify(body);
}
console.log('url', url)
console.log('requestInit', requestInit)
// Make the API request
const response = await fetch(url, requestInit).then(res => {
return res;
}).catch(err => {
console.log("error", err);
throw err;
});
if (!response.ok) {
throw new Error(`Privy API request failed: ${response.status}`);
}
return await response.json() as T;
} catch (error) {
console.error('Error making Privy API request:', error);
throw new Error('Failed to make Privy API request');
}
};
// Schema for sign-message request
const signMessageSchema = z.object({
walletId: z.string().nonempty(),
message: z.string().nonempty(),
address: z.string().nonempty()
});
// Schema for token-approval request
const tokenApprovalSchema = z.object({
walletId: z.string().nonempty(),
address: z.string().nonempty(),
ticker: z.string().nonempty(),
amount: z.bigint().positive().optional(),
chainId: z.number().positive().optional()
});
// Schema for token-sending request
const tokenSendSchema = z.object({
senderAddress: z.string().nonempty(),
recipientAddress: z.string().nonempty(),
ticker: z.string().nonempty(),
amount: z.bigint().positive(),
chainId: z.number().positive().optional()
});
// Schema for wallet balance request
const walletBalanceSchema = z.object({
address: z.string().nonempty(),
assets: z.array(z.nativeEnum(Ticker)).min(1), // Required: array of valid Ticker enum values
chains: z.array(z.string().nonempty()).min(1) // Required: array of chain names
});
/**
* Gets the chain name based on chain ID
* @param chainId The chain ID
* @returns The CAIP-2 identifier for the chain
*/
export const getChainName = (chainId: number): string => {
switch (chainId) {
case 1:
return 'eip155:1'; // Ethereum Mainnet
case 42161:
return 'eip155:42161'; // Arbitrum One
case 421613:
return 'eip155:421613'; // Arbitrum Goerli
case 8453:
return 'eip155:8453'; // Base
case 84531:
return 'eip155:84531'; // Base Goerli
default:
return `eip155:${chainId}`;
}
};
/**
* Signs a message using a Privy embedded wallet.
* @param walletId - The ID of the wallet to use for signing.
* @param message - The message to sign.
* @param address - The wallet address to use for signing.
* @returns The signature for the message.
*/
export const signMessage = async (walletId: string, message: string, address: string): Promise<string> => {
try {
const privy = getPrivyClient();
const {signature} = await privy.walletApi.ethereum.signMessage({
address: address,
chainType: 'ethereum',
message: message,
});
return signature;
} catch (error) {
console.error('Error signing message:', error);
throw new Error('Failed to sign message with embedded wallet');
}
};
/**
* Approves a token for spending using Privy wallet (implementation)
* @param walletId The wallet ID to use
* @param walletAddress The wallet address
* @param ticker The token ticker or enum value
* @param amount The amount to approve (optional, defaults to max amount)
* @param chainId The chain ID
* @param spenderAddress The address that will be allowed to spend the tokens
* @returns The transaction hash
*/
export const approveTokenImpl = async (
walletAddress: string,
ticker: string,
chainId?: number,
amount?: bigint,
): Promise<string> => {
try {
// Get token data from ticker
const tokenData = GetToken(ticker);
// Create contract interface for ERC20 token
const contractInterface = new ethers.Interface(Token.abi);
// Max uint256 value for unlimited approval
const approveAmount = amount ?
ethers.parseUnits(amount.toString(), tokenData.decimals) :
ethers.MaxUint256;
// Encode the approve function call
const data = contractInterface.encodeFunctionData("approve", [walletAddress, approveAmount]);
chainId = chainId ?? ARBITRUM;
// Get chain name in CAIP-2 format
const networkName = getChainName(chainId);
const privy = getPrivyClient();
// Send the transaction
const { hash } = await privy.walletApi.ethereum.sendTransaction({
address: walletAddress as Address,
chainType: 'ethereum',
caip2: networkName as string,
transaction: {
to: tokenData.address as Address,
data: data,
chainId: chainId,
},
} as any);
return hash;
} catch (error) {
console.error('Error approving token:', error);
throw new Error(`Failed to approve token: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
function GetToken(symbol: string) {
return TOKENS[ARBITRUM].find(token => token.symbol === symbol)
}
/**
* Approves a token for spending using Privy wallet
* @param this The FastifyRequest instance
* @param reply The FastifyReply instance
* @param address The wallet address
* @param ticker The token ticker or enum value
* @param chainId The chain ID
* @param amount The amount to approve (optional, defaults to max amount)
* @returns The response object with success status and transaction hash
*/
export async function approveToken(
this: FastifyRequest,
reply: FastifyReply,
address: string,
ticker: string,
chainId: number,
amount?: bigint
) {
try {
// Validate the request parameters
tokenApprovalSchema.parse({
address,
ticker,
amount,
chainId
});
if (!address) {
throw new Error('Wallet address is required for token approval');
}
if (!chainId) {
throw new Error('Chain ID is required for token approval');
}
// Call the approveTokenImpl function
const hash = await approveTokenImpl(address, ticker, chainId, amount);
return {
success: true,
hash: hash
};
} catch (error) {
this.log.error(error);
// Return appropriate error response
reply.status(error instanceof z.ZodError ? 400 : 500);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
}
/**
* Signs a message using Privy embedded wallet
* @param this The FastifyRequest instance
* @param reply The FastifyReply instance
* @param walletId The wallet ID
* @param message The message to sign
* @param address The wallet address
* @returns The signature of the signed message
*/
export async function signPrivyMessage(
this: FastifyRequest,
reply: FastifyReply,
walletId: string,
message: string,
address: string
) {
try {
// Validate the request parameters
signMessageSchema.parse({ walletId, message, address });
// Call the signMessage function
const signature = await signMessage(walletId, message, address);
return {
success: true,
signature: signature
};
} catch (error) {
this.log.error(error);
// Return appropriate error response
reply.status(error instanceof z.ZodError ? 400 : 500);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
}
/**
* Approves a specific contract address for spending using Privy wallet
* @param walletAddress The wallet address
* @param tokenAddress The token address to approve
* @param spenderAddress The contract address to approve
* @param chainId The chain ID
* @param amount The amount to approve (optional, defaults to max amount)
* @returns The transaction hash
*/
export const approveContractImpl = async (
walletAddress: string,
tokenAddress: string,
spenderAddress: string,
chainId?: number,
amount?: bigint,
): Promise<string> => {
try {
// Create contract interface for ERC20 token
const contractInterface = new ethers.Interface(Token.abi);
// Max uint256 value for unlimited approval
const approveAmount = amount ?? ethers.MaxUint256;
// Encode the approve function call
const data = contractInterface.encodeFunctionData("approve", [spenderAddress, approveAmount]);
chainId = chainId ?? ARBITRUM;
// Get chain name in CAIP-2 format
const networkName = getChainName(chainId);
const privy = getPrivyClient();
// Send the transaction
const { hash } = await privy.walletApi.ethereum.sendTransaction({
address: walletAddress as Address,
chainType: 'ethereum',
caip2: networkName as string,
transaction: {
to: tokenAddress as Address,
data: data,
chainId: chainId,
},
} as any);
return hash;
} catch (error) {
console.error('Error approving contract:', error);
throw new Error(`Failed to approve contract: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Gets the current allowance for a token for the GMX OrderVault contract
* @param ownerAddress The address of the token owner
* @param tokenAddress The address of the token contract
* @returns The current allowance as a BigNumber
*/
export const getTokenAllowance = async (
ownerAddress: string,
tokenAddress: string,
spenderAddress: string
): Promise<bigint> => {
try {
// use gmx sdk to get allowance
const sdk = await getClientForAddress(ownerAddress);
const allowance = await sdk.executeMulticall({
token: {
contractAddress: tokenAddress,
abiId: "ERC20",
calls: {
allowance: {
methodName: "allowance",
params: [ownerAddress, spenderAddress]
}
}
}
}).
then(res => {
return res.data.token.allowance.returnValues[0];
});
return allowance;
} catch (error) {
console.error('Error getting token allowance:', error);
throw new Error(`Failed to get token allowance: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Initializes a wallet address by approving USDC for GMX trading and GMX OrderVault
* @param address The wallet address to initialize
* @returns The transaction hashes for both approvals
*/
export const initAddressImpl = async (
address: string,
): Promise<{ usdcHash: string, orderVaultHash: string, exchangeRouterHash: string }> => {
try {
const sdk = await getClientForAddress(address);
const {tokensData} = await sdk.tokens.getTokensData();
const usdcTokenData = getTokenDataFromTicker(Ticker.USDC, tokensData);
const wrapperEtherData = getTokenDataFromTicker("WETH", tokensData);
let approveAmount = usdcTokenData.prices.maxPrice; // Large enough amount for trading
// Check approval for USDC
const usdcToken = GetToken('USDC');
const usdcAllowance = await getTokenAllowance(address, usdcToken.address, address);
let usdcHash = "";
if (usdcAllowance < approveAmount) {
// First approve USDC token for GMX trading
const usdcToken = GetToken('USDC');
usdcHash = await approveTokenImpl(
address,
usdcToken.symbol,
ARBITRUM,
usdcTokenData.prices.maxPrice
);
}else{
usdcHash = "Already allowed :" + usdcAllowance;
}
const orderVaultAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].OrderVault);
// Then approve GMX OrderVault with the correct amount
let orderVaultHash = "";
if (orderVaultAllowance < approveAmount) {
orderVaultHash = await approveContractImpl(
address,
usdcToken.address,
CONTRACTS[ARBITRUM].OrderVault,
ARBITRUM,
approveAmount
);
}else{
orderVaultHash = "Already allowed :" + orderVaultAllowance;
}
const wrapperEtherAllowance = await getTokenAllowance(address, wrapperEtherData.address, CONTRACTS[ARBITRUM].OrderVault);
let wrapperEtherHash = "";
if (wrapperEtherAllowance < approveAmount) {
wrapperEtherHash = await approveContractImpl(
address,
wrapperEtherData.address,
CONTRACTS[ARBITRUM].OrderVault,
ARBITRUM,
approveAmount
);
}else{
wrapperEtherHash = "Already allowed :" + wrapperEtherAllowance;
}
console.log('wrapperEtherAllowance', wrapperEtherAllowance)
const exchangeRouterAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].ExchangeRouter);
console.log('exchangeRouterAllowance', exchangeRouterAllowance)
let exchangeRouterHash = "";
if (exchangeRouterAllowance < approveAmount) {
exchangeRouterHash = await approveContractImpl(
address,
usdcToken.address,
CONTRACTS[ARBITRUM].ExchangeRouter,
ARBITRUM,
approveAmount
);
}else{
exchangeRouterHash = "Already allowed :" + exchangeRouterAllowance;
}
const wrapperEtherExchangeAllowance = await getTokenAllowance(address, wrapperEtherData.address, CONTRACTS[ARBITRUM].ExchangeRouter);
let wrapperEtherExchangeHash = "";
if (wrapperEtherExchangeAllowance < approveAmount) {
wrapperEtherExchangeHash = await approveContractImpl(
address,
wrapperEtherData.address,
CONTRACTS[ARBITRUM].ExchangeRouter,
ARBITRUM,
approveAmount
);
}else{
wrapperEtherExchangeHash = "Already allowed :" + wrapperEtherExchangeAllowance;
}
console.log('wrapperEtherExchangeAllowance', wrapperEtherExchangeAllowance)
const usdcSyntheticRouterAllowance = await getTokenAllowance(address, usdcToken.address, CONTRACTS[ARBITRUM].SyntheticsRouter);
let usdcSyntheticRouterHash = "";
if (usdcSyntheticRouterAllowance < approveAmount) {
usdcSyntheticRouterHash = await approveContractImpl(
address,
usdcToken.address,
CONTRACTS[ARBITRUM].SyntheticsRouter,
ARBITRUM,
approveAmount
);
}else{
usdcSyntheticRouterHash = "Already allowed :" + usdcSyntheticRouterAllowance;
}
console.log('usdcSyntheticRouterAllowance', usdcSyntheticRouterAllowance)
console.log('usdcHash', usdcHash)
console.log('orderVaultHash', orderVaultHash)
console.log('exchangeRouterHash', exchangeRouterHash)
return {
usdcHash,
orderVaultHash,
exchangeRouterHash
};
} catch (error) {
console.error('Error initializing address:', error);
throw new Error(`Failed to initialize address: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Initializes a wallet address for GMX trading
* @param this The FastifyRequest instance
* @param reply The FastifyReply instance
* @param address The wallet address to initialize
* @returns The response object with success status and transaction hashes
*/
export async function initAddress(
this: FastifyRequest,
reply: FastifyReply,
address: string
) {
try {
if (!address) {
throw new Error('Wallet address is required for initialization');
}
const { usdcHash, orderVaultHash } = await initAddressImpl(address);
return {
success: true,
usdcHash,
orderVaultHash
};
} catch (error) {
this.log.error(error);
reply.status(500);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
}
/**
* Sends tokens from one address to another using Privy wallet (implementation)
* @param senderAddress The sender's wallet address
* @param recipientAddress The recipient's wallet address
* @param ticker The token ticker or enum value
* @param amount The amount to send
* @param chainId The chain ID (optional, defaults to ARBITRUM)
* @returns The transaction hash
*/
export const sendTokenImpl = async (
senderAddress: string,
recipientAddress: string,
ticker: string,
amount: bigint,
chainId?: number,
): Promise<string> => {
try {
chainId = chainId ?? ARBITRUM;
const networkName = getChainName(chainId);
const privy = getPrivyClient();
if (ticker === 'ETH') {
// Native ETH transfer: no allowance, no data, value is amount as hex string
const { hash } = await privy.walletApi.ethereum.sendTransaction({
address: senderAddress as Address,
chainType: 'ethereum',
caip2: networkName as string,
transaction: {
to: recipientAddress as Address,
value: '0x' + amount.toString(16), // value in wei as hex string
chainId: chainId,
},
} as any);
return hash;
}
// ERC20 logic
// Get token data from ticker
const tokenData = GetToken(ticker);
if (!tokenData) {
throw new Error(`Token not found: ${ticker}`);
}
// Check if sender has sufficient allowance for the token transfer
const senderAllowance = await getTokenAllowance(senderAddress, tokenData.address, senderAddress);
// If insufficient allowance, approve the token first
if (senderAllowance < amount) {
console.log(`Insufficient allowance (${senderAllowance}). Approving token for amount: ${amount}`);
await approveContractImpl(
senderAddress,
tokenData.address,
senderAddress, // Approve self to spend tokens
chainId,
amount
);
console.log('Token approval completed');
}
// Create contract interface for ERC20 token
const contractInterface = new ethers.Interface(Token.abi);
// Amount is already in the smallest units (wei), so we don't need to convert it
const transferAmount = amount;
// Encode the transfer function call
const data = contractInterface.encodeFunctionData("transfer", [recipientAddress, transferAmount]);
// Send the transaction
const { hash } = await privy.walletApi.ethereum.sendTransaction({
address: senderAddress as Address,
chainType: 'ethereum',
caip2: networkName as string,
transaction: {
to: tokenData.address as Address,
data: data,
chainId: chainId,
},
} as any);
return hash;
} catch (error) {
console.error('Error sending token:', error);
throw new Error(`Failed to send token: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Gets the wallet balance for a specific address using Privy API
* @param address The wallet address to get balance for
* @param assets Array of assets to filter by using Ticker enum (e.g. [Ticker.USDC, Ticker.ETH])
* @param chains Array of chain names to filter by (e.g. ['arbitrum', 'ethereum'])
* @returns Array of Balance objects
*/
export const getWalletBalanceImpl = async (
address: string,
assets: Ticker[],
chains: string[]
): Promise<Balance[]> => {
try {
// First, get the user by wallet address using Privy Client
const privy = getPrivyClient();
// Get user by wallet address
const user = await privy.getUserByWalletAddress(address);
if (!user) {
throw new Error(`User not found for wallet address: ${address}`);
}
// Find the embedded wallet for this address
const embeddedWallet = user.linkedAccounts.find(
(account: any) =>
account.type === 'wallet' &&
account.chainType === 'ethereum' &&
account.address?.toLowerCase() === address.toLowerCase()
);
if (!embeddedWallet) {
throw new Error(`Embedded wallet not found for address: ${address}`);
}
// Extract wallet ID from the embedded wallet
const walletId = (embeddedWallet as any).walletId || (embeddedWallet as any).id;
if (!walletId) {
throw new Error(`Wallet ID not found for embedded wallet`);
}
// Make authenticated request to get wallet balance
let balanceUrl = `https://auth.privy.io/api/v1/wallets/${walletId}/balance`;
// Add required query parameters for multiple assets and chains
const queryParams = new URLSearchParams();
// Add multiple asset parameters
assets.forEach(asset => {
queryParams.append('asset', asset.toLowerCase()); // Convert Ticker enum to lowercase
});
// Add multiple chain parameters
chains.forEach(chain => {
queryParams.append('chain', chain);
});
// Add include_currency parameter for USD pricing
queryParams.append('include_currency', 'usd');
balanceUrl += `?${queryParams.toString()}`;
const balanceResponse = await makePrivyRequest<{
balances: Array<{
chain: string;
asset: string;
raw_value: string;
raw_value_decimals: number;
display_values: {
[key: string]: string;
usd: string;
};
}>;
}>(balanceUrl, {}, true, 'GET');
console.log('balanceResponse', balanceResponse)
// Convert to Balance interface format (matching ManagingApiTypes.ts)
const balances: Balance[] = balanceResponse.balances
.map(balance => {
// Parse the raw value using decimals
const amount = parseFloat(balance.raw_value) / Math.pow(10, balance.raw_value_decimals);
// Get USD price from display_values
const usdValue = parseFloat(balance.display_values.usd);
// Calculate price per token (if amount > 0)
const price = amount > 0 ? usdValue / amount : 0;
// Get chain ID from chain name
const chainId = getChainIdFromName(balance.chain);
const caip2 = getChainName(chainId);
const chain: Chain = {
id: caip2,
rpcUrl: undefined,
name: getChainNameFromCaip2(caip2),
chainId: chainId
};
return {
tokenImage: '', // Not provided in the response
tokenName: balance.asset.toUpperCase(), // Use asset name (usdc -> USDC)
amount: amount,
price: price,
value: usdValue,
tokenAdress: '', // Not provided in this response format
chain: chain
};
});
console.log('balances', balances)
return balances;
} catch (error) {
console.error('Error getting wallet balance:', error);
throw new Error(`Failed to get wallet balance: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Helper function to get chain name from CAIP-2 identifier
* @param caip2 The CAIP-2 identifier
* @returns Human readable chain name
*/
const getChainNameFromCaip2 = (caip2: string): string => {
switch (caip2) {
case 'eip155:1':
return 'Ethereum Mainnet';
case 'eip155:42161':
return 'Arbitrum One';
case 'eip155:421613':
return 'Arbitrum Goerli';
case 'eip155:8453':
return 'Base';
case 'eip155:84531':
return 'Base Goerli';
default:
return `Chain ${caip2}`;
}
};
/**
* Helper function to get chain ID from chain name
* @param chainName The chain name (e.g., 'arbitrum', 'ethereum', 'base')
* @returns The corresponding chain ID
*/
const getChainIdFromName = (chainName: string): number => {
switch (chainName.toLowerCase()) {
case 'ethereum':
case 'mainnet':
return 1;
case 'arbitrum':
case 'arbitrum-one':
return 42161;
case 'arbitrum-goerli':
return 421613;
case 'base':
return 8453;
case 'base-goerli':
return 84531;
default:
// Default to Arbitrum if unknown chain
return 42161;
}
};
/**
* Gets wallet balance using Privy embedded wallet
* @param this The FastifyRequest instance
* @param reply The FastifyReply instance
* @param address The wallet address to get balance for
* @param assets Array of assets to filter by using Ticker enum (e.g. [Ticker.USDC, Ticker.ETH])
* @param chains Array of chain names to filter by (e.g. ['arbitrum', 'ethereum'])
* @returns The response object with success status and balances array
*/
export async function getWalletBalance(
this: FastifyRequest,
reply: FastifyReply,
address: string,
assets: Ticker[],
chains: string[]
) {
try {
// Validate the request parameters
walletBalanceSchema.parse({
address,
assets,
chains
});
if (!address) {
throw new Error('Wallet address is required for balance retrieval');
}
if (!assets || assets.length === 0) {
throw new Error('At least one asset is required for balance retrieval');
}
if (!chains || chains.length === 0) {
throw new Error('At least one chain is required for balance retrieval');
}
// Call the getWalletBalanceImpl function
const balances = await getWalletBalanceImpl(address, assets, chains);
return {
success: true,
balances: balances
};
} catch (error) {
this.log.error(error);
// Return appropriate error response
reply.status(error instanceof z.ZodError ? 400 : 500);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
}
/**
* Sends tokens from one address to another using Privy wallet
* @param this The FastifyRequest instance
* @param reply The FastifyReply instance
* @param senderAddress The sender's wallet address
* @param recipientAddress The recipient's wallet address
* @param ticker The token ticker or enum value
* @param amount The amount to send
* @param chainId The chain ID (optional, defaults to ARBITRUM)
* @returns The response object with success status and transaction hash
*/
export async function sendToken(
this: FastifyRequest,
reply: FastifyReply,
senderAddress: string,
recipientAddress: string,
ticker: string,
amount: bigint,
chainId?: number
) {
try {
// Validate the request parameters
tokenSendSchema.parse({
senderAddress,
recipientAddress,
ticker,
amount,
chainId
});
if (!senderAddress) {
throw new Error('Sender address is required for token transfer');
}
if (!recipientAddress) {
throw new Error('Recipient address is required for token transfer');
}
if (!ticker) {
throw new Error('Token ticker is required for token transfer');
}
if (!amount || amount <= 0n) {
throw new Error('Valid amount is required for token transfer');
}
// Call the sendTokenImpl function
const hash = await sendTokenImpl(senderAddress, recipientAddress, ticker, amount, chainId);
return {
success: true,
hash: hash
};
} catch (error) {
this.log.error(error);
// Return appropriate error response
reply.status(error instanceof z.ZodError ? 400 : 500);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred'
};
}
}
/**
* The use of fastify-plugin is required to be able
* to export the decorators to the outer scope
*
* @see {@link https://github.com/fastify/fastify-plugin}
*/
export default fp(async (fastify) => {
// Decorate request with methods that use the Fastify instance
fastify.decorateRequest('signPrivyMessage', async function(this: FastifyRequest, reply: FastifyReply, walletId: string, message: string, address: string) {
return signPrivyMessage.call(this, reply, walletId, message, address);
});
fastify.decorateRequest('approveToken', async function(this: FastifyRequest, reply: FastifyReply, address: string, ticker: string, chainId: number, amount?: bigint) {
return approveToken.call(this, reply, address, ticker, chainId, amount);
});
fastify.decorateRequest('initAddress', async function(this: FastifyRequest, reply: FastifyReply, address: string) {
return initAddress.call(this, reply, address);
});
fastify.decorateRequest('sendToken', async function(this: FastifyRequest, reply: FastifyReply, senderAddress: string, recipientAddress: string, ticker: string, amount: bigint, chainId?: number) {
return sendToken.call(this, reply, senderAddress, recipientAddress, ticker, amount, chainId);
});
fastify.decorateRequest('getWalletBalance', async function(this: FastifyRequest, reply: FastifyReply, address: string, assets: Ticker[], chains: string[]) {
return getWalletBalance.call(this, reply, address, assets, chains);
});
// Test the Privy client initialization
try {
const testClient = getPrivyClient(fastify);
fastify.log.info('Privy client initialized successfully');
} catch (error) {
fastify.log.error('Failed to initialize Privy client:', error);
throw error;
}
}, {
name: 'privy-plugin'
});