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 +149 -0
- package/dist/client.js +163 -0
- package/dist/constants.js +1 -0
- package/dist/contracts.js +2 -0
- package/dist/index.js +2 -0
- package/dist/mailTemplate.hbs +69 -0
- package/dist/mailer.js +13 -0
- package/dist/pdf.js +19 -0
- package/dist/receipt.hbs +53 -0
- package/dist/render.js +24 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +1 -0
- package/dist/viem.config.js +12 -0
- package/package.json +37 -0
- package/pay_hash-1.0.0.tgz +0 -0
- package/src/client.ts +219 -0
- package/src/constants.ts +1 -0
- package/src/contracts.ts +4 -0
- package/src/index.ts +2 -0
- package/src/mailTemplate.hbs +69 -0
- package/src/mailer.ts +15 -0
- package/src/pdf.ts +27 -0
- package/src/receipt.hbs +53 -0
- package/src/render.ts +42 -0
- package/src/types.ts +74 -0
- package/src/utils.ts +0 -0
- package/src/viem.config.ts +13 -0
- package/tsconfig.json +40 -0
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;
|
package/dist/index.js
ADDED
|
@@ -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
|
+
©
|
|
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
|
+
}
|
package/dist/receipt.hbs
ADDED
|
@@ -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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const TEMPO_TESTNET_CHAIN_ID = 42429;
|
package/src/contracts.ts
ADDED
package/src/index.ts
ADDED
|
@@ -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
|
+
©
|
|
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
|
+
}
|
package/src/receipt.hbs
ADDED
|
@@ -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
|
+
// }
|