Skip to content

Commit 48499f8

Browse files
authored
feat(payments): add USDC payment verification for Hedera (#5)
Add `verifyUsdcPayment` and `waitForUsdcPayment` as convenience wrappers over the HTS verification engine, with 6-decimal amount parsing, an optional token-id override, and a verified USDC token registry. - `verifyUsdcPayment` / `waitForUsdcPayment`: required `network`, forced 6-decimal parsing, optional `tokenId` override for dev/mock tokens; result `asset` tagged `{ tokenId, decimals: 6, symbol: "USDC" }`. - USDC registry: `USDC_TOKEN_IDS`, `getUsdcTokenId`, `USDC_DECIMALS`, `isUsdcPaymentResult`. Token ids verified against the live Mirror Node and Circle's official docs: mainnet 0.0.456858, testnet 0.0.429274; previewnet has none and throws. - core: add `UnsupportedAssetError` (thrown for networks without a verified USDC token id). - `PaymentAsset` gains an additive optional `symbol?` (non-breaking). - Tests: 22 unit cases plus an opt-in, read-only live smoke test gated by `HBARKIT_LIVE=1`. - Docs/example/llms: new "Verify a USDC payment" guide, README sections, `verify:usdc` example script, and llms.txt/llms-full.txt updates. Changeset: payments minor, core minor.
1 parent eab0470 commit 48499f8

23 files changed

Lines changed: 751 additions & 11 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@hbar-kit/payments": minor
3+
"@hbar-kit/core": minor
4+
---
5+
6+
Add USDC payment verification helpers for Hedera.
7+
8+
Introduces `verifyUsdcPayment` and `waitForUsdcPayment` as convenience wrappers over HTS payment
9+
verification, with 6-decimal amount parsing, optional token ID override, docs, examples, and tests.
10+
Adds a verified USDC token registry (`USDC_TOKEN_IDS`, `getUsdcTokenId`) with mainnet/testnet token
11+
ids confirmed against the Mirror Node and Circle's official docs, plus `isUsdcPaymentResult` and an
12+
optional `symbol` field on `PaymentAsset` (additive, non-breaking). Adds a new `UnsupportedAssetError`
13+
to `@hbar-kit/core` for networks without a verified USDC token id (e.g. previewnet).

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,25 @@ if (result.matched) {
6060
No private keys, no signing, no funds held — `verifyHbarPayment` only **reads** public Mirror Node
6161
data and tells you whether the payment you expected actually arrived.
6262

63+
### Verify a USDC payment
64+
65+
USDC on Hedera is an HTS token. hbar-kit includes a USDC convenience helper for common invoice,
66+
checkout, and payment-link flows — it fills in the verified USDC token id and 6-decimal parsing.
67+
68+
```ts
69+
import { verifyUsdcPayment } from "@hbar-kit/payments"
70+
71+
const result = await verifyUsdcPayment({
72+
network: "mainnet",
73+
receiver: "0.0.12345",
74+
amount: "25.00",
75+
memo: "invoice_123",
76+
after: new Date(Date.now() - 30 * 60 * 1000),
77+
})
78+
```
79+
80+
See [Verify a USDC payment](https://devwhodevs.github.io/hbar-kit/guide/verify-usdc-payment).
81+
6382
## Use cases
6483

6584
- Hedera payment links

docs/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export default defineConfig({
3939
items: [
4040
{ text: "Verify an HBAR payment", link: "/guide/verify-hbar" },
4141
{ text: "Verify an HTS token payment", link: "/guide/verify-hts" },
42+
{ text: "Verify a USDC payment", link: "/guide/verify-usdc-payment" },
4243
{ text: "Wait for a payment", link: "/guide/wait-for-payment" },
4344
{ text: "Use a custom Mirror Node", link: "/guide/custom-mirror-node" },
4445
{ text: "Partial & duplicate payments", link: "/guide/partial-and-duplicate" },

docs/guide/amounts-and-decimals.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ formatUnits(2_889_029n, 6) // "2.889029"
1212
```
1313

1414
Parsing is **strict**: a value with more decimals than allowed throws `InvalidAmountError`.
15+
16+
USDC on Hedera uses **6 decimals**. [`verifyUsdcPayment`](/guide/verify-usdc-payment) always parses
17+
amounts at 6 decimals, so `"25.001234"` is fine but `"25.0012345"` (7 dp) throws
18+
`InvalidAmountError`.

docs/guide/verify-hts.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ const result = await verifyHtsPayment({
1515
```
1616

1717
If you omit `decimals`, hbar-kit fetches it from the token info endpoint and caches it.
18+
19+
> Paying in **USDC**? Use [`verifyUsdcPayment`](/guide/verify-usdc-payment) — a thin wrapper that
20+
> fills in the verified USDC token id and 6-decimal parsing for you.

docs/guide/verify-usdc-payment.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Verify a USDC payment on Hedera
2+
3+
USDC on Hedera is a normal **HTS token**, so hbar-kit can already verify it with
4+
[`verifyHtsPayment`](/guide/verify-hts). `verifyUsdcPayment` is a thin convenience wrapper that
5+
fills in the **verified USDC token id** for the network and parses amounts at **6 decimals**, so you
6+
don't rebuild that boilerplate for every invoice, checkout, or payment-link flow.
7+
8+
It is the same read-only, non-custodial verification as the rest of hbar-kit: no private keys, no
9+
funds moved — it only reads public Mirror Node data and tells you whether the expected payment
10+
arrived.
11+
12+
```ts
13+
import { verifyUsdcPayment } from "@hbar-kit/payments"
14+
15+
const result = await verifyUsdcPayment({
16+
network: "mainnet",
17+
receiver: "0.0.12345",
18+
amount: "25.00", // 25 USDC — a decimal string, never a float
19+
memo: "invoice_123", // your order / invoice id
20+
after: new Date(Date.now() - 30 * 60 * 1000),
21+
})
22+
23+
if (result.matched) {
24+
// Confirmed. Mark the invoice paid using result.transactionId as your idempotency key.
25+
}
26+
```
27+
28+
## What it does under the hood
29+
30+
`verifyUsdcPayment` calls `verifyHtsPayment` with:
31+
32+
- the canonical USDC token id for the selected `network`,
33+
- `decimals = 6`,
34+
- the same `receiver` / `amount` / `memo` / time-window rules,
35+
- the same [payment status model](/guide/concepts).
36+
37+
The returned `PaymentResult` is identical to the HTS one, except its `asset` is tagged as USDC:
38+
39+
```ts
40+
result.asset // { tokenId: "0.0.456858", decimals: 6, symbol: "USDC" }
41+
```
42+
43+
Use `isUsdcPaymentResult(result)` (or inspect `result.asset.tokenId`) to detect USDC results in a
44+
mixed pipeline.
45+
46+
## Verified token ids
47+
48+
USDC token ids are **network-specific**, so `network` is **required** — the USDC helper never
49+
assumes mainnet. The ids are verified against the live Hedera Mirror Node and Circle's official
50+
[USDC contract addresses](https://developers.circle.com/stablecoins/usdc-contract-addresses):
51+
52+
| Network | USDC token id |
53+
| ---------- | --------------------------------------------------- |
54+
| mainnet | `0.0.456858` |
55+
| testnet | `0.0.429274` |
56+
| previewnet | _not issued by Circle_ → throws `UnsupportedAssetError` |
57+
58+
```ts
59+
import { getUsdcTokenId, USDC_TOKEN_IDS } from "@hbar-kit/payments"
60+
61+
getUsdcTokenId("mainnet") // "0.0.456858"
62+
USDC_TOKEN_IDS.testnet // "0.0.429274"
63+
```
64+
65+
## Custom / mock tokens (testnet & dev)
66+
67+
For local testing against a mock HTS token, pass an explicit `tokenId`. It is still parsed at 6
68+
decimals and tagged as USDC:
69+
70+
```ts
71+
const result = await verifyUsdcPayment({
72+
network: "testnet",
73+
tokenId: process.env.TESTNET_USDC_TOKEN_ID!, // your dev/mock token
74+
receiver: "0.0.12345",
75+
amount: "10.00",
76+
memo: "test_invoice_1",
77+
})
78+
```
79+
80+
Production mainnet flows should omit `tokenId` and use the verified canonical id.
81+
82+
## Amount precision
83+
84+
Amounts are **decimal strings**, never floats, and USDC is always **6 decimals**:
85+
86+
```ts
87+
"25.00" // ✅ 25 USDC
88+
"25.001234" // ✅ 6 decimals
89+
"25.0012345" // ❌ 7 decimals → throws InvalidAmountError
90+
```
91+
92+
## Production checklist
93+
94+
- **Always verify server-side.** Never trust a client-supplied amount, receiver, token id, or memo
95+
— look them up from your own records, keyed by an opaque order id.
96+
- **Always use a time window** (`after`/`before`) to bound the search.
97+
- **Use a unique memo per payment request** so duplicates are detectable.
98+
- **Handle every status**, not just `confirmed`: `underpaid`, `overpaid`, `duplicate`, `mismatch`,
99+
`pending`, `expired`.
100+
- **Use `comparison: "atLeast"` only if you accept overpayment**; the default is exact-match.
101+
- **Use `transactionId` for idempotency** in your own database so each payment is processed once.
102+
103+
```ts
104+
const result = await verifyUsdcPayment({
105+
network: "mainnet",
106+
receiver: "0.0.12345",
107+
amount: "25.00",
108+
memo: "invoice_123",
109+
after: new Date(Date.now() - 30 * 60 * 1000),
110+
})
111+
112+
switch (result.status) {
113+
case "confirmed":
114+
// result.matched === true — settle the order, store result.transactionId
115+
break
116+
case "underpaid":
117+
case "overpaid":
118+
case "duplicate":
119+
case "mismatch":
120+
case "pending":
121+
// branch on each explicitly
122+
break
123+
}
124+
```
125+
126+
## Wait for a USDC payment
127+
128+
`waitForUsdcPayment` polls until the payment is confirmed (or `overpaid`/`duplicate`), or until
129+
`timeoutMs` elapses (then `status: "expired"`). It mirrors `waitForHbarPayment` /
130+
`waitForHtsPayment` and accepts `timeoutMs`, `pollIntervalMs`, and an `AbortSignal`:
131+
132+
```ts
133+
import { waitForUsdcPayment } from "@hbar-kit/payments"
134+
135+
const result = await waitForUsdcPayment({
136+
network: "mainnet",
137+
receiver: "0.0.12345",
138+
amount: "25.00",
139+
memo: "invoice_123",
140+
timeoutMs: 5 * 60 * 1000,
141+
pollIntervalMs: 3000,
142+
})
143+
```
144+
145+
## When to use which
146+
147+
- **`verifyUsdcPayment`** — you're accepting USDC and want the token id + 6-decimal parsing handled
148+
for you. The common case.
149+
- **`verifyHtsPayment`** — any other HTS token, or when you need full control over `tokenId` and
150+
`decimals`.

docs/guide/wait-for-payment.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ const result = await waitForHbarPayment({
1616

1717
Pass an `AbortSignal` as `signal` to cancel early. Polling stops immediately on a confirmed,
1818
duplicate, or overpaid result.
19+
20+
The same polling exists for tokens — `waitForHtsPayment` and
21+
[`waitForUsdcPayment`](/guide/verify-usdc-payment) — with identical `timeoutMs` / `pollIntervalMs` /
22+
`signal` options.

docs/reference/payments.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ interface PaymentResult {
2626
| "expired"
2727
| "failed"
2828
receiver: string
29-
asset: "HBAR" | { tokenId: string; decimals: number }
29+
asset: "HBAR" | { tokenId: string; decimals: number; symbol?: string }
3030
transactionId?: string
3131
payer?: string
3232
amount?: string
@@ -43,6 +43,17 @@ interface PaymentResult {
4343

4444
Adds `tokenId: string` and optional `decimals?: number` (auto-fetched when omitted).
4545

46-
## waitForHbarPayment / waitForHtsPayment
46+
## verifyUsdcPayment(params)
47+
48+
Wrapper over `verifyHtsPayment` that resolves the verified USDC token id for `network` (**required**;
49+
mainnet `0.0.456858`, testnet `0.0.429274`, previewnet throws `UnsupportedAssetError`) and forces
50+
`decimals = 6`. Accepts an optional `tokenId` override (dev/mock tokens, still 6 decimals). The
51+
result `asset` is tagged `{ tokenId, decimals: 6, symbol: "USDC" }`. See
52+
[Verify a USDC payment](/guide/verify-usdc-payment).
53+
54+
Also exported: `getUsdcTokenId(network)`, `USDC_TOKEN_IDS`, `USDC_DECIMALS` (= 6), and
55+
`isUsdcPaymentResult(result)`.
56+
57+
## waitForHbarPayment / waitForHtsPayment / waitForUsdcPayment
4758

4859
Adds `timeoutMs?`, `pollIntervalMs?`, `signal?`. Resolves `confirmed` or `expired`.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
# Used by verify:hbar and verify:hts (network is hardcoded to testnet in those scripts)
12
HBARKIT_RECEIVER=0.0.12345
23
HBARKIT_AMOUNT=25
34
HBARKIT_MEMO=order_6471727153206
45
HBARKIT_TOKEN=0.0.456858
6+
7+
# Used by verify:usdc
8+
HEDERA_NETWORK=mainnet
9+
RECEIVER=0.0.12345
10+
AMOUNT=25.00
11+
MEMO=invoice_123
12+
# USDC_TOKEN_ID=0.0.429274 # optional override for a dev/testnet mock token

examples/node-payment-verifier/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,18 @@ pnpm --filter @hbar-kit/example-node-payment-verifier verify:hbar
2222

2323
# Verify an HTS token payment
2424
pnpm --filter @hbar-kit/example-node-payment-verifier verify:hts
25+
26+
# Verify a USDC payment
27+
pnpm --filter @hbar-kit/example-node-payment-verifier verify:usdc
2528
```
2629

27-
Each script prints `PAID` / `NOT PAID` plus the full `PaymentResult` (status, amount, payer), and —
28-
when matched — a HashScan explorer deep-link.
30+
`verify:hbar` / `verify:hts` print `PAID` / `NOT PAID` plus the full `PaymentResult` (status,
31+
amount, payer), and — when matched — a HashScan explorer deep-link.
32+
33+
`verify:usdc` reads `HEDERA_NETWORK`, `RECEIVER`, `AMOUNT`, `MEMO`, and an optional `USDC_TOKEN_ID`
34+
override (for a dev/testnet mock token), then prints `PAID` / `NOT PAID`, the status, transaction id,
35+
payer, amount, and a HashScan link when matched. With no `USDC_TOKEN_ID` it uses the verified
36+
canonical USDC token id for the network.
2937

3038
## Key idea: server-side verification
3139

0 commit comments

Comments
 (0)