@sip-protocol/sdk 0.5.1 → 0.6.1
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/README.md +58 -0
- package/dist/TransportWebUSB-TQ7WZ4LE.mjs +3098 -0
- package/dist/browser.d.mts +4 -4
- package/dist/browser.d.ts +4 -4
- package/dist/browser.js +10133 -4664
- package/dist/browser.mjs +32 -1
- package/dist/chunk-7QZPORY5.mjs +15604 -0
- package/dist/chunk-C2NPCUAJ.mjs +17010 -0
- package/dist/chunk-FCVLFUIC.mjs +16699 -0
- package/dist/chunk-G5UHXECN.mjs +16340 -0
- package/dist/chunk-GEDEIZHJ.mjs +16798 -0
- package/dist/chunk-KBS3OMSZ.mjs +14737 -0
- package/dist/chunk-MTNYSNR7.mjs +16269 -0
- package/dist/chunk-O5PIB2EA.mjs +16698 -0
- package/dist/chunk-PCFM7FQO.mjs +17010 -0
- package/dist/chunk-QK464ARC.mjs +16946 -0
- package/dist/chunk-TK3FWQNC.mjs +14737 -0
- package/dist/chunk-UJCSKKID.mjs +30 -0
- package/dist/chunk-VNBMNGC3.mjs +16698 -0
- package/dist/chunk-W5TUELDQ.mjs +16947 -0
- package/dist/index-05W_S8A7.d.mts +9237 -0
- package/dist/index-C5ehlFhR.d.mts +9443 -0
- package/dist/index-CD_zShu-.d.ts +10870 -0
- package/dist/index-CNzhx-WH.d.mts +9316 -0
- package/dist/index-CQBYdLYy.d.mts +10976 -0
- package/dist/index-Cg9TYEPv.d.mts +11321 -0
- package/dist/index-CqSppS4i.d.ts +9237 -0
- package/dist/index-CqZJOO8C.d.mts +11323 -0
- package/dist/index-CywN9Bnp.d.ts +11321 -0
- package/dist/index-DBa_jiZF.d.mts +9606 -0
- package/dist/index-DHy5ZjCD.d.ts +10976 -0
- package/dist/index-DLNdSQFQ.d.ts +9316 -0
- package/dist/index-DfsVsmxu.d.ts +11323 -0
- package/dist/index-Ink8HnKW.d.ts +9606 -0
- package/dist/index-ObjwyVDX.d.mts +10870 -0
- package/dist/index-h7B23m5b.d.ts +9443 -0
- package/dist/index-m0xbSfmT.d.mts +11318 -0
- package/dist/index-rWLEgvhN.d.ts +11318 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +10112 -4637
- package/dist/index.mjs +32 -1
- package/dist/noir-DKfEzWy9.d.mts +482 -0
- package/dist/noir-DKfEzWy9.d.ts +482 -0
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/proofs/noir.js +12 -3
- package/dist/proofs/noir.mjs +13 -3
- package/package.json +5 -3
- package/src/adapters/near-intents.ts +13 -3
- package/src/auction/index.ts +20 -0
- package/src/auction/sealed-bid.ts +1037 -0
- package/src/compliance/derivation.ts +13 -3
- package/src/compliance/reports.ts +5 -4
- package/src/cosmos/ibc-stealth.ts +2 -2
- package/src/cosmos/stealth.ts +2 -2
- package/src/crypto.ts +79 -9
- package/src/governance/index.ts +19 -0
- package/src/governance/private-vote.ts +1116 -0
- package/src/index.ts +50 -2
- package/src/intent.ts +227 -12
- package/src/nft/index.ts +27 -0
- package/src/nft/private-nft.ts +811 -0
- package/src/privacy.ts +88 -2
- package/src/proofs/browser-utils.ts +1 -7
- package/src/proofs/noir.ts +34 -7
- package/src/settlement/backends/direct-chain.ts +14 -3
- package/src/sip.ts +324 -25
- package/src/stealth.ts +120 -9
- package/src/types/browser.d.ts +67 -0
- package/src/validation.ts +4 -2
- package/src/wallet/bitcoin/adapter.ts +159 -15
- package/src/wallet/bitcoin/types.ts +340 -15
- package/src/wallet/cosmos/mock.ts +16 -12
- package/src/wallet/hardware/ledger.ts +83 -14
- package/src/wallet/hardware/types.ts +2 -0
package/src/stealth.ts
CHANGED
|
@@ -36,12 +36,66 @@ import {
|
|
|
36
36
|
import { secureWipe, secureWipeAll } from './secure-memory'
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Generate a new stealth meta-address keypair
|
|
39
|
+
* Generate a new stealth meta-address keypair for receiving private payments
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* Creates a reusable meta-address that senders can use to derive one-time stealth
|
|
42
|
+
* addresses. The recipient publishes the meta-address publicly, and senders generate
|
|
43
|
+
* unique payment addresses from it.
|
|
44
|
+
*
|
|
45
|
+
* **Security:** Private keys must be stored securely and never shared. The meta-address
|
|
46
|
+
* (containing only public keys) can be safely published.
|
|
47
|
+
*
|
|
48
|
+
* **Algorithm:** Uses secp256k1 elliptic curve (EIP-5564 style) for:
|
|
49
|
+
* - Ethereum, Polygon, Arbitrum, Optimism, Base, Bitcoin, Zcash
|
|
50
|
+
*
|
|
51
|
+
* For Solana/NEAR/Aptos/Sui chains, use {@link generateEd25519StealthMetaAddress} instead.
|
|
52
|
+
*
|
|
53
|
+
* @param chain - Target blockchain network (determines address format)
|
|
54
|
+
* @param label - Optional human-readable label for identification
|
|
55
|
+
* @returns Object containing:
|
|
56
|
+
* - `metaAddress`: Public keys to share with senders
|
|
57
|
+
* - `spendingPrivateKey`: Secret key for claiming funds (keep secure!)
|
|
58
|
+
* - `viewingPrivateKey`: Secret key for scanning incoming payments (keep secure!)
|
|
59
|
+
*
|
|
60
|
+
* @throws {ValidationError} If chain is invalid or not supported
|
|
61
|
+
*
|
|
62
|
+
* @example Generate stealth keys for Ethereum
|
|
63
|
+
* ```typescript
|
|
64
|
+
* import { generateStealthMetaAddress, encodeStealthMetaAddress } from '@sip-protocol/sdk'
|
|
65
|
+
*
|
|
66
|
+
* // Generate keys
|
|
67
|
+
* const { metaAddress, spendingPrivateKey, viewingPrivateKey } =
|
|
68
|
+
* generateStealthMetaAddress('ethereum', 'My Privacy Wallet')
|
|
69
|
+
*
|
|
70
|
+
* // Encode for sharing (QR code, website, etc.)
|
|
71
|
+
* const encoded = encodeStealthMetaAddress(metaAddress)
|
|
72
|
+
* console.log('Share this:', encoded)
|
|
73
|
+
* // Output: "sip:ethereum:0x02abc...123:0x03def...456"
|
|
74
|
+
*
|
|
75
|
+
* // Store private keys securely (e.g., encrypted keystore)
|
|
76
|
+
* secureStorage.save({
|
|
77
|
+
* spendingPrivateKey,
|
|
78
|
+
* viewingPrivateKey,
|
|
79
|
+
* })
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example Multi-chain setup
|
|
83
|
+
* ```typescript
|
|
84
|
+
* // Generate different stealth keys for each chain
|
|
85
|
+
* const ethKeys = generateStealthMetaAddress('ethereum', 'ETH Privacy')
|
|
86
|
+
* const zkKeys = generateStealthMetaAddress('zcash', 'ZEC Privacy')
|
|
87
|
+
*
|
|
88
|
+
* // Publish meta-addresses
|
|
89
|
+
* publishToProfile({
|
|
90
|
+
* ethereum: encodeStealthMetaAddress(ethKeys.metaAddress),
|
|
91
|
+
* zcash: encodeStealthMetaAddress(zkKeys.metaAddress),
|
|
92
|
+
* })
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @see {@link generateStealthAddress} to generate payment addresses as a sender
|
|
96
|
+
* @see {@link encodeStealthMetaAddress} to encode for sharing
|
|
97
|
+
* @see {@link deriveStealthPrivateKey} to claim funds as a recipient
|
|
98
|
+
* @see {@link generateEd25519StealthMetaAddress} for Solana/NEAR chains
|
|
45
99
|
*/
|
|
46
100
|
export function generateStealthMetaAddress(
|
|
47
101
|
chain: ChainId,
|
|
@@ -125,11 +179,68 @@ function validateStealthMetaAddress(
|
|
|
125
179
|
}
|
|
126
180
|
|
|
127
181
|
/**
|
|
128
|
-
* Generate a one-time stealth address for a recipient
|
|
182
|
+
* Generate a one-time stealth address for sending funds to a recipient
|
|
129
183
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
184
|
+
* As a sender, use this function to create a unique, unlinkable payment address
|
|
185
|
+
* from the recipient's public meta-address. Each call generates a new address
|
|
186
|
+
* that only the recipient can link to their identity.
|
|
187
|
+
*
|
|
188
|
+
* **Privacy Properties:**
|
|
189
|
+
* - Address is unique per transaction (prevents on-chain linkability)
|
|
190
|
+
* - Only recipient can detect and claim payments
|
|
191
|
+
* - Third-party observers cannot link payments to the same recipient
|
|
192
|
+
* - View tag enables efficient payment scanning
|
|
193
|
+
*
|
|
194
|
+
* **Algorithm (EIP-5564 DKSAP):**
|
|
195
|
+
* 1. Generate ephemeral keypair (r, R = r*G)
|
|
196
|
+
* 2. Compute shared secret: S = r * P_spend
|
|
197
|
+
* 3. Derive stealth address: A = P_view + hash(S)*G
|
|
198
|
+
* 4. Publish (R, A) on-chain; keep r secret
|
|
199
|
+
*
|
|
200
|
+
* @param recipientMetaAddress - Recipient's public stealth meta-address
|
|
201
|
+
* @returns Object containing:
|
|
202
|
+
* - `stealthAddress`: One-time payment address to publish on-chain
|
|
203
|
+
* - `sharedSecret`: Secret for sender's records (optional, don't publish!)
|
|
204
|
+
*
|
|
205
|
+
* @throws {ValidationError} If meta-address is invalid or malformed
|
|
206
|
+
*
|
|
207
|
+
* @example Send shielded payment
|
|
208
|
+
* ```typescript
|
|
209
|
+
* import { generateStealthAddress, decodeStealthMetaAddress } from '@sip-protocol/sdk'
|
|
210
|
+
*
|
|
211
|
+
* // Recipient shares their meta-address (e.g., on website, profile)
|
|
212
|
+
* const recipientMetaAddr = 'sip:ethereum:0x02abc...123:0x03def...456'
|
|
213
|
+
*
|
|
214
|
+
* // Decode the meta-address
|
|
215
|
+
* const metaAddress = decodeStealthMetaAddress(recipientMetaAddr)
|
|
216
|
+
*
|
|
217
|
+
* // Generate one-time payment address
|
|
218
|
+
* const { stealthAddress } = generateStealthAddress(metaAddress)
|
|
219
|
+
*
|
|
220
|
+
* // Use the stealth address in your transaction
|
|
221
|
+
* await sendPayment({
|
|
222
|
+
* to: stealthAddress.address, // One-time address
|
|
223
|
+
* amount: '1000000000000000000', // 1 ETH
|
|
224
|
+
* ephemeralKey: stealthAddress.ephemeralPublicKey, // Publish for recipient
|
|
225
|
+
* viewTag: stealthAddress.viewTag, // For efficient scanning
|
|
226
|
+
* })
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* @example Integrate with SIP intent
|
|
230
|
+
* ```typescript
|
|
231
|
+
* // In a shielded intent, the recipient stealth address is generated automatically
|
|
232
|
+
* const intent = await sip.createIntent({
|
|
233
|
+
* input: { asset: { chain: 'solana', symbol: 'SOL', address: null, decimals: 9 }, amount: 10n },
|
|
234
|
+
* output: { asset: { chain: 'ethereum', symbol: 'ETH', address: null, decimals: 18 }, minAmount: 0n, maxSlippage: 0.01 },
|
|
235
|
+
* privacy: PrivacyLevel.SHIELDED,
|
|
236
|
+
* recipientMetaAddress: 'sip:ethereum:0x02abc...123:0x03def...456',
|
|
237
|
+
* })
|
|
238
|
+
* // intent.recipientStealth contains the generated stealth address
|
|
239
|
+
* ```
|
|
240
|
+
*
|
|
241
|
+
* @see {@link generateStealthMetaAddress} to create meta-address as recipient
|
|
242
|
+
* @see {@link deriveStealthPrivateKey} for recipient to claim funds
|
|
243
|
+
* @see {@link checkStealthAddress} to scan for incoming payments
|
|
133
244
|
*/
|
|
134
245
|
export function generateStealthAddress(
|
|
135
246
|
recipientMetaAddress: StealthMetaAddress,
|
|
@@ -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
|
-
*
|
|
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
|
-
//
|
|
275
|
-
const
|
|
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
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
//
|
|
285
|
-
|
|
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
|
|
335
|
-
*
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
/**
|