@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 +21 -0
- package/README.md +23 -0
- package/dist/index.cjs +191 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +182 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/explorer.ts +10 -0
- package/src/index.ts +13 -0
- package/src/match.ts +25 -0
- package/src/types.ts +43 -0
- package/src/verify.ts +185 -0
- package/src/wait.ts +48 -0
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":";;;;;;;;AAIO,SAAS,aAAA,CAAc,EAAA,EAAiB,QAAA,EAAkB,KAAA,EAA6B;AAC5F,EAAA,IAAI,UAAU,MAAA,EAAQ;AACpB,IAAA,OAAO,GAAG,SAAA,CAAU,MAAA,CAAO,CAAC,CAAA,KAAM,EAAE,OAAA,KAAY,QAAQ,CAAA,CAAE,MAAA,CAAO,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,GAAI,CAAA,CAAE,QAAQ,EAAE,CAAA;AAAA,EAC7F;AACA,EAAA,OAAO,EAAA,CAAG,eACP,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,OAAA,KAAY,MAAM,OAAA,IAAW,CAAA,CAAE,YAAY,QAAQ,CAAA,CACnE,OAAO,CAAC,CAAA,EAAG,MAAM,CAAA,GAAI,CAAA,CAAE,QAAQ,EAAE,CAAA;AACtC;AAEO,SAAS,WAAA,CAAY,MAAA,EAAgB,QAAA,EAAkB,GAAA,GAAsB,EAAC,EAAY;AAC/F,EAAA,MAAM,IAAA,GAAO,IAAI,IAAA,IAAQ,OAAA;AACzB,EAAA,IAAI,SAAS,MAAA,EAAQ,OAAO,OAAO,IAAA,EAAK,KAAM,SAAS,IAAA,EAAK;AAC5D,EAAA,IAAI,SAAS,iBAAA,EAAmB,OAAO,OAAO,WAAA,EAAY,KAAM,SAAS,WAAA,EAAY;AACrF,EAAA,OAAO,MAAA,KAAW,QAAA;AACpB;AAGO,SAAS,cAAA,CAAe,KAAa,QAAA,EAA+B;AACzE,EAAA,IAAI,GAAA,KAAQ,UAAU,OAAO,OAAA;AAC7B,EAAA,OAAO,GAAA,GAAM,WAAW,WAAA,GAAc,UAAA;AACxC;ACrBO,SAAS,aAAA,CACd,OAAA,EACA,kBAAA,EACA,aAAA,EACQ;AACR,EAAA,OAAO,CAAA,EAAGA,aAAA,CAAS,OAAO,CAAA,CAAE,QAAQ,gBAAgB,kBAAkB,CAAA,KAAA,EAAQC,iBAAA,CAAa,aAAa,CAAC,CAAA,CAAA;AAC3G;;;AC0BA,IAAM,kBAAA,uBAAyB,GAAA,EAAoB;AAEnD,SAAS,cAAc,CAAA,EAAuE;AAC5F,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,CAAA,CAAE,MAAA,IAAUC,yBAAA,CAAmB,CAAC,CAAA;AAAA,IACxC,OAAA,EAAU,EAAE,OAAA,IAAW;AAAA,GACzB;AACF;AAEA,eAAe,SAAA,CACb,CAAA,EACA,KAAA,EACA,YAAA,EACA,QAAA,EACwB;AACxB,EAAA,IAAI,CAAA,CAAE,KAAA,IAAS,CAAA,CAAE,MAAA,IAAU,IAAI,IAAA,CAAK,CAAA,CAAE,KAAK,CAAA,GAAI,IAAI,IAAA,CAAK,CAAA,CAAE,MAAM,CAAA,EAAG;AACjE,IAAA,MAAM,IAAIC,wBAAmB,iCAAiC,CAAA;AAAA,EAChE;AACA,EAAA,MAAM,EAAE,MAAA,EAAQ,OAAA,EAAQ,GAAI,cAAc,CAAC,CAAA;AAC3C,EAAA,MAAM,OAAA,GAAU,KAAA,KAAU,MAAA,GAAS,MAAA,GAAY,KAAA,CAAM,OAAA;AAErD,EAAA,MAAM,aAA6B,EAAC;AACpC,EAAA,MAAM,OAAA,GAAU,CAAC,KAAA,KAAyB;AACxC,IAAA,KAAA,MAAW,MAAM,KAAA,EAAO;AACtB,MAAA,IAAI,EAAA,CAAG,WAAW,SAAA,EAAW;AAC7B,MAAA,MAAM,GAAA,GAAM,aAAA,CAAc,EAAA,EAAI,CAAA,CAAE,UAAU,KAAK,CAAA;AAC/C,MAAA,IAAI,OAAO,EAAA,EAAI;AACf,MAAA,IAAI,OAAA,IAAW,CAAC,EAAA,CAAG,cAAA,CAAe,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,OAAA,KAAY,OAAO,CAAA,EAAG;AACtE,MAAA,MAAM,QACJ,EAAA,CAAG,SAAA,CAAU,KAAK,CAAC,CAAA,KAAM,EAAE,MAAA,GAAS,EAAE,GAAG,OAAA,IACzC,EAAA,CAAG,eAAe,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,MAAA,GAAS,EAAE,CAAA,EAAG,OAAA;AAChD,MAAA,MAAM,KAAA,GAAsB;AAAA,QAC1B,eAAe,EAAA,CAAG,aAAA;AAAA,QAClB,kBAAA,EAAoB,GAAG,kBAAA,CAAmB,GAAA;AAAA,QAC1C,OAAA,EAAS,GAAA;AAAA,QACT,GAAA,EAAKC,gBAAA,CAAY,GAAA,EAAK,QAAQ,CAAA;AAAA,QAC9B,MAAM,EAAA,CAAG,IAAA;AAAA,QACT,WAAA,EAAa;AAAA,OACf;AACA,MAAA,IAAI,KAAA,KAAU,MAAA,EAAW,KAAA,CAAM,KAAA,GAAQ,KAAA;AACvC,MAAA,UAAA,CAAW,KAAK,KAAK,CAAA;AAAA,IACvB;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,UAAA,GAA6D;AAAA,IACjE,WAAW,CAAA,CAAE,QAAA;AAAA,IACb,eAAA,EAAiB,gBAAA;AAAA,IACjB,MAAA,EAAQ,SAAA;AAAA,IACR,KAAA,EAAO;AAAA,GACT;AACA,EAAA,IAAI,CAAA,CAAE,KAAA,KAAU,MAAA,EAAW,UAAA,CAAW,QAAQ,CAAA,CAAE,KAAA;AAChD,EAAA,IAAI,CAAA,CAAE,MAAA,KAAW,MAAA,EAAW,UAAA,CAAW,SAAS,CAAA,CAAE,MAAA;AAClD,EAAA,IAAI,IAAA,GAAO,MAAM,MAAA,CAAO,YAAA,CAAa,KAAK,UAAU,CAAA;AACpD,EAAA,OAAA,CAAQ,KAAK,KAAK,CAAA;AAClB,EAAA,OAAO,KAAK,IAAA,EAAM;AAChB,IAAA,MAAM,OAAQ,MAAM,MAAA,CAAO,SAAA,CAAU,GAAA,CAAI,KAAK,IAAI,CAAA;AAIlD,IAAA,OAAA,CAAA,CAAS,KAAK,YAAA,IAAgB,EAAC,EAAG,GAAA,CAAIC,2BAAoB,CAAC,CAAA;AAC3D,IAAA,IAAA,GAAO,EAAE,OAAO,EAAC,EAAG,MAAM,IAAA,CAAK,KAAA,EAAO,QAAQ,IAAA,EAAK;AAAA,EACrD;AAEA,EAAA,MAAM,YAAA,GAAe,CAAA,CAAE,IAAA,GACnB,UAAA,CAAW,OAAO,CAAC,CAAA,KAAM,WAAA,CAAY,CAAA,CAAE,MAAM,CAAA,CAAE,IAAA,EAAO,CAAA,CAAE,cAAc,CAAC,CAAA,GACvE,UAAA;AAEJ,EAAA,MAAM,IAAA,GAAO,CAAC,MAAA,EAAiC,MAAA,MAAmC;AAAA,IAChF,OAAA,EAAS,KAAA;AAAA,IACT,MAAA;AAAA,IACA,UAAU,CAAA,CAAE,QAAA;AAAA,IACZ,KAAA;AAAA,IACA,OAAA,EAAS,YAAA;AAAA,IACT;AAAA,GACF,CAAA;AACA,EAAA,IAAI,WAAW,MAAA,KAAW,CAAA;AACxB,IAAA,OAAO,IAAA,CAAK,WAAW,iDAAiD,CAAA;AAC1E,EAAA,IAAI,CAAA,CAAE,IAAA,IAAQ,YAAA,CAAa,MAAA,KAAW,CAAA;AACpC,IAAA,OAAO,IAAA,CAAK,YAAY,0CAA0C,CAAA;AAEpE,EAAA,MAAM,WAAA,GAAc,EAAE,UAAA,KAAe,SAAA;AACrC,EAAA,MAAM,aAAa,YAAA,CAAa,MAAA;AAAA,IAAO,CAAC,CAAA,KACtC,WAAA,GAAc,EAAE,OAAA,IAAW,YAAA,GAAe,EAAE,OAAA,KAAY;AAAA,GAC1D;AAEA,EAAA,MAAM,aAAa,CACjB,CAAA,EACA,MAAA,EACA,OAAA,EACA,SACA,MAAA,KACkB;AAClB,IAAA,MAAM,MAAA,GAAwB;AAAA,MAC5B,OAAA;AAAA,MACA,MAAA;AAAA,MACA,UAAU,CAAA,CAAE,QAAA;AAAA,MACZ,KAAA;AAAA,MACA,eAAe,CAAA,CAAE,aAAA;AAAA,MACjB,YAAY,CAAA,CAAE,OAAA;AAAA,MACd,QAAQ,CAAA,CAAE,GAAA;AAAA,MACV,MAAM,CAAA,CAAE,IAAA;AAAA,MACR,oBAAoB,CAAA,CAAE,kBAAA;AAAA,MACtB,aAAa,aAAA,CAAc,OAAA,EAAS,CAAA,CAAE,kBAAA,EAAoB,EAAE,aAAa,CAAA;AAAA,MACzE;AAAA,KACF;AACA,IAAA,IAAI,CAAA,CAAE,KAAA,KAAU,MAAA,EAAW,MAAA,CAAO,QAAQ,CAAA,CAAE,KAAA;AAC5C,IAAA,IAAI,MAAA,KAAW,MAAA,EAAW,MAAA,CAAO,MAAA,GAAS,MAAA;AAC1C,IAAA,OAAO,MAAA;AAAA,EACT,CAAA;AAEA,EAAA,IAAI,UAAA,CAAW,WAAW,CAAA,EAAG;AAC3B,IAAA,MAAM,IAAA,GAAO,aAAa,CAAC,CAAA;AAC3B,IAAA,MAAM,GAAA,GAAM,cAAA,CAAe,IAAA,CAAK,OAAA,EAAS,YAAY,CAAA;AACrD,IAAA,OAAO,UAAA;AAAA,MACL,IAAA;AAAA,MACA,GAAA,KAAQ,UAAU,WAAA,GAAc,GAAA;AAAA,MAChC,KAAA;AAAA,MACA,YAAA;AAAA,MACA,UAAU,GAAG,CAAA;AAAA,KACf;AAAA,EACF;AACA,EAAA,IAAI,UAAA,CAAW,SAAS,CAAA,EAAG;AACzB,IAAA,OAAO,UAAA;AAAA,MACL,WAAW,CAAC,CAAA;AAAA,MACZ,WAAA;AAAA,MACA,KAAA;AAAA,MACA,UAAA;AAAA,MACA,CAAA,EAAG,WAAW,MAAM,CAAA,kCAAA;AAAA,KACtB;AAAA,EACF;AACA,EAAA,OAAO,WAAW,UAAA,CAAW,CAAC,CAAA,EAAI,WAAA,EAAa,MAAM,UAAU,CAAA;AACjE;AAEA,eAAsB,kBAAkB,CAAA,EAA6C;AACnF,EAAA,OAAO,UAAU,CAAA,EAAG,MAAA,EAAQC,eAAU,CAAA,CAAE,MAAM,GAAG,CAAC,CAAA;AACpD;AAEA,eAAsB,iBAAiB,CAAA,EAA4C;AACjF,EAAA,IAAI,WAAW,CAAA,CAAE,QAAA;AACjB,EAAA,IAAI,aAAa,MAAA,EAAW;AAC1B,IAAA,MAAM,MAAA,GAAS,kBAAA,CAAmB,GAAA,CAAI,CAAA,CAAE,OAAO,CAAA;AAC/C,IAAA,IAAI,MAAA,KAAW,QAAW,QAAA,GAAW,MAAA;AAAA,SAChC;AACH,MAAA,MAAM,EAAE,MAAA,EAAO,GAAI,aAAA,CAAc,CAAC,CAAA;AAClC,MAAA,QAAA,GAAA,CAAY,MAAM,MAAA,CAAO,MAAA,CAAO,GAAA,CAAI,CAAA,CAAE,OAAO,CAAA,EAAG,QAAA;AAChD,MAAA,kBAAA,CAAmB,GAAA,CAAI,CAAA,CAAE,OAAA,EAAS,QAAQ,CAAA;AAAA,IAC5C;AAAA,EACF;AACA,EAAA,OAAO,SAAA,CAAU,CAAA,EAAG,EAAE,OAAA,EAAS,CAAA,CAAE,OAAA,EAAS,QAAA,EAAS,EAAGC,eAAA,CAAW,CAAA,CAAE,MAAA,EAAQ,QAAQ,GAAG,QAAQ,CAAA;AAChG;;;AC3KA,IAAM,KAAA,GAAQ,CAAC,EAAA,KAAe,IAAI,OAAA,CAAQ,CAAC,CAAA,KAAM,UAAA,CAAW,CAAA,EAAG,EAAE,CAAC,CAAA;AAElE,eAAe,IAAA,CACb,QACA,IAAA,EACwB;AACxB,EAAA,MAAM,SAAA,GAAY,IAAA,CAAK,SAAA,IAAa,EAAA,GAAK,EAAA,GAAK,GAAA;AAC9C,EAAA,MAAM,cAAA,GAAiB,KAAK,cAAA,IAAkB,GAAA;AAC9C,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA;AAC9B,EAAA,IAAI,IAAA;AACJ,EAAA,OAAO,IAAA,CAAK,GAAA,EAAI,GAAI,QAAA,EAAU;AAC5B,IAAA,IAAI,IAAA,CAAK,QAAQ,OAAA,EAAS;AAC1B,IAAA,IAAA,GAAO,MAAM,MAAA,EAAO;AACpB,IAAA,IAAI,IAAA,CAAK,SAAS,OAAO,IAAA;AACzB,IAAA,IAAI,KAAK,MAAA,KAAW,WAAA,IAAe,IAAA,CAAK,MAAA,KAAW,YAAY,OAAO,IAAA;AACtE,IAAA,MAAM,MAAM,cAAc,CAAA;AAAA,EAC5B;AACA,EAAA,OAAO,IAAA,IAAQ,KAAK,MAAA,KAAW,SAAA,GAC3B,EAAE,GAAG,IAAA,EAAM,MAAA,EAAQ,SAAA,EAAU,GAC7B;AAAA,IACE,OAAA,EAAS,KAAA;AAAA,IACT,MAAA,EAAQ,SAAA;AAAA,IACR,QAAA,EAAU,MAAM,QAAA,IAAY,EAAA;AAAA,IAC5B,KAAA,EAAO,MAAM,KAAA,IAAS,MAAA;AAAA,IACtB,SAAS,EAAC;AAAA,IACV,MAAA,EAAQ;AAAA,GACV;AACN;AAEO,SAAS,mBAAmB,CAAA,EAA2D;AAC5F,EAAA,OAAO,IAAA,CAAK,MAAM,iBAAA,CAAkB,CAAC,GAAG,CAAC,CAAA;AAC3C;AACO,SAAS,kBAAkB,CAAA,EAA0D;AAC1F,EAAA,OAAO,IAAA,CAAK,MAAM,gBAAA,CAAiB,CAAC,GAAG,CAAC,CAAA;AAC1C","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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|
package/src/explorer.ts
ADDED
|
@@ -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
|
+
}
|