@hbar-kit/mirror 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.
@@ -0,0 +1,81 @@
1
+ import { parseTimestamp } from "@hbar-kit/core"
2
+ import type {
3
+ RawTransaction,
4
+ Transaction,
5
+ RawToken,
6
+ Token,
7
+ RawAccount,
8
+ AccountBalance,
9
+ RawStatus,
10
+ } from "./types.js"
11
+
12
+ const decodeMemo = (b64: string): string => (b64 ? Buffer.from(b64, "base64").toString("utf8") : "")
13
+
14
+ export function normalizeTransaction(raw: RawTransaction): Transaction {
15
+ return {
16
+ transactionId: raw.transaction_id,
17
+ consensusTimestamp: parseTimestamp(raw.consensus_timestamp),
18
+ validStartTimestamp: raw.valid_start_timestamp
19
+ ? parseTimestamp(raw.valid_start_timestamp)
20
+ : undefined,
21
+ result: raw.result,
22
+ name: raw.name,
23
+ chargedTxFee: raw.charged_tx_fee,
24
+ memo: decodeMemo(raw.memo_base64),
25
+ nonce: raw.nonce,
26
+ scheduled: raw.scheduled,
27
+ parentConsensusTimestamp: raw.parent_consensus_timestamp,
28
+ transfers: (raw.transfers ?? []).map((t) => ({
29
+ account: t.account,
30
+ amount: t.amount,
31
+ isApproval: t.is_approval ?? false,
32
+ })),
33
+ tokenTransfers: (raw.token_transfers ?? []).map((t) => ({
34
+ tokenId: t.token_id,
35
+ account: t.account,
36
+ amount: t.amount,
37
+ isApproval: t.is_approval ?? false,
38
+ })),
39
+ nftTransfers: (raw.nft_transfers ?? []).map((t) => ({
40
+ tokenId: t.token_id,
41
+ sender: t.sender_account_id,
42
+ receiver: t.receiver_account_id,
43
+ serial: t.serial_number,
44
+ isApproval: t.is_approval ?? false,
45
+ })),
46
+ raw,
47
+ }
48
+ }
49
+
50
+ export function normalizeToken(raw: RawToken): Token {
51
+ return {
52
+ tokenId: raw.token_id,
53
+ decimals: Number(raw.decimals),
54
+ symbol: raw.symbol,
55
+ name: raw.name,
56
+ type: raw.type,
57
+ totalSupply: raw.total_supply != null ? BigInt(raw.total_supply) : undefined,
58
+ maxSupply: raw.max_supply != null ? BigInt(raw.max_supply) : undefined,
59
+ treasuryAccountId: raw.treasury_account_id,
60
+ raw,
61
+ }
62
+ }
63
+
64
+ export function normalizeAccountBalance(raw: RawAccount): AccountBalance {
65
+ return {
66
+ accountId: raw.account,
67
+ balance: raw.balance.balance,
68
+ tokens: (raw.balance.tokens ?? []).map((t) => ({ tokenId: t.token_id, balance: t.balance })),
69
+ raw,
70
+ }
71
+ }
72
+
73
+ /** Detect Mirror Node not-found: the _status envelope, or an empty transactions array. */
74
+ export function isNotFound(body: unknown): boolean {
75
+ if (!body || typeof body !== "object") return false
76
+ const status = (body as RawStatus)._status
77
+ if (status?.messages?.some((m) => /not found/i.test(m.message))) return true
78
+ const txs = (body as { transactions?: unknown[] }).transactions
79
+ if (Array.isArray(txs) && txs.length === 0) return true
80
+ return false
81
+ }
@@ -0,0 +1,23 @@
1
+ import type { Transport } from "./transport.js"
2
+
3
+ export interface PaginateOptions {
4
+ maxPages?: number
5
+ }
6
+
7
+ /** Async-iterate a list endpoint by following links.next verbatim until null. */
8
+ export async function* paginate<T>(
9
+ transport: Transport,
10
+ firstPath: string,
11
+ select: (page: unknown) => T[],
12
+ options: PaginateOptions = {},
13
+ ): AsyncGenerator<T, void, unknown> {
14
+ const maxPages = options.maxPages ?? 1000
15
+ let path: string | null = firstPath
16
+ let pages = 0
17
+ while (path && pages < maxPages) {
18
+ const page: unknown = await transport.get(path)
19
+ for (const item of select(page)) yield item
20
+ path = (page as { links?: { next: string | null } }).links?.next ?? null
21
+ pages++
22
+ }
23
+ }
@@ -0,0 +1,28 @@
1
+ import { assertEntityId } from "@hbar-kit/core"
2
+ import type { Transport } from "../transport.js"
3
+ import { normalizeAccountBalance } from "../normalize.js"
4
+ import type { AccountBalance, RawAccount } from "../types.js"
5
+
6
+ export interface AccountsResource {
7
+ getBalance(accountId: string): Promise<AccountBalance>
8
+ isAssociated(accountId: string, tokenId: string): Promise<boolean>
9
+ }
10
+
11
+ export function createAccountsResource(transport: Transport): AccountsResource {
12
+ return {
13
+ async getBalance(accountId) {
14
+ assertEntityId(accountId)
15
+ return normalizeAccountBalance(
16
+ (await transport.get(`/api/v1/accounts/${accountId}`)) as RawAccount,
17
+ )
18
+ },
19
+ async isAssociated(accountId, tokenId) {
20
+ assertEntityId(accountId)
21
+ assertEntityId(tokenId)
22
+ const body = (await transport.get(
23
+ `/api/v1/accounts/${accountId}/tokens?token.id=${tokenId}`,
24
+ )) as { tokens?: unknown[] }
25
+ return Array.isArray(body.tokens) && body.tokens.length > 0
26
+ },
27
+ }
28
+ }
@@ -0,0 +1,19 @@
1
+ import { assertEntityId } from "@hbar-kit/core"
2
+ import type { Transport } from "../transport.js"
3
+
4
+ export interface BalancesResource {
5
+ get(accountId: string): Promise<{ accountId: string; balance: bigint; timestamp: string }>
6
+ }
7
+
8
+ export function createBalancesResource(transport: Transport): BalancesResource {
9
+ return {
10
+ async get(accountId) {
11
+ assertEntityId(accountId)
12
+ const body = (await transport.get(`/api/v1/balances?account.id=${accountId}&limit=1`)) as {
13
+ timestamp: string
14
+ balances: { account: string; balance: bigint }[]
15
+ }
16
+ return { accountId, balance: body.balances?.[0]?.balance ?? 0n, timestamp: body.timestamp }
17
+ },
18
+ }
19
+ }
@@ -0,0 +1,17 @@
1
+ import { assertEntityId } from "@hbar-kit/core"
2
+ import type { Transport } from "../transport.js"
3
+ import { normalizeToken } from "../normalize.js"
4
+ import type { RawToken, Token } from "../types.js"
5
+
6
+ export interface TokensResource {
7
+ get(tokenId: string): Promise<Token>
8
+ }
9
+
10
+ export function createTokensResource(transport: Transport): TokensResource {
11
+ return {
12
+ async get(tokenId) {
13
+ assertEntityId(tokenId)
14
+ return normalizeToken((await transport.get(`/api/v1/tokens/${tokenId}`)) as RawToken)
15
+ },
16
+ }
17
+ }
@@ -0,0 +1,52 @@
1
+ import { tsFilter, txIdToMirror } from "@hbar-kit/core"
2
+ import type { Transport } from "../transport.js"
3
+ import { normalizeTransaction } from "../normalize.js"
4
+ import type { Page, RawTransaction, RawTransactionList, Transaction } from "../types.js"
5
+
6
+ export interface FindTransactionsParams {
7
+ accountId?: string
8
+ transactionType?: string
9
+ result?: "success" | "fail"
10
+ order?: "asc" | "desc"
11
+ limit?: number
12
+ after?: Date | string
13
+ before?: Date | string
14
+ }
15
+
16
+ export interface TransactionsResource {
17
+ find(params?: FindTransactionsParams): Promise<Page<Transaction>>
18
+ get(transactionId: string): Promise<Transaction[]>
19
+ }
20
+
21
+ function buildQuery(params: FindTransactionsParams): string {
22
+ const q = new URLSearchParams()
23
+ if (params.accountId) q.set("account.id", params.accountId)
24
+ if (params.transactionType) q.set("transactiontype", params.transactionType)
25
+ if (params.result) q.set("result", params.result)
26
+ if (params.order) q.set("order", params.order)
27
+ q.set("limit", String(Math.min(params.limit ?? 25, 100)))
28
+ if (params.after) q.append("timestamp", tsFilter("gte", params.after))
29
+ if (params.before) q.append("timestamp", tsFilter("lt", params.before))
30
+ return q.toString()
31
+ }
32
+
33
+ export function createTransactionsResource(transport: Transport): TransactionsResource {
34
+ return {
35
+ async find(params = {}) {
36
+ const body = (await transport.get(
37
+ `/api/v1/transactions?${buildQuery(params)}`,
38
+ )) as RawTransactionList
39
+ return {
40
+ items: (body.transactions ?? []).map(normalizeTransaction),
41
+ next: body.links?.next ?? null,
42
+ }
43
+ },
44
+ async get(transactionId) {
45
+ const id = txIdToMirror(transactionId)
46
+ const body = (await transport.get(`/api/v1/transactions/${id}`)) as {
47
+ transactions?: RawTransaction[]
48
+ }
49
+ return (body.transactions ?? []).map(normalizeTransaction)
50
+ },
51
+ }
52
+ }
@@ -0,0 +1,84 @@
1
+ import { MirrorHttpError, RateLimitError, TimeoutError, NetworkError } from "@hbar-kit/core"
2
+ import { parseJsonWithBigInt } from "./json.js"
3
+
4
+ export interface TransportOptions {
5
+ fetch?: typeof fetch
6
+ retryCount?: number
7
+ retryDelay?: number
8
+ timeout?: number
9
+ headers?: Record<string, string>
10
+ }
11
+ export interface Transport {
12
+ get(path: string): Promise<unknown>
13
+ baseUrl: string
14
+ }
15
+
16
+ const RETRYABLE = new Set([408, 425, 429, 500, 502, 503, 504])
17
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
18
+ const jitter = (ms: number) => ms * (0.5 + Math.random())
19
+
20
+ export function http(baseUrl: string, options: TransportOptions = {}): Transport {
21
+ const doFetch = options.fetch ?? globalThis.fetch
22
+ const retryCount = options.retryCount ?? 3
23
+ const retryDelay = options.retryDelay ?? 150
24
+ const timeout = options.timeout ?? 10_000
25
+ const base = baseUrl.replace(/\/+$/, "")
26
+
27
+ async function get(path: string): Promise<unknown> {
28
+ const url = path.startsWith("http") ? path : base + path
29
+ let attempt = 0
30
+ let lastErr: unknown
31
+ while (attempt <= retryCount) {
32
+ const controller = new AbortController()
33
+ const timer = setTimeout(() => controller.abort(), timeout)
34
+ try {
35
+ const response = await doFetch(url, {
36
+ signal: controller.signal,
37
+ headers: { accept: "application/json", ...options.headers },
38
+ })
39
+ clearTimeout(timer)
40
+ if (response.ok) {
41
+ const text = await response.text()
42
+ return text ? parseJsonWithBigInt(text) : null
43
+ }
44
+ if (RETRYABLE.has(response.status) && attempt < retryCount) {
45
+ const retryAfter = Number(response.headers.get("retry-after"))
46
+ const wait =
47
+ Number.isFinite(retryAfter) && retryAfter > 0
48
+ ? retryAfter * 1000
49
+ : jitter(retryDelay * 2 ** attempt)
50
+ await sleep(wait)
51
+ attempt++
52
+ continue
53
+ }
54
+ const body = await response.text().catch(() => "")
55
+ if (response.status === 429) {
56
+ const ra = Number(response.headers.get("retry-after"))
57
+ throw new RateLimitError(
58
+ `Mirror Node rate limited (429)`,
59
+ Number.isFinite(ra) ? { details: body, retryAfter: ra } : { details: body },
60
+ )
61
+ }
62
+ throw new MirrorHttpError(`Mirror Node HTTP ${response.status}`, response.status, {
63
+ details: body,
64
+ })
65
+ } catch (err) {
66
+ clearTimeout(timer)
67
+ if (err instanceof MirrorHttpError) throw err
68
+ if (err instanceof DOMException && err.name === "AbortError") {
69
+ throw new TimeoutError(`Mirror Node request timed out after ${timeout}ms`, { cause: err })
70
+ }
71
+ lastErr = err
72
+ if (attempt < retryCount) {
73
+ await sleep(jitter(retryDelay * 2 ** attempt))
74
+ attempt++
75
+ continue
76
+ }
77
+ throw new NetworkError(`Mirror Node request failed: ${String(err)}`, { cause: err })
78
+ }
79
+ }
80
+ throw new NetworkError(`Mirror Node request failed`, { cause: lastErr })
81
+ }
82
+
83
+ return { get, baseUrl: base }
84
+ }
package/src/types.ts ADDED
@@ -0,0 +1,120 @@
1
+ import type { HederaTimestamp } from "@hbar-kit/core"
2
+
3
+ // ---- Raw Mirror Node shapes ----
4
+ export interface RawTransfer {
5
+ account: string
6
+ amount: bigint
7
+ is_approval?: boolean
8
+ }
9
+ export interface RawTokenTransfer {
10
+ token_id: string
11
+ account: string
12
+ amount: bigint
13
+ is_approval?: boolean
14
+ }
15
+ export interface RawNftTransfer {
16
+ token_id: string
17
+ sender_account_id: string | null
18
+ receiver_account_id: string | null
19
+ serial_number: bigint
20
+ is_approval?: boolean
21
+ }
22
+ export interface RawTransaction {
23
+ transaction_id: string
24
+ consensus_timestamp: string
25
+ valid_start_timestamp?: string
26
+ result: string
27
+ name: string
28
+ charged_tx_fee: bigint
29
+ memo_base64: string
30
+ node?: string
31
+ nonce: number
32
+ scheduled: boolean
33
+ parent_consensus_timestamp: string | null
34
+ transfers: RawTransfer[]
35
+ token_transfers: RawTokenTransfer[]
36
+ nft_transfers: RawNftTransfer[]
37
+ }
38
+ export interface RawListLinks {
39
+ next: string | null
40
+ }
41
+ export interface RawTransactionList {
42
+ transactions: RawTransaction[]
43
+ links: RawListLinks
44
+ }
45
+ export interface RawToken {
46
+ token_id: string
47
+ decimals: string | number
48
+ symbol: string
49
+ name: string
50
+ type: string
51
+ total_supply?: string
52
+ max_supply?: string
53
+ treasury_account_id?: string
54
+ }
55
+ export interface RawAccount {
56
+ account: string
57
+ evm_address?: string
58
+ deleted?: boolean
59
+ balance: { balance: bigint; timestamp: string; tokens: { token_id: string; balance: bigint }[] }
60
+ }
61
+ export interface RawStatus {
62
+ _status?: { messages?: { message: string }[] }
63
+ }
64
+
65
+ // ---- Normalized shapes ----
66
+ export interface Transfer {
67
+ account: string
68
+ amount: bigint
69
+ isApproval: boolean
70
+ }
71
+ export interface TokenTransfer {
72
+ tokenId: string
73
+ account: string
74
+ amount: bigint
75
+ isApproval: boolean
76
+ }
77
+ export interface NftTransfer {
78
+ tokenId: string
79
+ sender: string | null
80
+ receiver: string | null
81
+ serial: bigint
82
+ isApproval: boolean
83
+ }
84
+ export interface Transaction {
85
+ transactionId: string
86
+ consensusTimestamp: HederaTimestamp
87
+ validStartTimestamp?: HederaTimestamp | undefined
88
+ result: string
89
+ name: string
90
+ chargedTxFee: bigint
91
+ memo: string
92
+ nonce: number
93
+ scheduled: boolean
94
+ parentConsensusTimestamp: string | null
95
+ transfers: Transfer[]
96
+ tokenTransfers: TokenTransfer[]
97
+ nftTransfers: NftTransfer[]
98
+ raw: RawTransaction
99
+ }
100
+ export interface Token {
101
+ tokenId: string
102
+ decimals: number
103
+ symbol: string
104
+ name: string
105
+ type: string
106
+ totalSupply?: bigint | undefined
107
+ maxSupply?: bigint | undefined
108
+ treasuryAccountId?: string | undefined
109
+ raw: RawToken
110
+ }
111
+ export interface AccountBalance {
112
+ accountId: string
113
+ balance: bigint
114
+ tokens: { tokenId: string; balance: bigint }[]
115
+ raw: RawAccount
116
+ }
117
+ export interface Page<T> {
118
+ items: T[]
119
+ next: string | null
120
+ }