bitgit 0.1.0
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/LICENSE +21 -0
- package/README.md +126 -0
- package/package.json +54 -0
- package/src/bsv/broadcast.ts +56 -0
- package/src/bsv/script.ts +103 -0
- package/src/bsv/tx.ts +90 -0
- package/src/bsv/utxo.ts +90 -0
- package/src/cli.ts +101 -0
- package/src/commands/init.ts +125 -0
- package/src/commands/push.ts +267 -0
- package/src/commands/register.ts +151 -0
- package/src/commands/status.ts +148 -0
- package/src/config.ts +101 -0
- package/src/db.ts +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 b0ase
|
|
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,126 @@
|
|
|
1
|
+
# bitgit
|
|
2
|
+
|
|
3
|
+
**`git push` for Bitcoin.** Inscribe content, register domains, and manage tokens on BSV.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm install -g bitgit
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bit init # scaffold .bit.yaml for your project
|
|
13
|
+
bit push # git push + inscribe changed content on BSV
|
|
14
|
+
bit register <domain> # inscribe a domain on DNS-DEX
|
|
15
|
+
bit status # show wallet, domain, token & version chain
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. Set up your project
|
|
22
|
+
cd your-project
|
|
23
|
+
bit init
|
|
24
|
+
|
|
25
|
+
# 2. Configure your BSV key
|
|
26
|
+
export BOASE_TREASURY_PRIVATE_KEY="your-wif-key"
|
|
27
|
+
|
|
28
|
+
# 3. Push content to Bitcoin
|
|
29
|
+
bit push
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`bit push` does two things:
|
|
33
|
+
1. `git push` to your remote (if there are commits to push)
|
|
34
|
+
2. Inscribes changed content on BSV (OP_RETURN with Bitcoin Schema)
|
|
35
|
+
|
|
36
|
+
Every inscription is chained — each transaction's change output feeds the next input, so you can inscribe dozens of files in one session without waiting for confirmations.
|
|
37
|
+
|
|
38
|
+
## .bit.yaml
|
|
39
|
+
|
|
40
|
+
`bit init` creates a `.bit.yaml` in your project root:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
project:
|
|
44
|
+
name: my-project
|
|
45
|
+
domain: my-project.com
|
|
46
|
+
token: MYTOKEN
|
|
47
|
+
|
|
48
|
+
wallet:
|
|
49
|
+
key_env: BOASE_TREASURY_PRIVATE_KEY
|
|
50
|
+
|
|
51
|
+
content:
|
|
52
|
+
type: blog # blog | repo | domain | custom
|
|
53
|
+
source: content/blog/ # directory to watch
|
|
54
|
+
format: bitcoin_schema # bitcoin_schema | op_return
|
|
55
|
+
protocol: my-project-blog
|
|
56
|
+
|
|
57
|
+
db:
|
|
58
|
+
supabase_url_env: NEXT_PUBLIC_SUPABASE_URL
|
|
59
|
+
supabase_key_env: SUPABASE_SERVICE_ROLE_KEY
|
|
60
|
+
version_table: blog_post_versions
|
|
61
|
+
|
|
62
|
+
dns_dex:
|
|
63
|
+
token_symbol: $my-project.com
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Inscription Formats
|
|
67
|
+
|
|
68
|
+
### Bitcoin Schema (default)
|
|
69
|
+
|
|
70
|
+
Uses the B + MAP + AIP Bitcom protocols:
|
|
71
|
+
- **B** — content storage (full markdown/JSON)
|
|
72
|
+
- **MAP** — queryable metadata (indexed by GorillaPool)
|
|
73
|
+
- **AIP** — cryptographic authorship proof (ECDSA signature)
|
|
74
|
+
|
|
75
|
+
### Simple OP_RETURN
|
|
76
|
+
|
|
77
|
+
`OP_FALSE OP_RETURN <protocol> <content-type> <payload>`
|
|
78
|
+
|
|
79
|
+
## DNS-DEX Domain Registration
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bit register kwegwong.com
|
|
83
|
+
bit register kwegwong.com --category=culture --supply=1000000000
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Inscribes a `dnsdex-domain` token on BSV and prints the DNS TXT records to add for verification.
|
|
87
|
+
|
|
88
|
+
## Broadcast Fallback Chain
|
|
89
|
+
|
|
90
|
+
Transactions are broadcast with automatic fallback:
|
|
91
|
+
1. WhatsOnChain API
|
|
92
|
+
2. GorillaPool ARC
|
|
93
|
+
3. TAAL ARC
|
|
94
|
+
|
|
95
|
+
## Dry Run
|
|
96
|
+
|
|
97
|
+
All commands support `--dry-run` to preview without broadcasting:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
bit push --dry-run
|
|
101
|
+
bit register example.com --dry-run
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Environment Variables
|
|
105
|
+
|
|
106
|
+
| Variable | Required | Description |
|
|
107
|
+
|----------|----------|-------------|
|
|
108
|
+
| `BOASE_TREASURY_PRIVATE_KEY` | Yes | BSV private key (WIF format) |
|
|
109
|
+
| `NEXT_PUBLIC_SUPABASE_URL` | Optional | Supabase URL for version chain DB |
|
|
110
|
+
| `SUPABASE_SERVICE_ROLE_KEY` | Optional | Supabase service role key |
|
|
111
|
+
|
|
112
|
+
## Lineage
|
|
113
|
+
|
|
114
|
+
`bitgit` is the evolution of [`bgit`](https://www.npmjs.com/package/bgit-cli) (v2, 2026). Same DNA — commit/push to Bitcoin — but instead of wrapping git with a payment gate, `bit` adds Bitcoin alongside git.
|
|
115
|
+
|
|
116
|
+
## Part of the PATH Protocol
|
|
117
|
+
|
|
118
|
+
- [$401](https://path401.com) — Identity
|
|
119
|
+
- [$402](https://path402.com) — Payment
|
|
120
|
+
- [$403](https://path403.com) — Conditions
|
|
121
|
+
- [DNS-DEX](https://dns-dex.com) — Domain tokenization
|
|
122
|
+
- [b0ase.com](https://b0ase.com) — Venture studio
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bitgit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "git push for Bitcoin — inscribe content, register domains, manage tokens on BSV",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bit": "./src/cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsx src/cli.ts",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"bitcoin",
|
|
15
|
+
"bsv",
|
|
16
|
+
"bitcoinsv",
|
|
17
|
+
"git",
|
|
18
|
+
"blockchain",
|
|
19
|
+
"inscription",
|
|
20
|
+
"op-return",
|
|
21
|
+
"cli",
|
|
22
|
+
"dns-dex",
|
|
23
|
+
"path402"
|
|
24
|
+
],
|
|
25
|
+
"author": "b0ase <richard@b0ase.com> (https://b0ase.com)",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/b0ase/bitgit.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/b0ase/bitgit#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/b0ase/bitgit/issues"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@bsv/sdk": "^1.1.23",
|
|
37
|
+
"@supabase/supabase-js": "^2.49.1",
|
|
38
|
+
"dotenv": "^16.4.7",
|
|
39
|
+
"yaml": "^2.7.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.12.0",
|
|
43
|
+
"tsx": "^4.19.2",
|
|
44
|
+
"typescript": "^5.7.3"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"src/",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction broadcast with multi-provider fallback
|
|
3
|
+
*
|
|
4
|
+
* Chain: WhatsOnChain → GorillaPool ARC → TAAL ARC
|
|
5
|
+
* Extracted from 8+ identical copies across b0ase.com.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const WHATSONCHAIN_API = 'https://api.whatsonchain.com/v1/bsv/main';
|
|
9
|
+
const GORILLAPOOL_ARC = 'https://arc.gorillapool.io/v1/tx';
|
|
10
|
+
const TAAL_ARC = 'https://arc.taal.com/v1/tx';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Broadcast a signed transaction to the BSV network.
|
|
14
|
+
* Tries WoC first, then GorillaPool ARC, then TAAL ARC.
|
|
15
|
+
* Always trims the returned TXID.
|
|
16
|
+
*/
|
|
17
|
+
export async function broadcast(rawTx: string): Promise<string> {
|
|
18
|
+
// 1. WhatsOnChain
|
|
19
|
+
const wocRes = await fetch(`${WHATSONCHAIN_API}/tx/raw`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify({ txhex: rawTx }),
|
|
23
|
+
});
|
|
24
|
+
if (wocRes.ok) {
|
|
25
|
+
return (await wocRes.text()).replace(/"/g, '').trim();
|
|
26
|
+
}
|
|
27
|
+
const wocErr = await wocRes.text();
|
|
28
|
+
|
|
29
|
+
// 2. GorillaPool ARC (binary)
|
|
30
|
+
const gpRes = await fetch(GORILLAPOOL_ARC, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
33
|
+
body: Buffer.from(rawTx, 'hex'),
|
|
34
|
+
});
|
|
35
|
+
if (gpRes.ok) {
|
|
36
|
+
const data = await gpRes.json();
|
|
37
|
+
return (data.txid || '').trim();
|
|
38
|
+
}
|
|
39
|
+
const gpErr = await gpRes.text();
|
|
40
|
+
|
|
41
|
+
// 3. TAAL ARC (binary)
|
|
42
|
+
const taalRes = await fetch(TAAL_ARC, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
45
|
+
body: Buffer.from(rawTx, 'hex'),
|
|
46
|
+
});
|
|
47
|
+
if (taalRes.ok) {
|
|
48
|
+
const data = await taalRes.json();
|
|
49
|
+
return (data.txid || '').trim();
|
|
50
|
+
}
|
|
51
|
+
const taalErr = await taalRes.text();
|
|
52
|
+
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Broadcast failed on all providers:\n WoC: ${wocErr}\n GorillaPool: ${gpErr}\n TAAL: ${taalErr}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OP_RETURN script builders
|
|
3
|
+
*
|
|
4
|
+
* Two formats:
|
|
5
|
+
* 1. Simple: OP_FALSE OP_RETURN <protocol> <content-type> <payload>
|
|
6
|
+
* 2. Bitcoin Schema: B + MAP + AIP (Bitcom protocols)
|
|
7
|
+
*
|
|
8
|
+
* Extracted from lib/blog-inscription.ts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { PrivateKey, Script } from '@bsv/sdk';
|
|
12
|
+
|
|
13
|
+
// Bitcom protocol addresses
|
|
14
|
+
const B_PROTOCOL = '19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAut';
|
|
15
|
+
const MAP_PROTOCOL = '1PuQa7K62MiKCtssSLKy1kh56WWU7MtUR5';
|
|
16
|
+
const AIP_PROTOCOL = '15PciHG22SNLQJXMoSUaWVi7WSqc7hCfva';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Simple OP_RETURN: protocol tag + content-type + payload
|
|
20
|
+
*/
|
|
21
|
+
export function buildOpReturn(
|
|
22
|
+
protocol: string,
|
|
23
|
+
contentType: string,
|
|
24
|
+
payload: string,
|
|
25
|
+
): Script {
|
|
26
|
+
const script = new Script();
|
|
27
|
+
script.writeOpCode(0); // OP_FALSE
|
|
28
|
+
script.writeOpCode(106); // OP_RETURN
|
|
29
|
+
script.writeBin(Buffer.from(protocol, 'utf8'));
|
|
30
|
+
script.writeBin(Buffer.from(contentType, 'utf8'));
|
|
31
|
+
script.writeBin(Buffer.from(payload, 'utf8'));
|
|
32
|
+
return script;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Bitcoin Schema OP_RETURN: B (content) + MAP (metadata) + AIP (signature)
|
|
37
|
+
*
|
|
38
|
+
* @param content - raw content (markdown, JSON, etc.)
|
|
39
|
+
* @param contentType - MIME type (e.g. 'text/markdown', 'application/json')
|
|
40
|
+
* @param mapData - key-value pairs for MAP protocol indexing
|
|
41
|
+
* @param signingKey - optional PrivateKey for AIP authorship proof
|
|
42
|
+
*/
|
|
43
|
+
export function buildBitcoinSchema(
|
|
44
|
+
content: string,
|
|
45
|
+
contentType: string,
|
|
46
|
+
mapData: Record<string, string>,
|
|
47
|
+
signingKey?: PrivateKey,
|
|
48
|
+
): Script {
|
|
49
|
+
const script = new Script();
|
|
50
|
+
const contentBytes = Buffer.from(content, 'utf8');
|
|
51
|
+
|
|
52
|
+
script.writeOpCode(0); // OP_FALSE
|
|
53
|
+
script.writeOpCode(106); // OP_RETURN
|
|
54
|
+
|
|
55
|
+
// --- B Protocol: content ---
|
|
56
|
+
script.writeBin(Buffer.from(B_PROTOCOL, 'utf8'));
|
|
57
|
+
script.writeBin(contentBytes);
|
|
58
|
+
script.writeBin(Buffer.from(contentType, 'utf8'));
|
|
59
|
+
script.writeBin(Buffer.from('utf-8', 'utf8'));
|
|
60
|
+
|
|
61
|
+
// --- Pipe separator ---
|
|
62
|
+
script.writeBin(Buffer.from('|', 'utf8'));
|
|
63
|
+
|
|
64
|
+
// --- MAP Protocol: metadata ---
|
|
65
|
+
script.writeBin(Buffer.from(MAP_PROTOCOL, 'utf8'));
|
|
66
|
+
script.writeBin(Buffer.from('SET', 'utf8'));
|
|
67
|
+
|
|
68
|
+
// Collect buffers for AIP signing payload
|
|
69
|
+
const sigParts: Buffer[] = [
|
|
70
|
+
Buffer.from(B_PROTOCOL),
|
|
71
|
+
contentBytes,
|
|
72
|
+
Buffer.from(contentType),
|
|
73
|
+
Buffer.from('utf-8'),
|
|
74
|
+
Buffer.from('|'),
|
|
75
|
+
Buffer.from(MAP_PROTOCOL),
|
|
76
|
+
Buffer.from('SET'),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const [key, value] of Object.entries(mapData)) {
|
|
80
|
+
script.writeBin(Buffer.from(key, 'utf8'));
|
|
81
|
+
script.writeBin(Buffer.from(value, 'utf8'));
|
|
82
|
+
sigParts.push(Buffer.from(key), Buffer.from(value));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- Pipe separator ---
|
|
86
|
+
script.writeBin(Buffer.from('|', 'utf8'));
|
|
87
|
+
sigParts.push(Buffer.from('|'));
|
|
88
|
+
|
|
89
|
+
// --- AIP Protocol: authorship proof ---
|
|
90
|
+
if (signingKey) {
|
|
91
|
+
const address = signingKey.toPublicKey().toAddress();
|
|
92
|
+
const signingPayload = Buffer.concat(sigParts);
|
|
93
|
+
const signature = signingKey.sign(Array.from(signingPayload));
|
|
94
|
+
const sigBase64 = signature.toDER('base64') as string;
|
|
95
|
+
|
|
96
|
+
script.writeBin(Buffer.from(AIP_PROTOCOL, 'utf8'));
|
|
97
|
+
script.writeBin(Buffer.from('BITCOIN_ECDSA', 'utf8'));
|
|
98
|
+
script.writeBin(Buffer.from(address, 'utf8'));
|
|
99
|
+
script.writeBin(Buffer.from(sigBase64, 'utf8'));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return script;
|
|
103
|
+
}
|
package/src/bsv/tx.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction building for OP_RETURN inscriptions
|
|
3
|
+
*
|
|
4
|
+
* Handles the mock-source-tx pattern and the sourceTXID overwrite bug.
|
|
5
|
+
* Extracted from 12+ identical copies across b0ase.com.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PrivateKey, Transaction, P2PKH, Script } from '@bsv/sdk';
|
|
9
|
+
import type { UTXO } from './utxo.js';
|
|
10
|
+
|
|
11
|
+
export interface InscriptionTxResult {
|
|
12
|
+
tx: Transaction;
|
|
13
|
+
txid: string;
|
|
14
|
+
fee: number;
|
|
15
|
+
changeSats: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a signed transaction with an OP_RETURN data output.
|
|
20
|
+
*
|
|
21
|
+
* @param privateKey - signing key
|
|
22
|
+
* @param utxo - input UTXO
|
|
23
|
+
* @param opReturnScript - pre-built OP_RETURN script (from script.ts)
|
|
24
|
+
* @param feeRate - sats per byte (default 0.5)
|
|
25
|
+
* @param payloadSize - estimated payload size for fee calc (if 0, uses 500 byte estimate)
|
|
26
|
+
*/
|
|
27
|
+
export async function buildInscriptionTx(opts: {
|
|
28
|
+
privateKey: PrivateKey;
|
|
29
|
+
utxo: UTXO;
|
|
30
|
+
opReturnScript: Script;
|
|
31
|
+
feeRate?: number;
|
|
32
|
+
payloadSize?: number;
|
|
33
|
+
}): Promise<InscriptionTxResult> {
|
|
34
|
+
const { privateKey, utxo, opReturnScript, feeRate = 0.5, payloadSize = 0 } = opts;
|
|
35
|
+
const address = privateKey.toPublicKey().toAddress();
|
|
36
|
+
|
|
37
|
+
const tx = new Transaction();
|
|
38
|
+
|
|
39
|
+
// Build mock source transaction for SIGHASH verification.
|
|
40
|
+
// We only need enough outputs to reach the UTXO's vout index.
|
|
41
|
+
const mockSourceTx = new Transaction();
|
|
42
|
+
for (let i = 0; i <= utxo.vout; i++) {
|
|
43
|
+
mockSourceTx.addOutput({
|
|
44
|
+
lockingScript: i === utxo.vout ? utxo.script : new Script(),
|
|
45
|
+
satoshis: i === utxo.vout ? utxo.satoshis : 1,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
tx.addInput({
|
|
50
|
+
sourceTXID: utxo.txid,
|
|
51
|
+
sourceOutputIndex: utxo.vout,
|
|
52
|
+
unlockingScriptTemplate: new P2PKH().unlock(privateKey),
|
|
53
|
+
sourceTransaction: mockSourceTx,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// CRITICAL: addInput() overwrites sourceTXID when sourceTransaction is provided.
|
|
57
|
+
// Must re-set it after addInput().
|
|
58
|
+
tx.inputs[0].sourceTXID = utxo.txid;
|
|
59
|
+
|
|
60
|
+
// OP_RETURN output (0 satoshis)
|
|
61
|
+
tx.addOutput({
|
|
62
|
+
lockingScript: opReturnScript,
|
|
63
|
+
satoshis: 0,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Fee calculation
|
|
67
|
+
const estimatedSize = (payloadSize || 500) + 200; // payload + tx overhead
|
|
68
|
+
const fee = Math.max(500, Math.ceil(estimatedSize * feeRate));
|
|
69
|
+
const changeSats = utxo.satoshis - fee;
|
|
70
|
+
|
|
71
|
+
if (changeSats < 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Insufficient sats for fee. Need ${fee}, have ${utxo.satoshis}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Change output
|
|
78
|
+
if (changeSats > 0) {
|
|
79
|
+
tx.addOutput({
|
|
80
|
+
lockingScript: new P2PKH().lock(address),
|
|
81
|
+
satoshis: changeSats,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await tx.sign();
|
|
86
|
+
|
|
87
|
+
const txid = tx.id('hex') as string;
|
|
88
|
+
|
|
89
|
+
return { tx, txid, fee, changeSats };
|
|
90
|
+
}
|
package/src/bsv/utxo.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTXO fetching from WhatsOnChain
|
|
3
|
+
*
|
|
4
|
+
* Extracted from 10+ identical copies across b0ase.com scripts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Script, P2PKH } from '@bsv/sdk';
|
|
8
|
+
|
|
9
|
+
const WHATSONCHAIN_API = 'https://api.whatsonchain.com/v1/bsv/main';
|
|
10
|
+
const FETCH_TIMEOUT_MS = 30_000;
|
|
11
|
+
|
|
12
|
+
export interface UTXO {
|
|
13
|
+
txid: string;
|
|
14
|
+
vout: number;
|
|
15
|
+
satoshis: number;
|
|
16
|
+
script: Script;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Raw shape returned by WhatsOnChain API */
|
|
20
|
+
interface WocUtxo {
|
|
21
|
+
tx_hash: string;
|
|
22
|
+
tx_pos: number;
|
|
23
|
+
value: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function fetchWithTimeout(
|
|
27
|
+
url: string,
|
|
28
|
+
options: RequestInit = {},
|
|
29
|
+
timeoutMs = FETCH_TIMEOUT_MS,
|
|
30
|
+
): Promise<Response> {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
35
|
+
clearTimeout(timeoutId);
|
|
36
|
+
return response;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
clearTimeout(timeoutId);
|
|
39
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
40
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fetch all UTXOs for an address, sorted largest-first.
|
|
48
|
+
*/
|
|
49
|
+
export async function fetchUtxos(address: string): Promise<UTXO[]> {
|
|
50
|
+
const url = `${WHATSONCHAIN_API}/address/${address}/unspent`;
|
|
51
|
+
const response = await fetchWithTimeout(url);
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error(`Failed to fetch UTXOs: ${response.statusText}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const raw: WocUtxo[] = await response.json();
|
|
57
|
+
const lockScript = new P2PKH().lock(address);
|
|
58
|
+
|
|
59
|
+
return raw
|
|
60
|
+
.sort((a, b) => b.value - a.value)
|
|
61
|
+
.map((u) => ({
|
|
62
|
+
txid: u.tx_hash,
|
|
63
|
+
vout: u.tx_pos,
|
|
64
|
+
satoshis: u.value,
|
|
65
|
+
script: lockScript,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Select a single UTXO with at least `minSats` satoshis.
|
|
71
|
+
* Optionally exclude specific txids (e.g. unconfirmed change).
|
|
72
|
+
*/
|
|
73
|
+
export async function selectUtxo(
|
|
74
|
+
address: string,
|
|
75
|
+
minSats = 1000,
|
|
76
|
+
excludeTxids: string[] = [],
|
|
77
|
+
): Promise<UTXO> {
|
|
78
|
+
const utxos = await fetchUtxos(address);
|
|
79
|
+
const eligible = utxos.filter(
|
|
80
|
+
(u) => u.satoshis >= minSats && !excludeTxids.includes(u.txid),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (eligible.length === 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`No UTXO with >= ${minSats} sats. Found ${utxos.length} total UTXOs.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return eligible[0]; // largest first
|
|
90
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* bit — Bitcoin CLI for the PATH Protocol
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bit init Scaffold .bit.yaml for a new project
|
|
7
|
+
* bit push git push + inscribe changed content on BSV
|
|
8
|
+
* bit register <domain> Inscribe a domain on DNS-DEX
|
|
9
|
+
* bit status Show Bitcoin state for this project
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as dotenv from 'dotenv';
|
|
13
|
+
import { resolve } from 'path';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
|
|
16
|
+
// Load .env.local from cwd, then .env
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
for (const envFile of ['.env.local', '.env']) {
|
|
19
|
+
const p = resolve(cwd, envFile);
|
|
20
|
+
if (existsSync(p)) {
|
|
21
|
+
dotenv.config({ path: p });
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const command = args[0];
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
switch (command) {
|
|
31
|
+
case 'init': {
|
|
32
|
+
const { init } = await import('./commands/init.js');
|
|
33
|
+
await init();
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case 'push': {
|
|
37
|
+
const { push } = await import('./commands/push.js');
|
|
38
|
+
await push(args.slice(1));
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case 'register': {
|
|
42
|
+
const domain = args[1];
|
|
43
|
+
if (!domain) {
|
|
44
|
+
console.error('Usage: bit register <domain>');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const { register } = await import('./commands/register.js');
|
|
48
|
+
await register(domain, args.slice(2));
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case 'status': {
|
|
52
|
+
const { status } = await import('./commands/status.js');
|
|
53
|
+
await status();
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case '--help':
|
|
57
|
+
case '-h':
|
|
58
|
+
case undefined: {
|
|
59
|
+
console.log(`
|
|
60
|
+
bit — Bitcoin CLI for the PATH Protocol
|
|
61
|
+
|
|
62
|
+
Commands:
|
|
63
|
+
bit init Scaffold .bit.yaml for a new project
|
|
64
|
+
bit push git push + inscribe changed content on BSV
|
|
65
|
+
bit register <domain> Inscribe a domain on DNS-DEX
|
|
66
|
+
bit status Show Bitcoin state for this project
|
|
67
|
+
|
|
68
|
+
Options:
|
|
69
|
+
--help, -h Show this help
|
|
70
|
+
--version Show version
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
bit init
|
|
74
|
+
bit push
|
|
75
|
+
bit push --dry-run
|
|
76
|
+
bit register kwegwong.com
|
|
77
|
+
bit status
|
|
78
|
+
`.trim());
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case '--version': {
|
|
82
|
+
const { readFileSync } = await import('fs');
|
|
83
|
+
const { resolve } = await import('path');
|
|
84
|
+
const { fileURLToPath } = await import('url');
|
|
85
|
+
const __dirname = resolve(fileURLToPath(import.meta.url), '..');
|
|
86
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8'));
|
|
87
|
+
console.log(pkg.version);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
default: {
|
|
91
|
+
console.error(`Unknown command: ${command}`);
|
|
92
|
+
console.error('Run `bit --help` for usage.');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main().catch((err) => {
|
|
99
|
+
console.error('Fatal:', err.message || err);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|