pay_hash 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # PayHash SDK
2
+
3
+ PayHash SDK is a professional TypeScript library designed to streamline blockchain payments, receipt generation, and automated email notifications. Built on top of `viem`, it provides a high-level interface for handling crypto transactions on the Tempo network, while automatically generating PDF receipts and sending them to both organizations and clients via SMTP.
4
+
5
+ ## Key Features
6
+
7
+ - **Seamless Payments**: Process single or batch token transfers with ease.
8
+ - **Automated Receipts**: Generates professional PDF receipts using Puppeteer and Handlebars.
9
+ - **Email Notifications**: Automatically sends email confirmations with attached PDF receipts.
10
+ - **Customizable Templates**: Fully customizable HTML email and PDF receipt templates.
11
+ - **Batch Processing**: Supports both sequential and parallel batch payments with advanced nonce management.
12
+ - **TypeScript First**: Robust type definitions for a superior developer experience.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install pay_hash viem
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { PayHashClient } from "pay_hash";
24
+ import { createClient, http, publicActions, walletActions } from "viem";
25
+ import { privateKeyToAccount } from "viem/accounts";
26
+ import { tempoTestnet } from "viem/chains";
27
+ import { tempoActions } from "viem/tempo";
28
+
29
+ // 1. Setup Viem Client
30
+ const client = createClient({
31
+ account: privateKeyToAccount("0x..."),
32
+ chain: tempoTestnet,
33
+ transport: http(),
34
+ })
35
+ .extend(publicActions)
36
+ .extend(walletActions)
37
+ .extend(tempoActions());
38
+
39
+ // 2. Configure SMTP
40
+ const smtp = {
41
+ host: "smtp.example.com",
42
+ port: 587,
43
+ secure: false,
44
+ user: "your-user",
45
+ pass: "your-password",
46
+ };
47
+
48
+ // 3. Define Email/Receipt Branding
49
+ const emailTemplate = {
50
+ brandName: "My Brand",
51
+ primaryColor: "#4F46E5",
52
+ receiptTitle: "Payment Receipt",
53
+ subject: "Success! Your Payment Receipt",
54
+ };
55
+
56
+ // 4. Initialize PayHash SDK
57
+ const payhash = new PayHashClient(client, smtp, emailTemplate);
58
+
59
+ // 5. Process Payment
60
+ const main = async () => {
61
+ const result = await payhash.pay({
62
+ amount: "100.50",
63
+ token: "0x...", // Token Address
64
+ orgAddress: "0x...", // Recipient Address
65
+ orgName: "My Company Inc",
66
+ orgMail: "billing@mycompany.com",
67
+ clientMail: "customer@example.com",
68
+ memo: "Order #789",
69
+ });
70
+
71
+ console.log(result.success ? "Paid!" : "Error: " + result.error);
72
+ };
73
+
74
+ main();
75
+ ```
76
+
77
+ ## Advanced Usage
78
+
79
+ ### Batch Payments
80
+
81
+ #### Sequential Batch (Safer)
82
+
83
+ Processes payments one by one, waiting for each to complete.
84
+
85
+ ```typescript
86
+ await payhash.batchPay([params1, params2]);
87
+ ```
88
+
89
+ #### Parallel Batch (Faster)
90
+
91
+ Sends multiple transactions simultaneously using advanced nonce tracking.
92
+
93
+ ```typescript
94
+ await payhash.batchPayAsync([params1, params2]);
95
+ ```
96
+
97
+ ### Customizing Templates
98
+
99
+ You can provide your own Handlebars (.hbs) files for both emails and PDF receipts.
100
+
101
+ ```typescript
102
+ const emailTemplate = {
103
+ brandName: "PayHash",
104
+ // Specify custom template paths
105
+ pdfTemplatePath: "./templates",
106
+ pdfTemplateName: "custom-receipt.hbs",
107
+ emailTemplatePath: "./templates",
108
+ emailTemplateName: "custom-email.hbs",
109
+ // Reusable branding
110
+ primaryColor: "#000",
111
+ logoUrl: "https://example.com/logo.png",
112
+ };
113
+ ```
114
+
115
+ ## API Reference
116
+
117
+ ### `PayHashClient`
118
+
119
+ - `constructor(client, smtp, template)`: Initializes the SDK.
120
+ - `pay(params)`: Executes a single token transfer, generates a receipt, and sends an email.
121
+ - `batchPay(params[])`: Executes multiple payments sequentially.
122
+ - `batchPayAsync(params[])`: Executes multiple payments in parallel using unique nonces.
123
+ - `verifyTransporter()`: Checks if the SMTP configuration is valid.
124
+
125
+ ## Configuration Types
126
+
127
+ ### `PayParams`
128
+
129
+ | Property | Type | Description |
130
+ | :----------- | :-------- | :------------------------------------------------ |
131
+ | `amount` | `string` | The amount to transfer (in human-readable units). |
132
+ | `token` | `Address` | The ERC20 token contract address. |
133
+ | `orgAddress` | `Address` | The organization's wallet address. |
134
+ | `orgMail` | `string` | The organization's support email. |
135
+ | `clientMail` | `string` | The client's email address. |
136
+ | `memo` | `string` | Optional payment reference. |
137
+
138
+ ### `EmailTemplate`
139
+
140
+ | Property | Type | Description |
141
+ | :------------- | :------- | :------------------------------------------------ |
142
+ | `brandName` | `string` | Your organization's name. |
143
+ | `primaryColor` | `string` | Primary hex color for the branding. |
144
+ | `logoUrl` | `string` | URL to your organization's logo. |
145
+ | `fontFamily` | `string` | Font family for the PDF (Inter, Roboto, Poppins). |
146
+
147
+ ## License
148
+
149
+ ISC License.
package/dist/client.js ADDED
@@ -0,0 +1,163 @@
1
+ import { parseUnits, stringToHex } from "viem";
2
+ import { generateReceiptPDF } from "./pdf";
3
+ import { createTransporter } from "./mailer";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { TEMPO_TESTNET_CHAIN_ID } from "./constants";
7
+ import { renderReceiptHtml } from "./render";
8
+ import { Actions } from "viem/tempo";
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const pdfPath = path.join(__dirname, "./receipt.hbs");
11
+ export class PayHashClient {
12
+ client;
13
+ transporter;
14
+ emailTemplate;
15
+ constructor(client, smtp, emailTemplate) {
16
+ this.client = client;
17
+ this.transporter = createTransporter(smtp);
18
+ this.emailTemplate = emailTemplate;
19
+ }
20
+ async verifyTransporter() {
21
+ return await this.transporter.verify();
22
+ }
23
+ // private loadEmailTemplate(filePath: string, fileName: string) {
24
+ // const templatePath = path.join(filePath, fileName);
25
+ // const html = fs.readFileSync(templatePath, "utf-8");
26
+ // return html;
27
+ // }
28
+ async pay(params) {
29
+ try {
30
+ const [account] = await this.client?.getAddresses();
31
+ const metadata = await this.client.token.getMetadata({
32
+ token: params.token,
33
+ });
34
+ const tokenName = metadata.symbol;
35
+ const decimals = metadata.decimals;
36
+ const chainId = this.client.chain.id;
37
+ let receipt = null;
38
+ if (chainId == TEMPO_TESTNET_CHAIN_ID) {
39
+ receipt = await this.client.token.transferSync({
40
+ amount: parseUnits(params.amount, decimals),
41
+ memo: stringToHex(params.memo || "0x"),
42
+ token: params.token,
43
+ to: params.orgAddress,
44
+ });
45
+ }
46
+ if (receipt == null) {
47
+ throw new Error("Receipt not found");
48
+ }
49
+ await this.sendReceiptAndEmail({
50
+ params: params,
51
+ receipt: receipt,
52
+ txHash: receipt.receipt.transactionHash,
53
+ tokenName: metadata.symbol,
54
+ });
55
+ return {
56
+ success: true,
57
+ receipt,
58
+ account,
59
+ };
60
+ }
61
+ catch (error) {
62
+ console.error("[PayHashClient Error]", error);
63
+ return {
64
+ success: false,
65
+ error: error?.message || "Unknown error occurred",
66
+ };
67
+ }
68
+ }
69
+ // bathch Pay sync to multiple addresses at once
70
+ async batchPay(params) {
71
+ for (const param of params) {
72
+ await this.pay(param);
73
+ }
74
+ return {
75
+ success: true,
76
+ message: "Batch payment successful",
77
+ };
78
+ }
79
+ // batch Pay async to multiple addresses at once
80
+ async batchPayAsync(params) {
81
+ const [account] = await this.client.getAddresses();
82
+ // 1️⃣ unique nonce key per tx
83
+ const nonceKeys = params.map((_, i) => BigInt(i + 1));
84
+ // 2️⃣ fetch all nonces in parallel
85
+ const nonces = await Promise.all(nonceKeys.map((nonceKey) => Actions.nonce.getNonce(this.client, { account, nonceKey })));
86
+ // 3️⃣ send all txs in parallel with per-item error handling
87
+ const results = await Promise.all(params.map(async (param, i) => {
88
+ try {
89
+ const metadata = await this.client.token.getMetadata({
90
+ token: param.token,
91
+ });
92
+ const decimals = metadata.decimals;
93
+ const receipt = await Actions.token.transferSync(this.client, {
94
+ amount: parseUnits(param.amount, decimals),
95
+ to: param.orgAddress,
96
+ token: param.token,
97
+ // memo: stringToHex(param.memo || "0x"),
98
+ nonceKey: nonceKeys[i],
99
+ nonce: Number(nonces[i]),
100
+ });
101
+ await this.sendReceiptAndEmail({
102
+ params: param,
103
+ receipt: receipt,
104
+ txHash: receipt.receipt.transactionHash,
105
+ tokenName: metadata.symbol,
106
+ });
107
+ return {
108
+ success: true,
109
+ index: i,
110
+ orgAddress: param.orgAddress,
111
+ txHash: receipt.receipt.transactionHash,
112
+ };
113
+ }
114
+ catch (error) {
115
+ console.error(`[BatchPay Error] index=${i}, org=${param.orgAddress}`, error);
116
+ return {
117
+ success: false,
118
+ index: i,
119
+ orgAddress: param.orgAddress,
120
+ error: error?.message ?? "Unknown error",
121
+ };
122
+ }
123
+ }));
124
+ return {
125
+ success: true,
126
+ results,
127
+ };
128
+ }
129
+ async sendReceiptAndEmail({ params, receipt, txHash, tokenName, }) {
130
+ /* Generate PDF */
131
+ const pdf = await generateReceiptPDF(path.join(this.emailTemplate?.pdfTemplatePath ?? __dirname, this.emailTemplate?.pdfTemplateName ?? "receipt.hbs"), {
132
+ brandName: this.emailTemplate?.brandName ?? params.orgName,
133
+ primaryColor: this.emailTemplate?.primaryColor ?? "#000",
134
+ receiptTitle: this.emailTemplate?.receiptTitle ?? "Payment Receipt",
135
+ amount: params.amount,
136
+ token: tokenName,
137
+ txHash,
138
+ date: new Date().toUTCString(),
139
+ footerText: this.emailTemplate?.footerText ?? "Powered by PayHash",
140
+ fontFamily: this.emailTemplate?.fontFamily ?? "Inter",
141
+ titleFontSize: this.emailTemplate?.titleFontSize ?? 24,
142
+ });
143
+ const html = renderReceiptHtml({
144
+ receipt,
145
+ template: this.emailTemplate,
146
+ params,
147
+ tokenName,
148
+ });
149
+ /* Send email */
150
+ await this.transporter.sendMail({
151
+ from: `${params.orgName} <${params.orgMail}>`,
152
+ to: `${params.clientMail}, ${params.orgMail}`,
153
+ subject: this.emailTemplate?.subject ?? "Payment Receipt",
154
+ html,
155
+ attachments: [
156
+ {
157
+ filename: this.emailTemplate?.pdfFileName ?? "receipt.pdf",
158
+ content: pdf,
159
+ },
160
+ ],
161
+ });
162
+ }
163
+ }
@@ -0,0 +1 @@
1
+ export const TEMPO_TESTNET_CHAIN_ID = 42429;
@@ -0,0 +1,2 @@
1
+ export const STABLE_TOKEN = "0x0000000000000000000000000000000000000000"; // Replace with actual address
2
+ export const PAYHASH_CONTRACT = "0x0000000000000000000000000000000000000000"; // Replace with actual address
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { PayHashClient } from "./client";
2
+ export * from "./types";
@@ -0,0 +1,69 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="UTF-8" />
4
+ <title>{{receiptTitle}}</title>
5
+ <style>
6
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust:
7
+ 100%; } body { margin: 0; padding: 0; font-family: Arial, sans-serif;
8
+ background-color: #f9f9f9; color: #111; } table { border-collapse:
9
+ collapse !important; } a { color: inherit; text-decoration: none; }
10
+ .container { max-width: 600px; margin: 40px auto; background-color:
11
+ #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px
12
+ rgba(0,0,0,0.05); padding: 32px; } .header { text-align: center;
13
+ border-bottom: 2px solid
14
+ {{primaryColor}}; padding-bottom: 16px; margin-bottom: 24px; } .header h1
15
+ { color:
16
+ {{primaryColor}}; font-size:
17
+ {{titleFontSize}}px; margin: 0; } .body-content { font-size:
18
+ {{bodyFontSize}}px; line-height: 1.6; color: #333; } .body-content p {
19
+ margin-bottom: 16px; } .highlight { font-weight: bold; color:
20
+ {{primaryColor}}; } .footer { font-size: 12px; color: #777; text-align:
21
+ center; margin-top: 32px; line-height: 1.4; } .btn { display:
22
+ inline-block; background-color:
23
+ {{primaryColor}}; color: #ffffff; padding: 12px 24px; border-radius: 4px;
24
+ font-weight: 600; margin-top: 16px; }
25
+ </style>
26
+ </head>
27
+ <body>
28
+ <div class="container">
29
+ <div class="header">
30
+ <img src="{{logoUrl}}" alt="{{brandName}}" />
31
+ <h1>{{receiptTitle}}</h1>
32
+ </div>
33
+
34
+ <div class="body-content">
35
+ <p>Hello {{payer}},</p>
36
+ <p class="highlight">
37
+ You have made a payment of
38
+ {{amount}}
39
+ {{token}}
40
+ to
41
+ {{brandName}}.
42
+ </p>
43
+ <p>
44
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec
45
+ odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla
46
+ quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent
47
+ mauris. Fusce nec tellus sed augue semper porta.
48
+ </p>
49
+ <p>
50
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
51
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
52
+ minim veniam, quis nostrud exercitation ullamco laboris nisi ut
53
+ aliquip ex ea commodo consequat.
54
+ </p>
55
+
56
+ <a
57
+ href="https://explore.tempo.xyz/receipt/{{txHash}}?live=false"
58
+ class="btn"
59
+ >View Transaction</a>
60
+ </div>
61
+
62
+ <div class="footer">
63
+ {{footerText}}
64
+ &copy;
65
+ {{year}}
66
+ </div>
67
+ </div>
68
+ </body>
69
+ </html>
package/dist/mailer.js ADDED
@@ -0,0 +1,13 @@
1
+ // mailer.ts
2
+ import nodemailer from "nodemailer";
3
+ export function createTransporter(smtp) {
4
+ return nodemailer.createTransport({
5
+ host: smtp.host,
6
+ port: smtp.port,
7
+ secure: smtp.secure ?? smtp.port === 465,
8
+ auth: {
9
+ user: smtp.user,
10
+ pass: smtp.pass,
11
+ },
12
+ });
13
+ }
package/dist/pdf.js ADDED
@@ -0,0 +1,19 @@
1
+ import puppeteer from "puppeteer";
2
+ import Handlebars from "handlebars";
3
+ import fs from "fs";
4
+ export async function generateReceiptPDF(templatePath, data) {
5
+ const html = fs.readFileSync(templatePath, "utf8");
6
+ const compiled = Handlebars.compile(html);
7
+ const content = compiled(data);
8
+ const browser = await puppeteer.launch({
9
+ args: ["--no-sandbox"],
10
+ });
11
+ const page = await browser.newPage();
12
+ await page.setContent(content, { waitUntil: "networkidle0" });
13
+ const pdf = await page.pdf({
14
+ format: "A4",
15
+ printBackground: true,
16
+ });
17
+ await browser.close();
18
+ return pdf;
19
+ }
@@ -0,0 +1,53 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <style>
6
+ body {
7
+ font-family: {{fontFamily}};
8
+ padding: 32px;
9
+ color: #111;
10
+ }
11
+
12
+ .header {
13
+ border-bottom: 2px solid {{primaryColor}};
14
+ margin-bottom: 24px;
15
+ }
16
+
17
+ .title {
18
+ font-size: {{titleFontSize}}px;
19
+ color: {{primaryColor}};
20
+ }
21
+
22
+ .row {
23
+ margin: 8px 0;
24
+ }
25
+
26
+ .label {
27
+ font-weight: bold;
28
+ }
29
+
30
+ footer {
31
+ margin-top: 32px;
32
+ font-size: 12px;
33
+ opacity: 0.7;
34
+ }
35
+ </style>
36
+ </head>
37
+
38
+ <body>
39
+ <div class="header">
40
+ <h1 class="title">{{receiptTitle}}</h1>
41
+ </div>
42
+
43
+ <div class="row"><span class="label">Brand:</span> {{brandName}}</div>
44
+ <div class="row"><span class="label">Amount:</span> {{amount}}</div>
45
+ <div class="row"><span class="label">Token:</span> {{token}}</div>
46
+ <div class="row"><span class="label">Transaction:</span> {{txHash}}</div>
47
+ <div class="row"><span class="label">Date:</span> {{date}}</div>
48
+
49
+ <footer>
50
+ {{footerText}}
51
+ </footer>
52
+ </body>
53
+ </html>
package/dist/render.js ADDED
@@ -0,0 +1,24 @@
1
+ import Handlebars from "handlebars";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ export function renderReceiptHtml({ receipt, template, params, tokenName, }) {
7
+ const templateSource = fs.readFileSync(path.join(template.emailTemplatePath ?? __dirname, template.emailTemplateName ?? "mailTemplate.hbs"), "utf8");
8
+ const templates = Handlebars.compile(templateSource);
9
+ return templates({
10
+ brandName: template.brandName || params.orgName,
11
+ logoUrl: template.logoUrl || "",
12
+ primaryColor: template.primaryColor || "#b5a8b0ff",
13
+ titleFontSize: template.titleFontSize || 20,
14
+ bodyFontSize: template.bodyFontSize || 14,
15
+ receiptTitle: template.subject || "Payment Receipt",
16
+ footerText: template.footerText || "Powered by PayHash",
17
+ txHash: receipt.transactionHash,
18
+ payer: receipt.from,
19
+ timestamp: new Date(receipt.timestamp).toUTCString(),
20
+ year: new Date().getFullYear(),
21
+ amount: params.amount,
22
+ token: tokenName,
23
+ });
24
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/utils.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,12 @@
1
+ import { createClient, http, publicActions, walletActions } from 'viem';
2
+ import { privateKeyToAccount } from 'viem/accounts';
3
+ import { tempoTestnet } from 'viem/chains';
4
+ import { tempoActions } from 'viem/tempo';
5
+ export const client = createClient({
6
+ account: privateKeyToAccount('0x...'),
7
+ chain: tempoTestnet,
8
+ transport: http(),
9
+ })
10
+ .extend(publicActions)
11
+ .extend(walletActions)
12
+ .extend(tempoActions());
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "pay_hash",
3
+ "version": "1.0.0",
4
+ "description": "PayHash SDK is a professional TypeScript library designed to streamline blockchain payments, receipt generation, and automated email notifications",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc && npm run copy-templates",
9
+ "copy-templates": "cpy 'src/**/*.hbs' 'dist'",
10
+ "dev": "tsc --watch"
11
+ },
12
+ "keywords": [],
13
+ "author": "",
14
+ "license": "ISC",
15
+ "type": "module",
16
+ "devDependencies": {
17
+ "@types/node": "^25.0.3",
18
+ "@types/nodemailer": "^6.4.14",
19
+ "cpy-cli": "^6.0.0",
20
+ "eslint": "^8.57.0",
21
+ "tsup": "^8.5.1",
22
+ "tsx": "^4.7.0",
23
+ "typescript": "^5.9.3"
24
+ },
25
+ "dependencies": {
26
+ "dotenv": "^16.4.5",
27
+ "express": "^4.19.2",
28
+ "handlebars": "^4.7.8",
29
+ "nodemailer": "7.0.12",
30
+ "puppeteer": "^22.8.2",
31
+ "viem": "^2.43.5",
32
+ "zod": "^3.23.8"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ }
37
+ }
Binary file
package/src/client.ts ADDED
@@ -0,0 +1,219 @@
1
+ import { parseUnits, stringToHex } from "viem";
2
+ import {
3
+ PayHashViemClient,
4
+ PayParams,
5
+ EmailTemplate,
6
+ SMTPConfig,
7
+ } from "./types";
8
+ import { generateReceiptPDF } from "./pdf";
9
+ import { createTransporter } from "./mailer";
10
+ import nodemailer from "nodemailer";
11
+ import path from "path";
12
+ import { fileURLToPath } from "url";
13
+ import { TEMPO_TESTNET_CHAIN_ID } from "./constants";
14
+ import fs from "fs";
15
+ import { renderReceiptHtml } from "./render";
16
+ import { Actions } from "viem/tempo";
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ const pdfPath = path.join(__dirname, "./receipt.hbs");
20
+
21
+ export class PayHashClient {
22
+ private client: PayHashViemClient;
23
+ private transporter: nodemailer.Transporter;
24
+ private emailTemplate: EmailTemplate;
25
+
26
+ constructor(
27
+ client: PayHashViemClient,
28
+ smtp: SMTPConfig,
29
+ emailTemplate: EmailTemplate
30
+ ) {
31
+ this.client = client;
32
+ this.transporter = createTransporter(smtp);
33
+ this.emailTemplate = emailTemplate;
34
+ }
35
+
36
+ async verifyTransporter() {
37
+ return await this.transporter.verify();
38
+ }
39
+
40
+ // private loadEmailTemplate(filePath: string, fileName: string) {
41
+ // const templatePath = path.join(filePath, fileName);
42
+ // const html = fs.readFileSync(templatePath, "utf-8");
43
+ // return html;
44
+ // }
45
+
46
+ async pay(params: PayParams) {
47
+ try {
48
+ const [account] = await this.client?.getAddresses();
49
+ const metadata = await this.client.token.getMetadata({
50
+ token: params.token,
51
+ });
52
+ const tokenName = metadata.symbol;
53
+ const decimals = metadata.decimals;
54
+ const chainId = this.client.chain.id;
55
+ let receipt: Actions.token.transferSync.ReturnValue | null = null;
56
+ if (chainId == TEMPO_TESTNET_CHAIN_ID) {
57
+ receipt = await this.client.token.transferSync({
58
+ amount: parseUnits(params.amount, decimals),
59
+ memo: stringToHex(params.memo || "0x"),
60
+ token: params.token,
61
+ to: params.orgAddress,
62
+ });
63
+ }
64
+ if (receipt == null) {
65
+ throw new Error("Receipt not found");
66
+ }
67
+
68
+ await this.sendReceiptAndEmail({
69
+ params: params,
70
+ receipt: receipt,
71
+ txHash: receipt.receipt.transactionHash,
72
+ tokenName: metadata.symbol,
73
+ });
74
+
75
+ return {
76
+ success: true,
77
+ receipt,
78
+ account,
79
+ };
80
+ } catch (error: any) {
81
+ console.error("[PayHashClient Error]", error);
82
+ return {
83
+ success: false,
84
+ error: error?.message || "Unknown error occurred",
85
+ };
86
+ }
87
+ }
88
+
89
+ // bathch Pay sync to multiple addresses at once
90
+ async batchPay(params: PayParams[]) {
91
+ for (const param of params) {
92
+ await this.pay(param);
93
+ }
94
+ return {
95
+ success: true,
96
+ message: "Batch payment successful",
97
+ };
98
+ }
99
+
100
+ // batch Pay async to multiple addresses at once
101
+ async batchPayAsync(params: PayParams[]) {
102
+ const [account] = await this.client.getAddresses();
103
+
104
+ // 1️⃣ unique nonce key per tx
105
+ const nonceKeys = params.map((_, i) => BigInt(i + 1));
106
+
107
+ // 2️⃣ fetch all nonces in parallel
108
+ const nonces = await Promise.all(
109
+ nonceKeys.map((nonceKey) =>
110
+ Actions.nonce.getNonce(this.client, { account, nonceKey })
111
+ )
112
+ );
113
+
114
+ // 3️⃣ send all txs in parallel with per-item error handling
115
+ const results = await Promise.all(
116
+ params.map(async (param, i) => {
117
+ try {
118
+ const metadata = await this.client.token.getMetadata({
119
+ token: param.token,
120
+ });
121
+
122
+ const decimals = metadata.decimals;
123
+
124
+ const receipt = await Actions.token.transferSync(this.client, {
125
+ amount: parseUnits(param.amount, decimals),
126
+ to: param.orgAddress,
127
+ token: param.token,
128
+ // memo: stringToHex(param.memo || "0x"),
129
+ nonceKey: nonceKeys[i],
130
+ nonce: Number(nonces[i]),
131
+ });
132
+
133
+ await this.sendReceiptAndEmail({
134
+ params: param,
135
+ receipt: receipt,
136
+ txHash: receipt.receipt.transactionHash,
137
+ tokenName: metadata.symbol,
138
+ });
139
+
140
+ return {
141
+ success: true,
142
+ index: i,
143
+ orgAddress: param.orgAddress,
144
+ txHash: receipt.receipt.transactionHash,
145
+ };
146
+ } catch (error: any) {
147
+ console.error(
148
+ `[BatchPay Error] index=${i}, org=${param.orgAddress}`,
149
+ error
150
+ );
151
+
152
+ return {
153
+ success: false,
154
+ index: i,
155
+ orgAddress: param.orgAddress,
156
+ error: error?.message ?? "Unknown error",
157
+ };
158
+ }
159
+ })
160
+ );
161
+
162
+ return {
163
+ success: true,
164
+ results,
165
+ };
166
+ }
167
+ private async sendReceiptAndEmail({
168
+ params,
169
+ receipt,
170
+ txHash,
171
+ tokenName,
172
+ }: {
173
+ params: PayParams;
174
+ receipt: Actions.token.transferSync.ReturnValue;
175
+ txHash: `0x${string}`;
176
+ tokenName: string;
177
+ }) {
178
+ /* Generate PDF */
179
+ const pdf = await generateReceiptPDF(
180
+ path.join(
181
+ this.emailTemplate?.pdfTemplatePath ?? __dirname,
182
+ this.emailTemplate?.pdfTemplateName ?? "receipt.hbs"
183
+ ),
184
+ {
185
+ brandName: this.emailTemplate?.brandName ?? params.orgName,
186
+ primaryColor: this.emailTemplate?.primaryColor ?? "#000",
187
+ receiptTitle: this.emailTemplate?.receiptTitle ?? "Payment Receipt",
188
+ amount: params.amount,
189
+ token: tokenName,
190
+ txHash,
191
+ date: new Date().toUTCString(),
192
+ footerText: this.emailTemplate?.footerText ?? "Powered by PayHash",
193
+ fontFamily: this.emailTemplate?.fontFamily ?? "Inter",
194
+ titleFontSize: this.emailTemplate?.titleFontSize ?? 24,
195
+ }
196
+ );
197
+
198
+ const html = renderReceiptHtml({
199
+ receipt,
200
+ template: this.emailTemplate,
201
+ params,
202
+ tokenName,
203
+ });
204
+
205
+ /* Send email */
206
+ await this.transporter.sendMail({
207
+ from: `${params.orgName} <${params.orgMail}>`,
208
+ to: `${params.clientMail}, ${params.orgMail}`,
209
+ subject: this.emailTemplate?.subject ?? "Payment Receipt",
210
+ html,
211
+ attachments: [
212
+ {
213
+ filename: this.emailTemplate?.pdfFileName ?? "receipt.pdf",
214
+ content: pdf,
215
+ },
216
+ ],
217
+ });
218
+ }
219
+ }
@@ -0,0 +1 @@
1
+ export const TEMPO_TESTNET_CHAIN_ID = 42429;
@@ -0,0 +1,4 @@
1
+ import { Address } from "viem";
2
+
3
+ export const STABLE_TOKEN: Address = "0x0000000000000000000000000000000000000000"; // Replace with actual address
4
+ export const PAYHASH_CONTRACT: Address = "0x0000000000000000000000000000000000000000"; // Replace with actual address
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { PayHashClient } from "./client";
2
+ export * from "./types";
@@ -0,0 +1,69 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="UTF-8" />
4
+ <title>{{receiptTitle}}</title>
5
+ <style>
6
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust:
7
+ 100%; } body { margin: 0; padding: 0; font-family: Arial, sans-serif;
8
+ background-color: #f9f9f9; color: #111; } table { border-collapse:
9
+ collapse !important; } a { color: inherit; text-decoration: none; }
10
+ .container { max-width: 600px; margin: 40px auto; background-color:
11
+ #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px
12
+ rgba(0,0,0,0.05); padding: 32px; } .header { text-align: center;
13
+ border-bottom: 2px solid
14
+ {{primaryColor}}; padding-bottom: 16px; margin-bottom: 24px; } .header h1
15
+ { color:
16
+ {{primaryColor}}; font-size:
17
+ {{titleFontSize}}px; margin: 0; } .body-content { font-size:
18
+ {{bodyFontSize}}px; line-height: 1.6; color: #333; } .body-content p {
19
+ margin-bottom: 16px; } .highlight { font-weight: bold; color:
20
+ {{primaryColor}}; } .footer { font-size: 12px; color: #777; text-align:
21
+ center; margin-top: 32px; line-height: 1.4; } .btn { display:
22
+ inline-block; background-color:
23
+ {{primaryColor}}; color: #ffffff; padding: 12px 24px; border-radius: 4px;
24
+ font-weight: 600; margin-top: 16px; }
25
+ </style>
26
+ </head>
27
+ <body>
28
+ <div class="container">
29
+ <div class="header">
30
+ <img src="{{logoUrl}}" alt="{{brandName}}" />
31
+ <h1>{{receiptTitle}}</h1>
32
+ </div>
33
+
34
+ <div class="body-content">
35
+ <p>Hello {{payer}},</p>
36
+ <p class="highlight">
37
+ You have made a payment of
38
+ {{amount}}
39
+ {{token}}
40
+ to
41
+ {{brandName}}.
42
+ </p>
43
+ <p>
44
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec
45
+ odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla
46
+ quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent
47
+ mauris. Fusce nec tellus sed augue semper porta.
48
+ </p>
49
+ <p>
50
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
51
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
52
+ minim veniam, quis nostrud exercitation ullamco laboris nisi ut
53
+ aliquip ex ea commodo consequat.
54
+ </p>
55
+
56
+ <a
57
+ href="https://explore.tempo.xyz/receipt/{{txHash}}?live=false"
58
+ class="btn"
59
+ >View Transaction</a>
60
+ </div>
61
+
62
+ <div class="footer">
63
+ {{footerText}}
64
+ &copy;
65
+ {{year}}
66
+ </div>
67
+ </div>
68
+ </body>
69
+ </html>
package/src/mailer.ts ADDED
@@ -0,0 +1,15 @@
1
+ // mailer.ts
2
+ import nodemailer from "nodemailer";
3
+ import { SMTPConfig } from "./types";
4
+
5
+ export function createTransporter(smtp: SMTPConfig) {
6
+ return nodemailer.createTransport({
7
+ host: smtp.host,
8
+ port: smtp.port,
9
+ secure: smtp.secure ?? smtp.port === 465,
10
+ auth: {
11
+ user: smtp.user,
12
+ pass: smtp.pass,
13
+ },
14
+ });
15
+ }
package/src/pdf.ts ADDED
@@ -0,0 +1,27 @@
1
+ import puppeteer from "puppeteer";
2
+ import Handlebars from "handlebars";
3
+ import fs from "fs";
4
+
5
+ export async function generateReceiptPDF(
6
+ templatePath: string,
7
+ data: Record<string, any>
8
+ ): Promise<Buffer> {
9
+ const html = fs.readFileSync(templatePath, "utf8");
10
+ const compiled = Handlebars.compile(html);
11
+ const content = compiled(data);
12
+
13
+ const browser = await puppeteer.launch({
14
+ args: ["--no-sandbox"],
15
+ });
16
+
17
+ const page = await browser.newPage();
18
+ await page.setContent(content, { waitUntil: "networkidle0" });
19
+
20
+ const pdf = await page.pdf({
21
+ format: "A4",
22
+ printBackground: true,
23
+ });
24
+
25
+ await browser.close();
26
+ return pdf;
27
+ }
@@ -0,0 +1,53 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <style>
6
+ body {
7
+ font-family: {{fontFamily}};
8
+ padding: 32px;
9
+ color: #111;
10
+ }
11
+
12
+ .header {
13
+ border-bottom: 2px solid {{primaryColor}};
14
+ margin-bottom: 24px;
15
+ }
16
+
17
+ .title {
18
+ font-size: {{titleFontSize}}px;
19
+ color: {{primaryColor}};
20
+ }
21
+
22
+ .row {
23
+ margin: 8px 0;
24
+ }
25
+
26
+ .label {
27
+ font-weight: bold;
28
+ }
29
+
30
+ footer {
31
+ margin-top: 32px;
32
+ font-size: 12px;
33
+ opacity: 0.7;
34
+ }
35
+ </style>
36
+ </head>
37
+
38
+ <body>
39
+ <div class="header">
40
+ <h1 class="title">{{receiptTitle}}</h1>
41
+ </div>
42
+
43
+ <div class="row"><span class="label">Brand:</span> {{brandName}}</div>
44
+ <div class="row"><span class="label">Amount:</span> {{amount}}</div>
45
+ <div class="row"><span class="label">Token:</span> {{token}}</div>
46
+ <div class="row"><span class="label">Transaction:</span> {{txHash}}</div>
47
+ <div class="row"><span class="label">Date:</span> {{date}}</div>
48
+
49
+ <footer>
50
+ {{footerText}}
51
+ </footer>
52
+ </body>
53
+ </html>
package/src/render.ts ADDED
@@ -0,0 +1,42 @@
1
+ import Handlebars from "handlebars";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { PayParams, EmailTemplate } from "./types";
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ export function renderReceiptHtml({
9
+ receipt,
10
+ template,
11
+ params,
12
+ tokenName,
13
+ }: {
14
+ receipt: any;
15
+ template: EmailTemplate;
16
+ params: PayParams;
17
+ tokenName: string;
18
+ }) {
19
+ const templateSource = fs.readFileSync(
20
+ path.join(
21
+ template.emailTemplatePath ?? __dirname,
22
+ template.emailTemplateName ?? "mailTemplate.hbs"
23
+ ),
24
+ "utf8"
25
+ );
26
+ const templates = Handlebars.compile(templateSource);
27
+ return templates({
28
+ brandName: template.brandName || params.orgName,
29
+ logoUrl: template.logoUrl || "",
30
+ primaryColor: template.primaryColor || "#b5a8b0ff",
31
+ titleFontSize: template.titleFontSize || 20,
32
+ bodyFontSize: template.bodyFontSize || 14,
33
+ receiptTitle: template.subject || "Payment Receipt",
34
+ footerText: template.footerText || "Powered by PayHash",
35
+ txHash: receipt.transactionHash,
36
+ payer: receipt.from,
37
+ timestamp: new Date(receipt.timestamp).toUTCString(),
38
+ year: new Date().getFullYear(),
39
+ amount: params.amount,
40
+ token: tokenName,
41
+ });
42
+ }
package/src/types.ts ADDED
@@ -0,0 +1,74 @@
1
+ import {
2
+ WalletActions,
3
+ PublicActions,
4
+ Transport,
5
+ Chain,
6
+ Account,
7
+ Client,
8
+ RpcSchema,
9
+ Address,
10
+ Hex,
11
+ WalletClient,
12
+ PublicClient,
13
+ } from "viem";
14
+ import { TempoActions } from "viem/tempo";
15
+
16
+ export type PayHashViemClient = WalletClient<Transport,Chain, Account> & WalletActions<Chain, Account> & PublicActions<Transport, Chain> &
17
+ TempoActions<Chain, Account> &
18
+ Omit<Client<Transport, Chain, Account, RpcSchema>, "cacheTime">;
19
+
20
+ export interface PayHashSDKConfig {
21
+ client: PayHashViemClient;
22
+ }
23
+
24
+ export interface PayParams {
25
+ token: Address;
26
+ amount: string; // already in token decimals
27
+ memo: Hex;
28
+ orgAddress: Address;
29
+ orgName:string,
30
+ orgMail:string,
31
+ clientMail:string,
32
+ additionalInfo?: Hex;
33
+ }
34
+
35
+ export interface SMTPConfig {
36
+ host: string;
37
+ port: number;
38
+ secure?: boolean;
39
+ user: string;
40
+ pass: string;
41
+ }
42
+
43
+ export type EmailTemplate = {
44
+ pdfFileName?: string;
45
+ pdfTemplatePath?: string;
46
+ pdfTemplateName?: string;
47
+ emailTemplatePath?: string;
48
+ emailTemplateName?: string;
49
+ subject?: string;
50
+ brandName: string;
51
+ logoUrl?: string;
52
+
53
+ // Colors
54
+ primaryColor?: string;
55
+ secondaryColor?: string;
56
+ backgroundColor?: string;
57
+ textColor?: string;
58
+
59
+ // Typography
60
+ fontFamily?: "inter" | "roboto" | "poppins";
61
+ titleFontSize?: number; // px
62
+ bodyFontSize?: number; // px
63
+ footerFontSize?: number; // px
64
+
65
+ // Layout
66
+ layout?: "compact" | "standard" | "spacious";
67
+ align?: "left" | "center";
68
+
69
+ // Content
70
+ receiptTitle?: string;
71
+ footerText?: string;
72
+ showTxHash?: boolean;
73
+ showTimestamp?: boolean;
74
+ };
package/src/utils.ts ADDED
File without changes
@@ -0,0 +1,13 @@
1
+ import { createClient, http, publicActions, walletActions } from 'viem'
2
+ import { privateKeyToAccount } from 'viem/accounts'
3
+ import { tempoTestnet } from 'viem/chains'
4
+ import { tempoActions } from 'viem/tempo'
5
+
6
+ export const client = createClient({
7
+ account: privateKeyToAccount('0x...'),
8
+ chain: tempoTestnet,
9
+ transport: http(),
10
+ })
11
+ .extend(publicActions)
12
+ .extend(walletActions)
13
+ .extend(tempoActions())
package/tsconfig.json ADDED
@@ -0,0 +1,40 @@
1
+ // {
2
+ // "compilerOptions": {
3
+ // "target": "es2022",
4
+ // "module": "commonjs",
5
+ // "lib": ["es2022", "dom"],
6
+ // "skipLibCheck": true,
7
+ // "strict": true,
8
+ // "esModuleInterop": true
9
+ // }
10
+ // }
11
+
12
+ {
13
+ "compilerOptions": {
14
+ "target": "es2022",
15
+ "module": "ESNext",
16
+ "lib": ["es2022", "dom"],
17
+ "outDir": "./dist",
18
+ "rootDir": "./src",
19
+ "strict": true,
20
+ "esModuleInterop": true,
21
+ "skipLibCheck": true,
22
+ "forceConsistentCasingInFileNames": true,
23
+ "moduleResolution": "node"
24
+ },
25
+ "include": ["src/**/*"],
26
+ "exclude": ["node_modules"]
27
+ }
28
+ // {
29
+ // "compilerOptions": {
30
+ // "target": "ES2020",
31
+ // "module": "ESNext",
32
+ // "moduleResolution": "Node",
33
+ // "outDir": "dist",
34
+ // "declaration": true,
35
+ // "strict": true,
36
+ // "esModuleInterop": true,
37
+ // "skipLibCheck": true
38
+ // },
39
+ // "include": ["src"]
40
+ // }