@hbar-kit/payments 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleksandr Ostrovskyi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @hbar-kit/payments
2
+
3
+ Verify HBAR and HTS payments against the Hedera Mirror Node in a few lines. Read-only,
4
+ backend-safe, non-custodial. Built on `@hbar-kit/mirror`.
5
+
6
+ ```ts
7
+ import { verifyHbarPayment } from "@hbar-kit/payments"
8
+
9
+ const result = await verifyHbarPayment({
10
+ network: "testnet",
11
+ receiver: "0.0.12345",
12
+ amount: "25",
13
+ memo: "order_6471727153206",
14
+ after: new Date(Date.now() - 30 * 60 * 1000),
15
+ })
16
+ if (result.matched) {
17
+ // status === "confirmed"; result.transactionId, result.explorerUrl, ...
18
+ }
19
+ ```
20
+
21
+ Statuses: `confirmed | pending | underpaid | overpaid | duplicate | mismatch | expired | failed`.
22
+ A non-match is a result (with `reason`), not a thrown error. See the
23
+ [docs](https://github.com/devwhodevs/hbar-kit).
package/dist/index.cjs ADDED
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ var core = require('@hbar-kit/core');
4
+ var mirror = require('@hbar-kit/mirror');
5
+
6
+ // src/verify.ts
7
+
8
+ // src/match.ts
9
+ function netToReceiver(tx, receiver, asset) {
10
+ if (asset === "HBAR") {
11
+ return tx.transfers.filter((t) => t.account === receiver).reduce((s, t) => s + t.amount, 0n);
12
+ }
13
+ return tx.tokenTransfers.filter((t) => t.tokenId === asset.tokenId && t.account === receiver).reduce((s, t) => s + t.amount, 0n);
14
+ }
15
+ function memoMatches(actual, expected, cmp = {}) {
16
+ const mode = cmp.mode ?? "exact";
17
+ if (mode === "trim") return actual.trim() === expected.trim();
18
+ if (mode === "caseInsensitive") return actual.toLowerCase() === expected.toLowerCase();
19
+ return actual === expected;
20
+ }
21
+ function classifyAmount(net, expected) {
22
+ if (net === expected) return "exact";
23
+ return net < expected ? "underpaid" : "overpaid";
24
+ }
25
+ function hashscanTxUrl(network, consensusTimestamp, transactionId) {
26
+ return `${core.NETWORKS[network].hashscan}/transaction/${consensusTimestamp}?tid=${core.txIdToMirror(transactionId)}`;
27
+ }
28
+
29
+ // src/verify.ts
30
+ var tokenDecimalsCache = /* @__PURE__ */ new Map();
31
+ function resolveClient(p) {
32
+ return {
33
+ client: p.client ?? mirror.createMirrorClient(p),
34
+ network: p.network ?? "mainnet"
35
+ };
36
+ }
37
+ async function runVerify(p, asset, expectedBase, decimals) {
38
+ if (p.after && p.before && new Date(p.after) > new Date(p.before)) {
39
+ throw new core.InvalidParamsError("`after` must be before `before`");
40
+ }
41
+ const { client, network } = resolveClient(p);
42
+ const tokenId = asset === "HBAR" ? void 0 : asset.tokenId;
43
+ const candidates = [];
44
+ const collect = (items) => {
45
+ for (const tx of items) {
46
+ if (tx.result !== "SUCCESS") continue;
47
+ const net = netToReceiver(tx, p.receiver, asset);
48
+ if (net <= 0n) continue;
49
+ if (tokenId && !tx.tokenTransfers.some((t) => t.tokenId === tokenId)) continue;
50
+ const payer = tx.transfers.find((t) => t.amount < 0n)?.account ?? tx.tokenTransfers.find((t) => t.amount < 0n)?.account;
51
+ const match = {
52
+ transactionId: tx.transactionId,
53
+ consensusTimestamp: tx.consensusTimestamp.raw,
54
+ netBase: net,
55
+ net: core.formatUnits(net, decimals),
56
+ memo: tx.memo,
57
+ transaction: tx
58
+ };
59
+ if (payer !== void 0) match.payer = payer;
60
+ candidates.push(match);
61
+ }
62
+ };
63
+ const findParams = {
64
+ accountId: p.receiver,
65
+ transactionType: "cryptotransfer",
66
+ result: "success",
67
+ order: "desc"
68
+ };
69
+ if (p.after !== void 0) findParams.after = p.after;
70
+ if (p.before !== void 0) findParams.before = p.before;
71
+ let page = await client.transactions.find(findParams);
72
+ collect(page.items);
73
+ while (page.next) {
74
+ const body = await client.transport.get(page.next);
75
+ collect((body.transactions ?? []).map(mirror.normalizeTransaction));
76
+ page = { items: [], next: body.links?.next ?? null };
77
+ }
78
+ const memoFiltered = p.memo ? candidates.filter((c) => memoMatches(c.memo, p.memo, p.memoComparison)) : candidates;
79
+ const fail = (status, reason) => ({
80
+ matched: false,
81
+ status,
82
+ receiver: p.receiver,
83
+ asset,
84
+ matches: memoFiltered,
85
+ reason
86
+ });
87
+ if (candidates.length === 0)
88
+ return fail("pending", "no matching transactions for receiver in window");
89
+ if (p.memo && memoFiltered.length === 0)
90
+ return fail("mismatch", "no transaction matched the expected memo");
91
+ const wantAtLeast = p.comparison === "atLeast";
92
+ const satisfying = memoFiltered.filter(
93
+ (c) => wantAtLeast ? c.netBase >= expectedBase : c.netBase === expectedBase
94
+ );
95
+ const resultFrom = (m, status, matched, matches, reason) => {
96
+ const result = {
97
+ matched,
98
+ status,
99
+ receiver: p.receiver,
100
+ asset,
101
+ transactionId: m.transactionId,
102
+ amountBase: m.netBase,
103
+ amount: m.net,
104
+ memo: m.memo,
105
+ consensusTimestamp: m.consensusTimestamp,
106
+ explorerUrl: hashscanTxUrl(network, m.consensusTimestamp, m.transactionId),
107
+ matches
108
+ };
109
+ if (m.payer !== void 0) result.payer = m.payer;
110
+ if (reason !== void 0) result.reason = reason;
111
+ return result;
112
+ };
113
+ if (satisfying.length === 0) {
114
+ const best = memoFiltered[0];
115
+ const cls = classifyAmount(best.netBase, expectedBase);
116
+ return resultFrom(
117
+ best,
118
+ cls === "exact" ? "confirmed" : cls,
119
+ false,
120
+ memoFiltered,
121
+ `amount ${cls}`
122
+ );
123
+ }
124
+ if (satisfying.length > 1) {
125
+ return resultFrom(
126
+ satisfying[0],
127
+ "duplicate",
128
+ false,
129
+ satisfying,
130
+ `${satisfying.length} transactions satisfy this request`
131
+ );
132
+ }
133
+ return resultFrom(satisfying[0], "confirmed", true, satisfying);
134
+ }
135
+ async function verifyHbarPayment(p) {
136
+ return runVerify(p, "HBAR", core.parseHbar(p.amount), 8);
137
+ }
138
+ async function verifyHtsPayment(p) {
139
+ let decimals = p.decimals;
140
+ if (decimals === void 0) {
141
+ const cached = tokenDecimalsCache.get(p.tokenId);
142
+ if (cached !== void 0) decimals = cached;
143
+ else {
144
+ const { client } = resolveClient(p);
145
+ decimals = (await client.tokens.get(p.tokenId)).decimals;
146
+ tokenDecimalsCache.set(p.tokenId, decimals);
147
+ }
148
+ }
149
+ return runVerify(p, { tokenId: p.tokenId, decimals }, core.parseUnits(p.amount, decimals), decimals);
150
+ }
151
+
152
+ // src/wait.ts
153
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
154
+ async function poll(verify, opts) {
155
+ const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1e3;
156
+ const pollIntervalMs = opts.pollIntervalMs ?? 3e3;
157
+ const deadline = Date.now() + timeoutMs;
158
+ let last;
159
+ while (Date.now() < deadline) {
160
+ if (opts.signal?.aborted) break;
161
+ last = await verify();
162
+ if (last.matched) return last;
163
+ if (last.status === "duplicate" || last.status === "overpaid") return last;
164
+ await sleep(pollIntervalMs);
165
+ }
166
+ return last && last.status !== "pending" ? { ...last, status: "expired" } : {
167
+ matched: false,
168
+ status: "expired",
169
+ receiver: last?.receiver ?? "",
170
+ asset: last?.asset ?? "HBAR",
171
+ matches: [],
172
+ reason: "timed out waiting for payment"
173
+ };
174
+ }
175
+ function waitForHbarPayment(p) {
176
+ return poll(() => verifyHbarPayment(p), p);
177
+ }
178
+ function waitForHtsPayment(p) {
179
+ return poll(() => verifyHtsPayment(p), p);
180
+ }
181
+
182
+ exports.classifyAmount = classifyAmount;
183
+ exports.hashscanTxUrl = hashscanTxUrl;
184
+ exports.memoMatches = memoMatches;
185
+ exports.netToReceiver = netToReceiver;
186
+ exports.verifyHbarPayment = verifyHbarPayment;
187
+ exports.verifyHtsPayment = verifyHtsPayment;
188
+ exports.waitForHbarPayment = waitForHbarPayment;
189
+ exports.waitForHtsPayment = waitForHtsPayment;
190
+ //# sourceMappingURL=index.cjs.map
191
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/match.ts","../src/explorer.ts","../src/verify.ts","../src/wait.ts"],"names":["NETWORKS","txIdToMirror","createMirrorClient","InvalidParamsError","formatUnits","normalizeTransaction","parseHbar","parseUnits"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { Transaction } from \"@hbar-kit/mirror\"\nimport type { MemoComparison, PaymentAsset } from \"./types.js\"\n\n/** Signed sum of ALL the receiver's legs (HBAR transfers or a specific token's transfers). */\nexport function netToReceiver(tx: Transaction, receiver: string, asset: PaymentAsset): bigint {\n if (asset === \"HBAR\") {\n return tx.transfers.filter((t) => t.account === receiver).reduce((s, t) => s + t.amount, 0n)\n }\n return tx.tokenTransfers\n .filter((t) => t.tokenId === asset.tokenId && t.account === receiver)\n .reduce((s, t) => s + t.amount, 0n)\n}\n\nexport function memoMatches(actual: string, expected: string, cmp: MemoComparison = {}): boolean {\n const mode = cmp.mode ?? \"exact\"\n if (mode === \"trim\") return actual.trim() === expected.trim()\n if (mode === \"caseInsensitive\") return actual.toLowerCase() === expected.toLowerCase()\n return actual === expected\n}\n\nexport type AmountClass = \"exact\" | \"underpaid\" | \"overpaid\"\nexport function classifyAmount(net: bigint, expected: bigint): AmountClass {\n if (net === expected) return \"exact\"\n return net < expected ? \"underpaid\" : \"overpaid\"\n}\n","import { NETWORKS, txIdToMirror, type HederaNetwork } from \"@hbar-kit/core\"\n\n/** HashScan transaction link: consensus timestamp in path, tx id as ?tid query param. */\nexport function hashscanTxUrl(\n network: HederaNetwork,\n consensusTimestamp: string,\n transactionId: string,\n): string {\n return `${NETWORKS[network].hashscan}/transaction/${consensusTimestamp}?tid=${txIdToMirror(transactionId)}`\n}\n","import {\n parseUnits,\n parseHbar,\n formatUnits,\n type HederaNetwork,\n type NetworkInput,\n InvalidParamsError,\n} from \"@hbar-kit/core\"\nimport {\n createMirrorClient,\n normalizeTransaction,\n type MirrorClient,\n type RawTransaction,\n type Transaction,\n} from \"@hbar-kit/mirror\"\nimport { netToReceiver, memoMatches, classifyAmount } from \"./match.js\"\nimport { hashscanTxUrl } from \"./explorer.js\"\nimport type { MemoComparison, PaymentAsset, PaymentMatch, PaymentResult } from \"./types.js\"\n\nexport interface VerifyBaseParams extends NetworkInput {\n client?: MirrorClient\n receiver: string\n amount: string\n memo?: string\n memoComparison?: MemoComparison\n comparison?: \"exact\" | \"atLeast\"\n after?: Date | string\n before?: Date | string\n}\nexport type VerifyHbarParams = VerifyBaseParams\nexport interface VerifyHtsParams extends VerifyBaseParams {\n tokenId: string\n decimals?: number\n}\n\nconst tokenDecimalsCache = new Map<string, number>()\n\nfunction resolveClient(p: VerifyBaseParams): { client: MirrorClient; network: HederaNetwork } {\n return {\n client: p.client ?? createMirrorClient(p),\n network: (p.network ?? \"mainnet\") as HederaNetwork,\n }\n}\n\nasync function runVerify(\n p: VerifyBaseParams,\n asset: PaymentAsset,\n expectedBase: bigint,\n decimals: number,\n): Promise<PaymentResult> {\n if (p.after && p.before && new Date(p.after) > new Date(p.before)) {\n throw new InvalidParamsError(\"`after` must be before `before`\")\n }\n const { client, network } = resolveClient(p)\n const tokenId = asset === \"HBAR\" ? undefined : asset.tokenId\n\n const candidates: PaymentMatch[] = []\n const collect = (items: Transaction[]) => {\n for (const tx of items) {\n if (tx.result !== \"SUCCESS\") continue\n const net = netToReceiver(tx, p.receiver, asset)\n if (net <= 0n) continue\n if (tokenId && !tx.tokenTransfers.some((t) => t.tokenId === tokenId)) continue\n const payer =\n tx.transfers.find((t) => t.amount < 0n)?.account ??\n tx.tokenTransfers.find((t) => t.amount < 0n)?.account\n const match: PaymentMatch = {\n transactionId: tx.transactionId,\n consensusTimestamp: tx.consensusTimestamp.raw,\n netBase: net,\n net: formatUnits(net, decimals),\n memo: tx.memo,\n transaction: tx,\n }\n if (payer !== undefined) match.payer = payer\n candidates.push(match)\n }\n }\n\n const findParams: Parameters<typeof client.transactions.find>[0] = {\n accountId: p.receiver,\n transactionType: \"cryptotransfer\",\n result: \"success\",\n order: \"desc\",\n }\n if (p.after !== undefined) findParams.after = p.after\n if (p.before !== undefined) findParams.before = p.before\n let page = await client.transactions.find(findParams)\n collect(page.items)\n while (page.next) {\n const body = (await client.transport.get(page.next)) as {\n transactions?: RawTransaction[]\n links?: { next: string | null }\n }\n collect((body.transactions ?? []).map(normalizeTransaction))\n page = { items: [], next: body.links?.next ?? null }\n }\n\n const memoFiltered = p.memo\n ? candidates.filter((c) => memoMatches(c.memo, p.memo!, p.memoComparison))\n : candidates\n\n const fail = (status: PaymentResult[\"status\"], reason: string): PaymentResult => ({\n matched: false,\n status,\n receiver: p.receiver,\n asset,\n matches: memoFiltered,\n reason,\n })\n if (candidates.length === 0)\n return fail(\"pending\", \"no matching transactions for receiver in window\")\n if (p.memo && memoFiltered.length === 0)\n return fail(\"mismatch\", \"no transaction matched the expected memo\")\n\n const wantAtLeast = p.comparison === \"atLeast\"\n const satisfying = memoFiltered.filter((c) =>\n wantAtLeast ? c.netBase >= expectedBase : c.netBase === expectedBase,\n )\n\n const resultFrom = (\n m: PaymentMatch,\n status: PaymentResult[\"status\"],\n matched: boolean,\n matches: PaymentMatch[],\n reason?: string,\n ): PaymentResult => {\n const result: PaymentResult = {\n matched,\n status,\n receiver: p.receiver,\n asset,\n transactionId: m.transactionId,\n amountBase: m.netBase,\n amount: m.net,\n memo: m.memo,\n consensusTimestamp: m.consensusTimestamp,\n explorerUrl: hashscanTxUrl(network, m.consensusTimestamp, m.transactionId),\n matches,\n }\n if (m.payer !== undefined) result.payer = m.payer\n if (reason !== undefined) result.reason = reason\n return result\n }\n\n if (satisfying.length === 0) {\n const best = memoFiltered[0]!\n const cls = classifyAmount(best.netBase, expectedBase)\n return resultFrom(\n best,\n cls === \"exact\" ? \"confirmed\" : cls,\n false,\n memoFiltered,\n `amount ${cls}`,\n )\n }\n if (satisfying.length > 1) {\n return resultFrom(\n satisfying[0]!,\n \"duplicate\",\n false,\n satisfying,\n `${satisfying.length} transactions satisfy this request`,\n )\n }\n return resultFrom(satisfying[0]!, \"confirmed\", true, satisfying)\n}\n\nexport async function verifyHbarPayment(p: VerifyHbarParams): Promise<PaymentResult> {\n return runVerify(p, \"HBAR\", parseHbar(p.amount), 8)\n}\n\nexport async function verifyHtsPayment(p: VerifyHtsParams): Promise<PaymentResult> {\n let decimals = p.decimals\n if (decimals === undefined) {\n const cached = tokenDecimalsCache.get(p.tokenId)\n if (cached !== undefined) decimals = cached\n else {\n const { client } = resolveClient(p)\n decimals = (await client.tokens.get(p.tokenId)).decimals\n tokenDecimalsCache.set(p.tokenId, decimals)\n }\n }\n return runVerify(p, { tokenId: p.tokenId, decimals }, parseUnits(p.amount, decimals), decimals)\n}\n","import {\n verifyHbarPayment,\n verifyHtsPayment,\n type VerifyHbarParams,\n type VerifyHtsParams,\n} from \"./verify.js\"\nimport type { PaymentResult } from \"./types.js\"\n\nexport interface WaitOptions {\n timeoutMs?: number\n pollIntervalMs?: number\n signal?: AbortSignal\n}\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))\n\nasync function poll(\n verify: () => Promise<PaymentResult>,\n opts: WaitOptions,\n): Promise<PaymentResult> {\n const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000\n const pollIntervalMs = opts.pollIntervalMs ?? 3000\n const deadline = Date.now() + timeoutMs\n let last: PaymentResult | undefined\n while (Date.now() < deadline) {\n if (opts.signal?.aborted) break\n last = await verify()\n if (last.matched) return last\n if (last.status === \"duplicate\" || last.status === \"overpaid\") return last\n await sleep(pollIntervalMs)\n }\n return last && last.status !== \"pending\"\n ? { ...last, status: \"expired\" }\n : {\n matched: false,\n status: \"expired\",\n receiver: last?.receiver ?? \"\",\n asset: last?.asset ?? \"HBAR\",\n matches: [],\n reason: \"timed out waiting for payment\",\n }\n}\n\nexport function waitForHbarPayment(p: VerifyHbarParams & WaitOptions): Promise<PaymentResult> {\n return poll(() => verifyHbarPayment(p), p)\n}\nexport function waitForHtsPayment(p: VerifyHtsParams & WaitOptions): Promise<PaymentResult> {\n return poll(() => verifyHtsPayment(p), p)\n}\n"]}
@@ -0,0 +1,72 @@
1
+ import { NetworkInput, HederaNetwork } from '@hbar-kit/core';
2
+ import { Transaction, MirrorClient } from '@hbar-kit/mirror';
3
+
4
+ type PaymentStatus = "confirmed" | "pending" | "underpaid" | "overpaid" | "duplicate" | "mismatch" | "expired" | "failed";
5
+ type PaymentAsset = "HBAR" | {
6
+ tokenId: string;
7
+ decimals: number;
8
+ };
9
+ interface PaymentMatch {
10
+ transactionId: string;
11
+ payer?: string;
12
+ consensusTimestamp: string;
13
+ netBase: bigint;
14
+ net: string;
15
+ memo: string;
16
+ transaction: Transaction;
17
+ }
18
+ interface PaymentResult {
19
+ matched: boolean;
20
+ status: PaymentStatus;
21
+ receiver: string;
22
+ asset: PaymentAsset;
23
+ transactionId?: string;
24
+ payer?: string;
25
+ amount?: string;
26
+ amountBase?: bigint;
27
+ memo?: string;
28
+ consensusTimestamp?: string;
29
+ explorerUrl?: string;
30
+ matches: PaymentMatch[];
31
+ reason?: string;
32
+ }
33
+ interface MemoComparison {
34
+ mode?: "exact" | "trim" | "caseInsensitive";
35
+ }
36
+
37
+ interface VerifyBaseParams extends NetworkInput {
38
+ client?: MirrorClient;
39
+ receiver: string;
40
+ amount: string;
41
+ memo?: string;
42
+ memoComparison?: MemoComparison;
43
+ comparison?: "exact" | "atLeast";
44
+ after?: Date | string;
45
+ before?: Date | string;
46
+ }
47
+ type VerifyHbarParams = VerifyBaseParams;
48
+ interface VerifyHtsParams extends VerifyBaseParams {
49
+ tokenId: string;
50
+ decimals?: number;
51
+ }
52
+ declare function verifyHbarPayment(p: VerifyHbarParams): Promise<PaymentResult>;
53
+ declare function verifyHtsPayment(p: VerifyHtsParams): Promise<PaymentResult>;
54
+
55
+ interface WaitOptions {
56
+ timeoutMs?: number;
57
+ pollIntervalMs?: number;
58
+ signal?: AbortSignal;
59
+ }
60
+ declare function waitForHbarPayment(p: VerifyHbarParams & WaitOptions): Promise<PaymentResult>;
61
+ declare function waitForHtsPayment(p: VerifyHtsParams & WaitOptions): Promise<PaymentResult>;
62
+
63
+ /** HashScan transaction link: consensus timestamp in path, tx id as ?tid query param. */
64
+ declare function hashscanTxUrl(network: HederaNetwork, consensusTimestamp: string, transactionId: string): string;
65
+
66
+ /** Signed sum of ALL the receiver's legs (HBAR transfers or a specific token's transfers). */
67
+ declare function netToReceiver(tx: Transaction, receiver: string, asset: PaymentAsset): bigint;
68
+ declare function memoMatches(actual: string, expected: string, cmp?: MemoComparison): boolean;
69
+ type AmountClass = "exact" | "underpaid" | "overpaid";
70
+ declare function classifyAmount(net: bigint, expected: bigint): AmountClass;
71
+
72
+ export { type MemoComparison, type PaymentAsset, type PaymentMatch, type PaymentResult, type PaymentStatus, type VerifyBaseParams, type VerifyHbarParams, type VerifyHtsParams, type WaitOptions, classifyAmount, hashscanTxUrl, memoMatches, netToReceiver, verifyHbarPayment, verifyHtsPayment, waitForHbarPayment, waitForHtsPayment };
@@ -0,0 +1,72 @@
1
+ import { NetworkInput, HederaNetwork } from '@hbar-kit/core';
2
+ import { Transaction, MirrorClient } from '@hbar-kit/mirror';
3
+
4
+ type PaymentStatus = "confirmed" | "pending" | "underpaid" | "overpaid" | "duplicate" | "mismatch" | "expired" | "failed";
5
+ type PaymentAsset = "HBAR" | {
6
+ tokenId: string;
7
+ decimals: number;
8
+ };
9
+ interface PaymentMatch {
10
+ transactionId: string;
11
+ payer?: string;
12
+ consensusTimestamp: string;
13
+ netBase: bigint;
14
+ net: string;
15
+ memo: string;
16
+ transaction: Transaction;
17
+ }
18
+ interface PaymentResult {
19
+ matched: boolean;
20
+ status: PaymentStatus;
21
+ receiver: string;
22
+ asset: PaymentAsset;
23
+ transactionId?: string;
24
+ payer?: string;
25
+ amount?: string;
26
+ amountBase?: bigint;
27
+ memo?: string;
28
+ consensusTimestamp?: string;
29
+ explorerUrl?: string;
30
+ matches: PaymentMatch[];
31
+ reason?: string;
32
+ }
33
+ interface MemoComparison {
34
+ mode?: "exact" | "trim" | "caseInsensitive";
35
+ }
36
+
37
+ interface VerifyBaseParams extends NetworkInput {
38
+ client?: MirrorClient;
39
+ receiver: string;
40
+ amount: string;
41
+ memo?: string;
42
+ memoComparison?: MemoComparison;
43
+ comparison?: "exact" | "atLeast";
44
+ after?: Date | string;
45
+ before?: Date | string;
46
+ }
47
+ type VerifyHbarParams = VerifyBaseParams;
48
+ interface VerifyHtsParams extends VerifyBaseParams {
49
+ tokenId: string;
50
+ decimals?: number;
51
+ }
52
+ declare function verifyHbarPayment(p: VerifyHbarParams): Promise<PaymentResult>;
53
+ declare function verifyHtsPayment(p: VerifyHtsParams): Promise<PaymentResult>;
54
+
55
+ interface WaitOptions {
56
+ timeoutMs?: number;
57
+ pollIntervalMs?: number;
58
+ signal?: AbortSignal;
59
+ }
60
+ declare function waitForHbarPayment(p: VerifyHbarParams & WaitOptions): Promise<PaymentResult>;
61
+ declare function waitForHtsPayment(p: VerifyHtsParams & WaitOptions): Promise<PaymentResult>;
62
+
63
+ /** HashScan transaction link: consensus timestamp in path, tx id as ?tid query param. */
64
+ declare function hashscanTxUrl(network: HederaNetwork, consensusTimestamp: string, transactionId: string): string;
65
+
66
+ /** Signed sum of ALL the receiver's legs (HBAR transfers or a specific token's transfers). */
67
+ declare function netToReceiver(tx: Transaction, receiver: string, asset: PaymentAsset): bigint;
68
+ declare function memoMatches(actual: string, expected: string, cmp?: MemoComparison): boolean;
69
+ type AmountClass = "exact" | "underpaid" | "overpaid";
70
+ declare function classifyAmount(net: bigint, expected: bigint): AmountClass;
71
+
72
+ export { type MemoComparison, type PaymentAsset, type PaymentMatch, type PaymentResult, type PaymentStatus, type VerifyBaseParams, type VerifyHbarParams, type VerifyHtsParams, type WaitOptions, classifyAmount, hashscanTxUrl, memoMatches, netToReceiver, verifyHbarPayment, verifyHtsPayment, waitForHbarPayment, waitForHtsPayment };
package/dist/index.js ADDED
@@ -0,0 +1,182 @@
1
+ import { NETWORKS, txIdToMirror, parseHbar, parseUnits, InvalidParamsError, formatUnits } from '@hbar-kit/core';
2
+ import { normalizeTransaction, createMirrorClient } from '@hbar-kit/mirror';
3
+
4
+ // src/verify.ts
5
+
6
+ // src/match.ts
7
+ function netToReceiver(tx, receiver, asset) {
8
+ if (asset === "HBAR") {
9
+ return tx.transfers.filter((t) => t.account === receiver).reduce((s, t) => s + t.amount, 0n);
10
+ }
11
+ return tx.tokenTransfers.filter((t) => t.tokenId === asset.tokenId && t.account === receiver).reduce((s, t) => s + t.amount, 0n);
12
+ }
13
+ function memoMatches(actual, expected, cmp = {}) {
14
+ const mode = cmp.mode ?? "exact";
15
+ if (mode === "trim") return actual.trim() === expected.trim();
16
+ if (mode === "caseInsensitive") return actual.toLowerCase() === expected.toLowerCase();
17
+ return actual === expected;
18
+ }
19
+ function classifyAmount(net, expected) {
20
+ if (net === expected) return "exact";
21
+ return net < expected ? "underpaid" : "overpaid";
22
+ }
23
+ function hashscanTxUrl(network, consensusTimestamp, transactionId) {
24
+ return `${NETWORKS[network].hashscan}/transaction/${consensusTimestamp}?tid=${txIdToMirror(transactionId)}`;
25
+ }
26
+
27
+ // src/verify.ts
28
+ var tokenDecimalsCache = /* @__PURE__ */ new Map();
29
+ function resolveClient(p) {
30
+ return {
31
+ client: p.client ?? createMirrorClient(p),
32
+ network: p.network ?? "mainnet"
33
+ };
34
+ }
35
+ async function runVerify(p, asset, expectedBase, decimals) {
36
+ if (p.after && p.before && new Date(p.after) > new Date(p.before)) {
37
+ throw new InvalidParamsError("`after` must be before `before`");
38
+ }
39
+ const { client, network } = resolveClient(p);
40
+ const tokenId = asset === "HBAR" ? void 0 : asset.tokenId;
41
+ const candidates = [];
42
+ const collect = (items) => {
43
+ for (const tx of items) {
44
+ if (tx.result !== "SUCCESS") continue;
45
+ const net = netToReceiver(tx, p.receiver, asset);
46
+ if (net <= 0n) continue;
47
+ if (tokenId && !tx.tokenTransfers.some((t) => t.tokenId === tokenId)) continue;
48
+ const payer = tx.transfers.find((t) => t.amount < 0n)?.account ?? tx.tokenTransfers.find((t) => t.amount < 0n)?.account;
49
+ const match = {
50
+ transactionId: tx.transactionId,
51
+ consensusTimestamp: tx.consensusTimestamp.raw,
52
+ netBase: net,
53
+ net: formatUnits(net, decimals),
54
+ memo: tx.memo,
55
+ transaction: tx
56
+ };
57
+ if (payer !== void 0) match.payer = payer;
58
+ candidates.push(match);
59
+ }
60
+ };
61
+ const findParams = {
62
+ accountId: p.receiver,
63
+ transactionType: "cryptotransfer",
64
+ result: "success",
65
+ order: "desc"
66
+ };
67
+ if (p.after !== void 0) findParams.after = p.after;
68
+ if (p.before !== void 0) findParams.before = p.before;
69
+ let page = await client.transactions.find(findParams);
70
+ collect(page.items);
71
+ while (page.next) {
72
+ const body = await client.transport.get(page.next);
73
+ collect((body.transactions ?? []).map(normalizeTransaction));
74
+ page = { items: [], next: body.links?.next ?? null };
75
+ }
76
+ const memoFiltered = p.memo ? candidates.filter((c) => memoMatches(c.memo, p.memo, p.memoComparison)) : candidates;
77
+ const fail = (status, reason) => ({
78
+ matched: false,
79
+ status,
80
+ receiver: p.receiver,
81
+ asset,
82
+ matches: memoFiltered,
83
+ reason
84
+ });
85
+ if (candidates.length === 0)
86
+ return fail("pending", "no matching transactions for receiver in window");
87
+ if (p.memo && memoFiltered.length === 0)
88
+ return fail("mismatch", "no transaction matched the expected memo");
89
+ const wantAtLeast = p.comparison === "atLeast";
90
+ const satisfying = memoFiltered.filter(
91
+ (c) => wantAtLeast ? c.netBase >= expectedBase : c.netBase === expectedBase
92
+ );
93
+ const resultFrom = (m, status, matched, matches, reason) => {
94
+ const result = {
95
+ matched,
96
+ status,
97
+ receiver: p.receiver,
98
+ asset,
99
+ transactionId: m.transactionId,
100
+ amountBase: m.netBase,
101
+ amount: m.net,
102
+ memo: m.memo,
103
+ consensusTimestamp: m.consensusTimestamp,
104
+ explorerUrl: hashscanTxUrl(network, m.consensusTimestamp, m.transactionId),
105
+ matches
106
+ };
107
+ if (m.payer !== void 0) result.payer = m.payer;
108
+ if (reason !== void 0) result.reason = reason;
109
+ return result;
110
+ };
111
+ if (satisfying.length === 0) {
112
+ const best = memoFiltered[0];
113
+ const cls = classifyAmount(best.netBase, expectedBase);
114
+ return resultFrom(
115
+ best,
116
+ cls === "exact" ? "confirmed" : cls,
117
+ false,
118
+ memoFiltered,
119
+ `amount ${cls}`
120
+ );
121
+ }
122
+ if (satisfying.length > 1) {
123
+ return resultFrom(
124
+ satisfying[0],
125
+ "duplicate",
126
+ false,
127
+ satisfying,
128
+ `${satisfying.length} transactions satisfy this request`
129
+ );
130
+ }
131
+ return resultFrom(satisfying[0], "confirmed", true, satisfying);
132
+ }
133
+ async function verifyHbarPayment(p) {
134
+ return runVerify(p, "HBAR", parseHbar(p.amount), 8);
135
+ }
136
+ async function verifyHtsPayment(p) {
137
+ let decimals = p.decimals;
138
+ if (decimals === void 0) {
139
+ const cached = tokenDecimalsCache.get(p.tokenId);
140
+ if (cached !== void 0) decimals = cached;
141
+ else {
142
+ const { client } = resolveClient(p);
143
+ decimals = (await client.tokens.get(p.tokenId)).decimals;
144
+ tokenDecimalsCache.set(p.tokenId, decimals);
145
+ }
146
+ }
147
+ return runVerify(p, { tokenId: p.tokenId, decimals }, parseUnits(p.amount, decimals), decimals);
148
+ }
149
+
150
+ // src/wait.ts
151
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
152
+ async function poll(verify, opts) {
153
+ const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1e3;
154
+ const pollIntervalMs = opts.pollIntervalMs ?? 3e3;
155
+ const deadline = Date.now() + timeoutMs;
156
+ let last;
157
+ while (Date.now() < deadline) {
158
+ if (opts.signal?.aborted) break;
159
+ last = await verify();
160
+ if (last.matched) return last;
161
+ if (last.status === "duplicate" || last.status === "overpaid") return last;
162
+ await sleep(pollIntervalMs);
163
+ }
164
+ return last && last.status !== "pending" ? { ...last, status: "expired" } : {
165
+ matched: false,
166
+ status: "expired",
167
+ receiver: last?.receiver ?? "",
168
+ asset: last?.asset ?? "HBAR",
169
+ matches: [],
170
+ reason: "timed out waiting for payment"
171
+ };
172
+ }
173
+ function waitForHbarPayment(p) {
174
+ return poll(() => verifyHbarPayment(p), p);
175
+ }
176
+ function waitForHtsPayment(p) {
177
+ return poll(() => verifyHtsPayment(p), p);
178
+ }
179
+
180
+ export { classifyAmount, hashscanTxUrl, memoMatches, netToReceiver, verifyHbarPayment, verifyHtsPayment, waitForHbarPayment, waitForHtsPayment };
181
+ //# sourceMappingURL=index.js.map
182
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/match.ts","../src/explorer.ts","../src/verify.ts","../src/wait.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Transaction } from \"@hbar-kit/mirror\"\nimport type { MemoComparison, PaymentAsset } from \"./types.js\"\n\n/** Signed sum of ALL the receiver's legs (HBAR transfers or a specific token's transfers). */\nexport function netToReceiver(tx: Transaction, receiver: string, asset: PaymentAsset): bigint {\n if (asset === \"HBAR\") {\n return tx.transfers.filter((t) => t.account === receiver).reduce((s, t) => s + t.amount, 0n)\n }\n return tx.tokenTransfers\n .filter((t) => t.tokenId === asset.tokenId && t.account === receiver)\n .reduce((s, t) => s + t.amount, 0n)\n}\n\nexport function memoMatches(actual: string, expected: string, cmp: MemoComparison = {}): boolean {\n const mode = cmp.mode ?? \"exact\"\n if (mode === \"trim\") return actual.trim() === expected.trim()\n if (mode === \"caseInsensitive\") return actual.toLowerCase() === expected.toLowerCase()\n return actual === expected\n}\n\nexport type AmountClass = \"exact\" | \"underpaid\" | \"overpaid\"\nexport function classifyAmount(net: bigint, expected: bigint): AmountClass {\n if (net === expected) return \"exact\"\n return net < expected ? \"underpaid\" : \"overpaid\"\n}\n","import { NETWORKS, txIdToMirror, type HederaNetwork } from \"@hbar-kit/core\"\n\n/** HashScan transaction link: consensus timestamp in path, tx id as ?tid query param. */\nexport function hashscanTxUrl(\n network: HederaNetwork,\n consensusTimestamp: string,\n transactionId: string,\n): string {\n return `${NETWORKS[network].hashscan}/transaction/${consensusTimestamp}?tid=${txIdToMirror(transactionId)}`\n}\n","import {\n parseUnits,\n parseHbar,\n formatUnits,\n type HederaNetwork,\n type NetworkInput,\n InvalidParamsError,\n} from \"@hbar-kit/core\"\nimport {\n createMirrorClient,\n normalizeTransaction,\n type MirrorClient,\n type RawTransaction,\n type Transaction,\n} from \"@hbar-kit/mirror\"\nimport { netToReceiver, memoMatches, classifyAmount } from \"./match.js\"\nimport { hashscanTxUrl } from \"./explorer.js\"\nimport type { MemoComparison, PaymentAsset, PaymentMatch, PaymentResult } from \"./types.js\"\n\nexport interface VerifyBaseParams extends NetworkInput {\n client?: MirrorClient\n receiver: string\n amount: string\n memo?: string\n memoComparison?: MemoComparison\n comparison?: \"exact\" | \"atLeast\"\n after?: Date | string\n before?: Date | string\n}\nexport type VerifyHbarParams = VerifyBaseParams\nexport interface VerifyHtsParams extends VerifyBaseParams {\n tokenId: string\n decimals?: number\n}\n\nconst tokenDecimalsCache = new Map<string, number>()\n\nfunction resolveClient(p: VerifyBaseParams): { client: MirrorClient; network: HederaNetwork } {\n return {\n client: p.client ?? createMirrorClient(p),\n network: (p.network ?? \"mainnet\") as HederaNetwork,\n }\n}\n\nasync function runVerify(\n p: VerifyBaseParams,\n asset: PaymentAsset,\n expectedBase: bigint,\n decimals: number,\n): Promise<PaymentResult> {\n if (p.after && p.before && new Date(p.after) > new Date(p.before)) {\n throw new InvalidParamsError(\"`after` must be before `before`\")\n }\n const { client, network } = resolveClient(p)\n const tokenId = asset === \"HBAR\" ? undefined : asset.tokenId\n\n const candidates: PaymentMatch[] = []\n const collect = (items: Transaction[]) => {\n for (const tx of items) {\n if (tx.result !== \"SUCCESS\") continue\n const net = netToReceiver(tx, p.receiver, asset)\n if (net <= 0n) continue\n if (tokenId && !tx.tokenTransfers.some((t) => t.tokenId === tokenId)) continue\n const payer =\n tx.transfers.find((t) => t.amount < 0n)?.account ??\n tx.tokenTransfers.find((t) => t.amount < 0n)?.account\n const match: PaymentMatch = {\n transactionId: tx.transactionId,\n consensusTimestamp: tx.consensusTimestamp.raw,\n netBase: net,\n net: formatUnits(net, decimals),\n memo: tx.memo,\n transaction: tx,\n }\n if (payer !== undefined) match.payer = payer\n candidates.push(match)\n }\n }\n\n const findParams: Parameters<typeof client.transactions.find>[0] = {\n accountId: p.receiver,\n transactionType: \"cryptotransfer\",\n result: \"success\",\n order: \"desc\",\n }\n if (p.after !== undefined) findParams.after = p.after\n if (p.before !== undefined) findParams.before = p.before\n let page = await client.transactions.find(findParams)\n collect(page.items)\n while (page.next) {\n const body = (await client.transport.get(page.next)) as {\n transactions?: RawTransaction[]\n links?: { next: string | null }\n }\n collect((body.transactions ?? []).map(normalizeTransaction))\n page = { items: [], next: body.links?.next ?? null }\n }\n\n const memoFiltered = p.memo\n ? candidates.filter((c) => memoMatches(c.memo, p.memo!, p.memoComparison))\n : candidates\n\n const fail = (status: PaymentResult[\"status\"], reason: string): PaymentResult => ({\n matched: false,\n status,\n receiver: p.receiver,\n asset,\n matches: memoFiltered,\n reason,\n })\n if (candidates.length === 0)\n return fail(\"pending\", \"no matching transactions for receiver in window\")\n if (p.memo && memoFiltered.length === 0)\n return fail(\"mismatch\", \"no transaction matched the expected memo\")\n\n const wantAtLeast = p.comparison === \"atLeast\"\n const satisfying = memoFiltered.filter((c) =>\n wantAtLeast ? c.netBase >= expectedBase : c.netBase === expectedBase,\n )\n\n const resultFrom = (\n m: PaymentMatch,\n status: PaymentResult[\"status\"],\n matched: boolean,\n matches: PaymentMatch[],\n reason?: string,\n ): PaymentResult => {\n const result: PaymentResult = {\n matched,\n status,\n receiver: p.receiver,\n asset,\n transactionId: m.transactionId,\n amountBase: m.netBase,\n amount: m.net,\n memo: m.memo,\n consensusTimestamp: m.consensusTimestamp,\n explorerUrl: hashscanTxUrl(network, m.consensusTimestamp, m.transactionId),\n matches,\n }\n if (m.payer !== undefined) result.payer = m.payer\n if (reason !== undefined) result.reason = reason\n return result\n }\n\n if (satisfying.length === 0) {\n const best = memoFiltered[0]!\n const cls = classifyAmount(best.netBase, expectedBase)\n return resultFrom(\n best,\n cls === \"exact\" ? \"confirmed\" : cls,\n false,\n memoFiltered,\n `amount ${cls}`,\n )\n }\n if (satisfying.length > 1) {\n return resultFrom(\n satisfying[0]!,\n \"duplicate\",\n false,\n satisfying,\n `${satisfying.length} transactions satisfy this request`,\n )\n }\n return resultFrom(satisfying[0]!, \"confirmed\", true, satisfying)\n}\n\nexport async function verifyHbarPayment(p: VerifyHbarParams): Promise<PaymentResult> {\n return runVerify(p, \"HBAR\", parseHbar(p.amount), 8)\n}\n\nexport async function verifyHtsPayment(p: VerifyHtsParams): Promise<PaymentResult> {\n let decimals = p.decimals\n if (decimals === undefined) {\n const cached = tokenDecimalsCache.get(p.tokenId)\n if (cached !== undefined) decimals = cached\n else {\n const { client } = resolveClient(p)\n decimals = (await client.tokens.get(p.tokenId)).decimals\n tokenDecimalsCache.set(p.tokenId, decimals)\n }\n }\n return runVerify(p, { tokenId: p.tokenId, decimals }, parseUnits(p.amount, decimals), decimals)\n}\n","import {\n verifyHbarPayment,\n verifyHtsPayment,\n type VerifyHbarParams,\n type VerifyHtsParams,\n} from \"./verify.js\"\nimport type { PaymentResult } from \"./types.js\"\n\nexport interface WaitOptions {\n timeoutMs?: number\n pollIntervalMs?: number\n signal?: AbortSignal\n}\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))\n\nasync function poll(\n verify: () => Promise<PaymentResult>,\n opts: WaitOptions,\n): Promise<PaymentResult> {\n const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000\n const pollIntervalMs = opts.pollIntervalMs ?? 3000\n const deadline = Date.now() + timeoutMs\n let last: PaymentResult | undefined\n while (Date.now() < deadline) {\n if (opts.signal?.aborted) break\n last = await verify()\n if (last.matched) return last\n if (last.status === \"duplicate\" || last.status === \"overpaid\") return last\n await sleep(pollIntervalMs)\n }\n return last && last.status !== \"pending\"\n ? { ...last, status: \"expired\" }\n : {\n matched: false,\n status: \"expired\",\n receiver: last?.receiver ?? \"\",\n asset: last?.asset ?? \"HBAR\",\n matches: [],\n reason: \"timed out waiting for payment\",\n }\n}\n\nexport function waitForHbarPayment(p: VerifyHbarParams & WaitOptions): Promise<PaymentResult> {\n return poll(() => verifyHbarPayment(p), p)\n}\nexport function waitForHtsPayment(p: VerifyHtsParams & WaitOptions): Promise<PaymentResult> {\n return poll(() => verifyHtsPayment(p), p)\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@hbar-kit/payments",
3
+ "version": "0.1.0",
4
+ "description": "Business-level HBAR/HTS payment verification on top of @hbar-kit/mirror.",
5
+ "license": "MIT",
6
+ "homepage": "https://devwhodevs.github.io/hbar-kit/",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/devwhodevs/hbar-kit.git",
10
+ "directory": "packages/payments"
11
+ },
12
+ "bugs": "https://github.com/devwhodevs/hbar-kit/issues",
13
+ "keywords": [
14
+ "hedera",
15
+ "hbar",
16
+ "hts",
17
+ "payments",
18
+ "payment-verification",
19
+ "mirror-node",
20
+ "typescript"
21
+ ],
22
+ "type": "module",
23
+ "sideEffects": false,
24
+ "files": [
25
+ "dist",
26
+ "src",
27
+ "!src/**/*.test.ts"
28
+ ],
29
+ "exports": {
30
+ ".": {
31
+ "import": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/index.d.cts",
37
+ "default": "./dist/index.cjs"
38
+ }
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "main": "./dist/index.cjs",
43
+ "module": "./dist/index.js",
44
+ "types": "./dist/index.d.ts",
45
+ "dependencies": {
46
+ "@hbar-kit/core": "0.1.0",
47
+ "@hbar-kit/mirror": "0.1.0"
48
+ },
49
+ "devDependencies": {
50
+ "typescript": "^5.6.0",
51
+ "vitest": "^2.1.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "scripts": {
57
+ "build": "tsup",
58
+ "test": "vitest run",
59
+ "coverage": "vitest run --coverage",
60
+ "typecheck": "tsc --noEmit",
61
+ "lint": "eslint src",
62
+ "check:publish": "publint --strict && attw --pack ."
63
+ }
64
+ }
@@ -0,0 +1,10 @@
1
+ import { NETWORKS, txIdToMirror, type HederaNetwork } from "@hbar-kit/core"
2
+
3
+ /** HashScan transaction link: consensus timestamp in path, tx id as ?tid query param. */
4
+ export function hashscanTxUrl(
5
+ network: HederaNetwork,
6
+ consensusTimestamp: string,
7
+ transactionId: string,
8
+ ): string {
9
+ return `${NETWORKS[network].hashscan}/transaction/${consensusTimestamp}?tid=${txIdToMirror(transactionId)}`
10
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export { verifyHbarPayment, verifyHtsPayment } from "./verify.js"
2
+ export type { VerifyHbarParams, VerifyHtsParams, VerifyBaseParams } from "./verify.js"
3
+ export { waitForHbarPayment, waitForHtsPayment } from "./wait.js"
4
+ export type { WaitOptions } from "./wait.js"
5
+ export { hashscanTxUrl } from "./explorer.js"
6
+ export { netToReceiver, memoMatches, classifyAmount } from "./match.js"
7
+ export type {
8
+ PaymentResult,
9
+ PaymentStatus,
10
+ PaymentMatch,
11
+ PaymentAsset,
12
+ MemoComparison,
13
+ } from "./types.js"
package/src/match.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { Transaction } from "@hbar-kit/mirror"
2
+ import type { MemoComparison, PaymentAsset } from "./types.js"
3
+
4
+ /** Signed sum of ALL the receiver's legs (HBAR transfers or a specific token's transfers). */
5
+ export function netToReceiver(tx: Transaction, receiver: string, asset: PaymentAsset): bigint {
6
+ if (asset === "HBAR") {
7
+ return tx.transfers.filter((t) => t.account === receiver).reduce((s, t) => s + t.amount, 0n)
8
+ }
9
+ return tx.tokenTransfers
10
+ .filter((t) => t.tokenId === asset.tokenId && t.account === receiver)
11
+ .reduce((s, t) => s + t.amount, 0n)
12
+ }
13
+
14
+ export function memoMatches(actual: string, expected: string, cmp: MemoComparison = {}): boolean {
15
+ const mode = cmp.mode ?? "exact"
16
+ if (mode === "trim") return actual.trim() === expected.trim()
17
+ if (mode === "caseInsensitive") return actual.toLowerCase() === expected.toLowerCase()
18
+ return actual === expected
19
+ }
20
+
21
+ export type AmountClass = "exact" | "underpaid" | "overpaid"
22
+ export function classifyAmount(net: bigint, expected: bigint): AmountClass {
23
+ if (net === expected) return "exact"
24
+ return net < expected ? "underpaid" : "overpaid"
25
+ }
package/src/types.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { Transaction } from "@hbar-kit/mirror"
2
+
3
+ export type PaymentStatus =
4
+ | "confirmed"
5
+ | "pending"
6
+ | "underpaid"
7
+ | "overpaid"
8
+ | "duplicate"
9
+ | "mismatch"
10
+ | "expired"
11
+ | "failed"
12
+
13
+ export type PaymentAsset = "HBAR" | { tokenId: string; decimals: number }
14
+
15
+ export interface PaymentMatch {
16
+ transactionId: string
17
+ payer?: string
18
+ consensusTimestamp: string
19
+ netBase: bigint
20
+ net: string
21
+ memo: string
22
+ transaction: Transaction
23
+ }
24
+
25
+ export interface PaymentResult {
26
+ matched: boolean
27
+ status: PaymentStatus
28
+ receiver: string
29
+ asset: PaymentAsset
30
+ transactionId?: string
31
+ payer?: string
32
+ amount?: string
33
+ amountBase?: bigint
34
+ memo?: string
35
+ consensusTimestamp?: string
36
+ explorerUrl?: string
37
+ matches: PaymentMatch[]
38
+ reason?: string
39
+ }
40
+
41
+ export interface MemoComparison {
42
+ mode?: "exact" | "trim" | "caseInsensitive"
43
+ }
package/src/verify.ts ADDED
@@ -0,0 +1,185 @@
1
+ import {
2
+ parseUnits,
3
+ parseHbar,
4
+ formatUnits,
5
+ type HederaNetwork,
6
+ type NetworkInput,
7
+ InvalidParamsError,
8
+ } from "@hbar-kit/core"
9
+ import {
10
+ createMirrorClient,
11
+ normalizeTransaction,
12
+ type MirrorClient,
13
+ type RawTransaction,
14
+ type Transaction,
15
+ } from "@hbar-kit/mirror"
16
+ import { netToReceiver, memoMatches, classifyAmount } from "./match.js"
17
+ import { hashscanTxUrl } from "./explorer.js"
18
+ import type { MemoComparison, PaymentAsset, PaymentMatch, PaymentResult } from "./types.js"
19
+
20
+ export interface VerifyBaseParams extends NetworkInput {
21
+ client?: MirrorClient
22
+ receiver: string
23
+ amount: string
24
+ memo?: string
25
+ memoComparison?: MemoComparison
26
+ comparison?: "exact" | "atLeast"
27
+ after?: Date | string
28
+ before?: Date | string
29
+ }
30
+ export type VerifyHbarParams = VerifyBaseParams
31
+ export interface VerifyHtsParams extends VerifyBaseParams {
32
+ tokenId: string
33
+ decimals?: number
34
+ }
35
+
36
+ const tokenDecimalsCache = new Map<string, number>()
37
+
38
+ function resolveClient(p: VerifyBaseParams): { client: MirrorClient; network: HederaNetwork } {
39
+ return {
40
+ client: p.client ?? createMirrorClient(p),
41
+ network: (p.network ?? "mainnet") as HederaNetwork,
42
+ }
43
+ }
44
+
45
+ async function runVerify(
46
+ p: VerifyBaseParams,
47
+ asset: PaymentAsset,
48
+ expectedBase: bigint,
49
+ decimals: number,
50
+ ): Promise<PaymentResult> {
51
+ if (p.after && p.before && new Date(p.after) > new Date(p.before)) {
52
+ throw new InvalidParamsError("`after` must be before `before`")
53
+ }
54
+ const { client, network } = resolveClient(p)
55
+ const tokenId = asset === "HBAR" ? undefined : asset.tokenId
56
+
57
+ const candidates: PaymentMatch[] = []
58
+ const collect = (items: Transaction[]) => {
59
+ for (const tx of items) {
60
+ if (tx.result !== "SUCCESS") continue
61
+ const net = netToReceiver(tx, p.receiver, asset)
62
+ if (net <= 0n) continue
63
+ if (tokenId && !tx.tokenTransfers.some((t) => t.tokenId === tokenId)) continue
64
+ const payer =
65
+ tx.transfers.find((t) => t.amount < 0n)?.account ??
66
+ tx.tokenTransfers.find((t) => t.amount < 0n)?.account
67
+ const match: PaymentMatch = {
68
+ transactionId: tx.transactionId,
69
+ consensusTimestamp: tx.consensusTimestamp.raw,
70
+ netBase: net,
71
+ net: formatUnits(net, decimals),
72
+ memo: tx.memo,
73
+ transaction: tx,
74
+ }
75
+ if (payer !== undefined) match.payer = payer
76
+ candidates.push(match)
77
+ }
78
+ }
79
+
80
+ const findParams: Parameters<typeof client.transactions.find>[0] = {
81
+ accountId: p.receiver,
82
+ transactionType: "cryptotransfer",
83
+ result: "success",
84
+ order: "desc",
85
+ }
86
+ if (p.after !== undefined) findParams.after = p.after
87
+ if (p.before !== undefined) findParams.before = p.before
88
+ let page = await client.transactions.find(findParams)
89
+ collect(page.items)
90
+ while (page.next) {
91
+ const body = (await client.transport.get(page.next)) as {
92
+ transactions?: RawTransaction[]
93
+ links?: { next: string | null }
94
+ }
95
+ collect((body.transactions ?? []).map(normalizeTransaction))
96
+ page = { items: [], next: body.links?.next ?? null }
97
+ }
98
+
99
+ const memoFiltered = p.memo
100
+ ? candidates.filter((c) => memoMatches(c.memo, p.memo!, p.memoComparison))
101
+ : candidates
102
+
103
+ const fail = (status: PaymentResult["status"], reason: string): PaymentResult => ({
104
+ matched: false,
105
+ status,
106
+ receiver: p.receiver,
107
+ asset,
108
+ matches: memoFiltered,
109
+ reason,
110
+ })
111
+ if (candidates.length === 0)
112
+ return fail("pending", "no matching transactions for receiver in window")
113
+ if (p.memo && memoFiltered.length === 0)
114
+ return fail("mismatch", "no transaction matched the expected memo")
115
+
116
+ const wantAtLeast = p.comparison === "atLeast"
117
+ const satisfying = memoFiltered.filter((c) =>
118
+ wantAtLeast ? c.netBase >= expectedBase : c.netBase === expectedBase,
119
+ )
120
+
121
+ const resultFrom = (
122
+ m: PaymentMatch,
123
+ status: PaymentResult["status"],
124
+ matched: boolean,
125
+ matches: PaymentMatch[],
126
+ reason?: string,
127
+ ): PaymentResult => {
128
+ const result: PaymentResult = {
129
+ matched,
130
+ status,
131
+ receiver: p.receiver,
132
+ asset,
133
+ transactionId: m.transactionId,
134
+ amountBase: m.netBase,
135
+ amount: m.net,
136
+ memo: m.memo,
137
+ consensusTimestamp: m.consensusTimestamp,
138
+ explorerUrl: hashscanTxUrl(network, m.consensusTimestamp, m.transactionId),
139
+ matches,
140
+ }
141
+ if (m.payer !== undefined) result.payer = m.payer
142
+ if (reason !== undefined) result.reason = reason
143
+ return result
144
+ }
145
+
146
+ if (satisfying.length === 0) {
147
+ const best = memoFiltered[0]!
148
+ const cls = classifyAmount(best.netBase, expectedBase)
149
+ return resultFrom(
150
+ best,
151
+ cls === "exact" ? "confirmed" : cls,
152
+ false,
153
+ memoFiltered,
154
+ `amount ${cls}`,
155
+ )
156
+ }
157
+ if (satisfying.length > 1) {
158
+ return resultFrom(
159
+ satisfying[0]!,
160
+ "duplicate",
161
+ false,
162
+ satisfying,
163
+ `${satisfying.length} transactions satisfy this request`,
164
+ )
165
+ }
166
+ return resultFrom(satisfying[0]!, "confirmed", true, satisfying)
167
+ }
168
+
169
+ export async function verifyHbarPayment(p: VerifyHbarParams): Promise<PaymentResult> {
170
+ return runVerify(p, "HBAR", parseHbar(p.amount), 8)
171
+ }
172
+
173
+ export async function verifyHtsPayment(p: VerifyHtsParams): Promise<PaymentResult> {
174
+ let decimals = p.decimals
175
+ if (decimals === undefined) {
176
+ const cached = tokenDecimalsCache.get(p.tokenId)
177
+ if (cached !== undefined) decimals = cached
178
+ else {
179
+ const { client } = resolveClient(p)
180
+ decimals = (await client.tokens.get(p.tokenId)).decimals
181
+ tokenDecimalsCache.set(p.tokenId, decimals)
182
+ }
183
+ }
184
+ return runVerify(p, { tokenId: p.tokenId, decimals }, parseUnits(p.amount, decimals), decimals)
185
+ }
package/src/wait.ts ADDED
@@ -0,0 +1,48 @@
1
+ import {
2
+ verifyHbarPayment,
3
+ verifyHtsPayment,
4
+ type VerifyHbarParams,
5
+ type VerifyHtsParams,
6
+ } from "./verify.js"
7
+ import type { PaymentResult } from "./types.js"
8
+
9
+ export interface WaitOptions {
10
+ timeoutMs?: number
11
+ pollIntervalMs?: number
12
+ signal?: AbortSignal
13
+ }
14
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
15
+
16
+ async function poll(
17
+ verify: () => Promise<PaymentResult>,
18
+ opts: WaitOptions,
19
+ ): Promise<PaymentResult> {
20
+ const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000
21
+ const pollIntervalMs = opts.pollIntervalMs ?? 3000
22
+ const deadline = Date.now() + timeoutMs
23
+ let last: PaymentResult | undefined
24
+ while (Date.now() < deadline) {
25
+ if (opts.signal?.aborted) break
26
+ last = await verify()
27
+ if (last.matched) return last
28
+ if (last.status === "duplicate" || last.status === "overpaid") return last
29
+ await sleep(pollIntervalMs)
30
+ }
31
+ return last && last.status !== "pending"
32
+ ? { ...last, status: "expired" }
33
+ : {
34
+ matched: false,
35
+ status: "expired",
36
+ receiver: last?.receiver ?? "",
37
+ asset: last?.asset ?? "HBAR",
38
+ matches: [],
39
+ reason: "timed out waiting for payment",
40
+ }
41
+ }
42
+
43
+ export function waitForHbarPayment(p: VerifyHbarParams & WaitOptions): Promise<PaymentResult> {
44
+ return poll(() => verifyHbarPayment(p), p)
45
+ }
46
+ export function waitForHtsPayment(p: VerifyHtsParams & WaitOptions): Promise<PaymentResult> {
47
+ return poll(() => verifyHtsPayment(p), p)
48
+ }