diff --git a/README.md b/README.md index 34a3377..b1a0c99 100644 --- a/README.md +++ b/README.md @@ -29,17 +29,39 @@ Using bun: $ bun add ordapi ``` +## Import + +Import the client and types depending on your needs: + +```typescript +// Using default import +import OrdClient from 'ordapi'; + +const client = new OrdClient('https://ord-server.com'); +const block = await client.getBlock(0); +``` + +```typescript +// Using both client and types +import OrdClient, { Inscription } from 'ordapi'; + +async function getInscription(id: string): Promise { + const client = new OrdClient('https://ord-server.com'); + return await client.getInscription(id); +} +``` + ## Usage ```typescript -import { OrdClient, Block } from 'ordapi'; +import OrdClient, { Block } from 'ordapi'; function App() { const [blockInfo, setBlockInfo] = useState(null); useEffect(() => { // Create client instance - const client = new OrdClient('https://your-ord-server.xyz'); + const client = new OrdClient('https://ord-server.xyz'); // Fetch genesis block info async function fetchBlock() { @@ -57,9 +79,13 @@ function App() { return (

Genesis Block

-

Height: {blockInfo.height}

-

Hash: {blockInfo.hash}

-

Number of transactions: {blockInfo.transactions.length}

+ {blockInfo && ( + <> +

Height: {blockInfo.height}

+

Hash: {blockInfo.hash}

+

Number of transactions: {blockInfo.transactions.length}

+ + )}
); } diff --git a/package.json b/package.json index be4badf..fb741dc 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,22 @@ { "name": "ordapi", "version": "0.0.2", - "module": "dist/index.js", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "files": ["dist"], "scripts": { - "build": "bun build ./src/index.ts --outdir ./dist --target node", + "build": "tsc --emitDeclarationOnly && bun build ./src/index.ts --outdir ./dist --target node", "test": "bun test ./src/test/**/*.test.ts", "format": "prettier --write 'src/**/*.ts'", "lint": "eslint . && tsc -b", "prepublishOnly": "bun run build" }, - "imports": { - "#src/*": "./src/*", - "#schemas/*": "./src/schemas/*", - "#types/*": "./src/types/*" + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } }, "dependencies": { "typescript-eslint": "^8.22.0", diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..d208c6e --- /dev/null +++ b/src/api.ts @@ -0,0 +1,41 @@ +import { OutputType } from './types'; + +const api = { + address: (address: string) => `/address/${address}`, + block: (heightOrHash: number | string) => `/block/${heightOrHash}`, + blockcount: '/blockcount', + blockhash: { + latest: '/blockhash', + byHeight: (height: number) => `/blockhash/${height}`, + }, + blockheight: '/blockheight', + blocks: '/blocks', + blocktime: '/blocktime', + inscription: (id: string) => `/inscription/${id}`, + inscriptionChild: (id: string, child: number) => + `/inscription/${id}/${child}`, + inscriptions: { + base: '/inscriptions', + latest: '/inscriptions', + byPage: (page: number) => `/inscriptions/${page}`, + byBlock: (height: number) => `/inscriptions/block/${height}`, + }, + output: (outpoint: string) => `/output/${outpoint}`, + outputs: { + base: '/outputs', + byAddress: (address: string, type?: OutputType) => { + const base = `/outputs/${address}`; + return type ? `${base}?type=${type}` : base; + }, + }, + rune: (name: string) => `/rune/${name}`, + runes: { + latest: '/runes', + byPage: (page: number) => `/runes/${page}`, + }, + sat: (number: number) => `/sat/${number}`, + tx: (txId: string) => `/tx/${txId}`, + status: '/status', +} as const; + +export default api; diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..9ac5448 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,222 @@ +import { z } from 'zod'; +import api from './api'; +import { + BlockInfoSchema, + BlockHashSchema, + AddressInfoSchema, + BlocksResponseSchema, + InscriptionSchema, + InscriptionsResponseSchema, + OutputInfoSchema, + RuneResponseSchema, + RunesResponseSchema, + SatSchema, + ServerStatusSchema, + TransactionInfoSchema, +} from './schemas'; +import type { + BlockInfo, + BlockHash, + AddressInfo, + BlocksResponse, + InscriptionInfo, + InscriptionsResponse, + OutputInfo, + RuneResponse, + RunesResponse, + SatInfo, + TransactionInfo, + ServerStatus, + OutputType, +} from './types'; + +type ApiResponse = + | { success: true; data: T } + | { success: false; error: string }; + +export class OrdClient { + private headers: HeadersInit; + + constructor( + private baseUrl: string, + headers: HeadersInit = {}, + ) { + this.headers = { + Accept: 'application/json', + ...headers, + }; + } + + private async fetch( + endpoint: string, + schema: T, + ): Promise> { + const response = await fetch(`${this.baseUrl}${endpoint}`, { + headers: this.headers, + }); + + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`); + } + + const text = await response.text(); + const result = schema.safeParse(text); + + if (!result.success) { + try { + const json = JSON.parse(text); + const jsonResult = schema.safeParse(json); + if (jsonResult.success) { + return jsonResult.data; + } + } catch {} + + throw new Error(`Validation error: ${result.error.message}`); + } + + return result.data; + } + + private async fetchPost( + endpoint: string, + payload: P, + schema: T, + ): Promise> { + const response = await fetch(`${this.baseUrl}${endpoint}`, { + method: 'POST', + headers: { + ...this.headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`API request failed: ${response.statusText}`); + } + + const data = await response.json(); + const result = schema.safeParse(data); + + if (!result.success) { + throw new Error(`Validation error: ${result.error.message}`); + } + + return result.data; + } + + async getAddressInfo(address: string): Promise { + return this.fetch(api.address(address), AddressInfoSchema); + } + + async getBlock(heightOrHash: number | BlockHash): Promise { + return this.fetch(api.block(heightOrHash), BlockInfoSchema); + } + + async getBlockCount(): Promise { + return this.fetch(api.blockcount, z.number().int().nonnegative()); + } + + async getBlockHashByHeight(height: number): Promise { + return this.fetch(api.blockhash.byHeight(height), BlockHashSchema); + } + + async getLatestBlockHash(): Promise { + return this.fetch(api.blockhash.latest, BlockHashSchema); + } + + async getLatestBlockHeight(): Promise { + return this.fetch(api.blockheight, z.number().int().nonnegative()); + } + + async getLatestBlocks(): Promise { + return this.fetch(api.blocks, BlocksResponseSchema); + } + + async getLatestBlockTime(): Promise { + return this.fetch(api.blocktime, z.number().int()); + } + + async getInscription(id: string): Promise { + return this.fetch(api.inscription(id), InscriptionSchema); + } + + async getInscriptionChild( + id: string, + child: number, + ): Promise { + return this.fetch(api.inscriptionChild(id, child), InscriptionSchema); + } + + async getLatestInscriptions(): Promise { + return this.fetch(api.inscriptions.latest, InscriptionsResponseSchema); + } + + async getInscriptionsByIds(ids: string[]): Promise { + return this.fetchPost( + api.inscriptions.base, + ids, + z.array(InscriptionSchema), + ); + } + + async getInscriptionsByPage(page: number): Promise { + return this.fetch( + api.inscriptions.byPage(page), + InscriptionsResponseSchema, + ); + } + + async getInscriptionsByBlock(height: number): Promise { + return this.fetch( + api.inscriptions.byBlock(height), + InscriptionsResponseSchema, + ); + } + + async getOutput(outpoint: string): Promise { + return this.fetch(api.output(outpoint), OutputInfoSchema); + } + + async getOutputs(outpoints: string[]): Promise { + return this.fetchPost( + api.outputs.base, + outpoints, + z.array(OutputInfoSchema), + ); + } + + async getOutputsByAddress( + address: string, + type?: OutputType, + ): Promise { + return this.fetch( + api.outputs.byAddress(address, type), + z.array(OutputInfoSchema), + ); + } + + async getRune(name: string): Promise { + return this.fetch(api.rune(name), RuneResponseSchema); + } + + async getLatestRunes(): Promise { + return this.fetch(api.runes.latest, RunesResponseSchema); + } + + async getRunesByPage(page: number): Promise { + return this.fetch(api.runes.byPage(page), RunesResponseSchema); + } + + async getSat(number: number): Promise { + return this.fetch(api.sat(number), SatSchema); + } + + async getTransaction(txId: string): Promise { + return this.fetch(api.tx(txId), TransactionInfoSchema); + } + + async getServerStatus(): Promise { + return this.fetch(api.status, ServerStatusSchema); + } +} diff --git a/src/endpoints.ts b/src/endpoints.ts deleted file mode 100644 index a95df7c..0000000 --- a/src/endpoints.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const endpoints = { - address: (address: string) => `/address/${address}`, - block: (heightOrHash: number | string) => `/block/${heightOrHash}`, - blockcount: '/blockcount', - blockhashLatest: '/blockhash', - blockhashByHeight: (height: number) => `/blockhash/${height}`, - blockheight: '/blockheight', - blocks: '/blocks', - blocktime: '/blocktime', -} as const; diff --git a/src/index.ts b/src/index.ts index 1467bd3..f561812 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,89 +1,4 @@ -import { z } from 'zod'; -import { endpoints } from 'src/endpoints'; -import { - BlockSchema, - BlockHashSchema, - AddressInfoSchema, - BlocksResponseSchema, -} from 'schemas'; -import type { Block, BlockHash, AddressInfo, BlocksResponse } from 'types'; +import { OrdClient } from './client'; +export type * from './types'; -type ApiResponse = - | { success: true; data: T } - | { success: false; error: string }; - -export class OrdClient { - private headers: HeadersInit; - - constructor( - private baseUrl: string, - headers: HeadersInit = {}, - ) { - this.headers = { - Accept: 'application/json', - ...headers, - }; - } - - private async fetch( - endpoint: string, - schema: T, - ): Promise> { - const response = await fetch(`${this.baseUrl}${endpoint}`, { - headers: this.headers, - }); - - if (!response.ok) { - throw new Error(`API request failed: ${response.statusText}`); - } - - const text = await response.text(); - const result = schema.safeParse(text); - - if (!result.success) { - try { - const json = JSON.parse(text); - const jsonResult = schema.safeParse(json); - if (jsonResult.success) { - return jsonResult.data; - } - } catch {} - - throw new Error(`Validation error: ${result.error.message}`); - } - - return result.data; - } - - async getAddressInfo(address: string): Promise { - return this.fetch(endpoints.address(address), AddressInfoSchema); - } - - async getBlock(heightOrHash: number | BlockHash): Promise { - return this.fetch(endpoints.block(heightOrHash), BlockSchema); - } - - async getBlockCount(): Promise { - return this.fetch(endpoints.blockcount, z.number().int().nonnegative()); - } - - async getBlockHashByHeight(height: number): Promise { - return this.fetch(endpoints.blockhashByHeight(height), BlockHashSchema); - } - - async getLatestBlockHash(): Promise { - return this.fetch(endpoints.blockhashLatest, BlockHashSchema); - } - - async getLatestBlockHeight(): Promise { - return this.fetch(endpoints.blockheight, z.number().int().nonnegative()); - } - - async getLatestBlocks(): Promise { - return this.fetch(endpoints.blocks, BlocksResponseSchema); - } - - async getLatestBlockTime(): Promise { - return this.fetch(endpoints.blocktime, z.number().int()); - } -} +export default OrdClient; diff --git a/src/schemas/address.ts b/src/schemas/address.ts index 93641c3..f5480a3 100644 --- a/src/schemas/address.ts +++ b/src/schemas/address.ts @@ -1,10 +1,8 @@ import { z } from 'zod'; -export const RuneBalanceSchema = z.tuple([z.string(), z.string(), z.string()]); - export const AddressInfoSchema = z.object({ outputs: z.array(z.string()), - inscriptions: z.array(z.string()), + inscriptions: z.array(z.string()).nullable(), sat_balance: z.number().int().nonnegative(), - runes_balances: z.array(RuneBalanceSchema), + runes_balances: z.array(z.array(z.string())).nullable(), }); diff --git a/src/schemas/block.ts b/src/schemas/block.ts index 7d17158..ddd17d9 100644 --- a/src/schemas/block.ts +++ b/src/schemas/block.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { TransactionSchema } from 'schemas'; +import { TransactionSchema } from './transaction'; const isHexString = (str: string) => /^[0-9a-fA-F]+$/.test(str); @@ -11,7 +11,7 @@ export const BlockHashSchema = z 'Block hash must contain only hexadecimal characters (0-9, a-f, A-F)', ); -export const BlockSchema = z.object({ +export const BlockInfoSchema = z.object({ best_height: z.number().int().nonnegative(), hash: BlockHashSchema, height: z.number().int().nonnegative(), diff --git a/src/schemas/index.ts b/src/schemas/index.ts index f3e79ba..8b9bd1f 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -1,3 +1,8 @@ -export * from 'schemas/transaction'; -export * from 'schemas/block'; -export * from 'schemas/address'; +export * from './transaction'; +export * from './block'; +export * from './address'; +export * from './inscription'; +export * from './output'; +export * from './rune'; +export * from './sat'; +export * from './status'; diff --git a/src/schemas/inscription.ts b/src/schemas/inscription.ts new file mode 100644 index 0000000..d332f34 --- /dev/null +++ b/src/schemas/inscription.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; + +export const CharmSchema = z.enum([ + 'burned', + 'coin', + 'cursed', + 'epic', + 'legendary', + 'lost', + 'mythic', + 'nineball', + 'palindrome', + 'rare', + 'reinscription', + 'unbound', + 'uncommon', + 'vindicated', +]); + +export const InscriptionSchema = z.object({ + address: z.string().nullable(), + charms: z.array(CharmSchema), + child_count: z.number().int().nonnegative(), + children: z.array(z.string()), + content_length: z.number().int().nonnegative().nullable(), + content_type: z.string().nullable(), + effective_content_type: z.string().nullable(), + fee: z.number().int().nonnegative(), + height: z.number().int().nonnegative(), + id: z.string(), + next: z.string().nullable().nullable(), + number: z.number().int().nonnegative(), + parents: z.array(z.string()), + previous: z.string().nullable(), + rune: z.string().nullable(), + sat: z.number().int().nonnegative().nullable(), + satpoint: z.string(), + timestamp: z.number().int(), + value: z.number().int().nonnegative().nullable(), + metaprotocol: z.string().nullable(), +}); + +export const InscriptionsResponseSchema = z.object({ + ids: z.array(z.string()), + more: z.boolean(), + page_index: z.number().int().nonnegative(), +}); diff --git a/src/schemas/output.ts b/src/schemas/output.ts new file mode 100644 index 0000000..9e0c589 --- /dev/null +++ b/src/schemas/output.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +export const OutputTypeSchema = z.enum([ + 'any', + 'cardinal', + 'inscribed', + 'runic', +]); + +const SatRangeSchema = z.tuple([ + z.number().int().nonnegative(), + z.number().int().nonnegative(), +]); + +const RuneSchema = z.object({ + amount: z.number().int().nonnegative(), + divisibility: z.number().int().nonnegative(), + symbol: z.string(), +}); + +export const OutputInfoSchema = z.object({ + address: z.string().nullable(), + indexed: z.boolean(), + inscriptions: z.array(z.string()).nullable(), + outpoint: z.string(), + runes: z.record(z.string(), RuneSchema).nullable(), + sat_ranges: z.array(SatRangeSchema).nullable(), + script_pubkey: z.string(), + spent: z.boolean(), + transaction: z.string(), + value: z.number().int().nonnegative(), +}); diff --git a/src/schemas/rune.ts b/src/schemas/rune.ts new file mode 100644 index 0000000..257ff1d --- /dev/null +++ b/src/schemas/rune.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; + +export const RuneTermsSchema = z + .object({ + amount: z.number().int().nonnegative(), + cap: z.number().int().nonnegative(), + height: z.tuple([z.number().int().nullable(), z.number().int().nullable()]), + offset: z.tuple([z.number().int().nullable(), z.number().int().nullable()]), + }) + .nullable(); + +export const RuneSchema = z.object({ + block: z.number().int().nonnegative(), + burned: z.number().int().nonnegative(), + divisibility: z.number().int().nonnegative(), + etching: z.string(), + mints: z.number().int().nonnegative(), + number: z.number().int().nonnegative(), + premine: z.number().int().nonnegative(), + spaced_rune: z.string(), + symbol: z.string().nullable(), + terms: RuneTermsSchema, + timestamp: z.number().int(), + turbo: z.boolean(), +}); + +export const RuneResponseSchema = z.object({ + entry: RuneSchema, + id: z.string(), + mintable: z.boolean(), + parent: z.string().nullable(), +}); + +export const RunesResponseSchema = z.object({ + entries: z.array(z.tuple([z.string(), RuneSchema])), + more: z.boolean(), + prev: z.number().nullable(), + next: z.number().nullable(), +}); diff --git a/src/schemas/sat.ts b/src/schemas/sat.ts new file mode 100644 index 0000000..f8a29ba --- /dev/null +++ b/src/schemas/sat.ts @@ -0,0 +1,30 @@ +import { z } from 'zod'; +import { CharmSchema } from './inscription'; + +export const RaritySchema = z.enum([ + 'common', + 'uncommon', + 'rare', + 'epic', + 'legendary', + 'mythic', +]); + +export const SatSchema = z.object({ + address: z.string().nullable(), + block: z.number().int().nonnegative(), + charms: z.array(CharmSchema), + cycle: z.number().int().nonnegative(), + decimal: z.string(), + degree: z.string(), + epoch: z.number().int().nonnegative(), + inscriptions: z.array(z.string()), + name: z.string(), + number: z.number().int().nonnegative(), + offset: z.number().int().nonnegative(), + percentile: z.string(), + period: z.number().int().nonnegative(), + rarity: RaritySchema, + satpoint: z.string().nullable(), + timestamp: z.number().int(), +}); diff --git a/src/schemas/status.ts b/src/schemas/status.ts new file mode 100644 index 0000000..4663d77 --- /dev/null +++ b/src/schemas/status.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +const TimeSchema = z.object({ + secs: z.number().int().nonnegative(), + nanos: z.number().int().nonnegative(), +}); + +export const ServerStatusSchema = z.object({ + address_index: z.boolean(), + blessed_inscriptions: z.number().int().nonnegative(), + chain: z.string(), + cursed_inscriptions: z.number().int().nonnegative(), + height: z.number().int().nonnegative(), + initial_sync_time: TimeSchema, + inscriptions: z.number().int().nonnegative(), + lost_sats: z.number().int().nonnegative(), + minimum_rune_for_next_block: z.string().nullable(), + rune_index: z.boolean(), + runes: z.number().int().nonnegative(), + sat_index: z.boolean(), + started: z.string(), + transaction_index: z.boolean(), + unrecoverably_reorged: z.boolean(), + uptime: TimeSchema, +}); diff --git a/src/schemas/transaction.ts b/src/schemas/transaction.ts index d7989b0..dccf0e6 100644 --- a/src/schemas/transaction.ts +++ b/src/schemas/transaction.ts @@ -18,3 +18,11 @@ export const TransactionSchema = z.object({ input: z.array(InputSchema), output: z.array(OutputSchema), }); + +export const TransactionInfoSchema = z.object({ + chain: z.string(), + etching: z.string().nullable(), + inscription_count: z.number().int().nonnegative(), + transaction: TransactionSchema, + txid: z.string(), +}); diff --git a/src/test/data/test-data.ts b/src/test/data/test-data.ts index 89d6280..58aec5c 100644 --- a/src/test/data/test-data.ts +++ b/src/test/data/test-data.ts @@ -30,7 +30,7 @@ export const GENESIS_BLOCK = { ], }; -export const SAMPLE_ADDRESS = +export const SAMPLE_ORDINALS_ADDRESS = 'bc1pyy0ttst33sgv9vx0jnqueca7vsqqupwu2t38l43pfgpwjrqvdddsq73hzp'; export const SAMPLE_ADDRESS_INFO = { @@ -70,6 +70,17 @@ export const SAMPLE_BLOCKS_RESPONSE = { }, }; +export const SAMPLE_OUTPOINT_A = + 'e553c4f6742ec65893611778a2f90305ac6be25f84771f505366b729a179af8c:0'; + +export const SAMPLE_OUTPOINT_B = + '85abae61cf0f7f90efc67ab5059e6ee3e600c3015ea68e9b33e945d8555766ed:100'; + +export const SAMPLE_BTC_ADDRESS = '358mMRwcxuCSkKheuVWaXHJBGKrXo3f6JW'; + +export const SAMPLE_TX_ID = + '6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799'; + export const SAMPLE_TRANSACTION = { version: 1, lock_time: 0, @@ -100,3 +111,140 @@ export const SAMPLE_OUTPUT = { value: 5000000000, script_pubkey: '76a914...', }; + +export const SAMPLE_INSCRIPTION = { + address: 'bc1ppth27qnr74qhusy9pmcyeaelgvsfky6qzquv9nf56gqmte59vfhqwkqguh', + charms: ['vindicated'], + children: [ + '681b5373c03e3f819231afd9227f54101395299c9e58356bda278e2f32bef2cdi0', + 'b1ef66c2d1a047cbaa6260b74daac43813924378fe08ef8545da4cb79e8fcf00i0', + ], + child_count: 2, + content_length: 793, + content_type: 'image/png', + effective_content_type: 'image/png', + fee: 322, + height: 767430, + id: '6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0', + next: '26482871f33f1051f450f2da9af275794c0b5f1c61ebf35e4467fb42c2813403i0', + number: 0, + parents: ['parent1', 'parent2'], + previous: 'prev-inscription-id', + rune: 'SOME•RUNE', + sat: 5000000000, + satpoint: + '47c7260764af2ee17aa584d9c035f2e5429aefd96b8016cfe0e3f0bcf04869a3:0:0', + timestamp: 1671049920, + value: 606, + metaprotocol: 'protocol-name', +}; + +export const SAMPLE_INSCRIPTIONS_RESPONSE = { + ids: [ + '6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0', + '26482871f33f1051f450f2da9af275794c0b5f1c61ebf35e4467fb42c2813403i0', + ], + more: true, + page_index: 0, +}; + +export const SAMPLE_INSCRIPTION_ID = + '6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0'; + +export const SAMPLE_CHILD_ID = + 'ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi0'; + +export const SAMPLE_BLOCK_HEIGHT = 767430; + +export const SAMPLE_UTXO_INFO = { + address: 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', + indexed: true, + inscriptions: [ + '6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0', + ], + outpoint: + '6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799:0', + runes: { + 'TEST•RUNE': { + amount: 1000, + divisibility: 0, + symbol: '🎯', + }, + }, + sat_ranges: [[0, 100]], + script_pubkey: '0014751e76e8199196d454941c45d1b3a323f1433bd6', + spent: false, + transaction: + '6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799', + value: 100000000, +}; + +export const SAMPLE_RUNE = { + block: 840000, + burned: 0, + divisibility: 0, + etching: '6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0', + mints: 1, + number: 100, + premine: 1000, + spaced_rune: 'TEST•RUNE', + symbol: '🎯', + terms: { + amount: 100, + cap: 1000, + height: [840000, 850000], + offset: [1000, 9000], + }, + timestamp: 1677654321, + turbo: false, +}; + +export const SAMPLE_RUNE_NAME = 'MEMENTO•MORI'; + +export const SAMPLE_SAT_NUMBER = 2099994106992659; + +export const SAMPLE_SAT = { + address: 'bc1ptest', + block: 1000, + charms: ['uncommon', 'cursed'], + cycle: 0, + decimal: '1000.0', + degree: '0°1′0″0‴', + epoch: 0, + inscriptions: [], + name: 'nvtdijuwxlp', + number: 2099994106992659, + offset: 0, + percentile: '99.99%', + period: 0, + rarity: 'uncommon', + satpoint: null, + timestamp: 1231006505, +}; + +export const SAMPLE_STATUS = { + address_index: true, + blessed_inscriptions: 83938677, + chain: 'mainnet', + cursed_inscriptions: 472043, + height: 881492, + initial_sync_time: { + secs: 78661, + nanos: 442762000, + }, + inscription_index: true, + inscriptions: 84410720, + json_api: true, + lost_sats: 2895502904, + minimum_rune_for_next_block: 'QJDUTCPVQI', + rune_index: true, + runes: 170685, + sat_index: true, + started: '2025-01-27T22:21:24.022870640Z', + transaction_index: false, + unrecoverably_reorged: false, + uptime: { + secs: 225097, + nanos: 72225343, + }, +}; diff --git a/src/test/integration/api.test.ts b/src/test/integration/api.test.ts index 1b70e8f..4d999bc 100644 --- a/src/test/integration/api.test.ts +++ b/src/test/integration/api.test.ts @@ -1,9 +1,18 @@ import { expect, test, describe, beforeAll } from 'bun:test'; -import { OrdClient } from '../../index'; +import OrdClient from '../../index'; import { BASE_URL, TIMEOUT } from '../config/test-config'; import { GENESIS_BLOCK, - SAMPLE_ADDRESS, + SAMPLE_ORDINALS_ADDRESS, + SAMPLE_INSCRIPTION_ID, + SAMPLE_CHILD_ID, + SAMPLE_BLOCK_HEIGHT, + SAMPLE_SAT_NUMBER, + SAMPLE_TX_ID, + SAMPLE_RUNE_NAME, + SAMPLE_OUTPOINT_A, + SAMPLE_OUTPOINT_B, + SAMPLE_BTC_ADDRESS, SAMPLE_ADDRESS_INFO, } from '../data/test-data'; @@ -48,7 +57,7 @@ describe('API Integration Tests', () => { test( 'fetches address info successfully', async () => { - const info = await client.getAddressInfo(SAMPLE_ADDRESS); + const info = await client.getAddressInfo(SAMPLE_ORDINALS_ADDRESS); expect(info).toEqual(SAMPLE_ADDRESS_INFO); }, TIMEOUT, @@ -57,7 +66,9 @@ describe('API Integration Tests', () => { test( 'rejects invalid address format', async () => { - await expect(client.getAddressInfo('invalid-address')).rejects.toThrow(); + await expect( + client.getAddressInfo('invalid-address'), + ).rejects.toThrow(); }, TIMEOUT, ); @@ -66,7 +77,7 @@ describe('API Integration Tests', () => { 'handles server error', async () => { await expect( - invalidClient.getAddressInfo(SAMPLE_ADDRESS), + invalidClient.getAddressInfo(SAMPLE_ORDINALS_ADDRESS), ).rejects.toThrow(); }, TIMEOUT, @@ -201,4 +212,443 @@ describe('API Integration Tests', () => { TIMEOUT, ); }); + + describe('getInscription', () => { + test( + 'fetches inscription successfully', + async () => { + const inscription = await client.getInscription(SAMPLE_INSCRIPTION_ID); + expect(inscription.id).toBe(SAMPLE_INSCRIPTION_ID); + expect(inscription.address).toBeDefined(); + expect(Array.isArray(inscription.charms)).toBe(true); + expect(Array.isArray(inscription.children)).toBe(true); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect( + invalidClient.getInscription(SAMPLE_INSCRIPTION_ID), + ).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getInscriptionChild', () => { + test( + 'fetches child inscription successfully', + async () => { + const child = await client.getInscriptionChild( + SAMPLE_INSCRIPTION_ID, + 0, + ); + expect(child.id).toBeDefined(); + expect(Array.isArray(child.children)).toBe(true); + expect(child.parents).toContain(SAMPLE_INSCRIPTION_ID); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect( + invalidClient.getInscriptionChild(SAMPLE_INSCRIPTION_ID, 0), + ).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getLatestInscriptions', () => { + test( + 'fetches latest inscriptions successfully', + async () => { + const response = await client.getLatestInscriptions(); + expect(Array.isArray(response.ids)).toBe(true); + expect(response.ids.length).toBeGreaterThan(0); + expect(typeof response.more).toBe('boolean'); + expect(typeof response.page_index).toBe('number'); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect(invalidClient.getLatestInscriptions()).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getInscriptionsByPage', () => { + test( + 'fetches inscriptions by page successfully', + async () => { + const response = await client.getInscriptionsByPage(1); + expect(Array.isArray(response.ids)).toBe(true); + expect(response.page_index).toBe(1); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect(invalidClient.getInscriptionsByPage(1)).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getInscriptionsByBlock', () => { + test( + 'fetches inscriptions by block successfully', + async () => { + const response = + await client.getInscriptionsByBlock(SAMPLE_BLOCK_HEIGHT); + expect(Array.isArray(response.ids)).toBe(true); + expect(response.ids).toContain(SAMPLE_INSCRIPTION_ID); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect( + invalidClient.getInscriptionsByBlock(SAMPLE_BLOCK_HEIGHT), + ).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getInscriptionsByIds', () => { + test( + 'fetches multiple inscriptions successfully', + async () => { + const inscriptions = await client.getInscriptionsByIds([ + SAMPLE_INSCRIPTION_ID, + SAMPLE_CHILD_ID, + ]); + expect(Array.isArray(inscriptions)).toBe(true); + expect(inscriptions.length).toBe(2); + expect(inscriptions[0].id).toBe(SAMPLE_INSCRIPTION_ID); + expect(inscriptions[1].id).toBe(SAMPLE_CHILD_ID); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect( + invalidClient.getInscriptionsByIds([SAMPLE_INSCRIPTION_ID]), + ).rejects.toThrow(); + }, + TIMEOUT, + ); + + test( + 'handles empty array', + async () => { + const inscriptions = await client.getInscriptionsByIds([]); + expect(Array.isArray(inscriptions)).toBe(true); + expect(inscriptions.length).toBe(0); + }, + TIMEOUT, + ); + }); + + describe('getOutput', () => { + test( + 'fetches output successfully', + async () => { + const output = await client.getOutput(SAMPLE_OUTPOINT_A); + expect(output.outpoint).toBe(SAMPLE_OUTPOINT_A); + expect(output.value).toBeGreaterThan(0); + expect(Array.isArray(output.sat_ranges)).toBe(true); + }, + TIMEOUT, + ); + + test( + 'handles invalid outpoint format', + async () => { + await expect(client.getOutput('invalid-outpoint')).rejects.toThrow(); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect( + invalidClient.getOutput(SAMPLE_OUTPOINT_A), + ).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getOutputs', () => { + test( + 'fetches multiple outputs successfully', + async () => { + const outpoints = [SAMPLE_OUTPOINT_A, SAMPLE_OUTPOINT_B]; + const outputs = await client.getOutputs(outpoints); + expect(Array.isArray(outputs)).toBe(true); + expect(outputs[0].outpoint).toBe(SAMPLE_OUTPOINT_A); + expect(outputs[1].outpoint).toBe(SAMPLE_OUTPOINT_B); + }, + TIMEOUT, + ); + + test( + 'handles empty array', + async () => { + const outputs = await client.getOutputs([]); + expect(Array.isArray(outputs)).toBe(true); + expect(outputs.length).toBe(0); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect( + invalidClient.getOutputs([SAMPLE_OUTPOINT_A, SAMPLE_OUTPOINT_B]), + ).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getOutputsByAddress', () => { + test( + 'fetches outputs by address successfully', + async () => { + const outputs = await client.getOutputsByAddress(SAMPLE_BTC_ADDRESS); + expect(Array.isArray(outputs)).toBe(true); + if (outputs.length > 0) { + expect(outputs[0].address).toBe(SAMPLE_BTC_ADDRESS); + } + }, + TIMEOUT, + ); + + test( + 'fetches outputs with type filter', + async () => { + const outputs = await client.getOutputsByAddress( + SAMPLE_BTC_ADDRESS, + 'cardinal', + ); + expect(Array.isArray(outputs)).toBe(true); + }, + TIMEOUT, + ); + + test( + 'handles invalid address', + async () => { + await expect( + client.getOutputsByAddress('invalid-address'), + ).rejects.toThrow(); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect( + invalidClient.getOutputsByAddress(SAMPLE_BTC_ADDRESS), + ).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getRune', () => { + test( + 'fetches rune successfully', + async () => { + const rune = await client.getRune(SAMPLE_RUNE_NAME); + expect(rune.entry.spaced_rune).toBe(SAMPLE_RUNE_NAME); + expect(typeof rune.entry.block).toBe('number'); + expect(typeof rune.mintable).toBe('boolean'); + }, + TIMEOUT, + ); + + test( + 'handles invalid rune name', + async () => { + await expect(client.getRune('invalid/rune/name')).rejects.toThrow(); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect(invalidClient.getRune(SAMPLE_RUNE_NAME)).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getLatestRunes', () => { + test( + 'fetches latest runes successfully', + async () => { + const response = await client.getLatestRunes(); + expect(Array.isArray(response.entries)).toBe(true); + expect(typeof response.more).toBe('boolean'); + if (response.entries.length > 0) { + const [runeName, runeData] = response.entries[0]; + expect(typeof runeName).toBe('string'); + expect(typeof runeData.block).toBe('number'); + } + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect(invalidClient.getLatestRunes()).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getRunesByPage', () => { + test( + 'fetches runes by page successfully', + async () => { + const response = await client.getRunesByPage(0); + expect(Array.isArray(response.entries)).toBe(true); + expect(typeof response.more).toBe('boolean'); + expect( + response.prev === null || typeof response.prev === 'number', + ).toBe(true); + expect( + response.next === null || typeof response.next === 'number', + ).toBe(true); + }, + TIMEOUT, + ); + + test( + 'handles invalid page number', + async () => { + await expect(client.getRunesByPage(-1)).rejects.toThrow(); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect(invalidClient.getRunesByPage(1)).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getSat', () => { + test( + 'fetches sat info successfully', + async () => { + const sat = await client.getSat(SAMPLE_SAT_NUMBER); + expect(sat.number).toBe(SAMPLE_SAT_NUMBER); + expect(Array.isArray(sat.charms)).toBe(true); + expect(Array.isArray(sat.inscriptions)).toBe(true); + expect(sat.decimal).toBeDefined(); + expect(sat.degree).toBeDefined(); + }, + TIMEOUT, + ); + + test( + 'rejects negative sat number', + async () => { + await expect(client.getSat(-1)).rejects.toThrow(); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + await expect(invalidClient.getSat(SAMPLE_SAT_NUMBER)).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + describe('getTx', () => { + test( + 'fetches transaction successfully', + async () => { + const tx = await client.getTransaction(SAMPLE_TX_ID); + expect(tx.txid).toBe(SAMPLE_TX_ID); + expect(Array.isArray(tx.transaction.input)).toBe(true); + expect(Array.isArray(tx.transaction.output)).toBe(true); + expect(typeof tx.inscription_count).toBe('number'); + }, + TIMEOUT, + ); + + test( + 'rejects invalid transaction id', + async () => { + expect(client.getTransaction('invalid-txid')).rejects.toThrow(); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + expect(invalidClient.getTransaction(SAMPLE_TX_ID)).rejects.toThrow(); + }, + TIMEOUT, + ); + }); + + test( + 'fetches status successfully', + async () => { + const status = await client.getServerStatus(); + expect(typeof status.height).toBe('number'); + expect(status.height).toBeGreaterThan(0); + expect(typeof status.chain).toBe('string'); + expect(typeof status.inscriptions).toBe('number'); + expect(typeof status.lost_sats).toBe('number'); + expect(typeof status.address_index).toBe('boolean'); + expect(typeof status.sat_index).toBe('boolean'); + expect(typeof status.rune_index).toBe('boolean'); + expect(typeof status.transaction_index).toBe('boolean'); + expect(status.initial_sync_time).toBeDefined(); + expect(status.uptime).toBeDefined(); + }, + TIMEOUT, + ); + + test( + 'handles server error', + async () => { + expect(invalidClient.getServerStatus()).rejects.toThrow(); + }, + TIMEOUT, + ); }); diff --git a/src/test/unit/schemas.test.ts b/src/test/unit/schemas.test.ts index eb95b79..5f11a27 100644 --- a/src/test/unit/schemas.test.ts +++ b/src/test/unit/schemas.test.ts @@ -1,15 +1,23 @@ import { expect, test, describe } from 'bun:test'; import { - BlockSchema, + BlockInfoSchema, BlockHashSchema, BlocksResponseSchema, } from '../../schemas/block'; -import { AddressInfoSchema, RuneBalanceSchema } from '../../schemas/address'; +import { AddressInfoSchema } from '../../schemas/address'; import { InputSchema, OutputSchema, TransactionSchema, } from '../../schemas/transaction'; +import { + InscriptionSchema, + InscriptionsResponseSchema, +} from '../../schemas/inscription'; +import { OutputInfoSchema } from '../../schemas/output'; +import { RuneSchema, RunesResponseSchema } from '../../schemas/rune'; +import { SatSchema } from '../../schemas/sat'; +import { ServerStatusSchema } from '../../schemas/status'; import { GENESIS_BLOCK, SAMPLE_ADDRESS_INFO, @@ -18,26 +26,32 @@ import { SAMPLE_INPUT, SAMPLE_OUTPUT, SAMPLE_RUNE_BALANCE, + SAMPLE_INSCRIPTION, + SAMPLE_INSCRIPTIONS_RESPONSE, + SAMPLE_UTXO_INFO, + SAMPLE_RUNE, + SAMPLE_STATUS, + SAMPLE_SAT, } from '../data/test-data'; describe('Schema Validation', () => { - describe('Transaction Schemas', () => { - describe('InputSchema', () => { + describe('Tx Schemas', () => { + describe('TxInputSchema', () => { test('validates valid input', () => { expect(InputSchema.safeParse(SAMPLE_INPUT).success).toBe(true); }); test('rejects invalid sequence', () => { - const invalidInput = { + const invalidTxInput = { ...SAMPLE_INPUT, sequence: -1, }; - expect(InputSchema.safeParse(invalidInput).success).toBe(false); + expect(InputSchema.safeParse(invalidTxInput).success).toBe(false); }); test('rejects missing field', () => { - const { script_sig, ...invalidInput } = SAMPLE_INPUT; - expect(InputSchema.safeParse(invalidInput).success).toBe(false); + const { script_sig, ...invalidTxInput } = SAMPLE_INPUT; + expect(InputSchema.safeParse(invalidTxInput).success).toBe(false); }); }); @@ -60,7 +74,7 @@ describe('Schema Validation', () => { }); }); - describe('TransactionSchema', () => { + describe('TxSchema', () => { test('validates valid transaction', () => { expect(TransactionSchema.safeParse(SAMPLE_TRANSACTION).success).toBe( true, @@ -93,7 +107,7 @@ describe('Schema Validation', () => { }); test('rejects invalid length', () => { - const shortHash = GENESIS_BLOCK.hash.slice(0, -1); // Remove last character + const shortHash = GENESIS_BLOCK.hash.slice(0, -1); expect(BlockHashSchema.safeParse(shortHash).success).toBe(false); }); @@ -134,15 +148,15 @@ describe('Schema Validation', () => { describe('BlockSchema', () => { test('validates valid block', () => { - expect(BlockSchema.safeParse(GENESIS_BLOCK).success).toBe(true); + expect(BlockInfoSchema.safeParse(GENESIS_BLOCK).success).toBe(true); }); test('rejects invalid block height type', () => { const invalidBlock = { ...GENESIS_BLOCK, - best_height: '864325', // string instead of number + best_height: '864325', }; - expect(BlockSchema.safeParse(invalidBlock).success).toBe(false); + expect(BlockInfoSchema.safeParse(invalidBlock).success).toBe(false); }); test('rejects invalid block hash', () => { @@ -150,7 +164,7 @@ describe('Schema Validation', () => { ...GENESIS_BLOCK, hash: 'invalid_hash', }; - expect(BlockSchema.safeParse(invalidBlock).success).toBe(false); + expect(BlockInfoSchema.safeParse(invalidBlock).success).toBe(false); }); test('rejects negative height', () => { @@ -158,30 +172,12 @@ describe('Schema Validation', () => { ...GENESIS_BLOCK, height: -1, }; - expect(BlockSchema.safeParse(invalidBlock).success).toBe(false); + expect(BlockInfoSchema.safeParse(invalidBlock).success).toBe(false); }); }); }); describe('Address Schemas', () => { - describe('RuneBalanceSchema', () => { - test('validates valid rune balance', () => { - expect(RuneBalanceSchema.safeParse(SAMPLE_RUNE_BALANCE).success).toBe( - true, - ); - }); - - test('rejects invalid tuple length', () => { - const invalidBalance = ['TEST•RUNE', '100']; - expect(RuneBalanceSchema.safeParse(invalidBalance).success).toBe(false); - }); - - test('rejects invalid value type', () => { - const invalidBalance = ['TEST•RUNE', 100, '🎯']; - expect(RuneBalanceSchema.safeParse(invalidBalance).success).toBe(false); - }); - }); - describe('AddressInfoSchema', () => { test('validates valid address info', () => { expect(AddressInfoSchema.safeParse(SAMPLE_ADDRESS_INFO).success).toBe( @@ -212,10 +208,279 @@ describe('Schema Validation', () => { test('rejects invalid runes_balances format', () => { const invalidAddress = { ...SAMPLE_ADDRESS_INFO, - runes_balances: [['TEST•RUNE', 100, '🎯']], // number instead of string + runes_balances: [['TEST•RUNE', 100, '🎯']], }; expect(AddressInfoSchema.safeParse(invalidAddress).success).toBe(false); }); }); }); + + describe('Inscription Schemas', () => { + describe('InscriptionSchema', () => { + test('validates valid inscription', () => { + const result = InscriptionSchema.safeParse(SAMPLE_INSCRIPTION); + expect(result.success).toBe(true); + }); + + test('rejects invalid charm value', () => { + const inscriptionWithInvalidCharm = { + ...SAMPLE_INSCRIPTION, + charms: ['invalid_charm'], + }; + const result = InscriptionSchema.safeParse(inscriptionWithInvalidCharm); + expect(result.success).toBe(false); + }); + + test('validates empty arrays', () => { + const inscriptionWithEmptyArrays = { + ...SAMPLE_INSCRIPTION, + charms: [], + children: [], + parents: [], + }; + const result = InscriptionSchema.safeParse(inscriptionWithEmptyArrays); + expect(result.success).toBe(true); + }); + + test('validates all nullable fields', () => { + const nullableInscription = { + ...SAMPLE_INSCRIPTION, + address: null, + next: null, + previous: null, + rune: null, + sat: null, + metaprotocol: null, + }; + const result = InscriptionSchema.safeParse(nullableInscription); + expect(result.success).toBe(true); + }); + + test('rejects negative values', () => { + const inscriptionWithNegatives = { + ...SAMPLE_INSCRIPTION, + content_length: -1, + fee: -1, + value: -1, + }; + const result = InscriptionSchema.safeParse(inscriptionWithNegatives); + expect(result.success).toBe(false); + }); + }); + + describe('InscriptionsResponseSchema', () => { + test('validates valid response', () => { + const result = InscriptionsResponseSchema.safeParse( + SAMPLE_INSCRIPTIONS_RESPONSE, + ); + expect(result.success).toBe(true); + }); + + test('rejects invalid types', () => { + const invalidResponse = { + SAMPLE_INSCRIPTIONS_RESPONSE, + ids: [123], + page_index: -1, + }; + const result = InscriptionsResponseSchema.safeParse(invalidResponse); + expect(result.success).toBe(false); + }); + }); + }); + + describe('Output Info Schemas', () => { + describe('OutputInfoSchema', () => { + test('validates valid output', () => { + const result = OutputInfoSchema.safeParse(SAMPLE_UTXO_INFO); + expect(result.success).toBe(true); + }); + + test('rejects negative value', () => { + const invalidOutput = { + ...SAMPLE_UTXO_INFO, + value: -1, + }; + const result = OutputInfoSchema.safeParse(invalidOutput); + expect(result.success).toBe(false); + }); + + test('validates all nullable fields', () => { + const minimalOutput = { + ...SAMPLE_UTXO_INFO, + inscriptions: [], + runes: {}, + sat_ranges: null, + }; + const result = OutputInfoSchema.safeParse(minimalOutput); + expect(result.success).toBe(true); + }); + + test('rejests invalid field types', () => { + const invalidOutput = { + ...SAMPLE_UTXO_INFO, + inscriptions: 'invalid', + runes: -1, + sat_ranges: [[0]], + }; + const result = OutputInfoSchema.safeParse(invalidOutput); + expect(result.success).toBe(false); + }); + + test('rejects invalid sat ranges format', () => { + const invalidOutput = { + ...SAMPLE_UTXO_INFO, + sat_ranges: [[0]], + }; + const result = OutputInfoSchema.safeParse(invalidOutput); + expect(result.success).toBe(false); + }); + }); + }); + + describe('Rune Schemas', () => { + describe('RuneSchema', () => { + test('validates valid rune', () => { + const result = RuneSchema.safeParse(SAMPLE_RUNE); + expect(result.success).toBe(true); + }); + + test('validates nullable field', () => { + const runeWithNullSymbol = { + ...SAMPLE_RUNE, + symbol: null, + }; + const result = RuneSchema.safeParse(runeWithNullSymbol); + expect(result.success).toBe(true); + }); + + test('fails with negative numbers', () => { + const invalidRune = { + ...SAMPLE_RUNE, + block: -1, + burned: -1, + mints: -1, + }; + const result = RuneSchema.safeParse(invalidRune); + expect(result.success).toBe(false); + }); + + test('validates RunesResponse structure', () => { + const validResponse = { + entries: [['TEST•RUNE', SAMPLE_RUNE]], + more: true, + prev: 1, + next: 3, + }; + const result = RunesResponseSchema.safeParse(validResponse); + expect(result.success).toBe(true); + }); + + test('validates RunesResponse with null values', () => { + const responseWithNulls = { + entries: [['TEST•RUNE', SAMPLE_RUNE]], + more: false, + prev: null, + next: null, + }; + const result = RunesResponseSchema.safeParse(responseWithNulls); + expect(result.success).toBe(true); + }); + }); + }); + + describe('Sat Schema', () => { + describe('SatSchema', () => { + test('validates valid sat', () => { + const result = SatSchema.safeParse(SAMPLE_SAT); + expect(result.success).toBe(true); + }); + + test('validates nullable fields', () => { + const satWithNullSatpoint = { + ...SAMPLE_SAT, + address: null, + satpoint: null, + }; + const result = SatSchema.safeParse(satWithNullSatpoint); + expect(result.success).toBe(true); + }); + + test('rejects invalid charm value', () => { + const satWithInvalidCharm = { + ...SAMPLE_SAT, + charms: ['invalid_charm'], + }; + const result = SatSchema.safeParse(satWithInvalidCharm); + expect(result.success).toBe(false); + }); + + test('rejects invalid rarity value', () => { + const satWithInvalidRarity = { + ...SAMPLE_SAT, + rarity: 'invalid_rarity', + }; + const result = SatSchema.safeParse(satWithInvalidRarity); + expect(result.success).toBe(false); + }); + + test('rejects negative values', () => { + const satWithNegatives = { + ...SAMPLE_SAT, + number: -1, + offset: -1, + period: -1, + }; + const result = SatSchema.safeParse(satWithNegatives); + expect(result.success).toBe(false); + }); + }); + }); + + describe('Status Schema', () => { + describe('ServerStatusSchema', () => { + test('validates valid status', () => { + const result = ServerStatusSchema.safeParse(SAMPLE_STATUS); + expect(result.success).toBe(true); + }); + + test('validates nullable fields', () => { + const statusWithNullRune = { + ...SAMPLE_STATUS, + minimum_rune_for_next_block: null, + }; + const result = ServerStatusSchema.safeParse(statusWithNullRune); + expect(result.success).toBe(true); + }); + + test('validates time fields', () => { + const statusWithMinimalTime = { + ...SAMPLE_STATUS, + initial_sync_time: { secs: 0, nanos: 0 }, + uptime: { secs: 0, nanos: 0 }, + }; + const result = ServerStatusSchema.safeParse(statusWithMinimalTime); + expect(result.success).toBe(true); + }); + + test('rejects negative values', () => { + const statusWithNegatives = { + ...SAMPLE_STATUS, + height: -1, + inscriptions: -1, + lost_sats: -1, + }; + const result = ServerStatusSchema.safeParse(statusWithNegatives); + expect(result.success).toBe(false); + }); + + test('rejects invalid time fields', () => { + const statusWithInvalidTime = { + ...SAMPLE_STATUS, + initial_sync_time: { secs: -1, nanos: -1 }, + }; + const result = ServerStatusSchema.safeParse(statusWithInvalidTime); + expect(result.success).toBe(false); + }); + }); + }); }); diff --git a/src/types/index.ts b/src/types/index.ts index 47c309f..29166ca 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,14 +1,47 @@ -import { z } from 'zod'; -import { - BlockSchema, +import type { z } from 'zod'; +import type { + BlockInfoSchema, BlockHashSchema, BlocksResponseSchema, TransactionSchema, + InputSchema, AddressInfoSchema, -} from 'schemas'; + InscriptionSchema, + CharmSchema, + InscriptionsResponseSchema, + OutputInfoSchema, + RuneSchema, + RunesResponseSchema, + SatSchema, + ServerStatusSchema, + RuneResponseSchema, + TransactionInfoSchema, + RaritySchema, + OutputSchema, + OutputTypeSchema, +} from '../schemas'; -export type Block = z.infer; +export type AddressInfo = z.infer; + +export type BlockInfo = z.infer; export type BlockHash = z.infer; export type BlocksResponse = z.infer; + export type Transaction = z.infer; -export type AddressInfo = z.infer; +export type TransactionInfo = z.infer; +export type Input = z.infer; +export type Output = z.infer; + +export type SatInfo = z.infer; +export type CharmType = z.infer; +export type RarityType = z.infer; +export type OutputType = z.infer; +export type OutputInfo = z.infer; +export type InscriptionInfo = z.infer; +export type InscriptionsResponse = z.infer; + +export type RuneInfo = z.infer; +export type RuneResponse = z.infer; +export type RunesResponse = z.infer; + +export type ServerStatus = z.infer; diff --git a/tsconfig.json b/tsconfig.json index c9c25c7..cb8d60c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,21 +4,14 @@ "module": "esnext", "moduleResolution": "bundler", "types": ["bun-types"], - "baseUrl": ".", - "paths": { - "src": ["src/*"], - "schemas": ["src/schemas"], - "schemas/*": ["src/schemas/*"], - "types": ["src/types"], - "types/*": ["src/types/*"], - }, "esModuleInterop": true, "strict": true, "skipLibCheck": true, "declaration": true, - "outDir": "./dist", - "rootDir": "./src" + "isolatedModules": true, + "rootDir": "./src", + "outDir": "./dist" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"] -} +} \ No newline at end of file