@utxopia/sdk 0.1.0-alpha.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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/package.json +108 -0
  4. package/packages/btc-client/src/esplora-client.ts +153 -0
  5. package/packages/btc-client/src/index.ts +3 -0
  6. package/packages/btc-client/src/op-return.ts +93 -0
  7. package/packages/btc-client/src/types.ts +112 -0
  8. package/packages/sdk/README.md +277 -0
  9. package/packages/sdk/dist/announcement-client.d.ts +64 -0
  10. package/packages/sdk/dist/announcement-client.js +337 -0
  11. package/packages/sdk/dist/auditor-ciphertext.d.ts +72 -0
  12. package/packages/sdk/dist/auditor-ciphertext.js +135 -0
  13. package/packages/sdk/dist/auditor.d.ts +115 -0
  14. package/packages/sdk/dist/auditor.js +292 -0
  15. package/packages/sdk/dist/bitcoin/ika.d.ts +47 -0
  16. package/packages/sdk/dist/bitcoin/ika.js +74 -0
  17. package/packages/sdk/dist/bitcoin/index.d.ts +2 -0
  18. package/packages/sdk/dist/bitcoin/index.js +1 -0
  19. package/packages/sdk/dist/bound-params.d.ts +96 -0
  20. package/packages/sdk/dist/bound-params.js +249 -0
  21. package/packages/sdk/dist/chadbuffer.d.ts +124 -0
  22. package/packages/sdk/dist/chadbuffer.js +405 -0
  23. package/packages/sdk/dist/claim-link.d.ts +30 -0
  24. package/packages/sdk/dist/claim-link.js +50 -0
  25. package/packages/sdk/dist/client.d.ts +233 -0
  26. package/packages/sdk/dist/client.js +416 -0
  27. package/packages/sdk/dist/commitment-tree.d.ts +268 -0
  28. package/packages/sdk/dist/commitment-tree.js +564 -0
  29. package/packages/sdk/dist/config.d.ts +177 -0
  30. package/packages/sdk/dist/config.js +426 -0
  31. package/packages/sdk/dist/core/esplora.d.ts +110 -0
  32. package/packages/sdk/dist/core/esplora.js +187 -0
  33. package/packages/sdk/dist/core/mempool.d.ts +60 -0
  34. package/packages/sdk/dist/core/mempool.js +107 -0
  35. package/packages/sdk/dist/crypto-babyjub.d.ts +105 -0
  36. package/packages/sdk/dist/crypto-babyjub.js +312 -0
  37. package/packages/sdk/dist/crypto-ed25519.d.ts +116 -0
  38. package/packages/sdk/dist/crypto-ed25519.js +235 -0
  39. package/packages/sdk/dist/crypto.d.ts +59 -0
  40. package/packages/sdk/dist/crypto.js +140 -0
  41. package/packages/sdk/dist/event-client.d.ts +64 -0
  42. package/packages/sdk/dist/event-client.js +165 -0
  43. package/packages/sdk/dist/events.d.ts +127 -0
  44. package/packages/sdk/dist/events.js +305 -0
  45. package/packages/sdk/dist/explorer.d.ts +86 -0
  46. package/packages/sdk/dist/explorer.js +197 -0
  47. package/packages/sdk/dist/index.d.ts +61 -0
  48. package/packages/sdk/dist/index.js +216 -0
  49. package/packages/sdk/dist/instructions.d.ts +1125 -0
  50. package/packages/sdk/dist/instructions.js +1760 -0
  51. package/packages/sdk/dist/keys.d.ts +467 -0
  52. package/packages/sdk/dist/keys.js +799 -0
  53. package/packages/sdk/dist/logger.d.ts +10 -0
  54. package/packages/sdk/dist/logger.js +40 -0
  55. package/packages/sdk/dist/magicblock.d.ts +90 -0
  56. package/packages/sdk/dist/magicblock.js +164 -0
  57. package/packages/sdk/dist/merkle.d.ts +82 -0
  58. package/packages/sdk/dist/merkle.js +141 -0
  59. package/packages/sdk/dist/note.d.ts +348 -0
  60. package/packages/sdk/dist/note.js +483 -0
  61. package/packages/sdk/dist/pda.d.ts +160 -0
  62. package/packages/sdk/dist/pda.js +384 -0
  63. package/packages/sdk/dist/pool-state.d.ts +100 -0
  64. package/packages/sdk/dist/pool-state.js +126 -0
  65. package/packages/sdk/dist/poseidon.d.ts +53 -0
  66. package/packages/sdk/dist/poseidon.js +136 -0
  67. package/packages/sdk/dist/prover/index.d.ts +10 -0
  68. package/packages/sdk/dist/prover/index.js +10 -0
  69. package/packages/sdk/dist/prover/mobile.d.ts +45 -0
  70. package/packages/sdk/dist/prover/mobile.js +227 -0
  71. package/packages/sdk/dist/prover/web.d.ts +136 -0
  72. package/packages/sdk/dist/prover/web.js +597 -0
  73. package/packages/sdk/dist/psbt.d.ts +73 -0
  74. package/packages/sdk/dist/psbt.js +202 -0
  75. package/packages/sdk/dist/selective-disclosure.d.ts +181 -0
  76. package/packages/sdk/dist/selective-disclosure.js +172 -0
  77. package/packages/sdk/dist/sender-memo.d.ts +149 -0
  78. package/packages/sdk/dist/sender-memo.js +250 -0
  79. package/packages/sdk/dist/sns-resolver.d.ts +121 -0
  80. package/packages/sdk/dist/sns-resolver.js +229 -0
  81. package/packages/sdk/dist/solana/connection.d.ts +77 -0
  82. package/packages/sdk/dist/solana/connection.js +133 -0
  83. package/packages/sdk/dist/solana/priority-fee.d.ts +78 -0
  84. package/packages/sdk/dist/solana/priority-fee.js +137 -0
  85. package/packages/sdk/dist/stealth.d.ts +429 -0
  86. package/packages/sdk/dist/stealth.js +703 -0
  87. package/packages/sdk/dist/taproot.d.ts +172 -0
  88. package/packages/sdk/dist/taproot.js +494 -0
  89. package/packages/sdk/dist/token-registry.d.ts +96 -0
  90. package/packages/sdk/dist/token-registry.js +122 -0
  91. package/packages/sdk/dist/utils/encoding.d.ts +19 -0
  92. package/packages/sdk/dist/utils/encoding.js +29 -0
  93. package/packages/sdk/dist/vk-registry.d.ts +100 -0
  94. package/packages/sdk/dist/vk-registry.js +212 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 UTXOpia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # UTXOpia SDK
2
+
3
+ TypeScript client for [UTXOpia](https://utxopia.com) — a privacy-preserving Bitcoin-to-Solana
4
+ bridge. Deposits arrive as commitments in a Merkle tree, transfers move between commitments with
5
+ Groth16 JoinSplit proofs, and an amount only becomes public when someone withdraws.
6
+
7
+ > **Alpha.** `0.1.0-alpha.1`. The protocol has not been deployed to mainnet — do not point this at
8
+ > funds you cannot lose.
9
+ >
10
+ > **The export surface is not a contract yet.** Roughly 430 things are exported; about a third are
11
+ > exercised by the reference apps, and the rest is protocol surface nobody has needed yet or
12
+ > helpers that were never deliberately made public. Being pre-1.0 is what makes that survivable:
13
+ > the surface will be narrowed to the workflows below before 1.0, and anything outside them may
14
+ > disappear in a minor release. Build against what `docs/API-MAP.md` marks as used; if you need
15
+ > something else, open an issue so it gets kept on purpose rather than by accident.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ bun add @utxopia/sdk
21
+ ```
22
+
23
+ Installing from git instead? Pin a commit, never a branch. A floating `#main` means two apps that
24
+ installed a week apart disagree about the note format, and the failure surfaces as a proof that
25
+ will not verify rather than as an install error:
26
+
27
+ ```jsonc
28
+ // package.json
29
+ "@utxopia/sdk": "github:UTXOpia/utxopia-sdk#<full-commit-sha>"
30
+ ```
31
+
32
+ `snarkjs` and `react` are optional peers — install `snarkjs` only if you generate proofs, `react`
33
+ only if you use the hooks.
34
+
35
+ ## What the pieces are
36
+
37
+ | Concept | What it means here |
38
+ |---|---|
39
+ | **Note** | A commitment `Poseidon(npk, token, amount)` in the tree. Owning one means knowing the key behind `npk`. |
40
+ | **Nullifier** | `Poseidon(nullifyingKey, leafIndex)`. Published on spend so a note cannot be spent twice; it reveals nothing about which note it was. |
41
+ | **Stealth address** | A recipient publishes a meta-address; each sender derives a fresh one-time `npk` from it, so payments to the same person are unlinkable on-chain. |
42
+ | **JoinSplit(N,M)** | One proof spends N notes and creates M. Shapes with `N + M ≤ 10` are accepted on-chain, 45 in total. |
43
+ | **Viewing key** | Decrypts incoming announcements. Separate from the spending key, so scanning can be delegated. |
44
+
45
+ ## Quick start
46
+
47
+ ```typescript
48
+ import {
49
+ deriveKeysFromWallet,
50
+ resolveSnsName,
51
+ createStealthDeposit,
52
+ scanAnnouncements,
53
+ } from "@utxopia/sdk";
54
+
55
+ // Keys come from a wallet signature over a fixed message — nothing to store.
56
+ const keys = await deriveKeysFromWallet(walletAdapter);
57
+
58
+ // Recipients can be a .utxopia.sol name instead of a raw meta-address.
59
+ const recipient = await resolveSnsName(connection, "alice");
60
+
61
+ const deposit = await createStealthDeposit(recipient, 100_000n);
62
+
63
+ // Scanning is a local trial-decrypt of every announcement — there is no
64
+ // "my transactions" endpoint to ask, by design.
65
+ const notes = await scanAnnouncements(keys, announcements);
66
+ ```
67
+
68
+ `docs/API-MAP.md` lists every export grouped by what you are building — config, keys, PDAs,
69
+ notes, stealth, Bitcoin, proving, instructions, chain reads, disclosure — and marks which ones
70
+ the reference apps actually use. `packages/sdk/README.md` covers each area in depth;
71
+ `packages/sdk/docs/SDK.md` has the full type reference.
72
+
73
+ ## Entry points
74
+
75
+ ```typescript
76
+ import { … } from "@utxopia/sdk"; // everything below is re-exported here
77
+ import { … } from "@utxopia/sdk/prover/web"; // browser proving (snarkjs + WebAssembly)
78
+ import { … } from "@utxopia/sdk/prover/mobile"; // React Native proving
79
+ import { … } from "@utxopia/sdk/bitcoin"; // taproot, Ika custody
80
+ import { … } from "@utxopia/sdk/stealth"; // stealth addresses on their own
81
+ import { … } from "@utxopia/sdk/btc-client"; // Esplora client, OP_RETURN encoding
82
+ ```
83
+
84
+ The root export is the whole surface. The subpaths exist so a bundle that only needs, say,
85
+ Esplora does not pull in the prover.
86
+
87
+ ## Naming
88
+
89
+ The prefix tells you what a call costs, so you can read an import list without opening anything:
90
+
91
+ | prefix | meaning |
92
+ |---|---|
93
+ | `derive*PDA` | pure address math, synchronous |
94
+ | `fetch*` | hits the network — always async |
95
+ | `get*` | reads local or already-fetched state — synchronous |
96
+ | `parse*` | bytes in, typed object out; no IO |
97
+ | `build*InstructionData` / `build*Instruction` | encode instruction data / assemble the full instruction |
98
+ | `compute*Sync` | Poseidon hashes. Only the `Sync` form exists — poseidon-lite has nothing to await |
99
+ | `encode`/`decode`, `pack`/`unpack`, `serialize`/`deserialize` | symmetric pairs, always both directions |
100
+
101
+ `PDA` is spelled in caps. Seven MagicBlock helpers used to say `Pda` and two RPC calls were named
102
+ `get*`; they were renamed before the first publish, while doing so was still free.
103
+
104
+ ## Proving
105
+
106
+ Proof generation needs circuit artifacts — a `.wasm` witness generator and a `.zkey` — which are
107
+ too large to ship in the package. Point the prover at a host serving them:
108
+
109
+ ```typescript
110
+ import { setCircuitPath, generateJoinSplitProof } from "@utxopia/sdk/prover/web";
111
+
112
+ setCircuitPath("https://circuit.utxopia.com/circuits/v2/groth16");
113
+ const proof = await generateJoinSplitProof(inputs);
114
+ ```
115
+
116
+ **Pin the artifacts you are willing to prove with.** The `.wasm` is the witness generator: it
117
+ receives every private input a JoinSplit has — spending key, nullifying key, note randomness,
118
+ amounts, the full Merkle path — in the clear. It arrives over plain `fetch`, where subresource
119
+ integrity does not apply, and CDNs serve it `immutable, max-age=31536000`, so a substituted file
120
+ survives in browser and edge caches long after the origin is cleaned. Proofs built from a poisoned
121
+ generator still verify, so nothing fails and no user notices.
122
+
123
+ ```typescript
124
+ import { setCircuitArtifactDigests } from "@utxopia/sdk/prover/web";
125
+ import digests from "./circuit-manifest.json"; // generated by your build, committed
126
+
127
+ setCircuitArtifactDigests(digests.artifacts);
128
+ ```
129
+
130
+ Keys are paths under the circuit base (`joinsplit_1x1/joinsplit_1x1_js/joinsplit_1x1.wasm`), so one
131
+ manifest covers local dev and the CDN. Once set it fails closed: an artifact with no recorded
132
+ digest is refused rather than trusted, so the manifest has to cover every shape your origin serves.
133
+ The SDK cannot ship these — they change whenever circuits are rebuilt.
134
+
135
+ ## Layout
136
+
137
+ ```
138
+ packages/sdk/ the SDK — keys, notes, stealth, proving, Solana instructions
139
+ packages/btc-client/ standalone Esplora client and deposit OP_RETURN encoding
140
+ ```
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ bun install
146
+ bun run test # 782 tests
147
+ bun run build # tsc
148
+ ```
149
+
150
+ Tests are pure unit tests — no network, no validator, no proving. They run in about eight seconds,
151
+ so there is no reason to skip them.
152
+
153
+ ## Security
154
+
155
+ Two things worth knowing before you build on this:
156
+
157
+ - **`createDelegatedViewKey` currently hands over the `nullifyingKey`.** Because
158
+ `nullifier = Poseidon(nullifyingKey, leafIndex)` does not depend on note contents and leaf
159
+ indices are small public integers, anyone holding it can compute every nullifier that key will
160
+ ever produce and link the owner's entire spend history, retroactively and permanently. The
161
+ `fromSlot`/`toSlot` and FULL/SCAN/INCOMING_ONLY fields on a delegated key are metadata, not
162
+ enforcement. Treat a delegated viewing key as handing over the whole account, not a slice of it.
163
+ `extractViewOnlyBundle` correctly omits the key and is the safer thing to reach for.
164
+ - Keys derive from a wallet signature, so **any party who can make that wallet sign the derivation
165
+ message can reconstruct the shielded identity.** A hardware wallet does not change this.
166
+
167
+ Report anything else privately rather than in a public issue.
168
+
169
+ ## License
170
+
171
+ MIT
package/package.json ADDED
@@ -0,0 +1,108 @@
1
+ {
2
+ "name": "@utxopia/sdk",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "UTXOpia public SDK workspace",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/UTXOpia/utxopia-sdk.git"
9
+ },
10
+ "homepage": "https://github.com/UTXOpia/utxopia-sdk#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/UTXOpia/utxopia-sdk/issues"
13
+ },
14
+ "type": "module",
15
+ "private": false,
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "files": [
20
+ "packages/sdk/dist",
21
+ "packages/btc-client/src",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "sideEffects": false,
26
+ "main": "./packages/sdk/dist/index.js",
27
+ "module": "./packages/sdk/dist/index.js",
28
+ "types": "./packages/sdk/dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./packages/sdk/dist/index.d.ts",
32
+ "import": "./packages/sdk/dist/index.js",
33
+ "default": "./packages/sdk/dist/index.js"
34
+ },
35
+ "./prover": {
36
+ "types": "./packages/sdk/dist/prover/index.d.ts",
37
+ "import": "./packages/sdk/dist/prover/index.js",
38
+ "default": "./packages/sdk/dist/prover/index.js"
39
+ },
40
+ "./prover/web": {
41
+ "types": "./packages/sdk/dist/prover/web.d.ts",
42
+ "import": "./packages/sdk/dist/prover/web.js",
43
+ "default": "./packages/sdk/dist/prover/web.js"
44
+ },
45
+ "./prover/mobile": {
46
+ "types": "./packages/sdk/dist/prover/mobile.d.ts",
47
+ "import": "./packages/sdk/dist/prover/mobile.js",
48
+ "default": "./packages/sdk/dist/prover/mobile.js"
49
+ },
50
+ "./bitcoin": {
51
+ "types": "./packages/sdk/dist/bitcoin/index.d.ts",
52
+ "import": "./packages/sdk/dist/bitcoin/index.js",
53
+ "default": "./packages/sdk/dist/bitcoin/index.js"
54
+ },
55
+ "./stealth": {
56
+ "types": "./packages/sdk/dist/stealth.d.ts",
57
+ "import": "./packages/sdk/dist/stealth.js",
58
+ "default": "./packages/sdk/dist/stealth.js"
59
+ },
60
+ "./btc-client": {
61
+ "types": "./packages/btc-client/src/index.ts",
62
+ "import": "./packages/btc-client/src/index.ts",
63
+ "default": "./packages/btc-client/src/index.ts"
64
+ }
65
+ },
66
+ "scripts": {
67
+ "build": "cd packages/sdk && bunx tsc",
68
+ "test": "cd packages/sdk && bun install --ignore-scripts && bun test",
69
+ "prepare": "cd packages/sdk && bunx tsc",
70
+ "copy-circuits": "cd packages/sdk && bun run copy-circuits"
71
+ },
72
+ "dependencies": {
73
+ "@magicblock-labs/ephemeral-rollups-kit": "0.16.2",
74
+ "@noble/ciphers": "^2.2.0",
75
+ "@noble/curves": "2.0.1",
76
+ "@noble/hashes": "1.8.0",
77
+ "@scure/base": "2.0.0",
78
+ "@scure/btc-signer": "2.0.1",
79
+ "@solana-program/system": "^0.10.0",
80
+ "@solana/kit": "^5.5.0",
81
+ "bech32": "^2.0.0",
82
+ "circomlibjs": "^0.1.7",
83
+ "poseidon-lite": "0.3.0"
84
+ },
85
+ "peerDependencies": {
86
+ "react": ">=17.0.0",
87
+ "snarkjs": "^0.7.5"
88
+ },
89
+ "peerDependenciesMeta": {
90
+ "react": {
91
+ "optional": true
92
+ },
93
+ "snarkjs": {
94
+ "optional": true
95
+ }
96
+ },
97
+ "devDependencies": {
98
+ "@solana/web3.js": "^1.98.4",
99
+ "@types/node": "^20.10.0",
100
+ "@types/react": "^19",
101
+ "typescript": "^5.3.0"
102
+ },
103
+ "overrides": {
104
+ "uuid": "11.1.1",
105
+ "valibot": "1.4.2",
106
+ "ws": "8.21.1"
107
+ }
108
+ }
@@ -0,0 +1,153 @@
1
+ import type {
2
+ BitcoinAddressInfo,
3
+ BitcoinClient,
4
+ BitcoinDepositCandidate,
5
+ BitcoinMerkleProof,
6
+ BitcoinNetwork,
7
+ BitcoinOutspend,
8
+ BitcoinTransaction,
9
+ BitcoinTxStatus,
10
+ BitcoinUtxo,
11
+ } from "./types";
12
+ import { extractUtxopiaDepositOpReturn } from "./op-return";
13
+
14
+ const NETWORK_URLS: Record<BitcoinNetwork, string> = {
15
+ mainnet: "https://mempool.space/api",
16
+ testnet: "https://mempool.space/testnet/api",
17
+ testnet4: "https://mempool.space/testnet4/api",
18
+ signet: "https://mempool.space/signet/api",
19
+ regtest: "http://localhost:2140",
20
+ };
21
+
22
+ export interface EsploraClientOptions {
23
+ network?: BitcoinNetwork;
24
+ baseUrl?: string;
25
+ fetchImpl?: typeof fetch;
26
+ }
27
+
28
+ export class EsploraBitcoinClient implements BitcoinClient {
29
+ readonly network: BitcoinNetwork;
30
+ private readonly baseUrl: string;
31
+ private readonly fetchImpl: typeof fetch;
32
+
33
+ constructor(options: EsploraClientOptions = {}) {
34
+ this.network = options.network ?? "testnet";
35
+ this.baseUrl = (options.baseUrl ?? NETWORK_URLS[this.network]).replace(/\/$/, "");
36
+ this.fetchImpl = options.fetchImpl ?? fetch;
37
+ }
38
+
39
+ async getAddress(address: string): Promise<BitcoinAddressInfo> {
40
+ return this.getJson(`/address/${address}`);
41
+ }
42
+
43
+ async getAddressTxs(address: string, lastSeenTxid?: string): Promise<BitcoinTransaction[]> {
44
+ return this.getJson(lastSeenTxid ? `/address/${address}/txs/chain/${lastSeenTxid}` : `/address/${address}/txs`);
45
+ }
46
+
47
+ async getAddressTxsMempool(address: string): Promise<BitcoinTransaction[]> {
48
+ return this.getJson(`/address/${address}/txs/mempool`);
49
+ }
50
+
51
+ async getAddressUtxos(address: string): Promise<BitcoinUtxo[]> {
52
+ return this.getJson(`/address/${address}/utxo`);
53
+ }
54
+
55
+ async getTransaction(txid: string): Promise<BitcoinTransaction> {
56
+ return this.getJson(`/tx/${txid}`);
57
+ }
58
+
59
+ async getTxStatus(txid: string): Promise<BitcoinTxStatus> {
60
+ return this.getJson(`/tx/${txid}/status`);
61
+ }
62
+
63
+ async getTxHex(txid: string): Promise<string> {
64
+ return this.getText(`/tx/${txid}/hex`);
65
+ }
66
+
67
+ async getTxMerkleProof(txid: string): Promise<BitcoinMerkleProof> {
68
+ return this.getJson(`/tx/${txid}/merkle-proof`);
69
+ }
70
+
71
+ async getTxOutspend(txid: string, vout: number): Promise<BitcoinOutspend> {
72
+ return this.getJson(`/tx/${txid}/outspend/${vout}`);
73
+ }
74
+
75
+ async getBlockHeight(): Promise<number> {
76
+ return Number.parseInt(await this.getText("/blocks/tip/height"), 10);
77
+ }
78
+
79
+ async getBlockHash(height: number): Promise<string> {
80
+ return this.getText(`/block-height/${height}`);
81
+ }
82
+
83
+ async getBlockHeader(hash: string): Promise<string> {
84
+ return this.getText(`/block/${hash}/header`);
85
+ }
86
+
87
+ async broadcastTx(txHex: string): Promise<string> {
88
+ const res = await this.fetchImpl(`${this.baseUrl}/tx`, {
89
+ method: "POST",
90
+ headers: { "content-type": "text/plain" },
91
+ body: txHex,
92
+ });
93
+ if (!res.ok) {
94
+ throw new Error(`Esplora broadcast failed: ${res.status} ${res.statusText}: ${await res.text()}`);
95
+ }
96
+ return res.text();
97
+ }
98
+
99
+ async getConfirmations(txid: string): Promise<number> {
100
+ const status = await this.getTxStatus(txid);
101
+ if (!status.confirmed || status.block_height === undefined) {
102
+ return 0;
103
+ }
104
+
105
+ const tipHeight = await this.getBlockHeight();
106
+ return tipHeight - status.block_height + 1;
107
+ }
108
+
109
+ async findDepositCandidates(address: string, minConfirmations = 1): Promise<BitcoinDepositCandidate[]> {
110
+ const [utxos, tipHeight, txs] = await Promise.all([
111
+ this.getAddressUtxos(address),
112
+ this.getBlockHeight(),
113
+ this.getAddressTxs(address),
114
+ ]);
115
+ const txById = new Map(txs.map((tx) => [tx.txid, tx]));
116
+
117
+ return utxos
118
+ .map((utxo) => ({
119
+ address,
120
+ txid: utxo.txid,
121
+ vout: utxo.vout,
122
+ value: utxo.value,
123
+ confirmations: confirmationsForStatus(utxo.status, tipHeight),
124
+ status: utxo.status,
125
+ opReturn: extractUtxopiaDepositOpReturn(txById.get(utxo.txid)),
126
+ }))
127
+ .filter((candidate) => candidate.confirmations >= minConfirmations);
128
+ }
129
+
130
+ private async getJson<T>(endpoint: string): Promise<T> {
131
+ const res = await this.fetchImpl(`${this.baseUrl}${endpoint}`);
132
+ if (!res.ok) {
133
+ throw new Error(`Esplora API failed: ${res.status} ${res.statusText}`);
134
+ }
135
+ return res.json() as Promise<T>;
136
+ }
137
+
138
+ private async getText(endpoint: string): Promise<string> {
139
+ const res = await this.fetchImpl(`${this.baseUrl}${endpoint}`);
140
+ if (!res.ok) {
141
+ throw new Error(`Esplora API failed: ${res.status} ${res.statusText}`);
142
+ }
143
+ return res.text();
144
+ }
145
+ }
146
+
147
+ function confirmationsForStatus(status: BitcoinTxStatus, tipHeight: number): number {
148
+ if (!status.confirmed || status.block_height === undefined) {
149
+ return 0;
150
+ }
151
+
152
+ return tipHeight - status.block_height + 1;
153
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./esplora-client";
2
+ export * from "./op-return";
3
+ export * from "./types";
@@ -0,0 +1,93 @@
1
+ import type { BitcoinTransaction, UtxopiaDepositOpReturn } from "./types";
2
+
3
+ export const UTXOPIA_DEPOSIT_OP_RETURN_SIZE = 73;
4
+ export const UTXOPIA_DEPOSIT_OP_RETURN_VERSION = 1;
5
+ export const DESTINATION_CHAIN_SOLANA = 1;
6
+ export const BITCOIN_NETWORK_MAINNET = 0;
7
+ export const BITCOIN_NETWORK_TESTNET4 = 2;
8
+ export const BITCOIN_NETWORK_REGTEST = 3;
9
+
10
+ export function extractUtxopiaDepositOpReturn(
11
+ tx: BitcoinTransaction | undefined,
12
+ ): UtxopiaDepositOpReturn | undefined {
13
+ if (!tx) {
14
+ return undefined;
15
+ }
16
+
17
+ for (const output of tx.vout) {
18
+ const payload = parseOpReturnPayload(output.scriptpubkey);
19
+ if (payload?.length !== UTXOPIA_DEPOSIT_OP_RETURN_SIZE) {
20
+ continue;
21
+ }
22
+
23
+ const header = decodeDepositHeader(payload[0]);
24
+ if (!header) {
25
+ continue;
26
+ }
27
+
28
+ const ephemeralPubkey = payload.slice(9, 41);
29
+ const notePublicKey = payload.slice(41, 73);
30
+ return {
31
+ ...header,
32
+ poolTag: payload.slice(1, 9),
33
+ ephemeralPubkey,
34
+ notePublicKey,
35
+ rawPayload: payload,
36
+ };
37
+ }
38
+
39
+ return undefined;
40
+ }
41
+
42
+ export function decodeDepositHeader(header: number): {
43
+ version: number;
44
+ destinationChain: number;
45
+ bitcoinNetwork: number;
46
+ } | undefined {
47
+ const version = header >> 6;
48
+ const destinationChain = (header >> 4) & 0x03;
49
+ const bitcoinNetwork = header & 0x0f;
50
+ if (version !== UTXOPIA_DEPOSIT_OP_RETURN_VERSION) return undefined;
51
+ if (destinationChain !== DESTINATION_CHAIN_SOLANA) return undefined;
52
+ if (
53
+ bitcoinNetwork !== BITCOIN_NETWORK_MAINNET
54
+ && bitcoinNetwork !== BITCOIN_NETWORK_TESTNET4
55
+ && bitcoinNetwork !== BITCOIN_NETWORK_REGTEST
56
+ ) {
57
+ return undefined;
58
+ }
59
+ return { version, destinationChain, bitcoinNetwork };
60
+ }
61
+
62
+ export function parseOpReturnPayload(scriptPubkeyHex: string): Uint8Array | undefined {
63
+ const script = hexToBytes(scriptPubkeyHex);
64
+ if (script.length < 2 || script[0] !== 0x6a) {
65
+ return undefined;
66
+ }
67
+
68
+ const pushOpcode = script[1];
69
+ if (pushOpcode > 0 && pushOpcode <= 75) {
70
+ const end = 2 + pushOpcode;
71
+ return end === script.length ? script.slice(2, end) : undefined;
72
+ }
73
+
74
+ if (pushOpcode === 0x4c && script.length >= 3) {
75
+ const len = script[2];
76
+ const end = 3 + len;
77
+ return end === script.length ? script.slice(3, end) : undefined;
78
+ }
79
+
80
+ return undefined;
81
+ }
82
+
83
+ export function hexToBytes(hex: string): Uint8Array {
84
+ if (hex.length % 2 !== 0) {
85
+ throw new Error("Invalid hex string length");
86
+ }
87
+
88
+ const bytes = new Uint8Array(hex.length / 2);
89
+ for (let i = 0; i < bytes.length; i += 1) {
90
+ bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
91
+ }
92
+ return bytes;
93
+ }
@@ -0,0 +1,112 @@
1
+ export type BitcoinNetwork = "mainnet" | "testnet" | "testnet4" | "signet" | "regtest";
2
+
3
+ export interface BitcoinTxStatus {
4
+ confirmed: boolean;
5
+ block_height?: number;
6
+ block_hash?: string;
7
+ block_time?: number;
8
+ }
9
+
10
+ export interface BitcoinTxInput {
11
+ txid: string;
12
+ vout: number;
13
+ prevout: BitcoinTxOutput | null;
14
+ scriptsig: string;
15
+ scriptsig_asm: string;
16
+ witness?: string[];
17
+ is_coinbase: boolean;
18
+ sequence: number;
19
+ }
20
+
21
+ export interface BitcoinTxOutput {
22
+ scriptpubkey: string;
23
+ scriptpubkey_asm: string;
24
+ scriptpubkey_type: string;
25
+ scriptpubkey_address?: string;
26
+ value: number;
27
+ }
28
+
29
+ export interface BitcoinTransaction {
30
+ txid: string;
31
+ version: number;
32
+ locktime: number;
33
+ vin: BitcoinTxInput[];
34
+ vout: BitcoinTxOutput[];
35
+ size: number;
36
+ weight: number;
37
+ fee: number;
38
+ status: BitcoinTxStatus;
39
+ }
40
+
41
+ export interface BitcoinAddressInfo {
42
+ address: string;
43
+ chain_stats: {
44
+ funded_txo_count: number;
45
+ funded_txo_sum: number;
46
+ spent_txo_count: number;
47
+ spent_txo_sum: number;
48
+ tx_count: number;
49
+ };
50
+ mempool_stats: {
51
+ funded_txo_count: number;
52
+ funded_txo_sum: number;
53
+ spent_txo_count: number;
54
+ spent_txo_sum: number;
55
+ tx_count: number;
56
+ };
57
+ }
58
+
59
+ export interface BitcoinUtxo {
60
+ txid: string;
61
+ vout: number;
62
+ status: BitcoinTxStatus;
63
+ value: number;
64
+ }
65
+
66
+ export interface BitcoinMerkleProof {
67
+ block_height: number;
68
+ merkle: string[];
69
+ pos: number;
70
+ }
71
+
72
+ export interface BitcoinOutspend {
73
+ spent: boolean;
74
+ txid?: string;
75
+ vin?: number;
76
+ status?: BitcoinTxStatus;
77
+ }
78
+
79
+ export interface BitcoinDepositCandidate {
80
+ address: string;
81
+ txid: string;
82
+ vout: number;
83
+ value: number;
84
+ confirmations: number;
85
+ status: BitcoinTxStatus;
86
+ opReturn?: UtxopiaDepositOpReturn;
87
+ }
88
+
89
+ export interface UtxopiaDepositOpReturn {
90
+ version: number;
91
+ destinationChain: number;
92
+ bitcoinNetwork: number;
93
+ poolTag: Uint8Array;
94
+ ephemeralPubkey: Uint8Array;
95
+ notePublicKey: Uint8Array;
96
+ rawPayload: Uint8Array;
97
+ }
98
+
99
+ export interface BitcoinClient {
100
+ getAddress(address: string): Promise<BitcoinAddressInfo>;
101
+ getAddressTxs(address: string, lastSeenTxid?: string): Promise<BitcoinTransaction[]>;
102
+ getAddressUtxos(address: string): Promise<BitcoinUtxo[]>;
103
+ getTransaction(txid: string): Promise<BitcoinTransaction>;
104
+ getTxStatus(txid: string): Promise<BitcoinTxStatus>;
105
+ getTxHex(txid: string): Promise<string>;
106
+ getTxMerkleProof(txid: string): Promise<BitcoinMerkleProof>;
107
+ getTxOutspend(txid: string, vout: number): Promise<BitcoinOutspend>;
108
+ getBlockHeight(): Promise<number>;
109
+ getBlockHash(height: number): Promise<string>;
110
+ getBlockHeader(hash: string): Promise<string>;
111
+ broadcastTx(txHex: string): Promise<string>;
112
+ }