@v3finance/monitoring-sdk 0.0.0-stage → 0.2.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.
Files changed (149) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +322 -2
  4. package/dist/cjs/client.d.ts +46 -0
  5. package/dist/cjs/client.d.ts.map +1 -0
  6. package/dist/cjs/client.js +80 -0
  7. package/dist/cjs/client.js.map +1 -0
  8. package/dist/cjs/core/errors.d.ts +64 -0
  9. package/dist/cjs/core/errors.d.ts.map +1 -0
  10. package/dist/cjs/core/errors.js +95 -0
  11. package/dist/cjs/core/errors.js.map +1 -0
  12. package/dist/cjs/core/http.d.ts +58 -0
  13. package/dist/cjs/core/http.d.ts.map +1 -0
  14. package/dist/cjs/core/http.js +143 -0
  15. package/dist/cjs/core/http.js.map +1 -0
  16. package/dist/cjs/core/pagination.d.ts +12 -0
  17. package/dist/cjs/core/pagination.d.ts.map +1 -0
  18. package/dist/cjs/core/pagination.js +15 -0
  19. package/dist/cjs/core/pagination.js.map +1 -0
  20. package/dist/cjs/core/scope.d.ts +8 -0
  21. package/dist/cjs/core/scope.d.ts.map +1 -0
  22. package/dist/cjs/core/scope.js +3 -0
  23. package/dist/cjs/core/scope.js.map +1 -0
  24. package/dist/cjs/generated/types.d.ts +336 -0
  25. package/dist/cjs/generated/types.d.ts.map +1 -0
  26. package/dist/cjs/generated/types.js +5 -0
  27. package/dist/cjs/generated/types.js.map +1 -0
  28. package/dist/cjs/index.d.ts +23 -0
  29. package/dist/cjs/index.d.ts.map +1 -0
  30. package/dist/cjs/index.js +47 -0
  31. package/dist/cjs/index.js.map +1 -0
  32. package/dist/cjs/package.json +1 -0
  33. package/dist/cjs/resources/account.d.ts +22 -0
  34. package/dist/cjs/resources/account.d.ts.map +1 -0
  35. package/dist/cjs/resources/account.js +31 -0
  36. package/dist/cjs/resources/account.js.map +1 -0
  37. package/dist/cjs/resources/addresses.d.ts +93 -0
  38. package/dist/cjs/resources/addresses.d.ts.map +1 -0
  39. package/dist/cjs/resources/addresses.js +128 -0
  40. package/dist/cjs/resources/addresses.js.map +1 -0
  41. package/dist/cjs/resources/catalog.d.ts +37 -0
  42. package/dist/cjs/resources/catalog.d.ts.map +1 -0
  43. package/dist/cjs/resources/catalog.js +52 -0
  44. package/dist/cjs/resources/catalog.js.map +1 -0
  45. package/dist/cjs/resources/deliveries.d.ts +61 -0
  46. package/dist/cjs/resources/deliveries.d.ts.map +1 -0
  47. package/dist/cjs/resources/deliveries.js +53 -0
  48. package/dist/cjs/resources/deliveries.js.map +1 -0
  49. package/dist/cjs/resources/transactions.d.ts +28 -0
  50. package/dist/cjs/resources/transactions.d.ts.map +1 -0
  51. package/dist/cjs/resources/transactions.js +34 -0
  52. package/dist/cjs/resources/transactions.js.map +1 -0
  53. package/dist/cjs/resources/webhooks.d.ts +22 -0
  54. package/dist/cjs/resources/webhooks.d.ts.map +1 -0
  55. package/dist/cjs/resources/webhooks.js +35 -0
  56. package/dist/cjs/resources/webhooks.js.map +1 -0
  57. package/dist/cjs/units.d.ts +29 -0
  58. package/dist/cjs/units.d.ts.map +1 -0
  59. package/dist/cjs/units.js +62 -0
  60. package/dist/cjs/units.js.map +1 -0
  61. package/dist/cjs/version.d.ts +5 -0
  62. package/dist/cjs/version.d.ts.map +1 -0
  63. package/dist/cjs/version.js +8 -0
  64. package/dist/cjs/version.js.map +1 -0
  65. package/dist/cjs/webhooks.d.ts +82 -0
  66. package/dist/cjs/webhooks.d.ts.map +1 -0
  67. package/dist/cjs/webhooks.js +133 -0
  68. package/dist/cjs/webhooks.js.map +1 -0
  69. package/dist/esm/client.d.ts +46 -0
  70. package/dist/esm/client.d.ts.map +1 -0
  71. package/dist/esm/client.js +76 -0
  72. package/dist/esm/client.js.map +1 -0
  73. package/dist/esm/core/errors.d.ts +64 -0
  74. package/dist/esm/core/errors.d.ts.map +1 -0
  75. package/dist/esm/core/errors.js +86 -0
  76. package/dist/esm/core/errors.js.map +1 -0
  77. package/dist/esm/core/http.d.ts +58 -0
  78. package/dist/esm/core/http.d.ts.map +1 -0
  79. package/dist/esm/core/http.js +138 -0
  80. package/dist/esm/core/http.js.map +1 -0
  81. package/dist/esm/core/pagination.d.ts +12 -0
  82. package/dist/esm/core/pagination.d.ts.map +1 -0
  83. package/dist/esm/core/pagination.js +12 -0
  84. package/dist/esm/core/pagination.js.map +1 -0
  85. package/dist/esm/core/scope.d.ts +8 -0
  86. package/dist/esm/core/scope.d.ts.map +1 -0
  87. package/dist/esm/core/scope.js +2 -0
  88. package/dist/esm/core/scope.js.map +1 -0
  89. package/dist/esm/generated/types.d.ts +336 -0
  90. package/dist/esm/generated/types.d.ts.map +1 -0
  91. package/dist/esm/generated/types.js +4 -0
  92. package/dist/esm/generated/types.js.map +1 -0
  93. package/dist/esm/index.d.ts +23 -0
  94. package/dist/esm/index.d.ts.map +1 -0
  95. package/dist/esm/index.js +20 -0
  96. package/dist/esm/index.js.map +1 -0
  97. package/dist/esm/resources/account.d.ts +22 -0
  98. package/dist/esm/resources/account.d.ts.map +1 -0
  99. package/dist/esm/resources/account.js +27 -0
  100. package/dist/esm/resources/account.js.map +1 -0
  101. package/dist/esm/resources/addresses.d.ts +93 -0
  102. package/dist/esm/resources/addresses.d.ts.map +1 -0
  103. package/dist/esm/resources/addresses.js +124 -0
  104. package/dist/esm/resources/addresses.js.map +1 -0
  105. package/dist/esm/resources/catalog.d.ts +37 -0
  106. package/dist/esm/resources/catalog.d.ts.map +1 -0
  107. package/dist/esm/resources/catalog.js +47 -0
  108. package/dist/esm/resources/catalog.js.map +1 -0
  109. package/dist/esm/resources/deliveries.d.ts +61 -0
  110. package/dist/esm/resources/deliveries.d.ts.map +1 -0
  111. package/dist/esm/resources/deliveries.js +49 -0
  112. package/dist/esm/resources/deliveries.js.map +1 -0
  113. package/dist/esm/resources/transactions.d.ts +28 -0
  114. package/dist/esm/resources/transactions.d.ts.map +1 -0
  115. package/dist/esm/resources/transactions.js +30 -0
  116. package/dist/esm/resources/transactions.js.map +1 -0
  117. package/dist/esm/resources/webhooks.d.ts +22 -0
  118. package/dist/esm/resources/webhooks.d.ts.map +1 -0
  119. package/dist/esm/resources/webhooks.js +31 -0
  120. package/dist/esm/resources/webhooks.js.map +1 -0
  121. package/dist/esm/units.d.ts +29 -0
  122. package/dist/esm/units.d.ts.map +1 -0
  123. package/dist/esm/units.js +58 -0
  124. package/dist/esm/units.js.map +1 -0
  125. package/dist/esm/version.d.ts +5 -0
  126. package/dist/esm/version.d.ts.map +1 -0
  127. package/dist/esm/version.js +5 -0
  128. package/dist/esm/version.js.map +1 -0
  129. package/dist/esm/webhooks.d.ts +82 -0
  130. package/dist/esm/webhooks.d.ts.map +1 -0
  131. package/dist/esm/webhooks.js +125 -0
  132. package/dist/esm/webhooks.js.map +1 -0
  133. package/package.json +69 -4
  134. package/src/client.ts +83 -0
  135. package/src/core/errors.ts +99 -0
  136. package/src/core/http.ts +183 -0
  137. package/src/core/pagination.ts +20 -0
  138. package/src/core/scope.ts +8 -0
  139. package/src/generated/types.ts +369 -0
  140. package/src/index.ts +43 -0
  141. package/src/resources/account.ts +33 -0
  142. package/src/resources/addresses.ts +165 -0
  143. package/src/resources/catalog.ts +56 -0
  144. package/src/resources/deliveries.ts +82 -0
  145. package/src/resources/transactions.ts +34 -0
  146. package/src/resources/webhooks.ts +36 -0
  147. package/src/units.ts +56 -0
  148. package/src/version.ts +5 -0
  149. package/src/webhooks.ts +138 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Receiving webhooks: signature verification and parsing. No API key or network access is needed — only your
3
+ * webhook secret. Uses Web Crypto, so it runs on Node.js 20+, Bun, Deno and Cloudflare Workers.
4
+ */
5
+ import type { WebhookPayload } from './generated/types.js';
6
+ /** Headers sent with every webhook. */
7
+ export declare const WEBHOOK_HEADERS: {
8
+ /** HMAC-SHA256 of the raw body keyed with your webhook secret, hex-encoded. */
9
+ readonly signature: 'x-signature';
10
+ /** Your client ID. */
11
+ readonly clientId: 'x-client-id';
12
+ /** `1` for test webhooks. */
13
+ readonly test: 'x-webhook-test';
14
+ };
15
+ /**
16
+ * The raw request body, exactly as received. Do not parse and re-serialize the JSON: the signature is computed over
17
+ * the original bytes.
18
+ */
19
+ export type WebhookBody = string | Uint8Array | ArrayBuffer;
20
+ /**
21
+ * Where to read the signature from: the value of the `x-signature` header, or all request headers — a Fetch
22
+ * `Headers` object (Next.js, Hono, Cloudflare Workers) or a Node.js headers object (`req.headers` in Express, Fastify).
23
+ */
24
+ export type SignatureSource = string | null | undefined | Headers | Record<string, string | string[] | undefined>;
25
+ /**
26
+ * Computes the signature of a body the way the API does. Useful for testing your webhook handler.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const body = JSON.stringify({ tx: { network: 'TRON', asset: 'USDT', … } })
31
+ * await fetch('http://localhost:3000/webhooks/monitoring', {
32
+ * method: 'POST',
33
+ * body,
34
+ * headers: { 'content-type': 'application/json', 'x-signature': await signWebhookBody(body, secret) },
35
+ * })
36
+ * ```
37
+ */
38
+ export declare function signWebhookBody(body: WebhookBody, secret: string): Promise<string>;
39
+ /**
40
+ * Verifies a webhook signature. Returns `true` when the body was sent by the API and was not modified.
41
+ *
42
+ * @param body - Raw request body.
43
+ * @param signature - The `x-signature` header, or all request headers.
44
+ * @param secret - Your webhook secret.
45
+ */
46
+ export declare function verifyWebhookSignature(body: WebhookBody, signature: SignatureSource, secret: string): Promise<boolean>;
47
+ /**
48
+ * Verifies the signature and returns the parsed webhook.
49
+ *
50
+ * Respond with a 2xx status within 15 seconds. Process webhooks idempotently by {@link webhookIdempotencyKey} — the
51
+ * same webhook may arrive more than once — and do not credit test webhooks ({@link isTestWebhook}). Webhooks are sent
52
+ * as soon as a transfer is included in a block; before crediting large amounts, check that the transaction is final
53
+ * with `monitoring.transactions.get()`.
54
+ *
55
+ * @throws {@link WebhookSignatureError} when the signature does not match. Respond with `401` and do not process it.
56
+ *
57
+ * @example Express (use the raw body)
58
+ * ```ts
59
+ * app.post('/webhooks/monitoring', express.raw({ type: 'application/json' }), async (req, res) => {
60
+ * const event = await parseWebhook(req.body, req.headers, process.env.MONITORING_WEBHOOK_SECRET!)
61
+ * if (!isTestWebhook(event)) await creditOnce(webhookIdempotencyKey(event), event.tx)
62
+ * res.sendStatus(200)
63
+ * })
64
+ * ```
65
+ *
66
+ * @example Next.js route handlers, Hono, Cloudflare Workers
67
+ * ```ts
68
+ * const event = await parseWebhook(await request.text(), request.headers, env.MONITORING_WEBHOOK_SECRET)
69
+ * ```
70
+ */
71
+ export declare function parseWebhook(body: WebhookBody, signature: SignatureSource, secret: string): Promise<WebhookPayload>;
72
+ /** `true` for test webhooks. Do not credit them. */
73
+ export declare function isTestWebhook(event: WebhookPayload): boolean;
74
+ /**
75
+ * Idempotency key of a webhook: network, transaction hash and log index. A transfer always has the same key, however
76
+ * many times its webhook is delivered. An EVM transaction can contain several token transfers; they differ by
77
+ * `logIndex`.
78
+ *
79
+ * @example `"TRON:1249cb…a5fc45:"`, `"ETHEREUM:0x3fbc…1d57:7"`
80
+ */
81
+ export declare function webhookIdempotencyKey(event: WebhookPayload): string;
82
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../src/webhooks.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D,uCAAuC;AACvC,eAAO,MAAM,eAAe;IAC1B,+EAA+E;aAC/E,SAAS,EAAE,aAAa;IACxB,sBAAsB;aACtB,QAAQ,EAAE,aAAa;IACvB,6BAA6B;aAC7B,IAAI,EAAE,gBAAgB;CACd,CAAA;AAEV;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,CAAA;AAE3D;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;AA0BjH;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAIxF;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAI5H;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAWzH;AAED,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE5D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAEnE"}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Receiving webhooks: signature verification and parsing. No API key or network access is needed — only your
3
+ * webhook secret. Uses Web Crypto, so it runs on Node.js 20+, Bun, Deno and Cloudflare Workers.
4
+ */
5
+ import { MonitoringError, WebhookSignatureError } from './core/errors.js';
6
+ /** Headers sent with every webhook. */
7
+ export const WEBHOOK_HEADERS = {
8
+ /** HMAC-SHA256 of the raw body keyed with your webhook secret, hex-encoded. */
9
+ signature: 'x-signature',
10
+ /** Your client ID. */
11
+ clientId: 'x-client-id',
12
+ /** `1` for test webhooks. */
13
+ test: 'x-webhook-test',
14
+ };
15
+ const encoder = new TextEncoder();
16
+ function bytesOf(body) {
17
+ if (typeof body === 'string')
18
+ return encoder.encode(body);
19
+ // copy: Web Crypto does not accept views over a SharedArrayBuffer (a Node.js Buffer is a Uint8Array too)
20
+ return body instanceof Uint8Array ? new Uint8Array(body) : new Uint8Array(body);
21
+ }
22
+ function signatureOf(source) {
23
+ if (source === null || source === undefined)
24
+ return undefined;
25
+ if (typeof source === 'string')
26
+ return source;
27
+ if (typeof source.get === 'function')
28
+ return source.get(WEBHOOK_HEADERS.signature) ?? undefined;
29
+ const value = source[WEBHOOK_HEADERS.signature];
30
+ return Array.isArray(value) ? value[0] : value;
31
+ }
32
+ /** Constant-time comparison. */
33
+ function safeEqual(a, b) {
34
+ if (a.length !== b.length)
35
+ return false;
36
+ let diff = 0;
37
+ for (let i = 0; i < a.length; i++)
38
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
39
+ return diff === 0;
40
+ }
41
+ /**
42
+ * Computes the signature of a body the way the API does. Useful for testing your webhook handler.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * const body = JSON.stringify({ tx: { network: 'TRON', asset: 'USDT', … } })
47
+ * await fetch('http://localhost:3000/webhooks/monitoring', {
48
+ * method: 'POST',
49
+ * body,
50
+ * headers: { 'content-type': 'application/json', 'x-signature': await signWebhookBody(body, secret) },
51
+ * })
52
+ * ```
53
+ */
54
+ export async function signWebhookBody(body, secret) {
55
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
56
+ const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, bytesOf(body)));
57
+ return Array.from(mac, (b) => b.toString(16).padStart(2, '0')).join('');
58
+ }
59
+ /**
60
+ * Verifies a webhook signature. Returns `true` when the body was sent by the API and was not modified.
61
+ *
62
+ * @param body - Raw request body.
63
+ * @param signature - The `x-signature` header, or all request headers.
64
+ * @param secret - Your webhook secret.
65
+ */
66
+ export async function verifyWebhookSignature(body, signature, secret) {
67
+ const given = signatureOf(signature)?.trim().toLowerCase();
68
+ if (!given)
69
+ return false;
70
+ return safeEqual(await signWebhookBody(body, secret), given);
71
+ }
72
+ /**
73
+ * Verifies the signature and returns the parsed webhook.
74
+ *
75
+ * Respond with a 2xx status within 15 seconds. Process webhooks idempotently by {@link webhookIdempotencyKey} — the
76
+ * same webhook may arrive more than once — and do not credit test webhooks ({@link isTestWebhook}). Webhooks are sent
77
+ * as soon as a transfer is included in a block; before crediting large amounts, check that the transaction is final
78
+ * with `monitoring.transactions.get()`.
79
+ *
80
+ * @throws {@link WebhookSignatureError} when the signature does not match. Respond with `401` and do not process it.
81
+ *
82
+ * @example Express (use the raw body)
83
+ * ```ts
84
+ * app.post('/webhooks/monitoring', express.raw({ type: 'application/json' }), async (req, res) => {
85
+ * const event = await parseWebhook(req.body, req.headers, process.env.MONITORING_WEBHOOK_SECRET!)
86
+ * if (!isTestWebhook(event)) await creditOnce(webhookIdempotencyKey(event), event.tx)
87
+ * res.sendStatus(200)
88
+ * })
89
+ * ```
90
+ *
91
+ * @example Next.js route handlers, Hono, Cloudflare Workers
92
+ * ```ts
93
+ * const event = await parseWebhook(await request.text(), request.headers, env.MONITORING_WEBHOOK_SECRET)
94
+ * ```
95
+ */
96
+ export async function parseWebhook(body, signature, secret) {
97
+ if (!(await verifyWebhookSignature(body, signature, secret)))
98
+ throw new WebhookSignatureError();
99
+ let payload;
100
+ try {
101
+ payload = JSON.parse(typeof body === 'string' ? body : new TextDecoder().decode(bytesOf(body)));
102
+ }
103
+ catch (error) {
104
+ throw new MonitoringError('webhook body is not valid JSON', { cause: error });
105
+ }
106
+ const tx = payload?.tx;
107
+ if (typeof tx !== 'object' || tx === null || typeof tx.id !== 'string')
108
+ throw new MonitoringError('webhook body has no tx');
109
+ return payload;
110
+ }
111
+ /** `true` for test webhooks. Do not credit them. */
112
+ export function isTestWebhook(event) {
113
+ return event.test === true;
114
+ }
115
+ /**
116
+ * Idempotency key of a webhook: network, transaction hash and log index. A transfer always has the same key, however
117
+ * many times its webhook is delivered. An EVM transaction can contain several token transfers; they differ by
118
+ * `logIndex`.
119
+ *
120
+ * @example `"TRON:1249cb…a5fc45:"`, `"ETHEREUM:0x3fbc…1d57:7"`
121
+ */
122
+ export function webhookIdempotencyKey(event) {
123
+ return `${event.tx.network}:${event.tx.id}:${event.tx.logIndex ?? ''}`;
124
+ }
125
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/webhooks.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@v3finance/monitoring-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Official TypeScript SDK for the V3 Finance Monitoring API — monitor TRON and EVM addresses and receive signed webhooks for every transfer.",
5
+ "keywords": [
6
+ "v3finance",
7
+ "monitoring",
8
+ "webhooks",
9
+ "tron",
10
+ "ethereum",
11
+ "evm",
12
+ "usdt",
13
+ "usdc",
14
+ "trc20",
15
+ "erc20",
16
+ "blockchain",
17
+ "payments",
18
+ "crypto",
19
+ "sdk",
20
+ "typescript"
21
+ ],
22
+ "homepage": "https://monitoring.v3.finance/docs",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/v3-finance/monitoring-sdk.git"
26
+ },
27
+ "license": "MIT",
28
+ "author": "V3 Finance",
29
+ "type": "module",
30
+ "sideEffects": false,
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "exports": {
35
+ ".": {
36
+ "import": {
37
+ "types": "./dist/esm/index.d.ts",
38
+ "default": "./dist/esm/index.js"
39
+ },
40
+ "require": {
41
+ "types": "./dist/cjs/index.d.ts",
42
+ "default": "./dist/cjs/index.js"
43
+ }
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "main": "./dist/cjs/index.js",
48
+ "module": "./dist/esm/index.js",
49
+ "types": "./dist/cjs/index.d.ts",
50
+ "files": [
51
+ "dist",
52
+ "src",
53
+ "CHANGELOG.md"
54
+ ],
55
+ "scripts": {
56
+ "spec:update": "node scripts/fetch-spec.ts",
57
+ "generate": "node scripts/generate.ts",
58
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.build.json --noEmit",
59
+ "test": "vitest run",
60
+ "build": "node scripts/build.ts",
61
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "22.20.5",
68
+ "typescript": "7.0.2",
69
+ "vitest": "4.1.11"
70
+ }
71
+ }
package/src/client.ts ADDED
@@ -0,0 +1,83 @@
1
+ import { MonitoringError } from './core/errors.js'
2
+ import { HttpClient, seg, type ClientOptions } from './core/http.js'
3
+ import type { Scope } from './core/scope.js'
4
+ import type { Identity } from './generated/types.js'
5
+ import { AccountResource } from './resources/account.js'
6
+ import { AddressesResource } from './resources/addresses.js'
7
+ import { AssetsResource, NetworksResource } from './resources/catalog.js'
8
+ import { DeliveriesResource } from './resources/deliveries.js'
9
+ import { TransactionsResource } from './resources/transactions.js'
10
+ import { WebhooksResource } from './resources/webhooks.js'
11
+ import { DEFAULT_BASE_URL } from './version.js'
12
+
13
+ /**
14
+ * Client for the V3 Finance Monitoring API.
15
+ *
16
+ * Use it on the server only: an API key in a browser is visible to anyone.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { MonitoringClient } from '@v3finance/monitoring-sdk'
21
+ *
22
+ * const monitoring = new MonitoringClient({ apiKey: process.env.MONITORING_API_KEY! })
23
+ *
24
+ * await monitoring.addresses.add([{ network: 'tron', address: 'TA8Hg…' }], { assets: ['USDT'], direction: 'in' })
25
+ * ```
26
+ */
27
+ export class MonitoringClient {
28
+ /** Your client: the account your API key belongs to. */
29
+ readonly account: AccountResource
30
+ /** Addresses you monitor and their subscriptions. */
31
+ readonly addresses: AddressesResource
32
+ /** Test your webhook endpoint. */
33
+ readonly webhooks: WebhooksResource
34
+ /** History of webhooks sent to you. */
35
+ readonly deliveries: DeliveriesResource
36
+ /** Any transaction by hash, with confirmations and finality. */
37
+ readonly transactions: TransactionsResource
38
+ /** Supported networks. */
39
+ readonly networks: NetworksResource
40
+ /** Supported native coins and tokens. */
41
+ readonly assets: AssetsResource
42
+
43
+ readonly #http: HttpClient
44
+ #clientId: Promise<string> | undefined
45
+
46
+ /**
47
+ * @param options - API key and optional settings.
48
+ * @throws {@link MonitoringError} when `apiKey` is missing or `fetch` is not available.
49
+ */
50
+ constructor(options: ClientOptions) {
51
+ this.#http = new HttpClient(options, DEFAULT_BASE_URL)
52
+ if (options.clientId) this.#clientId = Promise.resolve(options.clientId)
53
+ const scope: Scope = { http: this.#http, clientPath: async () => `/v1/clients/${seg(await this.#resolveClientId())}` }
54
+ this.account = new AccountResource(scope, () => this.#resolveClientId())
55
+ this.addresses = new AddressesResource(scope)
56
+ this.webhooks = new WebhooksResource(scope)
57
+ this.deliveries = new DeliveriesResource(scope)
58
+ this.transactions = new TransactionsResource(scope)
59
+ this.networks = new NetworksResource(scope)
60
+ this.assets = new AssetsResource(scope)
61
+ }
62
+
63
+ /** API base URL this client sends requests to. */
64
+ get baseUrl(): string {
65
+ return this.#http.baseUrl
66
+ }
67
+
68
+ /** Client ID of the API key: from the options, or resolved once from the API and cached. */
69
+ #resolveClientId(): Promise<string> {
70
+ this.#clientId ??= this.#http.request<Identity>({ method: 'GET', path: '/v1/me', idempotent: true }).then(
71
+ (identity) => {
72
+ if (!identity.actor.clientId) throw new MonitoringError('this API key is not bound to a client: pass clientId in the options')
73
+ return identity.actor.clientId
74
+ },
75
+ (error: unknown) => {
76
+ // a failed lookup is not cached: the next call tries again
77
+ this.#clientId = undefined
78
+ throw error
79
+ },
80
+ )
81
+ return this.#clientId
82
+ }
83
+ }
@@ -0,0 +1,99 @@
1
+ import type { ApiErrorBody } from '../generated/types.js'
2
+
3
+ /** A per-item problem of a batch request: the item's index in your request and the reason. */
4
+ export type ErrorDetail = NonNullable<ApiErrorBody['error']['details']>[number]
5
+
6
+ /**
7
+ * Base class of every error thrown by the SDK.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * try {
12
+ * await monitoring.addresses.add(items)
13
+ * } catch (error) {
14
+ * if (error instanceof MonitoringApiError && error.status === 400) console.log(error.details)
15
+ * else throw error
16
+ * }
17
+ * ```
18
+ */
19
+ export class MonitoringError extends Error {
20
+ constructor(message: string, options?: { cause?: unknown }) {
21
+ super(message, options)
22
+ this.name = 'MonitoringError'
23
+ }
24
+ }
25
+
26
+ /** The API responded with an error status (4xx or 5xx). */
27
+ export class MonitoringApiError extends MonitoringError {
28
+ /** HTTP status code. */
29
+ readonly status: number
30
+ /**
31
+ * Machine-readable error code, e.g. `invalid_request`, `unauthorized`, `forbidden`, `not_found`,
32
+ * `rate_limited`, `upstream_error`, `upstream_rate_limited`.
33
+ */
34
+ readonly code: string
35
+ /** Per-item problems of a batch request, such as invalid addresses. Empty when there are none. */
36
+ readonly details: ErrorDetail[]
37
+ /** Request ID. Include it when contacting support. */
38
+ readonly requestId: string | undefined
39
+ /** Raw response body. */
40
+ readonly body: unknown
41
+
42
+ constructor(init: { status: number; code: string; message: string; details?: ErrorDetail[]; requestId?: string; body?: unknown }) {
43
+ super(`${init.message} (HTTP ${init.status}, ${init.code})`)
44
+ this.name = 'MonitoringApiError'
45
+ this.status = init.status
46
+ this.code = init.code
47
+ this.details = init.details ?? []
48
+ this.requestId = init.requestId
49
+ this.body = init.body
50
+ }
51
+
52
+ /** `true` for transient errors — rate limits and unavailable upstream providers — that can be retried later. */
53
+ get retryable(): boolean {
54
+ return this.status === 429 || this.status === 503
55
+ }
56
+ }
57
+
58
+ /** The request did not get a response because of a network or connection failure. */
59
+ export class MonitoringConnectionError extends MonitoringError {
60
+ constructor(message: string, options?: { cause?: unknown }) {
61
+ super(message, options)
62
+ this.name = 'MonitoringConnectionError'
63
+ }
64
+ }
65
+
66
+ /** The request did not complete within `timeoutMs`. */
67
+ export class MonitoringTimeoutError extends MonitoringConnectionError {
68
+ constructor(timeoutMs: number) {
69
+ super(`request timed out after ${timeoutMs} ms`)
70
+ this.name = 'MonitoringTimeoutError'
71
+ }
72
+ }
73
+
74
+ /** The webhook signature does not match: the body was altered or is verified with the wrong secret. */
75
+ export class WebhookSignatureError extends MonitoringError {
76
+ constructor(message = 'webhook signature mismatch') {
77
+ super(message)
78
+ this.name = 'WebhookSignatureError'
79
+ }
80
+ }
81
+
82
+ /** Builds an error from any error body the API returns. */
83
+ export function apiErrorFrom(status: number, body: unknown, requestId: string | undefined): MonitoringApiError {
84
+ const b = (body ?? {}) as Record<string, unknown>
85
+ const v1 = typeof b.error === 'object' && b.error !== null ? (b.error as Record<string, unknown>) : undefined
86
+ const message =
87
+ (typeof v1?.message === 'string' && v1.message) ||
88
+ (typeof b.error === 'string' && b.error) ||
89
+ (typeof b.message === 'string' && b.message) ||
90
+ `HTTP ${status}`
91
+ const code = (typeof v1?.code === 'string' && v1.code) || (typeof b.code === 'string' && b.code.toLowerCase()) || defaultCode(status)
92
+ const details = (Array.isArray(v1?.details) ? v1.details : Array.isArray(b.details) ? b.details : []) as ErrorDetail[]
93
+ return new MonitoringApiError({ status, code, message, details, requestId, body })
94
+ }
95
+
96
+ function defaultCode(status: number): string {
97
+ const codes: Record<number, string> = { 400: 'invalid_request', 401: 'unauthorized', 403: 'forbidden', 404: 'not_found', 409: 'conflict', 429: 'rate_limited' }
98
+ return codes[status] ?? (status >= 500 ? 'server_error' : 'http_error')
99
+ }
@@ -0,0 +1,183 @@
1
+ import { SDK_VERSION } from '../version.js'
2
+ import { apiErrorFrom, MonitoringConnectionError, MonitoringError, MonitoringTimeoutError } from './errors.js'
3
+
4
+ /** Per-request options, accepted as the last argument of every method. */
5
+ export interface RequestOptions {
6
+ /** Cancels the request, including pending retries. */
7
+ signal?: AbortSignal
8
+ /** Timeout for this request in milliseconds. Overrides {@link ClientOptions.timeoutMs}. */
9
+ timeoutMs?: number
10
+ /** Maximum number of retries for this request. Overrides {@link ClientOptions.maxRetries}. */
11
+ maxRetries?: number
12
+ }
13
+
14
+ /** Options of {@link MonitoringClient}. */
15
+ export interface ClientOptions {
16
+ /** Your API key. Sent in the `x-api-key` header. */
17
+ apiKey: string
18
+ /**
19
+ * Your client ID. Optional: when omitted, it is resolved from the API key on first use and cached.
20
+ */
21
+ clientId?: string
22
+ /** API base URL. Defaults to `https://monitoring.v3.finance`. */
23
+ baseUrl?: string
24
+ /** Request timeout in milliseconds. Defaults to `30000`. */
25
+ timeoutMs?: number
26
+ /**
27
+ * Maximum number of retries for transient errors. Defaults to `2`.
28
+ *
29
+ * `429` and `503` responses are retried for every request, honoring `Retry-After`. Network errors and timeouts are
30
+ * retried only for requests that are safe to repeat. Backoff is exponential with jitter.
31
+ */
32
+ maxRetries?: number
33
+ /** Custom `fetch` implementation, e.g. for proxies or instrumentation. Defaults to the global `fetch`. */
34
+ fetch?: typeof fetch
35
+ /** Additional headers sent with every request. */
36
+ headers?: Record<string, string>
37
+ }
38
+
39
+ type Query = Record<string, string | number | boolean | null | undefined>
40
+
41
+ /** @internal A request to the API. */
42
+ export interface Call {
43
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE'
44
+ path: string
45
+ query?: Query
46
+ body?: unknown
47
+ /** The request can be repeated after a network failure without changing the outcome. */
48
+ idempotent: boolean
49
+ /** Statuses returned as data rather than thrown. */
50
+ acceptStatuses?: number[]
51
+ options?: RequestOptions | undefined
52
+ }
53
+
54
+ const DEFAULTS = { timeoutMs: 30_000, maxRetries: 2 }
55
+ const MAX_RETRY_DELAY_MS = 60_000
56
+
57
+ /** @internal Transport: base URL, authentication, timeouts, retries and error mapping. */
58
+ export class HttpClient {
59
+ readonly baseUrl: string
60
+ readonly #apiKey: string
61
+ readonly #timeoutMs: number
62
+ readonly #maxRetries: number
63
+ readonly #fetch: typeof fetch
64
+ readonly #headers: Record<string, string>
65
+
66
+ constructor(options: ClientOptions, defaultBaseUrl: string) {
67
+ if (!options?.apiKey) throw new MonitoringError('apiKey is required')
68
+ this.baseUrl = (options.baseUrl ?? defaultBaseUrl).replace(/\/+$/, '')
69
+ this.#apiKey = options.apiKey
70
+ this.#timeoutMs = options.timeoutMs ?? DEFAULTS.timeoutMs
71
+ this.#maxRetries = options.maxRetries ?? DEFAULTS.maxRetries
72
+ const f = options.fetch ?? globalThis.fetch
73
+ if (typeof f !== 'function') throw new MonitoringError('fetch is not available: use Node.js 20 or later, or pass options.fetch')
74
+ this.#fetch = f
75
+ this.#headers = { ...options.headers }
76
+ }
77
+
78
+ /** Sends a request and returns the parsed JSON response. */
79
+ async request<T>(call: Call): Promise<T> {
80
+ const url = this.#url(call.path, call.query)
81
+ const headers: Record<string, string> = {
82
+ accept: 'application/json',
83
+ 'user-agent': `@v3finance/monitoring-sdk/${SDK_VERSION}`,
84
+ ...this.#headers,
85
+ 'x-api-key': this.#apiKey,
86
+ }
87
+ if (call.body !== undefined) headers['content-type'] = 'application/json'
88
+ const body = call.body === undefined ? undefined : JSON.stringify(call.body)
89
+ const maxRetries = call.options?.maxRetries ?? this.#maxRetries
90
+ const timeoutMs = call.options?.timeoutMs ?? this.#timeoutMs
91
+ const signal = call.options?.signal
92
+
93
+ for (let attempt = 0; ; attempt++) {
94
+ let res: { status: number; headers: Headers; data: unknown }
95
+ try {
96
+ res = await this.#send(url, { method: call.method, headers, body }, timeoutMs, signal)
97
+ } catch (error) {
98
+ if (signal?.aborted) throw signal.reason
99
+ if (call.idempotent && attempt < maxRetries) {
100
+ await sleep(backoff(attempt), signal)
101
+ continue
102
+ }
103
+ throw error
104
+ }
105
+ if ((res.status >= 200 && res.status < 300) || call.acceptStatuses?.includes(res.status)) return res.data as T
106
+ const error = apiErrorFrom(res.status, res.data, res.headers.get('cf-ray') ?? undefined)
107
+ // 429 and 503 mean the request was not processed, so retrying is safe for any method
108
+ if (error.retryable && attempt < maxRetries) {
109
+ await sleep(retryAfter(res.headers) ?? backoff(attempt), signal)
110
+ continue
111
+ }
112
+ throw error
113
+ }
114
+ }
115
+
116
+ #url(path: string, query?: Query): string {
117
+ const url = new URL(this.baseUrl + path)
118
+ for (const [k, v] of Object.entries(query ?? {})) if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, String(v))
119
+ return url.toString()
120
+ }
121
+
122
+ /** One attempt, with a single timeout covering both the connection and the response body. */
123
+ async #send(url: string, init: RequestInit, timeoutMs: number, signal: AbortSignal | undefined) {
124
+ const controller = new AbortController()
125
+ const timer = setTimeout(() => controller.abort(new MonitoringTimeoutError(timeoutMs)), timeoutMs)
126
+ const onAbort = () => controller.abort(signal!.reason)
127
+ if (signal?.aborted) onAbort()
128
+ signal?.addEventListener('abort', onAbort, { once: true })
129
+ try {
130
+ const res = await this.#fetch(url, { ...init, signal: controller.signal })
131
+ const text = await res.text()
132
+ return { status: res.status, headers: res.headers, data: text ? parseJson(text) : undefined }
133
+ } catch (error) {
134
+ const reason: unknown = controller.signal.reason
135
+ if (reason instanceof MonitoringTimeoutError) throw reason
136
+ if (signal?.aborted) throw signal.reason
137
+ throw new MonitoringConnectionError(`request to ${new URL(url).origin} failed: ${(error as Error).message}`, { cause: error })
138
+ } finally {
139
+ clearTimeout(timer)
140
+ signal?.removeEventListener('abort', onAbort)
141
+ }
142
+ }
143
+ }
144
+
145
+ function parseJson(text: string): unknown {
146
+ try {
147
+ return JSON.parse(text)
148
+ } catch {
149
+ return text
150
+ }
151
+ }
152
+
153
+ /** Exponential backoff with jitter: ~0.5 s, 1 s, 2 s… capped at 8 s. */
154
+ function backoff(attempt: number): number {
155
+ return Math.min(8_000, 500 * 2 ** attempt) * (0.5 + Math.random() / 2)
156
+ }
157
+
158
+ /** Delay from a `Retry-After` header: seconds or an HTTP date. */
159
+ function retryAfter(headers: Headers): number | undefined {
160
+ const raw = headers.get('retry-after')
161
+ if (!raw) return undefined
162
+ const seconds = Number(raw)
163
+ const ms = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(raw) - Date.now()
164
+ return Number.isFinite(ms) ? Math.min(Math.max(ms, 0), MAX_RETRY_DELAY_MS) : undefined
165
+ }
166
+
167
+ function sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
168
+ return new Promise((resolve, reject) => {
169
+ if (signal?.aborted) return reject(signal.reason)
170
+ const timer = setTimeout(() => {
171
+ signal?.removeEventListener('abort', onAbort)
172
+ resolve()
173
+ }, ms)
174
+ const onAbort = () => {
175
+ clearTimeout(timer)
176
+ reject(signal!.reason)
177
+ }
178
+ signal?.addEventListener('abort', onAbort, { once: true })
179
+ })
180
+ }
181
+
182
+ /** @internal Encodes a path segment. */
183
+ export const seg = (value: string | number) => encodeURIComponent(String(value))