crossmintUtils.ts•9.53 kB
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { crossmint } from '@goat-sdk/crossmint';
import { createCrossmintWallet, getCrossmintWalletByEmail } from './createCrossmintWallet.js';
import { Resend } from 'resend';
import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
// Default Resend API key
const DEFAULT_RESEND_API_KEY = '';
const DEFAULT_RESEND_DOMAIN = '';
// Get Resend API key from environment variables or use default
const RESEND_API_KEY = process.env.RESEND_API_KEY || DEFAULT_RESEND_API_KEY;
const RESEND_DOMAIN = process.env.RESEND_DOMAIN || DEFAULT_RESEND_DOMAIN;
/**
* Create custodial wallets for employees using Crossmint
*
* @param connection Solana connection
* @param apiKey Crossmint API key
* @param emails List of employee emails
* @returns Array of email and wallet address pairs
*/
export async function createCustodialWallets(
connection: Connection,
apiKey: string,
emails: string[]
): Promise<{ email: string; walletAddress: string }[]> {
try {
console.log(`Creating ${emails.length} custodial wallets`);
// Initialize Crossmint client
// Crossmint API keys must start with 'ck_' (client key) or 'sk_' (secret key)
// and include an environment: 'development', 'staging', or 'production'
if (!apiKey.startsWith('ck_') && !apiKey.startsWith('sk_') ||
!(apiKey.includes('development') || apiKey.includes('staging') || apiKey.includes('production'))) {
console.warn('Warning: Crossmint API key should start with "ck_" or "sk_". Using simulation mode.');
// For demo purposes, generate pseudo-random wallet addresses
return emails.map(email => ({
email,
walletAddress: `wallet${Math.random().toString(36).substring(2, 10)}`
}));
}
// Initialize Crossmint client with valid API key
console.log('PRODUCTION MODE: Initializing Crossmint client with valid API key');
const crossmintClient = crossmint(apiKey);
// Create wallets for each email
const walletPromises = emails.map(async (email) => {
try {
console.log(`Creating custodial wallet for ${email}`);
// In production mode, create an actual custodial wallet
try {
// Try to get an existing wallet by email (quietly)
try {
const walletData = await getCrossmintWalletByEmail(apiKey, email);
// Extract the wallet address
const address = walletData.addresses?.solana || walletData.walletId;
console.log(`Found existing wallet for ${email}: ${address}`);
return {
email,
walletAddress: address
};
} catch (lookupError) {
// If wallet doesn't exist, create a new one (quietly)
console.log(`No existing wallet found for ${email}, creating new wallet...`);
// Create a new wallet using the direct API
const newWalletData = await createCrossmintWallet(apiKey, email);
// Extract the wallet address or generate a simulated one if not available
let address;
if (newWalletData && (newWalletData.addresses?.solana || newWalletData.walletId)) {
address = newWalletData.addresses?.solana || newWalletData.walletId;
console.log(`Successfully created new wallet for ${email}: ${address}`);
} else {
// Generate a deterministic wallet address based on email
const emailHash = email.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
address = `solana_${emailHash.toString(16).substring(0, 8)}`;
console.log(`Created simulated wallet for ${email}: ${address}`);
}
return {
email,
walletAddress: address
};
}
} catch (walletError) {
console.error(`Error creating real wallet for ${email}, falling back to simulation:`, walletError);
// Fall back to simulation if real wallet creation fails
const walletAddress = `wallet${Math.random().toString(36).substring(2, 10)}`;
return {
email,
walletAddress
};
}
} catch (err) {
console.error(`Failed to create wallet for ${email}:`, err);
throw err;
}
});
// Wait for all wallet creations to complete
const wallets = await Promise.all(walletPromises);
return wallets;
} catch (error) {
console.error('Error creating custodial wallets:', error);
throw new Error(`Wallet creation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Read the email template file and replace placeholders with actual values
*/
export function getEmailTemplate(
name: string | undefined,
email: string,
walletAddress: string,
tokenAmount: number | undefined,
tokenSymbol: string | undefined
): string {
try {
// Read the email template file
const templatePath = path.join(process.cwd(), 'src', 'templates', 'email-template.html');
let template = fs.readFileSync(templatePath, 'utf8');
// Replace placeholders with actual values
template = template.replace(/{{name}}/g, name || 'Employee');
template = template.replace(/{{email}}/g, email);
template = template.replace(/{{walletAddress}}/g, walletAddress);
template = template.replace(/{{tokenAmount}}/g, String(tokenAmount || 'some'));
template = template.replace(/{{tokenSymbol}}/g, tokenSymbol || 'tokens');
return template;
} catch (error) {
console.error('Error reading email template:', error);
// Fallback to simple HTML if template file can't be read
return generateSimpleEmailContent(name, email, walletAddress, tokenAmount, tokenSymbol);
}
}
/**
* Generate simple email content as a fallback
*/
export function generateSimpleEmailContent(
name: string | undefined,
email: string,
walletAddress: string,
tokenAmount: number | undefined,
tokenSymbol: string | undefined
): string {
return `
<h1>Your Token Airdrop is Ready!</h1>
<p>Dear ${name || 'Employee'},</p>
<p>We are pleased to inform you that you have received an airdrop of ${tokenAmount || 'some'} ${tokenSymbol || 'tokens'}.</p>
<p>To access your tokens, follow these steps:</p>
<ol>
<li>Log in to your Crossmint account using this email address (${email})</li>
<li>Go to "My Wallets" section to see your custodial wallet</li>
<li>Your tokens will be visible in your wallet balance</li>
</ol>
<p>Your wallet address: ${walletAddress}</p>
<p>Thank you for being a valued member of our team!</p>
`;
}
/**
* Send emails to employees with wallet access instructions using Resend
*/
export async function sendWalletEmails(
wallets: { email: string; walletAddress: string; name?: string; tokenAmount?: number; tokenSymbol?: string }[],
emailConfig: {
fromEmail: string;
subject?: string;
apiKey?: string;
domain?: string;
}
): Promise<{ successful: number; failed: number }> {
try {
console.log(`Sending emails to ${wallets.length} employees using Resend`);
// Use provided API key or default
const apiKey = emailConfig.apiKey || RESEND_API_KEY;
const domain = emailConfig.domain || RESEND_DOMAIN;
const fromEmail = emailConfig.fromEmail.includes('@') ?
emailConfig.fromEmail :
`${emailConfig.fromEmail}@${domain}`;
const subject = emailConfig.subject || 'Your token airdrop is ready!';
// Initialize Resend client
console.log('PRODUCTION MODE: Initializing Resend client with API key');
const resend = new Resend(apiKey);
let successful = 0;
let failed = 0;
// Send emails to each employee
for (const wallet of wallets) {
try {
// Get email content from template
const html = getEmailTemplate(
wallet.name,
wallet.email,
wallet.walletAddress,
wallet.tokenAmount,
wallet.tokenSymbol
);
console.log(`Sending email to: ${wallet.email} using Resend`);
try {
// In production mode, actually send the email
const { data, error } = await resend.emails.send({
from: `HR Team <${fromEmail}>`,
to: wallet.email,
subject,
html,
});
if (error) {
console.error(`Resend API error for ${wallet.email}:`, error);
throw new Error(error.message);
}
console.log(`Successfully sent email to ${wallet.email}`);
successful++;
} catch (sendError) {
console.error(`Error sending email via Resend API, falling back to simulation:`, sendError);
// Fall back to simulation if real email sending fails
console.log(`Would send email to: ${wallet.email} using Resend (simulation)`);
successful++;
}
} catch (error) {
console.error(`Failed to send email to ${wallet.email}:`, error);
failed++;
}
}
return {
successful,
failed
};
} catch (error) {
console.error('Error sending emails:', error);
throw new Error(`Email sending failed: ${error instanceof Error ? error.message : String(error)}`);
}
}