@sip-protocol/sdk 0.6.0 → 0.6.2

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.
Files changed (63) hide show
  1. package/README.md +58 -0
  2. package/dist/browser.d.mts +4 -4
  3. package/dist/browser.d.ts +4 -4
  4. package/dist/browser.js +2752 -448
  5. package/dist/browser.mjs +31 -1
  6. package/dist/chunk-7QZPORY5.mjs +15604 -0
  7. package/dist/chunk-C2NPCUAJ.mjs +17010 -0
  8. package/dist/chunk-FCVLFUIC.mjs +16699 -0
  9. package/dist/chunk-G5UHXECN.mjs +16340 -0
  10. package/dist/chunk-GEDEIZHJ.mjs +16798 -0
  11. package/dist/chunk-GOOEOAMV.mjs +17026 -0
  12. package/dist/chunk-MTNYSNR7.mjs +16269 -0
  13. package/dist/chunk-O5PIB2EA.mjs +16698 -0
  14. package/dist/chunk-PCFM7FQO.mjs +17010 -0
  15. package/dist/chunk-QK464ARC.mjs +16946 -0
  16. package/dist/chunk-VNBMNGC3.mjs +16698 -0
  17. package/dist/chunk-W5TUELDQ.mjs +16947 -0
  18. package/dist/index-CD_zShu-.d.ts +10870 -0
  19. package/dist/index-CQBYdLYy.d.mts +10976 -0
  20. package/dist/index-Cg9TYEPv.d.mts +11321 -0
  21. package/dist/index-CqZJOO8C.d.mts +11323 -0
  22. package/dist/index-CywN9Bnp.d.ts +11321 -0
  23. package/dist/index-DHy5ZjCD.d.ts +10976 -0
  24. package/dist/index-DfsVsmxu.d.ts +11323 -0
  25. package/dist/index-ObjwyVDX.d.mts +10870 -0
  26. package/dist/index-m0xbSfmT.d.mts +11318 -0
  27. package/dist/index-rWLEgvhN.d.ts +11318 -0
  28. package/dist/index.d.mts +3 -3
  29. package/dist/index.d.ts +3 -3
  30. package/dist/index.js +2737 -427
  31. package/dist/index.mjs +31 -1
  32. package/dist/noir-DKfEzWy9.d.mts +482 -0
  33. package/dist/noir-DKfEzWy9.d.ts +482 -0
  34. package/dist/proofs/noir.d.mts +1 -1
  35. package/dist/proofs/noir.d.ts +1 -1
  36. package/dist/proofs/noir.js +12 -3
  37. package/dist/proofs/noir.mjs +12 -3
  38. package/package.json +16 -14
  39. package/src/adapters/near-intents.ts +13 -3
  40. package/src/auction/index.ts +20 -0
  41. package/src/auction/sealed-bid.ts +1037 -0
  42. package/src/compliance/derivation.ts +13 -3
  43. package/src/compliance/reports.ts +5 -4
  44. package/src/cosmos/ibc-stealth.ts +2 -2
  45. package/src/cosmos/stealth.ts +2 -2
  46. package/src/governance/index.ts +19 -0
  47. package/src/governance/private-vote.ts +1116 -0
  48. package/src/index.ts +50 -2
  49. package/src/intent.ts +145 -8
  50. package/src/nft/index.ts +27 -0
  51. package/src/nft/private-nft.ts +811 -0
  52. package/src/proofs/browser-utils.ts +1 -7
  53. package/src/proofs/noir.ts +34 -7
  54. package/src/settlement/backends/direct-chain.ts +14 -3
  55. package/src/stealth.ts +31 -13
  56. package/src/types/browser.d.ts +67 -0
  57. package/src/validation.ts +4 -2
  58. package/src/wallet/bitcoin/adapter.ts +159 -15
  59. package/src/wallet/bitcoin/types.ts +340 -15
  60. package/src/wallet/cosmos/mock.ts +16 -12
  61. package/src/wallet/hardware/ledger.ts +82 -12
  62. package/src/wallet/hardware/types.ts +2 -0
  63. package/LICENSE +0 -21
@@ -241,10 +241,8 @@ export function getMobileDeviceInfo(): MobileDeviceInfo {
241
241
  isTablet: isTablet(),
242
242
  supportsTouch: supportsTouch(),
243
243
  deviceMemoryGB:
244
- // @ts-expect-error - deviceMemory is non-standard
245
244
  typeof navigator !== 'undefined' && navigator.deviceMemory
246
- ? // @ts-expect-error - deviceMemory is non-standard
247
- navigator.deviceMemory
245
+ ? navigator.deviceMemory
248
246
  : null,
249
247
  hardwareConcurrency:
250
248
  typeof navigator !== 'undefined' && navigator.hardwareConcurrency
@@ -487,10 +485,8 @@ export async function estimateAvailableMemory(): Promise<number | null> {
487
485
  if (!isBrowser()) return null
488
486
 
489
487
  // Use Performance API if available (Chrome)
490
- // @ts-expect-error - Performance.measureUserAgentSpecificMemory is Chrome-specific
491
488
  if (typeof performance !== 'undefined' && performance.measureUserAgentSpecificMemory) {
492
489
  try {
493
- // @ts-expect-error - Chrome-specific API
494
490
  const result = await performance.measureUserAgentSpecificMemory()
495
491
  return result.bytes
496
492
  } catch {
@@ -499,10 +495,8 @@ export async function estimateAvailableMemory(): Promise<number | null> {
499
495
  }
500
496
 
501
497
  // Use navigator.deviceMemory if available (Chrome, Opera)
502
- // @ts-expect-error - deviceMemory is non-standard
503
498
  if (typeof navigator !== 'undefined' && navigator.deviceMemory) {
504
499
  // Returns approximate device memory in GB
505
- // @ts-expect-error - deviceMemory is non-standard
506
500
  return navigator.deviceMemory * 1024 * 1024 * 1024
507
501
  }
508
502
 
@@ -71,9 +71,25 @@ export interface NoirProviderConfig {
71
71
 
72
72
  /**
73
73
  * Oracle public key for verifying attestations in fulfillment proofs
74
- * Required for production use. If not provided, proofs will use placeholder keys.
74
+ * Required for production use. If not provided and strictMode is true,
75
+ * fulfillment proof generation will throw an error.
75
76
  */
76
77
  oraclePublicKey?: PublicKeyCoordinates
78
+
79
+ /**
80
+ * Enable strict mode for production use
81
+ *
82
+ * When true:
83
+ * - Fulfillment proofs require configured oraclePublicKey
84
+ * - Missing configuration throws errors instead of warnings
85
+ *
86
+ * When false (default):
87
+ * - Placeholder keys are used when oraclePublicKey not configured
88
+ * - Warnings are logged for missing configuration
89
+ *
90
+ * @default false
91
+ */
92
+ strictMode?: boolean
77
93
  }
78
94
 
79
95
  /**
@@ -114,6 +130,7 @@ export class NoirProofProvider implements ProofProvider {
114
130
  this.config = {
115
131
  backend: 'barretenberg',
116
132
  verbose: false,
133
+ strictMode: false,
117
134
  ...config,
118
135
  }
119
136
  }
@@ -588,15 +605,25 @@ export class NoirProofProvider implements ProofProvider {
588
605
  attestation.blockNumber
589
606
  )
590
607
 
591
- // Use configured oracle public key, or placeholder if not configured
592
- // In production, the oracle public key should always be configured
608
+ // Validate oracle public key configuration
609
+ if (!this.config.oraclePublicKey) {
610
+ if (this.config.strictMode) {
611
+ throw new ProofGenerationError(
612
+ 'fulfillment',
613
+ 'Oracle public key is required in strict mode. Configure oraclePublicKey in NoirProviderConfig for production use.'
614
+ )
615
+ }
616
+ // Always warn when using placeholder keys, not just in verbose mode
617
+ console.warn(
618
+ '[NoirProofProvider] WARNING: No oracle public key configured. Using placeholder keys. ' +
619
+ 'Proofs will NOT be valid for production. Set strictMode: true to enforce configuration.'
620
+ )
621
+ }
622
+
623
+ // Use configured oracle public key, or placeholder if not in strict mode
593
624
  const oraclePubKeyX = this.config.oraclePublicKey?.x ?? new Array(32).fill(0)
594
625
  const oraclePubKeyY = this.config.oraclePublicKey?.y ?? new Array(32).fill(0)
595
626
 
596
- if (!this.config.oraclePublicKey && this.config.verbose) {
597
- console.warn('[NoirProofProvider] Warning: No oracle public key configured. Using placeholder keys.')
598
- }
599
-
600
627
  // Prepare witness inputs for the circuit
601
628
  const witnessInputs = {
602
629
  // Public inputs
@@ -39,6 +39,8 @@ import {
39
39
  ed25519PublicKeyToNearAddress,
40
40
  isEd25519Chain,
41
41
  } from '../../stealth'
42
+ import { ed25519PublicKeyToAptosAddress } from '../../move/aptos'
43
+ import { ed25519PublicKeyToSuiAddress } from '../../move/sui'
42
44
  import { ValidationError } from '../../errors'
43
45
  import { randomBytes, bytesToHex } from '@noble/hashes/utils'
44
46
 
@@ -161,6 +163,8 @@ export class DirectChainBackend implements SettlementBackend {
161
163
  'optimism',
162
164
  'base',
163
165
  'bitcoin',
166
+ 'aptos',
167
+ 'sui',
164
168
  ],
165
169
  supportedDestinationChains: [
166
170
  'ethereum',
@@ -172,6 +176,8 @@ export class DirectChainBackend implements SettlementBackend {
172
176
  'optimism',
173
177
  'base',
174
178
  'bitcoin',
179
+ 'aptos',
180
+ 'sui',
175
181
  ],
176
182
  supportedPrivacyLevels: [
177
183
  PrivacyLevel.TRANSPARENT,
@@ -280,7 +286,7 @@ export class DirectChainBackend implements SettlementBackend {
280
286
  : params.recipientMetaAddress
281
287
 
282
288
  if (isEd25519Chain(chain)) {
283
- // Ed25519 chains (Solana, NEAR)
289
+ // Ed25519 chains (Solana, NEAR, Aptos, Sui)
284
290
  const { stealthAddress } = generateEd25519StealthAddress(metaAddr)
285
291
  stealthData = stealthAddress
286
292
 
@@ -288,11 +294,16 @@ export class DirectChainBackend implements SettlementBackend {
288
294
  recipientAddress = ed25519PublicKeyToSolanaAddress(stealthAddress.address)
289
295
  } else if (chain === 'near') {
290
296
  recipientAddress = ed25519PublicKeyToNearAddress(stealthAddress.address)
297
+ } else if (chain === 'aptos') {
298
+ recipientAddress = ed25519PublicKeyToAptosAddress(stealthAddress.address)
299
+ } else if (chain === 'sui') {
300
+ recipientAddress = ed25519PublicKeyToSuiAddress(stealthAddress.address)
291
301
  } else {
302
+ // This should not happen if ED25519_CHAINS is kept in sync
292
303
  throw new ValidationError(
293
- `Ed25519 address derivation not implemented for ${chain}`,
304
+ `Ed25519 address derivation not implemented for ${chain}. Please add support in direct-chain.ts.`,
294
305
  'toChain',
295
- { chain }
306
+ { chain, hint: 'Add address derivation function for this chain' }
296
307
  )
297
308
  }
298
309
  } else {
package/src/stealth.ts CHANGED
@@ -144,6 +144,7 @@ export function generateStealthMetaAddress(
144
144
 
145
145
  /**
146
146
  * Validate a StealthMetaAddress object
147
+ * Supports both secp256k1 (EVM chains) and ed25519 (Solana, NEAR, etc.) key formats
147
148
  */
148
149
  function validateStealthMetaAddress(
149
150
  metaAddress: StealthMetaAddress,
@@ -161,20 +162,37 @@ function validateStealthMetaAddress(
161
162
  )
162
163
  }
163
164
 
164
- // Validate spending key
165
- if (!isValidCompressedPublicKey(metaAddress.spendingKey)) {
166
- throw new ValidationError(
167
- 'spendingKey must be a valid compressed secp256k1 public key (33 bytes, starting with 02 or 03)',
168
- `${field}.spendingKey`
169
- )
170
- }
165
+ // Determine key type based on chain (ed25519 vs secp256k1)
166
+ const isEd25519 = isEd25519Chain(metaAddress.chain)
171
167
 
172
- // Validate viewing key
173
- if (!isValidCompressedPublicKey(metaAddress.viewingKey)) {
174
- throw new ValidationError(
175
- 'viewingKey must be a valid compressed secp256k1 public key (33 bytes, starting with 02 or 03)',
176
- `${field}.viewingKey`
177
- )
168
+ if (isEd25519) {
169
+ // Ed25519 chains (Solana, NEAR, Aptos, Sui) use 32-byte public keys
170
+ if (!isValidEd25519PublicKey(metaAddress.spendingKey)) {
171
+ throw new ValidationError(
172
+ 'spendingKey must be a valid ed25519 public key (32 bytes)',
173
+ `${field}.spendingKey`
174
+ )
175
+ }
176
+ if (!isValidEd25519PublicKey(metaAddress.viewingKey)) {
177
+ throw new ValidationError(
178
+ 'viewingKey must be a valid ed25519 public key (32 bytes)',
179
+ `${field}.viewingKey`
180
+ )
181
+ }
182
+ } else {
183
+ // Secp256k1 chains (Ethereum, etc.) use 33-byte compressed public keys
184
+ if (!isValidCompressedPublicKey(metaAddress.spendingKey)) {
185
+ throw new ValidationError(
186
+ 'spendingKey must be a valid compressed secp256k1 public key (33 bytes, starting with 02 or 03)',
187
+ `${field}.spendingKey`
188
+ )
189
+ }
190
+ if (!isValidCompressedPublicKey(metaAddress.viewingKey)) {
191
+ throw new ValidationError(
192
+ 'viewingKey must be a valid compressed secp256k1 public key (33 bytes, starting with 02 or 03)',
193
+ `${field}.viewingKey`
194
+ )
195
+ }
178
196
  }
179
197
  }
180
198
 
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Type declarations for non-standard browser APIs
3
+ *
4
+ * These declarations extend the standard DOM types to include
5
+ * browser-specific APIs used for device capability detection.
6
+ *
7
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory
8
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/measureUserAgentSpecificMemory
9
+ */
10
+
11
+ /**
12
+ * Extend Navigator interface with non-standard properties
13
+ */
14
+ interface Navigator {
15
+ /**
16
+ * Device memory in gigabytes (rounded to preserve privacy)
17
+ *
18
+ * Only available in Chrome, Edge, Opera (Chromium-based browsers)
19
+ * Returns values like: 0.25, 0.5, 1, 2, 4, 8
20
+ *
21
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory
22
+ */
23
+ readonly deviceMemory?: number
24
+ }
25
+
26
+ /**
27
+ * Memory measurement result from Chrome-specific API
28
+ */
29
+ interface MemoryMeasurement {
30
+ /** Total bytes used */
31
+ bytes: number
32
+ /** Breakdown of memory usage by type */
33
+ breakdown: Array<{
34
+ bytes: number
35
+ types: string[]
36
+ attribution?: Array<{
37
+ url: string
38
+ scope: string
39
+ }>
40
+ }>
41
+ }
42
+
43
+ /**
44
+ * Extend Performance interface with Chrome-specific APIs
45
+ */
46
+ interface Performance {
47
+ /**
48
+ * Measures memory usage with breakdown by type
49
+ *
50
+ * Chrome-specific API for detailed memory profiling
51
+ * Requires cross-origin isolation headers in some contexts
52
+ *
53
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/measureUserAgentSpecificMemory
54
+ */
55
+ measureUserAgentSpecificMemory?(): Promise<MemoryMeasurement>
56
+ }
57
+
58
+ /**
59
+ * Extend Window interface with vendor-prefixed APIs
60
+ */
61
+ interface Window {
62
+ /**
63
+ * Safari's webkit-prefixed AudioContext
64
+ * Used for audio fingerprinting fallback
65
+ */
66
+ webkitAudioContext?: typeof AudioContext
67
+ }
package/src/validation.ts CHANGED
@@ -118,9 +118,11 @@ export function isValidSlippage(value: number): boolean {
118
118
  /**
119
119
  * SIP stealth meta-address format:
120
120
  * sip:<chain>:<spendingKey>:<viewingKey>
121
- * Each key is 33 bytes compressed (66 hex chars with 0x prefix)
121
+ * Keys can be:
122
+ * - secp256k1: 33 bytes compressed (66 hex chars with 0x prefix)
123
+ * - ed25519: 32 bytes (64 hex chars with 0x prefix)
122
124
  */
123
- const STEALTH_META_ADDRESS_REGEX = /^sip:[a-z]+:0x[0-9a-fA-F]{66}:0x[0-9a-fA-F]{66}$/
125
+ const STEALTH_META_ADDRESS_REGEX = /^sip:[a-z]+:0x[0-9a-fA-F]{64,66}:0x[0-9a-fA-F]{64,66}$/
124
126
 
125
127
  /**
126
128
  * Check if a string is a valid stealth meta-address
@@ -271,18 +271,33 @@ export class BitcoinWalletAdapter extends BaseWalletAdapter {
271
271
  }
272
272
 
273
273
  try {
274
- // First, sign the transaction
275
- const signed = await this.signTransaction(tx)
276
-
277
- // Extract the signed PSBT hex (without 0x prefix)
278
- const signedPsbt = signed.serialized.slice(2)
274
+ // Extract PSBT from transaction data
275
+ const psbtHex = tx.data as string
276
+ const options = tx.metadata?.signPsbtOptions as SignPsbtOptions | undefined
279
277
 
280
- // If PSBT is not finalized, we need to finalize it
281
- // For now, assume it's finalized (autoFinalized: true in options)
282
- // TODO: Add PSBT finalization logic here
278
+ // Sign with autoFinalized: true to get a finalized PSBT ready for broadcast
279
+ const signedPsbt = await this.provider.signPsbt(psbtHex, {
280
+ ...options,
281
+ autoFinalized: true, // Request wallet to finalize the PSBT
282
+ })
283
283
 
284
- // Broadcast the transaction
285
- const txid = await this.provider.pushTx(signedPsbt)
284
+ // Try to broadcast the finalized transaction
285
+ let txid: string
286
+
287
+ try {
288
+ // First, try to push the signed PSBT directly
289
+ // Some wallets return a raw transaction after finalization
290
+ txid = await this.provider.pushTx(signedPsbt)
291
+ } catch (pushError) {
292
+ // If pushTx fails (e.g., Xverse/Leather), try extracting raw tx
293
+ // The signed PSBT may need extraction of the final transaction
294
+ const rawTx = this.extractRawTransaction(signedPsbt)
295
+ if (rawTx) {
296
+ txid = await this.provider.pushTx(rawTx)
297
+ } else {
298
+ throw pushError
299
+ }
300
+ }
286
301
 
287
302
  return {
288
303
  txHash: ('0x' + txid) as HexString,
@@ -306,6 +321,44 @@ export class BitcoinWalletAdapter extends BaseWalletAdapter {
306
321
  }
307
322
  }
308
323
 
324
+ /**
325
+ * Extract raw transaction from a finalized PSBT
326
+ *
327
+ * A finalized PSBT has all inputs signed and can be converted to a raw transaction.
328
+ * This is a simplified extraction - full implementation would use bitcoinjs-lib.
329
+ *
330
+ * @param psbtHex - Hex-encoded finalized PSBT
331
+ * @returns Raw transaction hex or undefined if extraction fails
332
+ */
333
+ private extractRawTransaction(psbtHex: string): string | undefined {
334
+ try {
335
+ // PSBT format: magic bytes (4) + separator (1) + key-value pairs
336
+ // After finalization, the PSBT contains the final scriptSig/witness for each input
337
+ // Full extraction requires parsing the PSBT structure
338
+
339
+ // For browser environments, we rely on the wallet to finalize properly
340
+ // This is a fallback that checks if the hex looks like a raw transaction
341
+ // Raw transactions start with version (4 bytes), typically 01000000 or 02000000
342
+
343
+ const cleanHex = psbtHex.startsWith('0x') ? psbtHex.slice(2) : psbtHex
344
+
345
+ // Check if it's already a raw transaction (not a PSBT)
346
+ // PSBT magic: 70736274ff (psbt + 0xff)
347
+ if (!cleanHex.startsWith('70736274ff')) {
348
+ // Might already be a raw transaction
349
+ if (cleanHex.startsWith('01000000') || cleanHex.startsWith('02000000')) {
350
+ return cleanHex
351
+ }
352
+ }
353
+
354
+ // Cannot extract without proper PSBT parsing library
355
+ // Return undefined to let the caller handle the error
356
+ return undefined
357
+ } catch {
358
+ return undefined
359
+ }
360
+ }
361
+
309
362
  /**
310
363
  * Get native BTC balance
311
364
  */
@@ -331,8 +384,10 @@ export class BitcoinWalletAdapter extends BaseWalletAdapter {
331
384
  /**
332
385
  * Get token balance
333
386
  *
334
- * For Bitcoin, this would return balance of inscriptions or BRC-20 tokens
335
- * Not implemented yet - returns 0
387
+ * For Bitcoin, this returns BRC-20 token balances.
388
+ * Uses Unisat Open API for balance queries.
389
+ *
390
+ * @param asset - Asset with token symbol (e.g., 'ordi', 'sats')
336
391
  */
337
392
  async getTokenBalance(asset: Asset): Promise<bigint> {
338
393
  this.requireConnected()
@@ -344,9 +399,98 @@ export class BitcoinWalletAdapter extends BaseWalletAdapter {
344
399
  )
345
400
  }
346
401
 
347
- // TODO: Implement BRC-20 token balance query
348
- // For now, return 0
349
- return 0n
402
+ if (!this._address) {
403
+ throw new WalletError('No address connected', WalletErrorCode.NOT_CONNECTED)
404
+ }
405
+
406
+ try {
407
+ // Query BRC-20 balance from indexer API
408
+ const balance = await this.queryBrc20Balance(this._address, asset.symbol)
409
+ return balance
410
+ } catch (error) {
411
+ // Return 0 if query fails (token might not exist or API unavailable)
412
+ console.warn(`Failed to query BRC-20 balance for ${asset.symbol}:`, error)
413
+ return 0n
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Query BRC-20 token balance from indexer API
419
+ *
420
+ * Uses Unisat Open API as the default indexer.
421
+ * Can be overridden by setting brc20ApiUrl in config.
422
+ *
423
+ * @param address - Bitcoin address
424
+ * @param ticker - BRC-20 token ticker (e.g., 'ordi', 'sats')
425
+ */
426
+ private async queryBrc20Balance(address: string, ticker: string): Promise<bigint> {
427
+ // Unisat Open API endpoint for BRC-20 balances
428
+ // API docs: https://open-api.unisat.io/swagger.html
429
+ const apiUrl = `https://open-api.unisat.io/v1/indexer/address/${address}/brc20/${ticker.toLowerCase()}/info`
430
+
431
+ try {
432
+ const response = await fetch(apiUrl, {
433
+ method: 'GET',
434
+ headers: {
435
+ 'Content-Type': 'application/json',
436
+ // Note: Production usage may require API key
437
+ // 'Authorization': `Bearer ${apiKey}`
438
+ },
439
+ })
440
+
441
+ if (!response.ok) {
442
+ if (response.status === 404) {
443
+ // Token not found or no balance
444
+ return 0n
445
+ }
446
+ throw new Error(`API returned ${response.status}`)
447
+ }
448
+
449
+ const data = await response.json()
450
+
451
+ // Unisat API response format:
452
+ // { code: 0, msg: 'ok', data: { ticker, overallBalance, transferableBalance, availableBalance } }
453
+ if (data.code === 0 && data.data) {
454
+ // availableBalance is the spendable balance (not in pending transfers)
455
+ const balance = data.data.availableBalance || data.data.overallBalance || '0'
456
+ // BRC-20 balances are strings, convert to bigint
457
+ // Note: BRC-20 uses 18 decimal places by default
458
+ return BigInt(balance.replace('.', '').padEnd(18, '0'))
459
+ }
460
+
461
+ return 0n
462
+ } catch (error) {
463
+ // Fallback: try alternative API or return 0
464
+ return this.queryBrc20BalanceFallback(address, ticker)
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Fallback BRC-20 balance query using Hiro/Ordinals API
470
+ */
471
+ private async queryBrc20BalanceFallback(address: string, ticker: string): Promise<bigint> {
472
+ try {
473
+ // Hiro Ordinals API
474
+ const apiUrl = `https://api.hiro.so/ordinals/v1/brc-20/balances/${address}`
475
+
476
+ const response = await fetch(apiUrl)
477
+ if (!response.ok) return 0n
478
+
479
+ const data = await response.json()
480
+
481
+ // Find the specific ticker in results
482
+ const tokenBalance = data.results?.find(
483
+ (t: { ticker: string }) => t.ticker.toLowerCase() === ticker.toLowerCase()
484
+ )
485
+
486
+ if (tokenBalance?.overall_balance) {
487
+ return BigInt(tokenBalance.overall_balance)
488
+ }
489
+
490
+ return 0n
491
+ } catch {
492
+ return 0n
493
+ }
350
494
  }
351
495
 
352
496
  /**