@faremeter/facilitator 0.7.0 → 0.9.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.
@@ -0,0 +1,4 @@
1
+ export declare function sleep<T>(sleepMs: number, value?: T): Promise<T | undefined>;
2
+ export declare function timeout<T>(timeoutMs: number, msg?: string): Promise<T>;
3
+ export declare function allSettledWithTimeout<T>(promises: readonly Promise<T>[], timeoutMs: number): Promise<PromiseSettledResult<Awaited<Awaited<T>>>[]>;
4
+ //# sourceMappingURL=promise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promise.d.ts","sourceRoot":"","sources":["../../src/promise.ts"],"names":[],"mappings":"AAAA,wBAAgB,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAE3E;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,cAIzD;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,QAAQ,EAAE,SAAS,OAAO,CAAC,CAAC,CAAC,EAAE,EAC/B,SAAS,EAAE,MAAM,wDAOlB"}
@@ -0,0 +1,10 @@
1
+ export function sleep(sleepMs, value) {
2
+ return new Promise((resolve) => setTimeout(() => resolve(value), sleepMs));
3
+ }
4
+ export function timeout(timeoutMs, msg) {
5
+ return new Promise((_, reject) => setTimeout(() => reject(new Error(msg ?? "timed out")), timeoutMs));
6
+ }
7
+ export function allSettledWithTimeout(promises, timeoutMs) {
8
+ const timedPromises = promises.map((p) => Promise.race([p, timeout(timeoutMs, "request timed out")]));
9
+ return Promise.allSettled(timedPromises);
10
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env pnpm tsx
2
+ export {};
3
+ //# sourceMappingURL=promise.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promise.test.d.ts","sourceRoot":"","sources":["../../src/promise.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env pnpm tsx
2
+ import t from "tap";
3
+ import { sleep, allSettledWithTimeout } from "./promise.js";
4
+ async function checkDuration(min, max, f) {
5
+ if (max <= min) {
6
+ throw new Error("max duration must be greater than min");
7
+ }
8
+ const start = Date.now();
9
+ const res = await f();
10
+ const delta = Date.now() - start;
11
+ t.ok(delta >= min, `duration took ${delta} ms, which isn't >= ${min} ms`);
12
+ t.ok(delta <= max, `duration took ${delta} ms, which isn't <= ${max} ms`);
13
+ return res;
14
+ }
15
+ await t.test("checkAllSettledNoTimeout", async (t) => {
16
+ const results = await checkDuration(25, 50, () => allSettledWithTimeout([sleep(10, "good"), sleep(30, "to"), sleep(1, "go")], 100));
17
+ t.equal(results.length, 3);
18
+ results.forEach((x) => {
19
+ if (x === undefined) {
20
+ t.bailout();
21
+ }
22
+ t.matchOnly(x.status, "fulfilled");
23
+ });
24
+ t.matchOnly(results.map((x) => x.value), ["good", "to", "go"]);
25
+ t.pass();
26
+ t.end();
27
+ });
28
+ await t.test("checkAllSettledWithTimeout", async (t) => {
29
+ const results = await checkDuration(20, 125, () => allSettledWithTimeout([sleep(10), sleep(500)], 100));
30
+ t.equal(results.length, 2);
31
+ const [result0, result1] = results;
32
+ if (result0 == undefined) {
33
+ return t.bailout();
34
+ }
35
+ if (result1 == undefined) {
36
+ return t.bailout();
37
+ }
38
+ t.matchOnly(result0.status, "fulfilled");
39
+ t.matchOnly(result1.status, "rejected");
40
+ t.pass();
41
+ t.end();
42
+ });
43
+ await t.test("checkAllSettleException", async (t) => {
44
+ const results = await checkDuration(10, 45, async () => allSettledWithTimeout([
45
+ sleep(10, 42),
46
+ (async () => {
47
+ throw new Error("an error happened");
48
+ })(),
49
+ sleep(15, 1337),
50
+ ], 50));
51
+ t.equal(results.length, 3);
52
+ const [result0, result1, result2] = results;
53
+ if (result0 === undefined) {
54
+ return t.bailout();
55
+ }
56
+ if (result1 === undefined) {
57
+ return t.bailout();
58
+ }
59
+ if (result2 === undefined) {
60
+ return t.bailout();
61
+ }
62
+ if (result0.status !== "fulfilled") {
63
+ return t.fail();
64
+ }
65
+ t.matchOnly(result0.value, 42);
66
+ if (result1.status !== "rejected") {
67
+ return t.fail();
68
+ }
69
+ t.matchOnly(result1.reason, new Error("an error happened"));
70
+ if (result2.status !== "fulfilled") {
71
+ return t.fail();
72
+ }
73
+ t.matchOnly(result2.value, 1337);
74
+ });
@@ -2,6 +2,10 @@ import { Hono } from "hono";
2
2
  import type { FacilitatorHandler } from "@faremeter/types/facilitator";
3
3
  type CreateFacilitatorRoutesArgs = {
4
4
  handlers: FacilitatorHandler[];
5
+ timeout?: {
6
+ getRequirements?: number;
7
+ getSupported?: number;
8
+ };
5
9
  };
6
10
  export declare function createFacilitatorRoutes(args: CreateFacilitatorRoutesArgs): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
7
11
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAgB,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAIvE,KAAK,2BAA2B,GAAG;IACjC,QAAQ,EAAE,kBAAkB,EAAE,CAAC;CAChC,CAAC;AAIF,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,2BAA2B,8EAsGxE"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAgB,MAAM,MAAM,CAAC;AAI1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAKvE,KAAK,2BAA2B,GAAG;IACjC,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,OAAO,CAAC,EAAE;QACR,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,2BAA2B,8EAmLxE"}
@@ -2,7 +2,17 @@ import { getLogger } from "@logtape/logtape";
2
2
  import { Hono } from "hono";
3
3
  import * as x from "@faremeter/types/x402";
4
4
  import { isValidationError } from "@faremeter/types";
5
+ import {} from "@faremeter/types/x402";
6
+ import { allSettledWithTimeout } from "./promise.js";
5
7
  const logger = getLogger(["faremeter", "facilitator"]);
8
+ function summarizeRequirements({ scheme, network, asset, payTo, }) {
9
+ return {
10
+ scheme,
11
+ network,
12
+ asset,
13
+ payTo,
14
+ };
15
+ }
6
16
  export function createFacilitatorRoutes(args) {
7
17
  const router = new Hono();
8
18
  function sendSettleError(c, status, msg) {
@@ -30,6 +40,7 @@ export function createFacilitatorRoutes(args) {
30
40
  if (isValidationError(paymentPayload)) {
31
41
  return sendSettleError(c, 400, `couldn't validate x402 payload: ${paymentPayload.summary}`);
32
42
  }
43
+ logger.debug("starting settlement attempt for request: {*}", x402Req);
33
44
  for (const handler of args.handlers) {
34
45
  let t;
35
46
  try {
@@ -50,21 +61,71 @@ export function createFacilitatorRoutes(args) {
50
61
  if (t === null) {
51
62
  continue;
52
63
  }
64
+ logger.info(`${t.success ? "succeeded" : "failed"} settlement request: {*}`, {
65
+ requirements: summarizeRequirements(x402Req.paymentRequirements),
66
+ txHash: t.txHash,
67
+ });
53
68
  return c.json(t);
54
69
  }
55
70
  sendSettleError(c, 400, "no matching payment handler found");
71
+ logger.warning("attempt to settle was made with no handler found, requirements summary was: {*}", summarizeRequirements(x402Req.paymentRequirements));
56
72
  });
57
73
  router.post("/accepts", async (c) => {
58
74
  const x402Req = x.x402PaymentRequiredResponse(await c.req.json());
59
75
  if (isValidationError(x402Req)) {
60
76
  return sendSettleError(c, 400, `couldn't parse required response: ${x402Req.summary}`);
61
77
  }
62
- const accepts = (await Promise.all(args.handlers.flatMap((x) => x.getRequirements(x402Req.accepts)))).flat();
78
+ const results = await allSettledWithTimeout(args.handlers.flatMap((x) => x.getRequirements(x402Req.accepts)), args.timeout?.getRequirements ?? 500);
79
+ const accepts = results
80
+ .filter((x) => x.status === "fulfilled")
81
+ .map((x) => x.value)
82
+ .flat();
83
+ results.forEach((x) => {
84
+ if (x.status === "rejected") {
85
+ let message;
86
+ if (x.reason instanceof Error) {
87
+ message = x.reason.message;
88
+ }
89
+ else {
90
+ message = "unknown reason";
91
+ }
92
+ logger.error(`failed to retrieve requirements from facilitator handler: ${message}`, x.reason);
93
+ }
94
+ });
95
+ logger.debug(`returning ${accepts.length} accepts: {*}`, {
96
+ accepts: accepts.map(summarizeRequirements),
97
+ });
63
98
  c.status(200);
64
99
  return c.json({
65
100
  x402Version: 1,
66
101
  accepts,
67
102
  });
68
103
  });
104
+ router.get("/supported", async (c) => {
105
+ const results = await allSettledWithTimeout(args.handlers.flatMap((x) => (x.getSupported ? x.getSupported() : [])), args.timeout?.getSupported ?? 500);
106
+ const kinds = results
107
+ .filter((x) => x.status === "fulfilled")
108
+ .map((x) => x.value)
109
+ .flat();
110
+ results.forEach((x) => {
111
+ if (x.status === "rejected") {
112
+ let message;
113
+ if (x.reason instanceof Error) {
114
+ message = x.reason.message;
115
+ }
116
+ else {
117
+ message = "unknown reason";
118
+ }
119
+ logger.error(`failed to retrieve supported from facilitator handler: ${message}`, x.reason);
120
+ }
121
+ });
122
+ logger.debug(`returning ${kinds.length} kinds supported: {*}`, {
123
+ kinds,
124
+ });
125
+ c.status(200);
126
+ return c.json({
127
+ kinds,
128
+ });
129
+ });
69
130
  return router;
70
131
  }