@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.
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/index.cjs +254 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +202 -0
- package/dist/index.d.ts +202 -0
- package/dist/index.js +246 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
- package/src/client.ts +38 -0
- package/src/index.ts +13 -0
- package/src/json.ts +17 -0
- package/src/normalize.ts +81 -0
- package/src/paginate.ts +23 -0
- package/src/resources/accounts.ts +28 -0
- package/src/resources/balances.ts +19 -0
- package/src/resources/tokens.ts +17 -0
- package/src/resources/transactions.ts +52 -0
- package/src/transport.ts +84 -0
- package/src/types.ts +120 -0
package/src/normalize.ts
ADDED
|
@@ -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
|
+
}
|
package/src/paginate.ts
ADDED
|
@@ -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
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -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
|
+
}
|