bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
358 lines (318 loc) • 10.5 kB
JavaScript
require('ts-node/register');
const config = require('../config');
const fs = require('fs');
const path = require('path');
const Mustache = require('mustache');
const os = require('os');
const { exec } = require('child_process');
const sgMail = require('@sendgrid/mail');
const juice = require('juice');
const iconMap = {
new_copayer: 'person-plus.png',
new_incoming_tx: 'down-arrow-green.png',
new_incoming_tx_testnet: 'down-arrow-green.png',
new_outgoing_tx: 'up-arrow-gray.png',
new_tx_proposal: 'writing-gray.png',
new_zero_outgoing_tx: 'up-arrow-gray.png',
tx_confirmation: 'green-check.png',
tx_confirmation_receiver: 'green-check.png',
tx_confirmation_sender: 'green-check.png',
txp_finally_rejected: 'failed-icon.png',
wallet_complete: 'green-check.png'
};
function getIconHtml (templateName) {
const iconFile = iconMap[templateName];
if (!iconFile) {
return null;
}
const staticUrl = config.baseUrl || 'https://bws.bitpay.com';
const iconUrl = `${staticUrl}/bws/static/images/${iconFile}`;
return `<img src="${iconUrl}" alt="${templateName} icon" style="width: 50px; height: 50px;" />`
};
// Parse command line arguments
const args = process.argv.slice(2);
const templateName = args[0] || 'new_copayer';
const language = args[1] || 'en';
const sendEmail = args[2] === 'send';
const recipientEmail = args[3]; // Required if sendEmail is true
// Available email templates
const TEMPLATE_TYPES = [
'new_copayer',
'wallet_complete',
'new_tx_proposal',
'new_outgoing_tx',
'new_incoming_tx',
'new_incoming_tx_testnet',
'new_zero_outgoing_tx',
'txp_finally_rejected',
'tx_confirmation',
'tx_confirmation_receiver',
'tx_confirmation_sender'
];
// Template directory
const TEMPLATE_DIR = path.join(__dirname, '../../templates');
const MASTER_TEMPLATE_PATH = path.join(TEMPLATE_DIR, 'master-template.html');
const CONTENT_TEMPLATE_PATH = path.join(TEMPLATE_DIR, language, `${templateName}.html`);
console.log('Template directories:');
console.log('TEMPLATE_DIR:', TEMPLATE_DIR);
// Validate arguments
if (!templateName || !TEMPLATE_TYPES.includes(templateName)) {
console.log('Usage: node preview-email.js <templateName> [language] [send] [recipientEmail]');
console.log(' - templateName: Name of the template to preview');
console.log(' - language: Language code (default: en)');
console.log(' - send: Set to "send" to send an actual email');
console.log(' - recipientEmail: Email address to send to (required if send=true)');
console.log('\nAvailable templates:');
TEMPLATE_TYPES.forEach(type => console.log(`- ${type}`));
process.exit(1);
}
if (sendEmail && !recipientEmail) {
console.error('Error: Recipient email is required when sending an actual email');
process.exit(1);
}
// Create sample data based on template type
const createSampleData = (templateName) => {
// Common data for all templates
const commonData = {
title: templateName.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '),
subjectPrefix: '[Wallet Service] ',
walletId: 'testWalletId',
walletName: 'Test Wallet',
walletM: 2,
walletN: 3,
copayerId: 'testCopayerId',
copayerName: 'John Doe'
};
const iconObj = getIconHtml(templateName, sendEmail);
if (iconObj) {
commonData.icon = iconObj;
}
try {
const plainTextPath = path.join(TEMPLATE_DIR, language, `${templateName}.plain`);
const plainContent = fs.readFileSync(plainTextPath, 'utf8');
const firstLine = plainContent.split('\n')[0];
if (firstLine && firstLine.startsWith('{{subjectPrefix}}')) {
commonData.title = firstLine.replace('{{subjectPrefix}}', '');
}
} catch (err) {
}
switch (templateName) {
case 'new_copayer':
return commonData;
case 'wallet_complete': {
const title = commonData.title;
return {
...commonData,
title: title || 'Wallet Complete'
};
}
case 'new_tx_proposal': {
const title = commonData.title;
return {
...commonData,
title: title || 'New Payment Proposal',
txProposalId: 'txp-123',
amount: '1.5 BTC',
toAddress: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
creatorName: 'John Doe'
};
}
case 'new_outgoing_tx':
case 'new_incoming_tx': {
const title = commonData.title;
return {
...commonData,
title: title || (templateName === 'new_outgoing_tx' ? 'Payment Sent' : 'New Payment Received'),
txid: '1234567890abcdef',
amount: '1.5 BTC',
address: {
coin: 'btc'
},
urlForTx: 'https://explorer.bitpay.com/tx/1234567890abcdef'
};
}
case 'new_incoming_tx_testnet': {
const title = commonData.title;
return {
...commonData,
title: title || 'New Payment Received',
txid: '1234567890abcdef',
amount: '1.5 BTC',
address: {
coin: 'btc'
},
networkStr: ' en TESTNET',
urlForTx: 'https://explorer.bitpay.com/tx/1234567890abcdef'
};
}
case 'new_zero_outgoing_tx': {
const title = commonData.title;
return {
...commonData,
title: title || 'Payment Sent',
txid: '1234567890abcdef',
urlForTx: 'https://explorer.bitpay.com/tx/1234567890abcdef'
};
}
case 'txp_finally_rejected': {
const title = commonData.title;
return {
...commonData,
title: title || 'Payment Proposal Rejected',
txProposalId: 'txp-123',
rejectorsNames: 'Jane Smith, Bob Johnson'
};
}
case 'tx_confirmation': {
const title = commonData.title;
return {
...commonData,
title: title || 'Transaction Confirmed',
txid: '1234567890abcdef',
amount: '1.5 BTC',
urlForTx: 'https://explorer.bitpay.com/tx/1234567890abcdef'
};
}
case 'tx_confirmation_receiver': {
const title = commonData.title;
return {
...commonData,
title: title || 'Transaction Confirmed',
txid: '1234567890abcdef',
amount: '1.5 BTC',
urlForTx: 'https://explorer.bitpay.com/tx/1234567890abcdef'
};
}
case 'tx_confirmation_sender': {
const title = commonData.title;
return {
...commonData,
title: title || 'Transaction Confirmed',
txid: '1234567890abcdef',
amount: '1.5 BTC',
urlForTx: 'https://explorer.bitpay.com/tx/1234567890abcdef'
};
}
default:
return commonData;
}
};
const masterTemplate = fs.readFileSync(MASTER_TEMPLATE_PATH, 'utf8');
function createBasicHtmlFromPlain(templateName) {
const plainTextPath = path.join(TEMPLATE_DIR, language, `${templateName}.plain`);
let plainContent = '';
try {
plainContent = fs.readFileSync(plainTextPath, 'utf8')
.split('\n')
.slice(2) // Skip the subject line and empty line
.join('\n')
.trim();
} catch (err) {
plainContent = `This is a sample email template for ${templateName}`;
}
return `
<h1 class="mb-2">{{title}}</h1>
<div class="divider"></div>
<div class="text-16">
${plainContent}
</div>
`;
}
// Save HTML to temp file and open in browser
const previewInBrowser = (html) => {
const tempDir = os.tmpdir();
const previewDir = path.join(tempDir, `bitcore-email-preview-${Date.now()}`);
const tempFilePath = path.join(previewDir, 'index.html');
fs.mkdirSync(previewDir);
fs.writeFileSync(tempFilePath, html, 'utf8');
console.log(`Opening preview in browser: ${tempFilePath}`);
const platform = process.platform;
try {
if (platform === 'darwin') { // macOS
exec(`open "${tempFilePath}"`);
} else if (platform === 'win32') { // Windows
exec(`start "" "${tempFilePath}"`);
} else { // Linux and others
exec(`xdg-open "${tempFilePath}"`);
}
console.log('Preview opened in your default browser');
} catch (err) {
console.error('Failed to open browser automatically:', err);
console.log('Please open the HTML file manually at:', tempFilePath);
}
return tempFilePath;
};
// Send actual email
const sendTestEmail = async (subject, plainBody, htmlBody, recipient) => {
const SENDGRID_API_KEY = process.env.SENDGRID_API_KEY;
if (!SENDGRID_API_KEY) {
console.error('Error: SENDGRID_API_KEY environment variable is required to send emails');
process.exit(1);
}
sgMail.setApiKey(SENDGRID_API_KEY);
const msg = {
to: recipient,
from: process.env.FROM_EMAIL || 'noreply@example.com',
subject: subject,
text: plainBody,
html: htmlBody
};
try {
await sgMail.send(msg);
console.log(`Email successfully sent to ${recipient}`);
} catch (error) {
console.error('Error sending email:', error);
if (error.response) {
console.error(error.response.body);
}
}
};
// Main function
const previewTemplate = async () => {
console.log(`Testing template: ${templateName} (${language})`);
// Create sample data
const data = createSampleData(templateName);
// Read template
const contentTemplate = fs.existsSync(CONTENT_TEMPLATE_PATH)
? fs.readFileSync(CONTENT_TEMPLATE_PATH, 'utf8')
: createBasicHtmlFromPlain(templateName);
// Replace content placeholder with actual content
const html = masterTemplate.replace('{{> htmlContent}}', contentTemplate);
// Render final HTML with data
const renderedHtml = Mustache.render(html, data);
// Inline CSS for both preview and email
const inlinedHtml = juice(renderedHtml, {
removeStyleTags: false,
preserveImportant: true,
preserveMediaQueries: true,
preserveFontFaces: true,
applyStyleTags: true
});
// Display subject
console.log('\nSubject:', data.title);
// Preview HTML in browser
if (inlinedHtml) {
const tempFilePath = previewInBrowser(inlinedHtml);
console.log('HTML preview saved to:', tempFilePath);
} else {
console.error('Failed to generate HTML preview');
}
// Send actual email if requested
if (sendEmail) {
console.log(`\nSending test email to ${recipientEmail}...`);
// Generate plain text version
const plainText = `${data.title}\n\nA new copayer just joined your wallet.`;
await sendTestEmail(
data.title,
plainText,
inlinedHtml,
recipientEmail
);
}
};
// Run the preview
previewTemplate().catch(err => {
console.error('Error:', err);
process.exit(1);
});