┌──────────────┐ WebSocket / HTTP-JSON ┌──────────────────┐
│ CJ Client │ <-- register / sign / finalize --> │ CJ Coordinator │
└──────────────┘ └──────────────────┘
▲ │
│ uses │ broadcasts
│ ▼
┌──────────────────┐ ┌──────────────────┐
│ Wallet Adapter │–– talk to local wallet RPC ––▶│ Bitcoin network │
└──────────────────┘ └──────────────────┘
Component | Role | Key files / packages |
---|---|---|
cj_client (library + CLI) | Talks to the coordinator; wraps txo_parser |
@myorg/cj_client |
cj_coordinator | Stateless HTTP/WebSocket API that orchestrates a round | @myorg/cj_coordinator |
wallet_adapter | Pluggable module that turns a wallet’s UTXO list ⇄ TXO URIs; signs PSBTs | @myorg/wallet_adapter_xxx |
shared-types | TypeScript *.d.ts exported for both sides |
@myorg/cj_types |
// cj_types/src/messages.ts
export interface RegisterInputReq {
type: 'register_input'
txo_uri: string // e.g. "txo:btc:<txid>:0?amount=0.1"
denomination: string // "0.1" (as decimal string)
blinded_output: string // credential-request for output address
}
export interface RegisterInputAck {
type: 'register_input_ok'
blinded_signature: string
}
export interface CommitOutputReq {
type: 'commit_output'
output_script: string // hex
amount: string // must equal denomination
}
export interface FinalPsbt {
type: 'psbt'
base64_psbt: string
}
Why this works well for LLMs
- Keys are full English words (
blinded_output
notbout
) - Snake-case everywhere → crystal token boundaries
- Explicit
type
field → easy pattern matching in GPT / copilot code
Step | Who | Endpoint | Payload (excerpt) |
---|---|---|---|
1. Round info | Client → Coord. | GET /round |
{ denomination: "0.1", round_id: "abc123" } |
2. Input registration | Client → Coord. (POST) | /round/abc123/register-input |
RegisterInputReq |
3. Output commitment | Client → Coord. (POST) | /round/abc123/commit-output |
CommitOutputReq |
4. PSBT distribution | Coord. → Client (WS) | – | FinalPsbt |
5. Signature collection | Client signs & returns | /round/abc123/sign |
{ signed_psbt } |
6. Broadcast | Coord. → Bitcoin | – | raw tx |
All numeric amounts remain decimal strings in JSON to dodge JS Number
pitfalls.
import {
parseTxoUri,
isValidTxoUri,
formatTxoUri,
} from 'txo_parser'
function prepareInput(txoUri: string): RegisterInputReq {
if (!isValidTxoUri(txoUri)) throw new Error('Bad TXO URI')
const { amount } = parseTxoUri(txoUri)
const blinded = credentialClient.blindOutputKey()
return {
type: 'register_input',
txo_uri: txoUri,
denomination: amount.toString(),
blinded_output: blinded.request,
}
}
// Display back to user
console.log(
'Registered:',
formatTxoUri(parseTxoUri(txoUri)) // canonical form
)
// wallet_adapter_electrum.ts
import ElectrumClient from 'electrum-client'
import { formatTxoUri } from 'txo_parser'
export async function listSpendableUris(): Promise<string[]> {
const utxos = await electrum.listUnspent()
return utxos.map(u =>
formatTxoUri({
network: 'btc',
txid: u.tx_hash,
output: u.tx_pos,
amount: u.value / 1e8,
})
)
}
Same adapter signs the PSBT and returns partial sigs.
Risk | Mitigation tied to TXO URI |
---|---|
Linking inputs ↔ outputs | Use blind credential scheme; outputs never reference origin TXO URI |
URI leaks private_key | Coordinator rejects any URI that contains private_key key |
Denial-of-service spam | Coordinator checks tx_id on mempool before accepting |
packages/
txo_parser/ ← your existing lib
cj_types/ ← shared message/DTO defs
cj_client/
src/
index.ts
round.ts
cj_coordinator/
src/
api.ts
round_engine.ts
wallet_adapter_electrum/
# user flow
npm i -g cj_client wallet_adapter_electrum
cj-client discover # lists open rounds
cj-client join \
--txo $(wallet-list-utxos | fzf) \
--round abc123
Under the hood every UTXO is fed in as a single line TXO URI, keeping pipes and scripts dead simple.
- Uniform resource string (
txo:*
) means prompts don’t juggle multiple representations. - Snake_case everywhere → fewer sub-tokens, higher embedding overlap (
output_index
≈ “output” + “index”). - Topic-aligned filenames (
round_engine.ts
,wallet_adapter.ts
) boost code-completion precision. - All numeric strings are plain decimals—no scientific notation surprises.