@wopr-network/platform-core 1.61.1 → 1.62.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/dist/billing/crypto/watcher-service.js +18 -4
- package/dist/email/client.d.ts +4 -5
- package/dist/email/client.js +20 -7
- package/dist/email/client.test.js +2 -2
- package/dist/email/postmark-transport.d.ts +19 -0
- package/dist/email/postmark-transport.js +45 -0
- package/package.json +2 -1
- package/src/billing/crypto/watcher-service.ts +16 -4
- package/src/email/client.test.ts +2 -2
- package/src/email/client.ts +19 -7
- package/src/email/postmark-transport.ts +59 -0
|
@@ -284,8 +284,15 @@ export async function startWatchers(opts) {
|
|
|
284
284
|
if (!method.rpcUrl)
|
|
285
285
|
continue;
|
|
286
286
|
const rpcCall = createRpcCaller(method.rpcUrl);
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
let latestBlock;
|
|
288
|
+
try {
|
|
289
|
+
const latestHex = (await rpcCall("eth_blockNumber", []));
|
|
290
|
+
latestBlock = Number.parseInt(latestHex, 16);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
log("Skipping ETH watcher — RPC unreachable", { chain: method.chain, token: method.token, error: String(err) });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
289
296
|
const backfillStart = Math.max(0, latestBlock - BACKFILL_BLOCKS);
|
|
290
297
|
const activeAddresses = await chargeStore.listActiveDepositAddresses();
|
|
291
298
|
// Only watch addresses for native charges on this chain (not ERC20 charges)
|
|
@@ -346,8 +353,15 @@ export async function startWatchers(opts) {
|
|
|
346
353
|
if (!method.rpcUrl || !method.contractAddress)
|
|
347
354
|
continue;
|
|
348
355
|
const rpcCall = createRpcCaller(method.rpcUrl);
|
|
349
|
-
|
|
350
|
-
|
|
356
|
+
let latestBlock;
|
|
357
|
+
try {
|
|
358
|
+
const latestHex = (await rpcCall("eth_blockNumber", []));
|
|
359
|
+
latestBlock = Number.parseInt(latestHex, 16);
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
log("Skipping EVM watcher — RPC unreachable", { chain: method.chain, token: method.token, error: String(err) });
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
351
365
|
const activeAddresses = await chargeStore.listActiveDepositAddresses();
|
|
352
366
|
const chainAddresses = activeAddresses.filter((a) => a.chain === method.chain).map((a) => a.address);
|
|
353
367
|
const watcher = new EvmWatcher({
|
package/dist/email/client.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Email Client — Template-based transactional email sender.
|
|
3
3
|
*
|
|
4
|
-
* Supports
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* SES takes priority when both are configured.
|
|
4
|
+
* Supports three backends (first match wins):
|
|
5
|
+
* 1. **AWS SES**: Set AWS_SES_REGION env var (+ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
|
|
6
|
+
* 2. **Postmark**: Set POSTMARK_API_KEY env var
|
|
7
|
+
* 3. **Resend**: Set RESEND_API_KEY env var
|
|
9
8
|
*/
|
|
10
9
|
export interface EmailClientConfig {
|
|
11
10
|
apiKey: string;
|
package/dist/email/client.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Email Client — Template-based transactional email sender.
|
|
3
3
|
*
|
|
4
|
-
* Supports
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* SES takes priority when both are configured.
|
|
4
|
+
* Supports three backends (first match wins):
|
|
5
|
+
* 1. **AWS SES**: Set AWS_SES_REGION env var (+ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
|
|
6
|
+
* 2. **Postmark**: Set POSTMARK_API_KEY env var
|
|
7
|
+
* 3. **Resend**: Set RESEND_API_KEY env var
|
|
9
8
|
*/
|
|
10
9
|
import { Resend } from "resend";
|
|
11
10
|
import { logger } from "../config/logger.js";
|
|
11
|
+
import { PostmarkTransport } from "./postmark-transport.js";
|
|
12
12
|
import { SesTransport } from "./ses-transport.js";
|
|
13
13
|
/**
|
|
14
14
|
* Transactional email client with pluggable transport.
|
|
@@ -94,7 +94,8 @@ class ResendTransport {
|
|
|
94
94
|
*
|
|
95
95
|
* Backend selection (first match wins):
|
|
96
96
|
* 1. AWS SES — AWS_SES_REGION is set
|
|
97
|
-
* 2.
|
|
97
|
+
* 2. Postmark — POSTMARK_API_KEY is set
|
|
98
|
+
* 3. Resend — RESEND_API_KEY is set
|
|
98
99
|
*
|
|
99
100
|
* Common env vars:
|
|
100
101
|
* - EMAIL_FROM (default: "noreply@wopr.bot") — sender address
|
|
@@ -105,6 +106,9 @@ class ResendTransport {
|
|
|
105
106
|
* - AWS_ACCESS_KEY_ID
|
|
106
107
|
* - AWS_SECRET_ACCESS_KEY
|
|
107
108
|
*
|
|
109
|
+
* Postmark env vars:
|
|
110
|
+
* - POSTMARK_API_KEY (server token from Postmark dashboard)
|
|
111
|
+
*
|
|
108
112
|
* Resend env vars:
|
|
109
113
|
* - RESEND_API_KEY
|
|
110
114
|
*
|
|
@@ -129,10 +133,19 @@ export function getEmailClient() {
|
|
|
129
133
|
_client = new EmailClient(transport);
|
|
130
134
|
logger.info("Email client initialized with AWS SES", { region: sesRegion, from });
|
|
131
135
|
}
|
|
136
|
+
else if (process.env.POSTMARK_API_KEY) {
|
|
137
|
+
const transport = new PostmarkTransport({
|
|
138
|
+
apiKey: process.env.POSTMARK_API_KEY,
|
|
139
|
+
from,
|
|
140
|
+
replyTo,
|
|
141
|
+
});
|
|
142
|
+
_client = new EmailClient(transport);
|
|
143
|
+
logger.info("Email client initialized with Postmark", { from });
|
|
144
|
+
}
|
|
132
145
|
else {
|
|
133
146
|
const apiKey = process.env.RESEND_API_KEY;
|
|
134
147
|
if (!apiKey) {
|
|
135
|
-
throw new Error("
|
|
148
|
+
throw new Error("Set AWS_SES_REGION, POSTMARK_API_KEY, or RESEND_API_KEY environment variable");
|
|
136
149
|
}
|
|
137
150
|
_client = new EmailClient({ apiKey, from, replyTo });
|
|
138
151
|
logger.info("Email client initialized with Resend", { from });
|
|
@@ -91,8 +91,8 @@ describe("getEmailClient / setEmailClient / resetEmailClient", () => {
|
|
|
91
91
|
delete process.env.RESEND_FROM;
|
|
92
92
|
delete process.env.RESEND_REPLY_TO;
|
|
93
93
|
});
|
|
94
|
-
it("should throw if
|
|
95
|
-
expect(() => getEmailClient()).toThrow("
|
|
94
|
+
it("should throw if no email provider is configured", () => {
|
|
95
|
+
expect(() => getEmailClient()).toThrow("Set AWS_SES_REGION, POSTMARK_API_KEY, or RESEND_API_KEY");
|
|
96
96
|
});
|
|
97
97
|
it("should create client from env vars", () => {
|
|
98
98
|
process.env.RESEND_API_KEY = "re_test123";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postmark email transport.
|
|
3
|
+
*
|
|
4
|
+
* Env vars:
|
|
5
|
+
* POSTMARK_API_KEY — server API token from Postmark
|
|
6
|
+
*/
|
|
7
|
+
import type { EmailSendResult, EmailTransport, SendTemplateEmailOpts } from "./client.js";
|
|
8
|
+
export interface PostmarkTransportConfig {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
from: string;
|
|
11
|
+
replyTo?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class PostmarkTransport implements EmailTransport {
|
|
14
|
+
private client;
|
|
15
|
+
private from;
|
|
16
|
+
private replyTo;
|
|
17
|
+
constructor(config: PostmarkTransportConfig);
|
|
18
|
+
send(opts: SendTemplateEmailOpts): Promise<EmailSendResult>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postmark email transport.
|
|
3
|
+
*
|
|
4
|
+
* Env vars:
|
|
5
|
+
* POSTMARK_API_KEY — server API token from Postmark
|
|
6
|
+
*/
|
|
7
|
+
import { ServerClient } from "postmark";
|
|
8
|
+
import { logger } from "../config/logger.js";
|
|
9
|
+
export class PostmarkTransport {
|
|
10
|
+
client;
|
|
11
|
+
from;
|
|
12
|
+
replyTo;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.client = new ServerClient(config.apiKey);
|
|
15
|
+
this.from = config.from;
|
|
16
|
+
this.replyTo = config.replyTo;
|
|
17
|
+
}
|
|
18
|
+
async send(opts) {
|
|
19
|
+
const result = await this.client.sendEmail({
|
|
20
|
+
From: this.from,
|
|
21
|
+
To: opts.to,
|
|
22
|
+
Subject: opts.subject,
|
|
23
|
+
HtmlBody: opts.html,
|
|
24
|
+
TextBody: opts.text,
|
|
25
|
+
ReplyTo: this.replyTo,
|
|
26
|
+
MessageStream: "outbound",
|
|
27
|
+
});
|
|
28
|
+
if (result.ErrorCode !== 0) {
|
|
29
|
+
logger.error("Failed to send email via Postmark", {
|
|
30
|
+
to: opts.to,
|
|
31
|
+
template: opts.templateName,
|
|
32
|
+
error: result.Message,
|
|
33
|
+
code: result.ErrorCode,
|
|
34
|
+
});
|
|
35
|
+
throw new Error(`Postmark error ${result.ErrorCode}: ${result.Message}`);
|
|
36
|
+
}
|
|
37
|
+
logger.info("Email sent via Postmark", {
|
|
38
|
+
emailId: result.MessageID,
|
|
39
|
+
to: opts.to,
|
|
40
|
+
template: opts.templateName,
|
|
41
|
+
userId: opts.userId,
|
|
42
|
+
});
|
|
43
|
+
return { id: result.MessageID, success: true };
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wopr-network/platform-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.62.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
"@scure/bip39": "^2.0.1",
|
|
139
139
|
"handlebars": "^4.7.8",
|
|
140
140
|
"js-yaml": "^4.1.1",
|
|
141
|
+
"postmark": "^4.0.7",
|
|
141
142
|
"viem": "^2.47.4",
|
|
142
143
|
"yaml": "^2.8.2"
|
|
143
144
|
}
|
|
@@ -373,8 +373,14 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
|
|
|
373
373
|
if (!method.rpcUrl) continue;
|
|
374
374
|
|
|
375
375
|
const rpcCall = createRpcCaller(method.rpcUrl);
|
|
376
|
-
|
|
377
|
-
|
|
376
|
+
let latestBlock: number;
|
|
377
|
+
try {
|
|
378
|
+
const latestHex = (await rpcCall("eth_blockNumber", [])) as string;
|
|
379
|
+
latestBlock = Number.parseInt(latestHex, 16);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
log("Skipping ETH watcher — RPC unreachable", { chain: method.chain, token: method.token, error: String(err) });
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
378
384
|
const backfillStart = Math.max(0, latestBlock - BACKFILL_BLOCKS);
|
|
379
385
|
|
|
380
386
|
const activeAddresses = await chargeStore.listActiveDepositAddresses();
|
|
@@ -446,8 +452,14 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
|
|
|
446
452
|
if (!method.rpcUrl || !method.contractAddress) continue;
|
|
447
453
|
|
|
448
454
|
const rpcCall = createRpcCaller(method.rpcUrl);
|
|
449
|
-
|
|
450
|
-
|
|
455
|
+
let latestBlock: number;
|
|
456
|
+
try {
|
|
457
|
+
const latestHex = (await rpcCall("eth_blockNumber", [])) as string;
|
|
458
|
+
latestBlock = Number.parseInt(latestHex, 16);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
log("Skipping EVM watcher — RPC unreachable", { chain: method.chain, token: method.token, error: String(err) });
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
451
463
|
|
|
452
464
|
const activeAddresses = await chargeStore.listActiveDepositAddresses();
|
|
453
465
|
const chainAddresses = activeAddresses.filter((a) => a.chain === method.chain).map((a) => a.address);
|
package/src/email/client.test.ts
CHANGED
|
@@ -116,8 +116,8 @@ describe("getEmailClient / setEmailClient / resetEmailClient", () => {
|
|
|
116
116
|
delete process.env.RESEND_REPLY_TO;
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
-
it("should throw if
|
|
120
|
-
expect(() => getEmailClient()).toThrow("
|
|
119
|
+
it("should throw if no email provider is configured", () => {
|
|
120
|
+
expect(() => getEmailClient()).toThrow("Set AWS_SES_REGION, POSTMARK_API_KEY, or RESEND_API_KEY");
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it("should create client from env vars", () => {
|
package/src/email/client.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Email Client — Template-based transactional email sender.
|
|
3
3
|
*
|
|
4
|
-
* Supports
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* SES takes priority when both are configured.
|
|
4
|
+
* Supports three backends (first match wins):
|
|
5
|
+
* 1. **AWS SES**: Set AWS_SES_REGION env var (+ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
|
|
6
|
+
* 2. **Postmark**: Set POSTMARK_API_KEY env var
|
|
7
|
+
* 3. **Resend**: Set RESEND_API_KEY env var
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import { Resend } from "resend";
|
|
12
11
|
import { logger } from "../config/logger.js";
|
|
12
|
+
import { PostmarkTransport } from "./postmark-transport.js";
|
|
13
13
|
import { SesTransport } from "./ses-transport.js";
|
|
14
14
|
|
|
15
15
|
export interface EmailClientConfig {
|
|
@@ -134,7 +134,8 @@ class ResendTransport implements EmailTransport {
|
|
|
134
134
|
*
|
|
135
135
|
* Backend selection (first match wins):
|
|
136
136
|
* 1. AWS SES — AWS_SES_REGION is set
|
|
137
|
-
* 2.
|
|
137
|
+
* 2. Postmark — POSTMARK_API_KEY is set
|
|
138
|
+
* 3. Resend — RESEND_API_KEY is set
|
|
138
139
|
*
|
|
139
140
|
* Common env vars:
|
|
140
141
|
* - EMAIL_FROM (default: "noreply@wopr.bot") — sender address
|
|
@@ -145,6 +146,9 @@ class ResendTransport implements EmailTransport {
|
|
|
145
146
|
* - AWS_ACCESS_KEY_ID
|
|
146
147
|
* - AWS_SECRET_ACCESS_KEY
|
|
147
148
|
*
|
|
149
|
+
* Postmark env vars:
|
|
150
|
+
* - POSTMARK_API_KEY (server token from Postmark dashboard)
|
|
151
|
+
*
|
|
148
152
|
* Resend env vars:
|
|
149
153
|
* - RESEND_API_KEY
|
|
150
154
|
*
|
|
@@ -170,10 +174,18 @@ export function getEmailClient(): EmailClient {
|
|
|
170
174
|
});
|
|
171
175
|
_client = new EmailClient(transport);
|
|
172
176
|
logger.info("Email client initialized with AWS SES", { region: sesRegion, from });
|
|
177
|
+
} else if (process.env.POSTMARK_API_KEY) {
|
|
178
|
+
const transport = new PostmarkTransport({
|
|
179
|
+
apiKey: process.env.POSTMARK_API_KEY,
|
|
180
|
+
from,
|
|
181
|
+
replyTo,
|
|
182
|
+
});
|
|
183
|
+
_client = new EmailClient(transport);
|
|
184
|
+
logger.info("Email client initialized with Postmark", { from });
|
|
173
185
|
} else {
|
|
174
186
|
const apiKey = process.env.RESEND_API_KEY;
|
|
175
187
|
if (!apiKey) {
|
|
176
|
-
throw new Error("
|
|
188
|
+
throw new Error("Set AWS_SES_REGION, POSTMARK_API_KEY, or RESEND_API_KEY environment variable");
|
|
177
189
|
}
|
|
178
190
|
_client = new EmailClient({ apiKey, from, replyTo });
|
|
179
191
|
logger.info("Email client initialized with Resend", { from });
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postmark email transport.
|
|
3
|
+
*
|
|
4
|
+
* Env vars:
|
|
5
|
+
* POSTMARK_API_KEY — server API token from Postmark
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ServerClient } from "postmark";
|
|
9
|
+
import { logger } from "../config/logger.js";
|
|
10
|
+
import type { EmailSendResult, EmailTransport, SendTemplateEmailOpts } from "./client.js";
|
|
11
|
+
|
|
12
|
+
export interface PostmarkTransportConfig {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
from: string;
|
|
15
|
+
replyTo?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class PostmarkTransport implements EmailTransport {
|
|
19
|
+
private client: ServerClient;
|
|
20
|
+
private from: string;
|
|
21
|
+
private replyTo: string | undefined;
|
|
22
|
+
|
|
23
|
+
constructor(config: PostmarkTransportConfig) {
|
|
24
|
+
this.client = new ServerClient(config.apiKey);
|
|
25
|
+
this.from = config.from;
|
|
26
|
+
this.replyTo = config.replyTo;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async send(opts: SendTemplateEmailOpts): Promise<EmailSendResult> {
|
|
30
|
+
const result = await this.client.sendEmail({
|
|
31
|
+
From: this.from,
|
|
32
|
+
To: opts.to,
|
|
33
|
+
Subject: opts.subject,
|
|
34
|
+
HtmlBody: opts.html,
|
|
35
|
+
TextBody: opts.text,
|
|
36
|
+
ReplyTo: this.replyTo,
|
|
37
|
+
MessageStream: "outbound",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (result.ErrorCode !== 0) {
|
|
41
|
+
logger.error("Failed to send email via Postmark", {
|
|
42
|
+
to: opts.to,
|
|
43
|
+
template: opts.templateName,
|
|
44
|
+
error: result.Message,
|
|
45
|
+
code: result.ErrorCode,
|
|
46
|
+
});
|
|
47
|
+
throw new Error(`Postmark error ${result.ErrorCode}: ${result.Message}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
logger.info("Email sent via Postmark", {
|
|
51
|
+
emailId: result.MessageID,
|
|
52
|
+
to: opts.to,
|
|
53
|
+
template: opts.templateName,
|
|
54
|
+
userId: opts.userId,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { id: result.MessageID, success: true };
|
|
58
|
+
}
|
|
59
|
+
}
|