Compare commits
1
Commits
master
..
0e709f8547
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e709f8547 |
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
File diff suppressed because it is too large
Load Diff
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
@@ -1,271 +0,0 @@
|
|||||||
# Bitcoin Phrase Checker
|
|
||||||
|
|
||||||
A modular and extensible Go program that derives Bitcoin addresses from a BIP39 mnemonic phrase and provides comprehensive transaction history and balance analysis using public APIs.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Core Functionality
|
|
||||||
- **BIP39 Support**: Validates and processes mnemonic phrases (12/24 words) with optional passphrases
|
|
||||||
- **Multi-Path Derivation**: Automatically derives addresses from standard Bitcoin derivation paths:
|
|
||||||
- **BIP44 (Legacy)**: `m/44'/0'/0'/0` - P2PKH addresses (starting with `1...`)
|
|
||||||
- **BIP49 (SegWit)**: `m/49'/0'/0'/0` - P2SH-wrapped SegWit (starting with `3...`)
|
|
||||||
- **BIP84 (Native SegWit)**: `m/84'/0'/0'/0` - Bech32 addresses (starting with `bc1...`)
|
|
||||||
- **Comprehensive Analysis**: For each address, displays:
|
|
||||||
- Current balance
|
|
||||||
- Total received amount
|
|
||||||
- Total sent amount
|
|
||||||
- Transaction count
|
|
||||||
- Derivation path
|
|
||||||
|
|
||||||
### Transaction History
|
|
||||||
- **Chronological Display**: Shows all transactions sorted by date (YYYY-MM-DD format)
|
|
||||||
- **Running Balance**: Calculates and displays balance after each transaction
|
|
||||||
- **Transaction Types**: Identifies received, sent, and self-transfer transactions
|
|
||||||
- **Detailed Statistics**:
|
|
||||||
- Total received amount and count
|
|
||||||
- Total sent amount and count
|
|
||||||
- Transaction confirmation status
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
- **Modular Design**: Organized into separate packages for easy extension
|
|
||||||
- `types/`: Common interfaces and data structures
|
|
||||||
- `bitcoin/`: Bitcoin-specific implementation (derivation, API, analysis)
|
|
||||||
- Ready for additional blockchain support (e.g., Ethereum)
|
|
||||||
- **Testable**: Comprehensive test suite with network tests using known mnemonic
|
|
||||||
- **Public API Integration**: Uses Blockstream.info API with built-in rate limiting
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone or download the project
|
|
||||||
cd btcphrasechecker
|
|
||||||
|
|
||||||
# Download dependencies
|
|
||||||
go mod download
|
|
||||||
|
|
||||||
# Build
|
|
||||||
go build -o btcphrasechecker
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Basic Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check wallet with mnemonic only
|
|
||||||
./btcphrasechecker -mnemonic "your twelve or twenty four word mnemonic phrase here"
|
|
||||||
|
|
||||||
# With optional passphrase
|
|
||||||
./btcphrasechecker -mnemonic "your mnemonic phrase" -passphrase "your passphrase"
|
|
||||||
|
|
||||||
# Customize number of addresses to check (default: 20)
|
|
||||||
./btcphrasechecker -mnemonic "your mnemonic" -count 50
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example Output
|
|
||||||
|
|
||||||
```
|
|
||||||
Bitcoin Wallet Analysis
|
|
||||||
======================
|
|
||||||
|
|
||||||
Checking BIP44 (Legacy): m/44'/0'/0'/0 - P2PKH addresses (1...)
|
|
||||||
-----------------------------------------------------------
|
|
||||||
1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA
|
|
||||||
Path: m/44'/0'/0'/0/0
|
|
||||||
Balance: 0.00000000 BTC
|
|
||||||
Received: 0.01146203 BTC
|
|
||||||
Sent: 0.01146203 BTC
|
|
||||||
Txs: 46
|
|
||||||
|
|
||||||
Checking BIP49 (SegWit): m/49'/0'/0'/0 - P2SH-wrapped SegWit (3...)
|
|
||||||
-----------------------------------------------------------
|
|
||||||
37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf
|
|
||||||
Path: m/49'/0'/0'/0/0
|
|
||||||
Balance: 0.00000000 BTC
|
|
||||||
Received: 0.06783787 BTC
|
|
||||||
Sent: 0.06783787 BTC
|
|
||||||
Txs: 22
|
|
||||||
|
|
||||||
Checking BIP84 (Native SegWit): m/84'/0'/0'/0 - Bech32 addresses (bc1...)
|
|
||||||
-----------------------------------------------------------
|
|
||||||
bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu
|
|
||||||
Path: m/84'/0'/0'/0/0
|
|
||||||
Balance: 0.00000000 BTC
|
|
||||||
Received: 0.04021550 BTC
|
|
||||||
Sent: 0.04021550 BTC
|
|
||||||
Txs: 166
|
|
||||||
|
|
||||||
Fetching transaction history...
|
|
||||||
|
|
||||||
======================
|
|
||||||
Summary
|
|
||||||
======================
|
|
||||||
Total Balance: 0.00000000 BTC
|
|
||||||
Total Received: 0.16710427 BTC (94 transactions)
|
|
||||||
Total Sent: 0.16710427 BTC (91 transactions)
|
|
||||||
Total Transactions: 360
|
|
||||||
Active Addresses: 44
|
|
||||||
|
|
||||||
Active Addresses Summary:
|
|
||||||
-----------------------------------------------------------
|
|
||||||
1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA
|
|
||||||
Path: m/44'/0'/0'/0/0
|
|
||||||
Balance: 0.00000000 BTC
|
|
||||||
Received: 0.01146203 BTC
|
|
||||||
Sent: 0.01146203 BTC
|
|
||||||
Txs: 46
|
|
||||||
|
|
||||||
======================
|
|
||||||
Transaction History
|
|
||||||
======================
|
|
||||||
Date Type Confirmed Amount (BTC) Balance (BTC) TxID
|
|
||||||
---------------------------------------------------------------------------------------------------------------------------
|
|
||||||
2014-11-09 03:31:39 + Received Yes 0.00130000 0.00130000 d6f136091b72cb4f...
|
|
||||||
2014-11-18 07:42:59 - Sent Yes 0.00130000 0.00000000 f0ac3836d27b2914...
|
|
||||||
2015-02-13 04:23:20 + Received Yes 0.00016924 0.00016924 762ff05a44abf82e...
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
The project includes a comprehensive test suite that uses the well-known test mnemonic:
|
|
||||||
|
|
||||||
```
|
|
||||||
abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
# Run tests with verbose output
|
|
||||||
go test -v ./...
|
|
||||||
|
|
||||||
# Run specific test
|
|
||||||
go test -v ./bitcoin -run TestDeriveAddresses
|
|
||||||
|
|
||||||
# Run with network timeout (tests make real API calls)
|
|
||||||
go test ./... -timeout 5m
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Coverage
|
|
||||||
|
|
||||||
- **Derivation Tests**: Verifies correct address generation for all BIP standards
|
|
||||||
- **API Tests**: Tests Blockstream API integration with real network calls
|
|
||||||
- **Integration Tests**: End-to-end wallet analysis with the test mnemonic
|
|
||||||
- **Passphrase Tests**: Validates different seeds from different passphrases
|
|
||||||
- **Known Address Tests**: Validates against known good addresses
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
btcphrasechecker/
|
|
||||||
├── main.go # Main entry point and CLI handling
|
|
||||||
├── main_test.go # Integration tests
|
|
||||||
├── go.mod # Go module dependencies
|
|
||||||
├── README.md # This file
|
|
||||||
│
|
|
||||||
├── types/
|
|
||||||
│ └── types.go # Common types and interfaces
|
|
||||||
│
|
|
||||||
└── bitcoin/
|
|
||||||
├── derivation.go # BIP32/44/49/84 address derivation
|
|
||||||
├── derivation_test.go # Derivation tests
|
|
||||||
├── api.go # Blockstream API client
|
|
||||||
├── api_test.go # API tests
|
|
||||||
└── analyzer.go # Wallet analysis logic
|
|
||||||
```
|
|
||||||
|
|
||||||
## Extensibility
|
|
||||||
|
|
||||||
The codebase is designed to be easily extensible for additional blockchain support:
|
|
||||||
|
|
||||||
1. **Add New Chain**: Implement the `ChainAnalyzer` interface in `types/types.go`
|
|
||||||
2. **Create Package**: Add a new package (e.g., `ethereum/`) with chain-specific logic
|
|
||||||
3. **Update Main**: Add chain selection logic in `main.go`
|
|
||||||
|
|
||||||
Example interface:
|
|
||||||
```go
|
|
||||||
type ChainAnalyzer interface {
|
|
||||||
DeriveAddresses(seed []byte, count int) ([]AddressInfo, error)
|
|
||||||
GetAddressInfo(address string) (balance, txCount uint64, received, sent uint64, err error)
|
|
||||||
GetTransactions(address string) ([]Transaction, error)
|
|
||||||
GetChain() Chain
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
|
|
||||||
- **Never share your mnemonic phrase** - This tool is for authorized wallet analysis only
|
|
||||||
- **Read-only**: This tool only reads blockchain data; it cannot spend funds
|
|
||||||
- **Public API**: Uses Blockstream.info public API which may log requests
|
|
||||||
- **Privacy**: For maximum privacy, consider using a VPN or running your own Bitcoin node with API
|
|
||||||
- **Rate Limiting**: 100ms delay between requests to avoid API throttling
|
|
||||||
- **No Key Export**: Private keys are never exposed or stored
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- `github.com/tyler-smith/go-bip39` - BIP39 mnemonic implementation
|
|
||||||
- `github.com/btcsuite/btcd` - Bitcoin library suite including:
|
|
||||||
- Address generation and encoding
|
|
||||||
- BIP32 HD key derivation
|
|
||||||
- Script building for P2SH-wrapped SegWit
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go build -o btcphrasechecker
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run tests
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
# Run with coverage
|
|
||||||
go test -cover ./...
|
|
||||||
|
|
||||||
# Run with race detection
|
|
||||||
go test -race ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Format code
|
|
||||||
go fmt ./...
|
|
||||||
|
|
||||||
# Lint code (requires golangci-lint)
|
|
||||||
golangci-lint run
|
|
||||||
|
|
||||||
# Vet code
|
|
||||||
go vet ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
- [ ] Ethereum support
|
|
||||||
- [ ] Bitcoin testnet support
|
|
||||||
- [ ] Custom derivation paths
|
|
||||||
- [ ] Export to CSV/JSON
|
|
||||||
- [ ] Local Bitcoin Core RPC support
|
|
||||||
- [ ] UTXO tracking
|
|
||||||
- [ ] Multi-signature wallet support
|
|
||||||
- [ ] Hardware wallet integration
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Please ensure:
|
|
||||||
- All tests pass
|
|
||||||
- Code is formatted with `go fmt`
|
|
||||||
- New features include tests
|
|
||||||
- Security best practices are followed
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
package bitcoin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"btcphrasechecker/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Analyzer implements the ChainAnalyzer interface for Bitcoin
|
|
||||||
type Analyzer struct {
|
|
||||||
addressCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAnalyzer creates a new Bitcoin analyzer
|
|
||||||
func NewAnalyzer(addressCount int) *Analyzer {
|
|
||||||
return &Analyzer{
|
|
||||||
addressCount: addressCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeriveAddresses derives Bitcoin addresses from seed
|
|
||||||
func (a *Analyzer) DeriveAddresses(seed []byte, count int) ([]types.AddressInfo, error) {
|
|
||||||
return DeriveAddresses(seed, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAddressInfo fetches address information
|
|
||||||
func (a *Analyzer) GetAddressInfo(address string) (balance, txCount uint64, received, sent uint64, err error) {
|
|
||||||
return GetAddressInfo(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTransactions fetches transactions for an address
|
|
||||||
func (a *Analyzer) GetTransactions(address string) ([]types.Transaction, error) {
|
|
||||||
return GetTransactions(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChain returns the chain type
|
|
||||||
func (a *Analyzer) GetChain() types.Chain {
|
|
||||||
return types.ChainBitcoin
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnalyzeWallet performs comprehensive wallet analysis
|
|
||||||
func AnalyzeWallet(seed []byte, addressCount int, verbose bool) (*types.WalletSummary, error) {
|
|
||||||
summary := &types.WalletSummary{
|
|
||||||
ActiveAddresses: make([]types.AddressInfo, 0),
|
|
||||||
TransactionHistory: make([]types.TransactionDetail, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive all addresses
|
|
||||||
addresses, err := DeriveAddresses(seed, addressCount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Println("Bitcoin Wallet Analysis")
|
|
||||||
fmt.Println("======================")
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check each derivation path
|
|
||||||
pathStats := make(map[string]struct {
|
|
||||||
count int
|
|
||||||
received uint64
|
|
||||||
sent uint64
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, pathInfo := range StandardPaths {
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("Checking %s: %s - %s\n", pathInfo.Name, pathInfo.Path, pathInfo.Desc)
|
|
||||||
fmt.Println("-----------------------------------------------------------")
|
|
||||||
}
|
|
||||||
|
|
||||||
pathAddresses := filterByPath(addresses, pathInfo.Path)
|
|
||||||
|
|
||||||
for _, addr := range pathAddresses {
|
|
||||||
balance, txCount, received, sent, err := GetAddressInfo(addr.Address)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
addr.Balance = balance
|
|
||||||
addr.TxCount = int(txCount)
|
|
||||||
addr.Received = received
|
|
||||||
addr.Sent = sent
|
|
||||||
|
|
||||||
if txCount > 0 {
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf(" %s\n", addr.Address)
|
|
||||||
fmt.Printf(" Path: %s\n", addr.Path)
|
|
||||||
fmt.Printf(" Balance: %.8f BTC\n", float64(balance)/100000000.0)
|
|
||||||
fmt.Printf(" Received: %.8f BTC\n", float64(received)/100000000.0)
|
|
||||||
fmt.Printf(" Sent: %.8f BTC\n", float64(sent)/100000000.0)
|
|
||||||
fmt.Printf(" Transactions: %d\n", txCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.TotalBalance += balance
|
|
||||||
summary.TotalReceived += received
|
|
||||||
summary.TotalSent += sent
|
|
||||||
summary.TotalTxCount += int(txCount)
|
|
||||||
summary.ActiveAddresses = append(summary.ActiveAddresses, addr)
|
|
||||||
|
|
||||||
// Update path stats
|
|
||||||
ps := pathStats[pathInfo.Path]
|
|
||||||
ps.count++
|
|
||||||
ps.received += received
|
|
||||||
ps.sent += sent
|
|
||||||
pathStats[pathInfo.Path] = ps
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limiting
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return summary, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchTransactionHistory fetches and processes all transactions for active addresses
|
|
||||||
func FetchTransactionHistory(summary *types.WalletSummary) error {
|
|
||||||
var allTransactions []types.TransactionDetail
|
|
||||||
receiveTxMap := make(map[string]bool)
|
|
||||||
sendTxMap := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, addr := range summary.ActiveAddresses {
|
|
||||||
if addr.TxCount == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
txs, err := GetTransactions(addr.Address)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tx := range txs {
|
|
||||||
txType := "self"
|
|
||||||
amount := uint64(0)
|
|
||||||
|
|
||||||
if tx.Received > 0 && tx.Sent == 0 {
|
|
||||||
txType = "received"
|
|
||||||
amount = tx.Received
|
|
||||||
receiveTxMap[tx.TxID] = true
|
|
||||||
} else if tx.Sent > 0 && tx.Received == 0 {
|
|
||||||
txType = "sent"
|
|
||||||
amount = tx.Sent
|
|
||||||
sendTxMap[tx.TxID] = true
|
|
||||||
} else if tx.Received > 0 && tx.Sent > 0 {
|
|
||||||
txType = "self"
|
|
||||||
amount = tx.Received
|
|
||||||
}
|
|
||||||
|
|
||||||
detail := types.TransactionDetail{
|
|
||||||
TxID: tx.TxID,
|
|
||||||
Time: tx.Time,
|
|
||||||
BlockHeight: tx.BlockHeight,
|
|
||||||
Confirmed: tx.Confirmed,
|
|
||||||
Type: txType,
|
|
||||||
Amount: amount,
|
|
||||||
Address: addr.Address,
|
|
||||||
}
|
|
||||||
|
|
||||||
allTransactions = append(allTransactions, detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limiting
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by time (oldest first)
|
|
||||||
sort.Slice(allTransactions, func(i, j int) bool {
|
|
||||||
return allTransactions[i].Time.Before(allTransactions[j].Time)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Calculate running balance
|
|
||||||
balance := uint64(0)
|
|
||||||
for i := range allTransactions {
|
|
||||||
tx := &allTransactions[i]
|
|
||||||
if tx.Type == "received" {
|
|
||||||
balance += tx.Amount
|
|
||||||
} else if tx.Type == "sent" {
|
|
||||||
if balance >= tx.Amount {
|
|
||||||
balance -= tx.Amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tx.Balance = balance
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.TransactionHistory = allTransactions
|
|
||||||
summary.ReceiveTxCount = len(receiveTxMap)
|
|
||||||
summary.SendTxCount = len(sendTxMap)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterByPath filters addresses by derivation path prefix
|
|
||||||
func filterByPath(addresses []types.AddressInfo, pathPrefix string) []types.AddressInfo {
|
|
||||||
var filtered []types.AddressInfo
|
|
||||||
for _, addr := range addresses {
|
|
||||||
if len(addr.Path) >= len(pathPrefix) && addr.Path[:len(pathPrefix)] == pathPrefix {
|
|
||||||
filtered = append(filtered, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
package bitcoin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"btcphrasechecker/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// API base URL (using blockstream.info)
|
|
||||||
apiBaseURL = "https://blockstream.info/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BlockstreamTransaction represents a transaction from Blockstream API
|
|
||||||
type BlockstreamTransaction struct {
|
|
||||||
Txid string `json:"txid"`
|
|
||||||
Status struct {
|
|
||||||
Confirmed bool `json:"confirmed"`
|
|
||||||
BlockHeight int `json:"block_height"`
|
|
||||||
BlockTime int64 `json:"block_time"`
|
|
||||||
} `json:"status"`
|
|
||||||
Vin []struct {
|
|
||||||
Txid string `json:"txid"`
|
|
||||||
Vout int `json:"vout"`
|
|
||||||
Prevout struct {
|
|
||||||
Scriptpubkey string `json:"scriptpubkey"`
|
|
||||||
ScriptpubkeyAddress string `json:"scriptpubkey_address"`
|
|
||||||
Value uint64 `json:"value"`
|
|
||||||
} `json:"prevout"`
|
|
||||||
} `json:"vin"`
|
|
||||||
Vout []struct {
|
|
||||||
Scriptpubkey string `json:"scriptpubkey"`
|
|
||||||
ScriptpubkeyAddress string `json:"scriptpubkey_address"`
|
|
||||||
Value uint64 `json:"value"`
|
|
||||||
} `json:"vout"`
|
|
||||||
Fee uint64 `json:"fee"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAddressInfo fetches address information from Blockstream API
|
|
||||||
func GetAddressInfo(address string) (balance, txCount uint64, received, sent uint64, err error) {
|
|
||||||
url := fmt.Sprintf("%s/address/%s", apiBaseURL, address)
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, 0, 0, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("API returned status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var addrData struct {
|
|
||||||
ChainStats struct {
|
|
||||||
FundedTxoSum uint64 `json:"funded_txo_sum"`
|
|
||||||
SpentTxoSum uint64 `json:"spent_txo_sum"`
|
|
||||||
TxCount int `json:"tx_count"`
|
|
||||||
} `json:"chain_stats"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(body, &addrData); err != nil {
|
|
||||||
return 0, 0, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
balance = addrData.ChainStats.FundedTxoSum - addrData.ChainStats.SpentTxoSum
|
|
||||||
txCount = uint64(addrData.ChainStats.TxCount)
|
|
||||||
received = addrData.ChainStats.FundedTxoSum
|
|
||||||
sent = addrData.ChainStats.SpentTxoSum
|
|
||||||
|
|
||||||
return balance, txCount, received, sent, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTransactions fetches all transactions for an address
|
|
||||||
func GetTransactions(address string) ([]types.Transaction, error) {
|
|
||||||
url := fmt.Sprintf("%s/address/%s/txs", apiBaseURL, address)
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var bsTxs []BlockstreamTransaction
|
|
||||||
if err := json.Unmarshal(body, &bsTxs); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to common transaction format
|
|
||||||
var transactions []types.Transaction
|
|
||||||
for _, bsTx := range bsTxs {
|
|
||||||
tx := convertTransaction(bsTx, address)
|
|
||||||
transactions = append(transactions, tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return transactions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertTransaction converts Blockstream transaction to common format
|
|
||||||
func convertTransaction(bsTx BlockstreamTransaction, forAddress string) types.Transaction {
|
|
||||||
tx := types.Transaction{
|
|
||||||
TxID: bsTx.Txid,
|
|
||||||
BlockHeight: bsTx.Status.BlockHeight,
|
|
||||||
Confirmed: bsTx.Status.Confirmed,
|
|
||||||
Fee: bsTx.Fee,
|
|
||||||
}
|
|
||||||
|
|
||||||
if bsTx.Status.BlockTime > 0 {
|
|
||||||
tx.Time = time.Unix(bsTx.Status.BlockTime, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate received and sent amounts for this address
|
|
||||||
var received, sent uint64
|
|
||||||
addressSet := make(map[string]bool)
|
|
||||||
|
|
||||||
// Check inputs (sent from address)
|
|
||||||
for _, vin := range bsTx.Vin {
|
|
||||||
if vin.Prevout.ScriptpubkeyAddress == forAddress {
|
|
||||||
sent += vin.Prevout.Value
|
|
||||||
}
|
|
||||||
addressSet[vin.Prevout.ScriptpubkeyAddress] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check outputs (received by address)
|
|
||||||
for _, vout := range bsTx.Vout {
|
|
||||||
if vout.ScriptpubkeyAddress == forAddress {
|
|
||||||
received += vout.Value
|
|
||||||
}
|
|
||||||
addressSet[vout.ScriptpubkeyAddress] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.Received = received
|
|
||||||
tx.Sent = sent
|
|
||||||
tx.NetChange = int64(received) - int64(sent)
|
|
||||||
|
|
||||||
// Collect all unique addresses
|
|
||||||
for addr := range addressSet {
|
|
||||||
if addr != "" && addr != forAddress {
|
|
||||||
tx.Addresses = append(tx.Addresses, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package bitcoin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetAddressInfo(t *testing.T) {
|
|
||||||
// Test with a known address from the test mnemonic
|
|
||||||
testAddress := "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA"
|
|
||||||
|
|
||||||
balance, txCount, received, sent, err := GetAddressInfo(testAddress)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetAddressInfo failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This address has been used in tests, so it should have transactions
|
|
||||||
if txCount == 0 {
|
|
||||||
t.Log("Warning: Test address has no transactions. This might be expected if blockchain state changed.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Balance should equal received - sent
|
|
||||||
expectedBalance := received - sent
|
|
||||||
if balance != expectedBalance {
|
|
||||||
t.Errorf("Balance mismatch: balance=%d, received=%d, sent=%d, expected=%d",
|
|
||||||
balance, received, sent, expectedBalance)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Address: %s", testAddress)
|
|
||||||
t.Logf("Balance: %d satoshis (%.8f BTC)", balance, float64(balance)/100000000.0)
|
|
||||||
t.Logf("Received: %d satoshis", received)
|
|
||||||
t.Logf("Sent: %d satoshis", sent)
|
|
||||||
t.Logf("Transactions: %d", txCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetTransactions(t *testing.T) {
|
|
||||||
// Test with a known address from the test mnemonic
|
|
||||||
testAddress := "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
|
|
||||||
|
|
||||||
txs, err := GetTransactions(testAddress)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetTransactions failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Found %d transactions for address %s", len(txs), testAddress)
|
|
||||||
|
|
||||||
// Verify transaction structure
|
|
||||||
for i, tx := range txs {
|
|
||||||
if tx.TxID == "" {
|
|
||||||
t.Errorf("Transaction %d has empty TxID", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tx.Time.IsZero() && tx.Confirmed {
|
|
||||||
t.Errorf("Transaction %d is confirmed but has zero time", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// At least one of Received or Sent should be non-zero
|
|
||||||
if tx.Received == 0 && tx.Sent == 0 {
|
|
||||||
t.Logf("Warning: Transaction %d has zero received and sent amounts", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf(" Tx %d: %s, Received: %d, Sent: %d, NetChange: %d, Time: %s",
|
|
||||||
i, tx.TxID[:16], tx.Received, tx.Sent, tx.NetChange, tx.Time.Format("2006-01-02"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetAddressInfoInvalidAddress(t *testing.T) {
|
|
||||||
// Test with an invalid address
|
|
||||||
_, _, _, _, err := GetAddressInfo("invalid_address")
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for invalid address, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package bitcoin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"btcphrasechecker/types"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
|
||||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
|
||||||
"github.com/btcsuite/btcd/txscript"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DerivationPath represents a BIP derivation path configuration
|
|
||||||
type DerivationPath struct {
|
|
||||||
Name string
|
|
||||||
Path string
|
|
||||||
Purpose uint32
|
|
||||||
Desc string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard Bitcoin derivation paths
|
|
||||||
var StandardPaths = []DerivationPath{
|
|
||||||
{
|
|
||||||
Name: "BIP44 (Legacy)",
|
|
||||||
Path: "m/44'/0'/0'/0",
|
|
||||||
Purpose: 44,
|
|
||||||
Desc: "P2PKH addresses (1...)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "BIP49 (SegWit)",
|
|
||||||
Path: "m/49'/0'/0'/0",
|
|
||||||
Purpose: 49,
|
|
||||||
Desc: "P2SH-wrapped SegWit (3...)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "BIP84 (Native SegWit)",
|
|
||||||
Path: "m/84'/0'/0'/0",
|
|
||||||
Purpose: 84,
|
|
||||||
Desc: "Bech32 addresses (bc1...)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeriveAddresses derives Bitcoin addresses from a seed for all standard paths
|
|
||||||
func DeriveAddresses(seed []byte, addressCount int) ([]types.AddressInfo, error) {
|
|
||||||
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create master key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var allAddresses []types.AddressInfo
|
|
||||||
|
|
||||||
for _, pathInfo := range StandardPaths {
|
|
||||||
addresses, err := derivePathAddresses(masterKey, pathInfo, addressCount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
allAddresses = append(allAddresses, addresses...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return allAddresses, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// derivePathAddresses derives addresses for a specific derivation path
|
|
||||||
func derivePathAddresses(masterKey *hdkeychain.ExtendedKey, pathInfo DerivationPath, count int) ([]types.AddressInfo, error) {
|
|
||||||
var addresses []types.AddressInfo
|
|
||||||
|
|
||||||
// Derive base path: m/purpose'/coin_type'/account'/change
|
|
||||||
// For Bitcoin: coin_type = 0, account = 0, change = 0 (external addresses)
|
|
||||||
key := masterKey
|
|
||||||
key, _ = key.Derive(hdkeychain.HardenedKeyStart + pathInfo.Purpose)
|
|
||||||
key, _ = key.Derive(hdkeychain.HardenedKeyStart + 0) // coin_type = 0 for Bitcoin
|
|
||||||
key, _ = key.Derive(hdkeychain.HardenedKeyStart + 0) // account = 0
|
|
||||||
key, _ = key.Derive(0) // change = 0 (external)
|
|
||||||
|
|
||||||
// Derive individual addresses
|
|
||||||
for i := uint32(0); i < uint32(count); i++ {
|
|
||||||
childKey, err := key.Derive(i)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pubKey, err := childKey.ECPubKey()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pubKeyBytes := pubKey.SerializeCompressed()
|
|
||||||
address, err := deriveAddress(pubKeyBytes, pathInfo.Purpose)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
addresses = append(addresses, types.AddressInfo{
|
|
||||||
Address: address,
|
|
||||||
Path: fmt.Sprintf("%s/%d", pathInfo.Path, i),
|
|
||||||
Chain: types.ChainBitcoin,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return addresses, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deriveAddress creates an address from a public key based on the purpose
|
|
||||||
func deriveAddress(pubKeyBytes []byte, purpose uint32) (string, error) {
|
|
||||||
|
|
||||||
switch purpose {
|
|
||||||
case 44:
|
|
||||||
// Legacy P2PKH
|
|
||||||
addr, err := btcutil.NewAddressPubKey(pubKeyBytes, &chaincfg.MainNetParams)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return addr.EncodeAddress(), nil
|
|
||||||
|
|
||||||
case 49:
|
|
||||||
// P2SH-wrapped SegWit (BIP49)
|
|
||||||
// Create witness program for P2WPKH
|
|
||||||
pubKeyHash := btcutil.Hash160(pubKeyBytes)
|
|
||||||
witnessProgram, err := txscript.NewScriptBuilder().
|
|
||||||
AddOp(txscript.OP_0).
|
|
||||||
AddData(pubKeyHash).
|
|
||||||
Script()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// Wrap witness program in P2SH
|
|
||||||
scriptAddr, err := btcutil.NewAddressScriptHash(witnessProgram, &chaincfg.MainNetParams)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return scriptAddr.EncodeAddress(), nil
|
|
||||||
|
|
||||||
case 84:
|
|
||||||
// Native SegWit (bech32)
|
|
||||||
addr, err := btcutil.NewAddressWitnessPubKeyHash(
|
|
||||||
btcutil.Hash160(pubKeyBytes),
|
|
||||||
&chaincfg.MainNetParams,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return addr.EncodeAddress(), nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("unsupported purpose: %d", purpose)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package bitcoin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
|
||||||
"github.com/tyler-smith/go-bip39"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDeriveAddresses(t *testing.T) {
|
|
||||||
seed := bip39.NewSeed(testMnemonic, "")
|
|
||||||
|
|
||||||
addresses, err := DeriveAddresses(seed, 5)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("DeriveAddresses failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect 5 addresses per path * 3 paths = 15 addresses
|
|
||||||
expectedCount := 5 * len(StandardPaths)
|
|
||||||
if len(addresses) != expectedCount {
|
|
||||||
t.Errorf("Expected %d addresses, got %d", expectedCount, len(addresses))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test known addresses for the test mnemonic
|
|
||||||
expectedAddresses := map[string]string{
|
|
||||||
"m/44'/0'/0'/0/0": "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA",
|
|
||||||
"m/84'/0'/0'/0/0": "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu",
|
|
||||||
}
|
|
||||||
|
|
||||||
addressMap := make(map[string]string)
|
|
||||||
for _, addr := range addresses {
|
|
||||||
addressMap[addr.Path] = addr.Address
|
|
||||||
}
|
|
||||||
|
|
||||||
for path, expectedAddr := range expectedAddresses {
|
|
||||||
if actualAddr, exists := addressMap[path]; !exists {
|
|
||||||
t.Errorf("Address for path %s not found", path)
|
|
||||||
} else if actualAddr != expectedAddr {
|
|
||||||
t.Errorf("Address mismatch for path %s: expected %s, got %s", path, expectedAddr, actualAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDerivePathAddresses(t *testing.T) {
|
|
||||||
seed := bip39.NewSeed(testMnemonic, "")
|
|
||||||
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create master key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
path DerivationPath
|
|
||||||
addressIndex int
|
|
||||||
expectedAddress string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
path: StandardPaths[0], // BIP44
|
|
||||||
addressIndex: 0,
|
|
||||||
expectedAddress: "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: StandardPaths[1], // BIP49
|
|
||||||
addressIndex: 0,
|
|
||||||
expectedAddress: "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: StandardPaths[2], // BIP84
|
|
||||||
addressIndex: 0,
|
|
||||||
expectedAddress: "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.path.Name, func(t *testing.T) {
|
|
||||||
addresses, err := derivePathAddresses(masterKey, tt.path, 5)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("derivePathAddresses failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(addresses) != 5 {
|
|
||||||
t.Errorf("Expected 5 addresses, got %d", len(addresses))
|
|
||||||
}
|
|
||||||
|
|
||||||
if addresses[tt.addressIndex].Address != tt.expectedAddress {
|
|
||||||
t.Errorf("Address mismatch: expected %s, got %s",
|
|
||||||
tt.expectedAddress, addresses[tt.addressIndex].Address)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeriveAddressesWithPassphrase(t *testing.T) {
|
|
||||||
seed := bip39.NewSeed(testMnemonic, "TREZOR")
|
|
||||||
|
|
||||||
addresses, err := DeriveAddresses(seed, 1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("DeriveAddresses failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// With passphrase, addresses should be different
|
|
||||||
if len(addresses) == 0 {
|
|
||||||
t.Error("Expected addresses to be generated")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The first BIP44 address with "TREZOR" passphrase should be different
|
|
||||||
addressMap := make(map[string]string)
|
|
||||||
for _, addr := range addresses {
|
|
||||||
addressMap[addr.Path] = addr.Address
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should NOT match the address without passphrase
|
|
||||||
if addr, exists := addressMap["m/44'/0'/0'/0/0"]; exists {
|
|
||||||
if addr == "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA" {
|
|
||||||
t.Error("Address should be different with passphrase")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
module btcphrasechecker
|
|
||||||
|
|
||||||
go 1.21
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/btcsuite/btcd v0.24.0
|
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.5
|
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
|
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
|
||||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
|
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
|
||||||
golang.org/x/crypto v0.17.0 // indirect
|
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
|
||||||
)
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
|
||||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
|
||||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
|
||||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
|
||||||
github.com/btcsuite/btcd v0.24.0 h1:gL3uHE/IaFj6fcZSu03SvqPMSx7s/dPzfpG/atRwWdo=
|
|
||||||
github.com/btcsuite/btcd v0.24.0/go.mod h1:K4IDc1593s8jKXIF7yS7yCTSxrknB9z0STzc2j6XgE4=
|
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
|
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
|
||||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
|
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
|
||||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
|
|
||||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
|
||||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
|
||||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
|
||||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
|
||||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
|
||||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
|
||||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
|
||||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
|
||||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
|
||||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
|
||||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
|
||||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
|
||||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
|
||||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
|
||||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
|
||||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
|
||||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"btcphrasechecker/bitcoin"
|
|
||||||
"btcphrasechecker/types"
|
|
||||||
|
|
||||||
"github.com/tyler-smith/go-bip39"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultAddressCount = 20
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
mnemonic := flag.String("mnemonic", "", "BIP39 mnemonic phrase")
|
|
||||||
passphrase := flag.String("passphrase", "", "Optional passphrase")
|
|
||||||
addressCount := flag.Int("count", defaultAddressCount, "Number of addresses to check per derivation path")
|
|
||||||
chain := flag.String("chain", "bitcoin", "Blockchain to analyze (bitcoin)")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *mnemonic == "" {
|
|
||||||
fmt.Println("Usage: btcphrasechecker -mnemonic \"your mnemonic phrase\" [-passphrase \"optional passphrase\"] [-count 20] [-chain bitcoin]")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate mnemonic
|
|
||||||
if !bip39.IsMnemonicValid(*mnemonic) {
|
|
||||||
fmt.Println("Error: Invalid mnemonic phrase")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate seed from mnemonic
|
|
||||||
seed := bip39.NewSeed(*mnemonic, *passphrase)
|
|
||||||
|
|
||||||
// Analyze based on chain
|
|
||||||
switch types.Chain(*chain) {
|
|
||||||
case types.ChainBitcoin:
|
|
||||||
if err := analyzeBitcoin(seed, *addressCount); err != nil {
|
|
||||||
fmt.Printf("Error analyzing Bitcoin wallet: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
fmt.Printf("Error: Unsupported chain '%s'\n", *chain)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzeBitcoin(seed []byte, addressCount int) error {
|
|
||||||
// Perform wallet analysis
|
|
||||||
summary, err := bitcoin.AnalyzeWallet(seed, addressCount, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch transaction history for active addresses
|
|
||||||
if len(summary.ActiveAddresses) > 0 {
|
|
||||||
fmt.Println("Fetching transaction history...")
|
|
||||||
fmt.Println()
|
|
||||||
if err := bitcoin.FetchTransactionHistory(summary); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display summary
|
|
||||||
displaySummary(summary)
|
|
||||||
|
|
||||||
// Display transaction history
|
|
||||||
if len(summary.TransactionHistory) > 0 {
|
|
||||||
displayTransactionHistory(summary)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func displaySummary(summary *types.WalletSummary) {
|
|
||||||
fmt.Println("======================")
|
|
||||||
fmt.Println("Summary")
|
|
||||||
fmt.Println("======================")
|
|
||||||
fmt.Printf("Total Balance: %.8f BTC\n", float64(summary.TotalBalance)/100000000.0)
|
|
||||||
fmt.Printf("Total Received: %.8f BTC (%d transactions)\n",
|
|
||||||
float64(summary.TotalReceived)/100000000.0,
|
|
||||||
summary.ReceiveTxCount)
|
|
||||||
fmt.Printf("Total Sent: %.8f BTC (%d transactions)\n",
|
|
||||||
float64(summary.TotalSent)/100000000.0,
|
|
||||||
summary.SendTxCount)
|
|
||||||
fmt.Printf("Total Transactions: %d\n", summary.TotalTxCount)
|
|
||||||
fmt.Printf("Active Addresses: %d\n", len(summary.ActiveAddresses))
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
if len(summary.ActiveAddresses) > 0 {
|
|
||||||
fmt.Println("Active Addresses Summary:")
|
|
||||||
fmt.Println("-----------------------------------------------------------")
|
|
||||||
for _, addr := range summary.ActiveAddresses {
|
|
||||||
fmt.Printf(" %s\n", addr.Address)
|
|
||||||
fmt.Printf(" Path: %s\n", addr.Path)
|
|
||||||
fmt.Printf(" Balance: %.8f BTC\n", float64(addr.Balance)/100000000.0)
|
|
||||||
fmt.Printf(" Received: %.8f BTC\n", float64(addr.Received)/100000000.0)
|
|
||||||
fmt.Printf(" Sent: %.8f BTC\n", float64(addr.Sent)/100000000.0)
|
|
||||||
fmt.Printf(" Txs: %d\n", addr.TxCount)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func displayTransactionHistory(summary *types.WalletSummary) {
|
|
||||||
fmt.Println("======================")
|
|
||||||
fmt.Println("Transaction History")
|
|
||||||
fmt.Println("======================")
|
|
||||||
fmt.Printf("%-20s %-12s %-10s %-15s %-15s %s\n",
|
|
||||||
"Date", "Type", "Confirmed", "Amount (BTC)", "Balance (BTC)", "TxID")
|
|
||||||
fmt.Println("---------------------------------------------------------------------------------------------------------------------------")
|
|
||||||
|
|
||||||
for _, tx := range summary.TransactionHistory {
|
|
||||||
dateStr := tx.Time.Format("2006-01-02 15:04:05")
|
|
||||||
confirmedStr := "No"
|
|
||||||
if tx.Confirmed {
|
|
||||||
confirmedStr = "Yes"
|
|
||||||
}
|
|
||||||
|
|
||||||
typeSymbol := ""
|
|
||||||
switch tx.Type {
|
|
||||||
case "received":
|
|
||||||
typeSymbol = "+ Received"
|
|
||||||
case "sent":
|
|
||||||
typeSymbol = "- Sent"
|
|
||||||
case "self":
|
|
||||||
typeSymbol = "↔ Self"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%-20s %-12s %-10s %14.8f %14.8f %s\n",
|
|
||||||
dateStr,
|
|
||||||
typeSymbol,
|
|
||||||
confirmedStr,
|
|
||||||
float64(tx.Amount)/100000000.0,
|
|
||||||
float64(tx.Balance)/100000000.0,
|
|
||||||
tx.TxID[:16]+"...")
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"btcphrasechecker/bitcoin"
|
|
||||||
"btcphrasechecker/types"
|
|
||||||
|
|
||||||
"github.com/tyler-smith/go-bip39"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBitcoinWalletAnalysis(t *testing.T) {
|
|
||||||
seed := bip39.NewSeed(testMnemonic, "")
|
|
||||||
|
|
||||||
summary, err := bitcoin.AnalyzeWallet(seed, 20, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("AnalyzeWallet failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify summary structure
|
|
||||||
if summary == nil {
|
|
||||||
t.Fatal("Summary is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Total Balance: %.8f BTC", float64(summary.TotalBalance)/100000000.0)
|
|
||||||
t.Logf("Total Received: %.8f BTC", float64(summary.TotalReceived)/100000000.0)
|
|
||||||
t.Logf("Total Sent: %.8f BTC", float64(summary.TotalSent)/100000000.0)
|
|
||||||
t.Logf("Total Transactions: %d", summary.TotalTxCount)
|
|
||||||
t.Logf("Active Addresses: %d", len(summary.ActiveAddresses))
|
|
||||||
|
|
||||||
// Verify that received - sent = balance
|
|
||||||
expectedBalance := summary.TotalReceived - summary.TotalSent
|
|
||||||
if summary.TotalBalance != expectedBalance {
|
|
||||||
t.Errorf("Balance mismatch: balance=%d, received=%d, sent=%d, expected=%d",
|
|
||||||
summary.TotalBalance, summary.TotalReceived, summary.TotalSent, expectedBalance)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify active addresses
|
|
||||||
for i, addr := range summary.ActiveAddresses {
|
|
||||||
if addr.Address == "" {
|
|
||||||
t.Errorf("Active address %d has empty address", i)
|
|
||||||
}
|
|
||||||
if addr.Path == "" {
|
|
||||||
t.Errorf("Active address %d has empty path", i)
|
|
||||||
}
|
|
||||||
if addr.Chain != types.ChainBitcoin {
|
|
||||||
t.Errorf("Active address %d has wrong chain: %s", i, addr.Chain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify address-level balance
|
|
||||||
addrExpectedBalance := addr.Received - addr.Sent
|
|
||||||
if addr.Balance != addrExpectedBalance {
|
|
||||||
t.Errorf("Address %s balance mismatch: balance=%d, received=%d, sent=%d",
|
|
||||||
addr.Address, addr.Balance, addr.Received, addr.Sent)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf(" Address %d: %s (Path: %s)", i, addr.Address, addr.Path)
|
|
||||||
t.Logf(" Balance: %.8f BTC, Received: %.8f BTC, Sent: %.8f BTC, Txs: %d",
|
|
||||||
float64(addr.Balance)/100000000.0,
|
|
||||||
float64(addr.Received)/100000000.0,
|
|
||||||
float64(addr.Sent)/100000000.0,
|
|
||||||
addr.TxCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTransactionHistory(t *testing.T) {
|
|
||||||
seed := bip39.NewSeed(testMnemonic, "")
|
|
||||||
|
|
||||||
summary, err := bitcoin.AnalyzeWallet(seed, 20, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("AnalyzeWallet failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(summary.ActiveAddresses) == 0 {
|
|
||||||
t.Skip("No active addresses found, skipping transaction history test")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = bitcoin.FetchTransactionHistory(summary)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("FetchTransactionHistory failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Total transactions in history: %d", len(summary.TransactionHistory))
|
|
||||||
t.Logf("Receive transactions: %d", summary.ReceiveTxCount)
|
|
||||||
t.Logf("Send transactions: %d", summary.SendTxCount)
|
|
||||||
|
|
||||||
// Verify transaction history
|
|
||||||
var prevTime int64
|
|
||||||
for i, tx := range summary.TransactionHistory {
|
|
||||||
if tx.TxID == "" {
|
|
||||||
t.Errorf("Transaction %d has empty TxID", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tx.Time.IsZero() {
|
|
||||||
t.Logf("Warning: Transaction %d has zero time (might be unconfirmed)", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify chronological order
|
|
||||||
currentTime := tx.Time.Unix()
|
|
||||||
if i > 0 && currentTime < prevTime {
|
|
||||||
t.Errorf("Transactions not in chronological order at index %d", i)
|
|
||||||
}
|
|
||||||
prevTime = currentTime
|
|
||||||
|
|
||||||
// Verify transaction type
|
|
||||||
if tx.Type != "received" && tx.Type != "sent" && tx.Type != "self" {
|
|
||||||
t.Errorf("Transaction %d has invalid type: %s", i, tx.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
if i < 10 { // Log first 10 transactions
|
|
||||||
t.Logf(" Tx %d: %s", i, tx.TxID[:16])
|
|
||||||
t.Logf(" Date: %s", tx.Time.Format("2006-01-02 15:04:05"))
|
|
||||||
t.Logf(" Type: %s, Amount: %.8f BTC, Balance: %.8f BTC",
|
|
||||||
tx.Type,
|
|
||||||
float64(tx.Amount)/100000000.0,
|
|
||||||
float64(tx.Balance)/100000000.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Final balance verification is complex due to self-transactions and
|
|
||||||
// transaction ordering across multiple addresses. The running balance is calculated
|
|
||||||
// per-address chronologically, but may not match the total wallet balance due to
|
|
||||||
// timing and internal transfers.
|
|
||||||
if len(summary.TransactionHistory) > 0 {
|
|
||||||
lastTx := summary.TransactionHistory[len(summary.TransactionHistory)-1]
|
|
||||||
t.Logf("Final tx balance: %.8f BTC, Total wallet balance: %.8f BTC",
|
|
||||||
float64(lastTx.Balance)/100000000.0,
|
|
||||||
float64(summary.TotalBalance)/100000000.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKnownAddresses(t *testing.T) {
|
|
||||||
seed := bip39.NewSeed(testMnemonic, "")
|
|
||||||
|
|
||||||
addresses, err := bitcoin.DeriveAddresses(seed, 20)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("DeriveAddresses failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map of known addresses for the test mnemonic
|
|
||||||
knownAddresses := map[string]string{
|
|
||||||
"m/44'/0'/0'/0/0": "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA",
|
|
||||||
"m/44'/0'/0'/0/1": "1Ak8PffB2meyfYnbXZR9EGfLfFZVpzJvQP",
|
|
||||||
"m/49'/0'/0'/0/0": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf",
|
|
||||||
"m/84'/0'/0'/0/0": "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu",
|
|
||||||
"m/84'/0'/0'/0/1": "bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g",
|
|
||||||
}
|
|
||||||
|
|
||||||
addressMap := make(map[string]string)
|
|
||||||
for _, addr := range addresses {
|
|
||||||
addressMap[addr.Path] = addr.Address
|
|
||||||
}
|
|
||||||
|
|
||||||
for path, expectedAddr := range knownAddresses {
|
|
||||||
actualAddr, exists := addressMap[path]
|
|
||||||
if !exists {
|
|
||||||
t.Errorf("Address for path %s not found", path)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if actualAddr != expectedAddr {
|
|
||||||
t.Errorf("Address mismatch for path %s:\n expected: %s\n got: %s",
|
|
||||||
path, expectedAddr, actualAddr)
|
|
||||||
} else {
|
|
||||||
t.Logf("✓ %s = %s", path, actualAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMnemonicValidation(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
mnemonic string
|
|
||||||
valid bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid 12-word mnemonic",
|
|
||||||
mnemonic: testMnemonic,
|
|
||||||
valid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid mnemonic",
|
|
||||||
mnemonic: "invalid mnemonic phrase that should not work",
|
|
||||||
valid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty mnemonic",
|
|
||||||
mnemonic: "",
|
|
||||||
valid: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
valid := bip39.IsMnemonicValid(tt.mnemonic)
|
|
||||||
if valid != tt.valid {
|
|
||||||
t.Errorf("Expected mnemonic validity to be %v, got %v", tt.valid, valid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPassphraseSupport(t *testing.T) {
|
|
||||||
// Test that different passphrases generate different seeds
|
|
||||||
seed1 := bip39.NewSeed(testMnemonic, "")
|
|
||||||
seed2 := bip39.NewSeed(testMnemonic, "password123")
|
|
||||||
|
|
||||||
if string(seed1) == string(seed2) {
|
|
||||||
t.Error("Seeds with different passphrases should be different")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that addresses are different with different passphrases
|
|
||||||
addresses1, err := bitcoin.DeriveAddresses(seed1, 1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("DeriveAddresses failed for seed1: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
addresses2, err := bitcoin.DeriveAddresses(seed2, 1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("DeriveAddresses failed for seed2: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(addresses1) == 0 || len(addresses2) == 0 {
|
|
||||||
t.Fatal("No addresses generated")
|
|
||||||
}
|
|
||||||
|
|
||||||
if addresses1[0].Address == addresses2[0].Address {
|
|
||||||
t.Error("Addresses should be different with different passphrases")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Without passphrase: %s", addresses1[0].Address)
|
|
||||||
t.Logf("With passphrase: %s", addresses2[0].Address)
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// Chain represents a blockchain type
|
|
||||||
type Chain string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ChainBitcoin Chain = "bitcoin"
|
|
||||||
ChainEthereum Chain = "ethereum"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddressInfo contains information about a derived address
|
|
||||||
type AddressInfo struct {
|
|
||||||
Address string
|
|
||||||
Path string
|
|
||||||
Balance uint64
|
|
||||||
TxCount int
|
|
||||||
Chain Chain
|
|
||||||
Received uint64
|
|
||||||
Sent uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaction represents a blockchain transaction
|
|
||||||
type Transaction struct {
|
|
||||||
TxID string
|
|
||||||
Time time.Time
|
|
||||||
BlockHeight int
|
|
||||||
Confirmed bool
|
|
||||||
Received uint64
|
|
||||||
Sent uint64
|
|
||||||
Fee uint64
|
|
||||||
NetChange int64 // Can be negative
|
|
||||||
Addresses []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionDetail provides detailed transaction information
|
|
||||||
type TransactionDetail struct {
|
|
||||||
TxID string
|
|
||||||
Time time.Time
|
|
||||||
BlockHeight int
|
|
||||||
Confirmed bool
|
|
||||||
Type string // "received", "sent", "self"
|
|
||||||
Amount uint64
|
|
||||||
Balance uint64 // Running balance after this tx
|
|
||||||
Address string
|
|
||||||
}
|
|
||||||
|
|
||||||
// WalletSummary contains overall wallet statistics
|
|
||||||
type WalletSummary struct {
|
|
||||||
TotalBalance uint64
|
|
||||||
TotalReceived uint64
|
|
||||||
TotalSent uint64
|
|
||||||
TotalTxCount int
|
|
||||||
ReceiveTxCount int
|
|
||||||
SendTxCount int
|
|
||||||
ActiveAddresses []AddressInfo
|
|
||||||
TransactionHistory []TransactionDetail
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChainAnalyzer defines the interface for blockchain analysis
|
|
||||||
type ChainAnalyzer interface {
|
|
||||||
DeriveAddresses(seed []byte, count int) ([]AddressInfo, error)
|
|
||||||
GetAddressInfo(address string) (balance, txCount uint64, received, sent uint64, err error)
|
|
||||||
GetTransactions(address string) ([]Transaction, error)
|
|
||||||
GetChain() Chain
|
|
||||||
}
|
|
||||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
@@ -1 +0,0 @@
|
|||||||
/find-and-enflac
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
BIN := find-and-enflac
|
|
||||||
PREFIX := $(HOME)
|
|
||||||
|
|
||||||
.PHONY: default install build check test lint fmt fmt-check clean
|
|
||||||
|
|
||||||
default: install
|
|
||||||
|
|
||||||
install: build
|
|
||||||
install -d $(PREFIX)/bin
|
|
||||||
install -m 0755 $(BIN) $(PREFIX)/bin/$(BIN)
|
|
||||||
|
|
||||||
build:
|
|
||||||
go build -o $(BIN) .
|
|
||||||
|
|
||||||
check: fmt-check lint test
|
|
||||||
|
|
||||||
test:
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
lint:
|
|
||||||
go vet ./...
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
gofmt -s -w .
|
|
||||||
|
|
||||||
fmt-check:
|
|
||||||
@out="$$(gofmt -s -l .)"; \
|
|
||||||
if [ -n "$$out" ]; then \
|
|
||||||
echo "unformatted files:"; echo "$$out"; exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f $(BIN)
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Container extensions holding uncompressed PCM. Raw headerless .pcm is
|
|
||||||
// excluded on purpose: sample rate, width and channel count cannot be
|
|
||||||
// recovered from the file itself.
|
|
||||||
var pcmExts = map[string]bool{
|
|
||||||
".wav": true,
|
|
||||||
".wave": true,
|
|
||||||
".bwf": true,
|
|
||||||
".rf64": true,
|
|
||||||
".bw64": true,
|
|
||||||
".w64": true,
|
|
||||||
".aif": true,
|
|
||||||
".aiff": true,
|
|
||||||
".aifc": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// findPCM returns every regular file under root whose extension names a
|
|
||||||
// PCM container. Symlinks are not followed.
|
|
||||||
func findPCM(root string) ([]string, error) {
|
|
||||||
var found []string
|
|
||||||
|
|
||||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !d.Type().IsRegular() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if pcmExts[strings.ToLower(filepath.Ext(path))] {
|
|
||||||
found = append(found, path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(found)
|
|
||||||
return found, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type encoder struct {
|
|
||||||
threads int
|
|
||||||
dryRun bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// encodeOne compresses src, verifies the result decodes back to
|
|
||||||
// identical audio, and only then removes src.
|
|
||||||
func (e *encoder) encodeOne(ctx context.Context, src string) error {
|
|
||||||
codecName, err := probeCodec(ctx, src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var dst, codec, level string
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(codecName, "pcm_f32"), strings.HasPrefix(codecName, "pcm_f64"):
|
|
||||||
dst, codec, level = replaceExt(src, ".wv"), "wavpack", "3"
|
|
||||||
case strings.HasPrefix(codecName, "pcm_"):
|
|
||||||
dst, codec, level = replaceExt(src, ".flac"), "flac", "12"
|
|
||||||
default:
|
|
||||||
slog.Warn("skipping, not PCM", "src", src, "codec", codecName)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Lstat(dst); err == nil {
|
|
||||||
return fmt.Errorf("destination already exists: %s", dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.dryRun {
|
|
||||||
slog.Info("would encode", "src", src, "dst", dst, "codec", codec)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
srcInfo, err := os.Stat(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ffmpeg(ctx,
|
|
||||||
"-threads", fmt.Sprint(e.threads),
|
|
||||||
"-i", src,
|
|
||||||
"-map", "0:a:0",
|
|
||||||
"-c:a", codec,
|
|
||||||
"-compression_level", level,
|
|
||||||
dst,
|
|
||||||
); err != nil {
|
|
||||||
os.Remove(dst)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
srcSum, err := audioMD5(ctx, src)
|
|
||||||
if err != nil {
|
|
||||||
os.Remove(dst)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dstSum, err := audioMD5(ctx, dst)
|
|
||||||
if err != nil {
|
|
||||||
os.Remove(dst)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if srcSum != dstSum {
|
|
||||||
os.Remove(dst)
|
|
||||||
return fmt.Errorf("verification failed, original kept: %s != %s", srcSum, dstSum)
|
|
||||||
}
|
|
||||||
|
|
||||||
dstInfo, err := os.Stat(dst)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Remove(src); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("encoded",
|
|
||||||
"src", src,
|
|
||||||
"dst", dst,
|
|
||||||
"ratio", fmt.Sprintf("%.3f", float64(dstInfo.Size())/float64(srcInfo.Size())),
|
|
||||||
)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// probeCodec reports the codec of the file's first audio stream, e.g.
|
|
||||||
// pcm_s24le or pcm_f32le.
|
|
||||||
func probeCodec(ctx context.Context, path string) (string, error) {
|
|
||||||
cmd := exec.CommandContext(ctx, "ffprobe",
|
|
||||||
"-v", "error",
|
|
||||||
"-select_streams", "a:0",
|
|
||||||
"-show_entries", "stream=codec_name",
|
|
||||||
"-of", "default=nw=1:nokey=1",
|
|
||||||
path,
|
|
||||||
)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return "", fmt.Errorf("ffprobe: %w: %s", err, strings.TrimSpace(stderr.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
codec := strings.TrimSpace(stdout.String())
|
|
||||||
if codec == "" {
|
|
||||||
return "", fmt.Errorf("no audio stream")
|
|
||||||
}
|
|
||||||
return codec, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// audioMD5 hashes the decoded audio, forced to 64-bit float so that
|
|
||||||
// neither 32-bit integer nor 32-bit float samples lose anything on the
|
|
||||||
// way to the hash. Without an explicit codec the md5 muxer would
|
|
||||||
// default to pcm_s16le and hide real differences.
|
|
||||||
func audioMD5(ctx context.Context, path string) (string, error) {
|
|
||||||
cmd := exec.CommandContext(ctx, "ffmpeg",
|
|
||||||
"-nostdin", "-v", "error",
|
|
||||||
"-i", path,
|
|
||||||
"-map", "0:a:0",
|
|
||||||
"-c:a", "pcm_f64le",
|
|
||||||
"-f", "md5", "-",
|
|
||||||
)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return "", fmt.Errorf("ffmpeg md5 of %s: %w: %s", path, err, strings.TrimSpace(stderr.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
sum := strings.TrimSpace(stdout.String())
|
|
||||||
if !strings.HasPrefix(sum, "MD5=") {
|
|
||||||
return "", fmt.Errorf("unexpected md5 output for %s: %q", path, sum)
|
|
||||||
}
|
|
||||||
return strings.TrimPrefix(sum, "MD5="), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ffmpeg(ctx context.Context, args ...string) error {
|
|
||||||
full := append([]string{"-nostdin", "-v", "error"}, args...)
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "ffmpeg", full...)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("ffmpeg: %w: %s", err, strings.TrimSpace(stderr.String()))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func replaceExt(path, ext string) string {
|
|
||||||
return strings.TrimSuffix(path, filepath.Ext(path)) + ext
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module git.eeqj.de/sneak/hacks/find-and-enflac
|
|
||||||
|
|
||||||
go 1.26
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
// Command find-and-enflac walks a directory tree, finds every
|
|
||||||
// uncompressed PCM audio file, and losslessly compresses them all in
|
|
||||||
// parallel. An original is deleted only after its encoded replacement
|
|
||||||
// has been verified to decode back to bit-identical audio.
|
|
||||||
//
|
|
||||||
// Integer PCM becomes FLAC. IEEE float PCM becomes WavPack, which is
|
|
||||||
// the only common lossless codec that stores float samples exactly;
|
|
||||||
// FLAC has no float sample type at all.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
jobs := flag.Int("jobs", runtime.NumCPU(), "files to encode concurrently")
|
|
||||||
threads := flag.Int("threads", runtime.NumCPU(), "threads per ffmpeg")
|
|
||||||
dryRun := flag.Bool("n", false, "report what would be done, change nothing")
|
|
||||||
|
|
||||||
flag.Usage = func() {
|
|
||||||
fmt.Fprintf(os.Stderr, "usage: %s [flags] [directory]\n\n", os.Args[0])
|
|
||||||
fmt.Fprintf(os.Stderr, "Losslessly compress PCM audio under directory (default \".\").\n\n")
|
|
||||||
flag.PrintDefaults()
|
|
||||||
}
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if flag.NArg() > 1 {
|
|
||||||
flag.Usage()
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
root := "."
|
|
||||||
if flag.NArg() == 1 {
|
|
||||||
root = flag.Arg(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := run(root, *jobs, *threads, *dryRun); err != nil {
|
|
||||||
slog.Error("fatal", "err", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(root string, jobs, threads int, dryRun bool) error {
|
|
||||||
for _, tool := range []string{"ffmpeg", "ffprobe"} {
|
|
||||||
if _, err := exec.LookPath(tool); err != nil {
|
|
||||||
return fmt.Errorf("%s not found in PATH: %w", tool, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if jobs < 1 || threads < 1 {
|
|
||||||
return fmt.Errorf("jobs and threads must be at least 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := os.Stat(root)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !info.IsDir() {
|
|
||||||
return fmt.Errorf("not a directory: %s", root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl-C cancels in-flight ffmpeg runs; a killed encode fails
|
|
||||||
// verification, so its partial output is removed and its source
|
|
||||||
// is left alone.
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
||||||
defer stop()
|
|
||||||
|
|
||||||
srcs, err := findPCM(root)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(srcs) == 0 {
|
|
||||||
slog.Info("no PCM audio found", "root", root)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
slog.Info("found PCM audio", "files", len(srcs), "jobs", jobs, "threads", threads)
|
|
||||||
|
|
||||||
enc := &encoder{threads: threads, dryRun: dryRun}
|
|
||||||
|
|
||||||
var failed atomic.Int64
|
|
||||||
queue := make(chan string)
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for range jobs {
|
|
||||||
wg.Go(func() {
|
|
||||||
for src := range queue {
|
|
||||||
if err := enc.encodeOne(ctx, src); err != nil {
|
|
||||||
slog.Error("failed", "src", src, "err", err)
|
|
||||||
failed.Add(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, src := range srcs {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
queue <- src
|
|
||||||
}
|
|
||||||
close(queue)
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if n := failed.Load(); n > 0 {
|
|
||||||
return fmt.Errorf("%d of %d file(s) failed", n, len(srcs))
|
|
||||||
}
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Regular → Executable
Regular → Executable
Regular → Executable
@@ -1,28 +0,0 @@
|
|||||||
module flappy
|
|
||||||
|
|
||||||
go 1.25
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
|
||||||
golang.org/x/text v0.3.8 // indirect
|
|
||||||
)
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
|
||||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
||||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
|
||||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
|
||||||
-382
@@ -1,382 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Physics tuned so per-frame displacement stays around one row, which keeps
|
|
||||||
// motion smooth and the 7-row gap threadable at 30fps.
|
|
||||||
const (
|
|
||||||
fps = 30
|
|
||||||
gravity = 0.09 // rows/frame^2
|
|
||||||
jumpVel = -0.95 // rows/frame; peak rise ≈ v²/2g ≈ 5 rows over ~0.35s
|
|
||||||
maxFall = 1.15 // terminal velocity, rows/frame — never skip >1 row
|
|
||||||
pipeSpeed = 0.5 // cells/frame → 15 cells/s
|
|
||||||
pipeGap = 7
|
|
||||||
pipeSpacing = 28 // cells between pipes → one pipe ≈ every 1.9s
|
|
||||||
pipeWidth = 3
|
|
||||||
groundH = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// tickMsg advances the simulation. gen guards against stale tick chains:
|
|
||||||
// without it, restarting can leave an old chain in flight and the game
|
|
||||||
// silently runs at double speed.
|
|
||||||
type tickMsg struct{ gen int }
|
|
||||||
|
|
||||||
func tick(gen int) tea.Cmd {
|
|
||||||
return tea.Tick(time.Second/fps, func(time.Time) tea.Msg {
|
|
||||||
return tickMsg{gen: gen}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type pipe struct {
|
|
||||||
x float64
|
|
||||||
gapY int
|
|
||||||
scored bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type star struct{ x, y int }
|
|
||||||
|
|
||||||
type model struct {
|
|
||||||
termW, termH int
|
|
||||||
width int // field width in cells
|
|
||||||
height int // field height in cells (terminal minus header+footer)
|
|
||||||
birdX int
|
|
||||||
birdY float64
|
|
||||||
birdVel float64
|
|
||||||
pipes []pipe
|
|
||||||
stars []star
|
|
||||||
score int
|
|
||||||
best int
|
|
||||||
gen int // tick generation
|
|
||||||
gameOver bool
|
|
||||||
started bool
|
|
||||||
resized bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newGame(best, gen int, termW, termH int) model {
|
|
||||||
m := model{best: best, gen: gen}
|
|
||||||
if termW > 0 && termH > 0 {
|
|
||||||
m.applySize(termW, termH)
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// applySize records the terminal size, sizes the field, and rebuilds the
|
|
||||||
// static starfield. The starfield must be stable between frames — rolling
|
|
||||||
// new random speckles every render is what caused full-screen flicker.
|
|
||||||
func (m *model) applySize(w, h int) {
|
|
||||||
m.termW, m.termH = w, h
|
|
||||||
m.width = w
|
|
||||||
fieldH := h - 2 // one row header, one row footer
|
|
||||||
if fieldH < 8 {
|
|
||||||
fieldH = 8
|
|
||||||
}
|
|
||||||
m.height = fieldH
|
|
||||||
m.birdX = w / 4
|
|
||||||
if m.birdX < 4 {
|
|
||||||
m.birdX = 4
|
|
||||||
}
|
|
||||||
if !m.resized {
|
|
||||||
m.birdY = float64(m.height-groundH) / 2
|
|
||||||
m.resized = true
|
|
||||||
}
|
|
||||||
// keep the bird inside the new bounds after a resize
|
|
||||||
floor := float64(m.height - groundH - 1)
|
|
||||||
if m.birdY > floor {
|
|
||||||
m.birdY = floor
|
|
||||||
}
|
|
||||||
// static starfield, ~1 speckle per 40 sky cells
|
|
||||||
skyH := m.height - groundH
|
|
||||||
m.stars = m.stars[:0]
|
|
||||||
n := m.width * skyH / 40
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
m.stars = append(m.stars, star{x: rand.Intn(m.width), y: rand.Intn(skyH)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd { return nil }
|
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
m.applySize(msg.Width, msg.Height)
|
|
||||||
return m, nil
|
|
||||||
|
|
||||||
case tea.KeyMsg:
|
|
||||||
switch msg.String() {
|
|
||||||
case " ", "space", "enter", "up", "k":
|
|
||||||
if m.gameOver {
|
|
||||||
// restart: bump generation so any in-flight tick is ignored
|
|
||||||
nm := newGame(m.best, m.gen+1, m.termW, m.termH)
|
|
||||||
nm.started = true
|
|
||||||
nm.birdVel = jumpVel // start with a flap, like the original
|
|
||||||
return nm, tick(nm.gen)
|
|
||||||
}
|
|
||||||
if !m.started {
|
|
||||||
m.started = true
|
|
||||||
m.birdVel = jumpVel
|
|
||||||
return m, tick(m.gen)
|
|
||||||
}
|
|
||||||
m.birdVel = jumpVel
|
|
||||||
case "q", "esc", "ctrl+c":
|
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
|
|
||||||
case tickMsg:
|
|
||||||
if msg.gen != m.gen {
|
|
||||||
return m, nil // stale chain — drop it
|
|
||||||
}
|
|
||||||
if !m.started || m.gameOver {
|
|
||||||
return m, nil // stop the loop; nothing animates on end screens
|
|
||||||
}
|
|
||||||
if m.resized {
|
|
||||||
m.step()
|
|
||||||
}
|
|
||||||
return m, tick(m.gen)
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *model) step() {
|
|
||||||
m.birdVel += gravity
|
|
||||||
if m.birdVel > maxFall {
|
|
||||||
m.birdVel = maxFall
|
|
||||||
}
|
|
||||||
m.birdY += m.birdVel
|
|
||||||
|
|
||||||
// ceiling clamp
|
|
||||||
if m.birdY < 0 {
|
|
||||||
m.birdY = 0
|
|
||||||
m.birdVel = 0
|
|
||||||
}
|
|
||||||
// ground collision
|
|
||||||
floor := float64(m.height - groundH - 1)
|
|
||||||
if m.birdY >= floor {
|
|
||||||
m.birdY = floor
|
|
||||||
m.gameOver = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// advance pipes
|
|
||||||
for i := range m.pipes {
|
|
||||||
m.pipes[i].x -= pipeSpeed
|
|
||||||
}
|
|
||||||
for len(m.pipes) > 0 && m.pipes[0].x+pipeWidth < 0 {
|
|
||||||
m.pipes = m.pipes[1:]
|
|
||||||
}
|
|
||||||
// spawn pipes
|
|
||||||
usableH := m.height - groundH - pipeGap - 4
|
|
||||||
if usableH < 1 {
|
|
||||||
usableH = 1
|
|
||||||
}
|
|
||||||
if len(m.pipes) == 0 || m.pipes[len(m.pipes)-1].x < float64(m.width-pipeSpacing) {
|
|
||||||
gapY := rand.Intn(usableH) + 2
|
|
||||||
m.pipes = append(m.pipes, pipe{x: float64(m.width), gapY: gapY})
|
|
||||||
}
|
|
||||||
|
|
||||||
by := int(m.birdY)
|
|
||||||
for i := range m.pipes {
|
|
||||||
p := &m.pipes[i]
|
|
||||||
px := int(p.x)
|
|
||||||
// score once the pipe's trailing edge passes the bird
|
|
||||||
if !p.scored && px+pipeWidth <= m.birdX {
|
|
||||||
p.scored = true
|
|
||||||
m.score++
|
|
||||||
if m.score > m.best {
|
|
||||||
m.best = m.score
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// collision with pipe body
|
|
||||||
if m.birdX >= px && m.birdX < px+pipeWidth {
|
|
||||||
if by < p.gapY || by > p.gapY+pipeGap {
|
|
||||||
m.gameOver = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// styles ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
var (
|
|
||||||
birdStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("220")).Bold(true)
|
|
||||||
birdUpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("226")).Bold(true)
|
|
||||||
birdDnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true)
|
|
||||||
pipeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("34"))
|
|
||||||
pipeHi = lipgloss.NewStyle().Foreground(lipgloss.Color("46"))
|
|
||||||
capStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("36"))
|
|
||||||
skyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("33"))
|
|
||||||
groundStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("142"))
|
|
||||||
dirtStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("130"))
|
|
||||||
scoreStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("15")).Bold(true)
|
|
||||||
bestStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true)
|
|
||||||
overStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Bold(true)
|
|
||||||
hintStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
|
|
||||||
titleStyle = lipgloss.NewStyle().
|
|
||||||
Foreground(lipgloss.Color("220")).
|
|
||||||
Background(lipgloss.Color("23")).
|
|
||||||
Bold(true)
|
|
||||||
)
|
|
||||||
|
|
||||||
func birdGlyph(vel float64) (string, lipgloss.Style) {
|
|
||||||
switch {
|
|
||||||
case vel < -0.3:
|
|
||||||
return "➚", birdUpStyle
|
|
||||||
case vel > 0.5:
|
|
||||||
return "➘", birdDnStyle
|
|
||||||
default:
|
|
||||||
return "➙", birdStyle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) View() string {
|
|
||||||
if !m.resized {
|
|
||||||
return hintStyle.Render(" sizing terminal… ")
|
|
||||||
}
|
|
||||||
width, height := m.width, m.height
|
|
||||||
grid := make([][]string, height)
|
|
||||||
for i := range grid {
|
|
||||||
grid[i] = make([]string, width)
|
|
||||||
for j := range grid[i] {
|
|
||||||
grid[i][j] = " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// static sky speckles (precomputed — stable between frames)
|
|
||||||
for _, s := range m.stars {
|
|
||||||
if s.y < height-groundH && s.x < width {
|
|
||||||
grid[s.y][s.x] = skyStyle.Render("·")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pipes
|
|
||||||
for _, p := range m.pipes {
|
|
||||||
px := int(p.x)
|
|
||||||
topCap := p.gapY - 1
|
|
||||||
botCap := p.gapY + pipeGap + 1
|
|
||||||
for x := px; x < px+pipeWidth && x < width; x++ {
|
|
||||||
if x < 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for y := 0; y < height-groundH; y++ {
|
|
||||||
if y < p.gapY || y > p.gapY+pipeGap {
|
|
||||||
if y == topCap || y == botCap {
|
|
||||||
grid[y][x] = capStyle.Render("▓")
|
|
||||||
} else if x == px {
|
|
||||||
grid[y][x] = pipeHi.Render("█")
|
|
||||||
} else {
|
|
||||||
grid[y][x] = pipeStyle.Render("█")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ground
|
|
||||||
for y := height - groundH; y < height; y++ {
|
|
||||||
for x := 0; x < width; x++ {
|
|
||||||
if y == height-groundH {
|
|
||||||
grid[y][x] = groundStyle.Render("▔")
|
|
||||||
} else {
|
|
||||||
grid[y][x] = dirtStyle.Render("▓")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// bird
|
|
||||||
by := int(m.birdY)
|
|
||||||
if by >= 0 && by < height && m.birdX >= 0 && m.birdX < width {
|
|
||||||
glyph, style := birdGlyph(m.birdVel)
|
|
||||||
grid[by][m.birdX] = style.Render(glyph)
|
|
||||||
}
|
|
||||||
|
|
||||||
// overlays are written directly into the grid, cell by cell, so styled
|
|
||||||
// field content underneath can never be corrupted by string splicing.
|
|
||||||
if !m.started && !m.gameOver {
|
|
||||||
m.overlayGrid(grid, []overline{
|
|
||||||
{" FLAPPY BIRD ", titleStyle},
|
|
||||||
{},
|
|
||||||
{"press SPACE to flap", hintStyle},
|
|
||||||
})
|
|
||||||
} else if m.gameOver {
|
|
||||||
m.overlayGrid(grid, []overline{
|
|
||||||
{"✖ GAME OVER ✖", overStyle},
|
|
||||||
{},
|
|
||||||
{fmt.Sprintf("score %d • SPACE to retry", m.score), hintStyle},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// compose field
|
|
||||||
var b strings.Builder
|
|
||||||
for _, row := range grid {
|
|
||||||
for _, c := range row {
|
|
||||||
b.WriteString(c)
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
field := b.String()
|
|
||||||
|
|
||||||
header := scoreStyle.Render(fmt.Sprintf(" SCORE %d ", m.score)) +
|
|
||||||
" " +
|
|
||||||
bestStyle.Render(fmt.Sprintf("BEST %d ", m.best))
|
|
||||||
footer := hintStyle.Render(" SPACE: flap • Q: quit ")
|
|
||||||
|
|
||||||
return header + "\n" + field + footer
|
|
||||||
}
|
|
||||||
|
|
||||||
// overline is one centred line of overlay text with its style.
|
|
||||||
type overline struct {
|
|
||||||
text string
|
|
||||||
style lipgloss.Style
|
|
||||||
}
|
|
||||||
|
|
||||||
// overlayGrid writes overlay lines into the middle of the grid, one styled
|
|
||||||
// cell per rune. Empty lines clear a centred band for readability.
|
|
||||||
func (m model) overlayGrid(grid [][]string, lines []overline) {
|
|
||||||
startY := (len(grid) - len(lines)) / 2
|
|
||||||
if startY < 0 {
|
|
||||||
startY = 0
|
|
||||||
}
|
|
||||||
// widest line determines the cleared band
|
|
||||||
maxW := 0
|
|
||||||
for _, ol := range lines {
|
|
||||||
if n := len([]rune(ol.text)); n > maxW {
|
|
||||||
maxW = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, ol := range lines {
|
|
||||||
y := startY + i
|
|
||||||
if y < 0 || y >= len(grid) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// clear a band as wide as the widest line so text sits on a clean row
|
|
||||||
bandX := (m.width - maxW - 2) / 2
|
|
||||||
for j := 0; j < maxW+2; j++ {
|
|
||||||
x := bandX + j
|
|
||||||
if x >= 0 && x < m.width {
|
|
||||||
grid[y][x] = " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runes := []rune(ol.text)
|
|
||||||
startX := (m.width - len(runes)) / 2
|
|
||||||
for j, r := range runes {
|
|
||||||
x := startX + j
|
|
||||||
if x < 0 || x >= m.width {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
grid[y][x] = ol.style.Render(string(r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
p := tea.NewProgram(newGame(0, 0, 0, 0), tea.WithAltScreen())
|
|
||||||
if _, err := p.Run(); err != nil {
|
|
||||||
fmt.Println("error running game:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Regular → Executable
Regular → Executable
@@ -1,168 +0,0 @@
|
|||||||
// Command geolocate estimates the machine's physical location by scanning
|
|
||||||
// nearby Wi-Fi access points and submitting their BSSIDs to a public
|
|
||||||
// Wi-Fi-to-location service, then records the result under ~/.data/location.
|
|
||||||
//
|
|
||||||
// This is a macOS port of the original Linux script. Two things changed since
|
|
||||||
// the original was written:
|
|
||||||
//
|
|
||||||
// - The original scanned access points with nmcli (NetworkManager). macOS
|
|
||||||
// has no equivalent, and Apple removed the `airport` CLI in macOS 14.4.
|
|
||||||
// On macOS 26, CoreWLAN only returns real BSSIDs to a Developer-ID-signed,
|
|
||||||
// notarized binary that the user has granted Location Services access. We
|
|
||||||
// get that via the macwifi package, which ships such a signed helper.
|
|
||||||
//
|
|
||||||
// - The original queried Mozilla Location Services, which shut down in 2024.
|
|
||||||
// We use BeaconDB instead: a community-run successor that speaks the same
|
|
||||||
// Ichnaea API (identical request/response shape).
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jaisonerick/macwifi"
|
|
||||||
)
|
|
||||||
|
|
||||||
// geolocateURL is BeaconDB's Ichnaea-compatible geolocate endpoint, the
|
|
||||||
// drop-in replacement for the defunct location.services.mozilla.com.
|
|
||||||
const geolocateURL = "https://api.beacondb.net/v1/geolocate"
|
|
||||||
|
|
||||||
// accessPoint is one observed Wi-Fi access point in the Ichnaea request
|
|
||||||
// schema. Only macAddress is required; signal strength and channel improve
|
|
||||||
// the position estimate when available.
|
|
||||||
type accessPoint struct {
|
|
||||||
MacAddress string `json:"macAddress"`
|
|
||||||
SignalStrength int `json:"signalStrength,omitempty"`
|
|
||||||
Channel int `json:"channel,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type geolocateRequest struct {
|
|
||||||
WifiAccessPoints []accessPoint `json:"wifiAccessPoints,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if err := run(); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, "geolocate:", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func run() error {
|
|
||||||
ts := time.Now().Unix()
|
|
||||||
|
|
||||||
// The first scan triggers macOS's Location Services prompt for the
|
|
||||||
// bundled helper; allow generous time for a user to approve it.
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Wi-Fi scanning requires Location Services. If it is disabled (or the
|
|
||||||
// scan otherwise fails), fall back to IP-based geolocation rather than
|
|
||||||
// aborting — matching the original, which posted an empty request when
|
|
||||||
// it couldn't find enough access points.
|
|
||||||
aps, err := listAccessPoints(ctx)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr,
|
|
||||||
"geolocate: wifi scan unavailable (%v); falling back to IP geolocation\n", err)
|
|
||||||
aps = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
loc, err := geolocate(aps)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
loc["timestamp"] = ts
|
|
||||||
|
|
||||||
out, err := json.Marshal(loc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dir := filepath.Join(home, ".data", "location")
|
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writeFile(filepath.Join(dir, "latest.json"), out); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return writeFile(filepath.Join(dir, fmt.Sprintf("%d.json", ts)), out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeFile(path string, content []byte) error {
|
|
||||||
return os.WriteFile(path, content, 0o644)
|
|
||||||
}
|
|
||||||
|
|
||||||
// listAccessPoints scans nearby Wi-Fi networks via CoreWLAN (through the
|
|
||||||
// macwifi helper) and returns their BSSIDs. It returns nil if fewer than two
|
|
||||||
// usable access points are found, mirroring the original script: the
|
|
||||||
// geolocation service needs at least two observations to triangulate.
|
|
||||||
func listAccessPoints(ctx context.Context) ([]accessPoint, error) {
|
|
||||||
networks, err := macwifi.Scan(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("scanning wifi: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
var aps []accessPoint
|
|
||||||
for _, n := range networks {
|
|
||||||
// BSSID is empty when Location Services access has not been
|
|
||||||
// granted, or for saved-but-not-visible networks.
|
|
||||||
if n.BSSID == "" || seen[n.BSSID] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[n.BSSID] = true
|
|
||||||
aps = append(aps, accessPoint{
|
|
||||||
MacAddress: n.BSSID,
|
|
||||||
SignalStrength: n.RSSI,
|
|
||||||
Channel: n.Channel,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(aps) < 2 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return aps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// geolocate posts the observed access points to BeaconDB and returns the
|
|
||||||
// decoded response (e.g. {"location":{"lat":..,"lng":..},"accuracy":..}).
|
|
||||||
// With no access points it degrades to a GeoIP estimate, matching the
|
|
||||||
// original behaviour of posting an empty request.
|
|
||||||
func geolocate(aps []accessPoint) (map[string]any, error) {
|
|
||||||
body, err := json.Marshal(geolocateRequest{WifiAccessPoints: aps})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.Post(geolocateURL, "application/json", bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
data, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("geolocation service returned %s: %s",
|
|
||||||
resp.Status, bytes.TrimSpace(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
var result map[string]any
|
|
||||||
if err := json.Unmarshal(data, &result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
Regular → Executable
@@ -1,5 +0,0 @@
|
|||||||
module sneak.berlin/go/geolocate
|
|
||||||
|
|
||||||
go 1.26.1
|
|
||||||
|
|
||||||
require github.com/jaisonerick/macwifi v1.0.0
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
github.com/jaisonerick/macwifi v1.0.0 h1:Jy86C7BBR/J7u1cxFWOWi+YlwYjiMMjYLUt6RGFD1Mc=
|
|
||||||
github.com/jaisonerick/macwifi v1.0.0/go.mod h1:AtCf/s2Y4izU7IYcBSPoGe2lp2Oj53T7rz9D08uqSRM=
|
|
||||||
Regular → Executable
Regular → Executable
Regular → Executable
@@ -0,0 +1,82 @@
|
|||||||
|
JUNKFILES = .bash_history .irb_history .pip .ScanSnap .nbems .fldigi .cpan
|
||||||
|
JUNKFILES += .gdb_history .mysql_history .sqlite_history
|
||||||
|
BREWPACKAGES := mosh duplicity pv offlineimap wget nmap tor torsocks
|
||||||
|
YYYYMM := $(shell date +%Y-%m)
|
||||||
|
|
||||||
|
HACKSREPO := ~/.paths/sneak-scratch/dev/hacks/
|
||||||
|
|
||||||
|
NO_COLOR = \033[0m
|
||||||
|
O1_COLOR = \033[0;01m
|
||||||
|
O2_COLOR = \033[32;01m
|
||||||
|
|
||||||
|
PREFIX = "$(O2_COLOR)==>$(O1_COLOR)"
|
||||||
|
SUFFIX = "$(NO_COLOR)"
|
||||||
|
|
||||||
|
default: routine
|
||||||
|
|
||||||
|
# FIXME make this do a local imap download too
|
||||||
|
routine: clean databackup
|
||||||
|
|
||||||
|
backup: clean mailoffsite databackup
|
||||||
|
|
||||||
|
dvbackup:
|
||||||
|
@echo $(PREFIX) $@ $(SUFFIX)
|
||||||
|
cd ~/Documents/datavibe/backup && make
|
||||||
|
|
||||||
|
imapbackup:
|
||||||
|
offlineimap
|
||||||
|
rsync -e "ssh -o Compression=no -x" \
|
||||||
|
-avPhzy --delete-after sneak@datavibe.net:.maildir/ \
|
||||||
|
$(HOME)/Documents/Archival/mail/sneak.datavibe.net.maildir/
|
||||||
|
|
||||||
|
mailoffsite: imapbackup
|
||||||
|
rsync -e "ssh -o Compression=no -x" \
|
||||||
|
-avPhzy --delete-after $(HOME)/Documents/Archival/mail/ \
|
||||||
|
sneak@datavibe.net:.mailbackup/
|
||||||
|
tar -xvf $(HOME)/Documents/Archival/mail/jp.eeqj.com | gzip > \
|
||||||
|
$(HOME)/Documents/Dropbox/eeqj/archives/mail.tgz.new && \
|
||||||
|
mv $(HOME)/Documents/Dropbox/eeqj/archives/mail.tgz.new \
|
||||||
|
$(HOME)/Documents/Dropbox/eeqj/archives/mail.tgz
|
||||||
|
|
||||||
|
databackup:
|
||||||
|
~/dev/hacks/bin/backup.command
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
-mkdir -p $(HOME)/Documents/$(YYYYMM)
|
||||||
|
-mv $(HOME)/Desktop/* $(HOME)/Documents/$(YYYYMM)
|
||||||
|
|
||||||
|
clean: cleanup
|
||||||
|
@echo $(PREFIX) $@ $(SUFFIX)
|
||||||
|
@-rm -rf ~/.tmp/*
|
||||||
|
@-rm -rf ~/.Trash/*
|
||||||
|
@-rm -rf ~/Library/Google
|
||||||
|
@-rm -rf ~/Library/ApplicationSupport/Google/RLZ
|
||||||
|
@-rm -rf $(JUNKFILES)
|
||||||
|
|
||||||
|
size:
|
||||||
|
du -sh $(HOME)
|
||||||
|
|
||||||
|
lifeboat:
|
||||||
|
mkdir -p $(HOME)/tmp/lifeboat.$(YYYYMM)
|
||||||
|
rsync -avP --exclude='*.pkg' $(HOME)/Documents/Secure/ \
|
||||||
|
$(HOME)/tmp/lifeboat.$(YYYYMM)/Secure/
|
||||||
|
rsync -avP $(HOME)/Library/ApplicationSupport/Bitcoin/wallet.dat \
|
||||||
|
$(HOME)/tmp/lifeboat.$(YYYYMM)/wallet.dat
|
||||||
|
tar -c $(HOME)/tmp/lifeboat.$(YYYYMM) | bzip2 | \
|
||||||
|
gpg --symmetric -a -o $(HOME)/lifeboat.$(YYYYMM).gpg
|
||||||
|
rm -rf $(HOME)/tmp/lifeboat.$(YYYYMM)
|
||||||
|
cp $(HOME)/lifeboat.$(YYYYMM).gpg \
|
||||||
|
$(HOME)/dev/eeqjcdn/sneak.datavibe.net/lifeboat/lifeboat.gpg
|
||||||
|
cd $(HOME)/dev/eeqjcdn && make
|
||||||
|
mv $(HOME)/lifeboat.$(YYYYMM).gpg $(HOME)/Documents/Dropbox/Backups/
|
||||||
|
|
||||||
|
verify:
|
||||||
|
duplicity verify --exclude-globbing-filelist \
|
||||||
|
$(HOME)/.local/etc/duplicity.exclude \
|
||||||
|
file:///Volumes/EXTUSB01/dup/ ~
|
||||||
|
|
||||||
|
remotebackup:
|
||||||
|
RBACKUPDEST="scp://jp.eeqj.de/backup" $(HOME)/.local/bin/backup.command
|
||||||
|
|
||||||
|
update:
|
||||||
|
ln -s $(HACKSREPO)/homedir.makefile/Makefile $(HOME)/Makefile
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
DEST="/Volumes/video"
|
||||||
|
D="root@las1"
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
if [[ ! -d "$DEST/2022" ]]; then
|
||||||
|
echo "wrong box" > /dev/stderr
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
SRC="$1"
|
||||||
|
cd "$SRC"
|
||||||
|
for FILE in * ; do
|
||||||
|
YYYY="$(echo "$FILE" | colrm 5)"
|
||||||
|
MM="$(echo "$FILE" | colrm 1 4 | colrm 3)"
|
||||||
|
DD="$(echo "$FILE" | colrm 1 6 | colrm 3)"
|
||||||
|
echo "ssh $D -- mkdir -p /storage/video/$YYYY/$YYYY-$MM/$YYYY-$MM-$DD"
|
||||||
|
echo "rsync -acvvvP \"$SRC/$FILE\" $D:/storage/video/$YYYY/$YYYY-$MM/$YYYY-$MM-$DD/"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main $*
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
PAR := 50
|
|
||||||
|
|
||||||
default: dl
|
|
||||||
|
|
||||||
dl:
|
|
||||||
rclone copy \
|
|
||||||
--progress \
|
|
||||||
--transfers $(PAR) \
|
|
||||||
--stats-unit bits \
|
|
||||||
--retries 10 \
|
|
||||||
--retries-sleep 60s \
|
|
||||||
--checkers $(PAR) \
|
|
||||||
--fast-list \
|
|
||||||
--s3-list-chunk 5000 \
|
|
||||||
--s3-disable-http2 \
|
|
||||||
--s3-sdk-log-mode Request,Response \
|
|
||||||
--no-traverse \
|
|
||||||
--check-first \
|
|
||||||
-vv --log-file=rclone-log-202505.txt \
|
|
||||||
mastodon-nbg1-s3:mastodon/ las1stor1-files:/srv/berlin.sneak.fs.mastodon-media/mastodon-bucket/
|
|
||||||
|
|
||||||
downloadold:
|
|
||||||
rclone copy \
|
|
||||||
--progress \
|
|
||||||
--transfers $(PAR) \
|
|
||||||
--stats-unit bits \
|
|
||||||
--retries 10 \
|
|
||||||
--retries-sleep 60s \
|
|
||||||
--fast-list \
|
|
||||||
--checkers=$(PAR) \
|
|
||||||
--check-first \
|
|
||||||
-vv --log-file=rclone-log.txt \
|
|
||||||
mastodon-nbg1-s3:mastodon/ ./mastodon-bucket/
|
|
||||||
|
|
||||||
copy:
|
|
||||||
rclone copy \
|
|
||||||
-v -v -v \
|
|
||||||
--progress \
|
|
||||||
--transfers $(PAR) \
|
|
||||||
--stats-unit bits \
|
|
||||||
--retries 10 \
|
|
||||||
--retries-sleep 60s \
|
|
||||||
--fast-list \
|
|
||||||
--checkers $(PAR) \
|
|
||||||
--check-first \
|
|
||||||
--log-file=rclone-copy-las1stor1.txt \
|
|
||||||
./ \
|
|
||||||
las1stor1-files:/srv/berlin.sneak.fs.mastodon-media/
|
|
||||||
Regular → Executable
Regular → Executable
Regular → Executable
@@ -1,8 +0,0 @@
|
|||||||
from serial import Serial
|
|
||||||
from pynmeagps import NMEAReader
|
|
||||||
with Serial('/dev/tty.usbmodem314101', 9600, timeout=3) as stream:
|
|
||||||
nmr = NMEAReader(stream)
|
|
||||||
raw_data, parsed_data = nmr.read()
|
|
||||||
if parsed_data is not None:
|
|
||||||
print(parsed_data)
|
|
||||||
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
"""
|
|
||||||
nmeapoller.py
|
|
||||||
|
|
||||||
This example illustrates how to read, write and display NMEA messages
|
|
||||||
"concurrently" using threads and queues. This represents a useful
|
|
||||||
generic pattern for many end user applications.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
python3 nmeapoller.py port=/dev/ttyACM0 baudrate=38400 timeout=3
|
|
||||||
|
|
||||||
It implements two threads which run concurrently:
|
|
||||||
1) an I/O thread which continuously reads NMEA data from the
|
|
||||||
receiver and sends any queued outbound command or poll messages.
|
|
||||||
2) a process thread which processes parsed NMEA data - in this example
|
|
||||||
it simply prints the parsed data to the terminal.
|
|
||||||
NMEA data is passed between threads using queues.
|
|
||||||
|
|
||||||
Press CTRL-C to terminate.
|
|
||||||
|
|
||||||
FYI: Since Python implements a Global Interpreter Lock (GIL),
|
|
||||||
threads are not strictly concurrent, though this is of minor
|
|
||||||
practical consequence here.
|
|
||||||
|
|
||||||
Created on 07 Aug 2021
|
|
||||||
|
|
||||||
:author: semuadmin
|
|
||||||
:copyright: SEMU Consulting © 2021
|
|
||||||
:license: BSD 3-Clause
|
|
||||||
"""
|
|
||||||
|
|
||||||
from queue import Queue
|
|
||||||
from sys import argv
|
|
||||||
from threading import Event, Thread
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
from serial import Serial
|
|
||||||
|
|
||||||
from pynmeagps import NMEA_MSGIDS, POLL, NMEAMessage, NMEAReader
|
|
||||||
|
|
||||||
|
|
||||||
def io_data(
|
|
||||||
stream: object,
|
|
||||||
nmr: NMEAReader,
|
|
||||||
readqueue: Queue,
|
|
||||||
sendqueue: Queue,
|
|
||||||
stop: Event,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
THREADED
|
|
||||||
Read and parse inbound NMEA data and place
|
|
||||||
raw and parsed data on queue.
|
|
||||||
|
|
||||||
Send any queued outbound messages to receiver.
|
|
||||||
"""
|
|
||||||
# pylint: disable=broad-exception-caught
|
|
||||||
|
|
||||||
while not stop.is_set():
|
|
||||||
try:
|
|
||||||
if stream.in_waiting:
|
|
||||||
(raw_data, parsed_data) = nmr.read()
|
|
||||||
if parsed_data:
|
|
||||||
readqueue.put((raw_data, parsed_data))
|
|
||||||
|
|
||||||
if not sendqueue.empty():
|
|
||||||
data = sendqueue.get(False)
|
|
||||||
if data is not None:
|
|
||||||
nmr.datastream.write(data.serialize())
|
|
||||||
sendqueue.task_done()
|
|
||||||
|
|
||||||
except Exception as err:
|
|
||||||
print(f"\n\nSomething went wrong {err}\n\n")
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
def process_data(queue: Queue, stop: Event):
|
|
||||||
"""
|
|
||||||
THREADED
|
|
||||||
Get NMEA data from queue and display.
|
|
||||||
"""
|
|
||||||
|
|
||||||
while not stop.is_set():
|
|
||||||
if queue.empty() is False:
|
|
||||||
(_, parsed) = queue.get()
|
|
||||||
print(parsed)
|
|
||||||
queue.task_done()
|
|
||||||
|
|
||||||
|
|
||||||
def main(**kwargs):
|
|
||||||
"""
|
|
||||||
Main routine.
|
|
||||||
"""
|
|
||||||
|
|
||||||
port = kwargs.get("serport", "/dev/ttyACM0")
|
|
||||||
baudrate = int(kwargs.get("baudrate", 38400))
|
|
||||||
timeout = float(kwargs.get("timeout", 3))
|
|
||||||
|
|
||||||
with Serial(port, baudrate, timeout=timeout) as serial_stream:
|
|
||||||
nmeareader = NMEAReader(serial_stream)
|
|
||||||
|
|
||||||
read_queue = Queue()
|
|
||||||
send_queue = Queue()
|
|
||||||
stop_event = Event()
|
|
||||||
|
|
||||||
io_thread = Thread(
|
|
||||||
target=io_data,
|
|
||||||
args=(
|
|
||||||
serial_stream,
|
|
||||||
nmeareader,
|
|
||||||
read_queue,
|
|
||||||
send_queue,
|
|
||||||
stop_event,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
process_thread = Thread(
|
|
||||||
target=process_data,
|
|
||||||
args=(
|
|
||||||
read_queue,
|
|
||||||
stop_event,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\nStarting handler threads. Press Ctrl-C to terminate...")
|
|
||||||
io_thread.start()
|
|
||||||
process_thread.start()
|
|
||||||
|
|
||||||
# loop until user presses Ctrl-C
|
|
||||||
while not stop_event.is_set():
|
|
||||||
try:
|
|
||||||
# DO STUFF IN THE BACKGROUND...
|
|
||||||
# Poll for each NMEA sentence type.
|
|
||||||
# NB: Your receiver may not support all types. It will return a
|
|
||||||
# GNTXT "NMEA unknown msg" response for any types it doesn't support.
|
|
||||||
for msgid in NMEA_MSGIDS:
|
|
||||||
print(
|
|
||||||
f"\nSending a GNQ message to poll for an {msgid} response...\n"
|
|
||||||
)
|
|
||||||
msg = NMEAMessage("EI", "GNQ", POLL, msgId=msgid)
|
|
||||||
send_queue.put(msg)
|
|
||||||
sleep(1)
|
|
||||||
sleep(3)
|
|
||||||
stop_event.set()
|
|
||||||
|
|
||||||
except KeyboardInterrupt: # capture Ctrl-C
|
|
||||||
print("\n\nTerminated by user.")
|
|
||||||
stop_event.set()
|
|
||||||
|
|
||||||
print("\nStop signal set. Waiting for threads to complete...")
|
|
||||||
io_thread.join()
|
|
||||||
process_thread.join()
|
|
||||||
print("\nProcessing complete")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
main(**dict(arg.split("=") for arg in argv[1:]))
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 345678901234567892123456789312345678941234567895123456789612345678971234567898
|
|
||||||
|
|
||||||
# mac homedir backup script (rclone edition).
|
|
||||||
# by jeffrey paul <sneak@datavibe.net>
|
|
||||||
# 5539 AD00 DE4C 42F3 AFE1 1575 0524 43F4 DF2A 55C2
|
|
||||||
# see README
|
|
||||||
|
|
||||||
# $RCBACKUPDEST is used as an rclone destination (e.g. "remote:bucket/path"
|
|
||||||
# or a local path). It can be overridden via the environment.
|
|
||||||
|
|
||||||
NOW="`date +%Y%m%d.%H%M%S`"
|
|
||||||
|
|
||||||
RCBACKUPDEST=${RCBACKUPDEST:-"ber1ds1-encrypted-backups:/$(hostname -s).backup"}
|
|
||||||
|
|
||||||
RCLONE="rclone"
|
|
||||||
|
|
||||||
OPTS=""
|
|
||||||
|
|
||||||
# -avP -> archive/verbose/progress. rclone preserves mod-times by default;
|
|
||||||
# --links stores symlinks, --metadata preserves perms/ownership where the
|
|
||||||
# destination backend supports it.
|
|
||||||
OPTS+=" --verbose --progress"
|
|
||||||
OPTS+=" --links --metadata"
|
|
||||||
# --delete-excluded --delete-after -> remove excluded and extraneous files
|
|
||||||
# from the destination after the transfer completes.
|
|
||||||
OPTS+=" --delete-excluded --delete-after"
|
|
||||||
OPTS+=" --progress --transfers 25 --stats-unit bits --retries 10"
|
|
||||||
OPTS+=" --check-first"
|
|
||||||
OPTS+=" --max-backlog 10000000000"
|
|
||||||
OPTS+=" --order-by modtime,mixed,10"
|
|
||||||
|
|
||||||
RE=""
|
|
||||||
|
|
||||||
RE+=" --exclude=.bundle/"
|
|
||||||
RE+=" --exclude=.cache/"
|
|
||||||
RE+=" --exclude=/.cpan/build/"
|
|
||||||
RE+=" --exclude=/.cpan/sources/"
|
|
||||||
RE+=" --exclude=/.gradle/caches/"
|
|
||||||
RE+=" --exclude=/.docker/"
|
|
||||||
RE+=" --exclude=/.local/share/containers/podman/machine/"
|
|
||||||
RE+=" --exclude=/.dropbox/"
|
|
||||||
RE+=" --exclude=/.minikube/cache/"
|
|
||||||
RE+=" --exclude=/Applications/Fortnite/"
|
|
||||||
RE+=" --exclude=/Documents/Dropbox/.dropbox.cache/"
|
|
||||||
RE+=" --exclude=/Documents/Steam?Content/"
|
|
||||||
RE+=" --exclude=/Library/Application?Support/Ableton/"
|
|
||||||
RE+=" --exclude=/Library/Application?Support/Adobe/Adobe?Device?Central?CS4/"
|
|
||||||
RE+=" --exclude=/Library/Application?Support/CrossOver?Games/"
|
|
||||||
RE+=" --exclude=/Library/Application?Support/Evernote/" # syncs to cloud
|
|
||||||
RE+=" --exclude=/Library/Application?Support/InsomniaX/"
|
|
||||||
RE+=" --exclude=/Library/Application?Support/MobileSync/"
|
|
||||||
RE+=" --exclude=/Library/Application?Support/SecondLife/cache/"
|
|
||||||
RE+=" --exclude=/Library/Application?Support/Steam/SteamApps/"
|
|
||||||
RE+=" --exclude=/Library/Application?Support/SyncServices/"
|
|
||||||
RE+=" --exclude=/Library/Application?Support/protonmail/bridge/cache"
|
|
||||||
RE+=' --exclude=/Library/Application?Support/Syncthing/index-*'
|
|
||||||
RE+=" --exclude=/Library/Metadata"
|
|
||||||
RE+=" --exclude=/Library/Caches/"
|
|
||||||
RE+=" --exclude=/Library/Containers/com.docker.docker/"
|
|
||||||
RE+=" --exclude=/Library/Group?Containers/group.com.apple.secure-control-center-preferences"
|
|
||||||
RE+=" --exclude=/Library/Cookies/"
|
|
||||||
RE+=" --exclude=/Library/Developer/"
|
|
||||||
RE+=" --exclude=/Library/Google/GoogleSoftwareUpdate/"
|
|
||||||
RE+=" --exclude=/Library/Homebrew/"
|
|
||||||
RE+=" --exclude=/Library/Logs/"
|
|
||||||
RE+=" --exclude=/Library/Mail/" # keep your mail on the server!
|
|
||||||
RE+=" --exclude=/Library/Mail?Downloads/"
|
|
||||||
RE+=" --exclude=/Library/Parallels/"
|
|
||||||
RE+=" --exclude=/Library/Suggestions/"
|
|
||||||
RE+=" --exclude=/Library/Preferences/Macromedia/Flash?Player/"
|
|
||||||
RE+=" --exclude=/Library/Preferences/SDMHelpData/"
|
|
||||||
RE+=" --exclude=/Library/PubSub/"
|
|
||||||
RE+=" --exclude=/Library/Safari/"
|
|
||||||
RE+=" --exclude=/Library/Safari/HistoryIndex.sk"
|
|
||||||
RE+=" --exclude=/Library/Syncthing/folders/" # syncthing is its own backup
|
|
||||||
RE+=" --exclude=/Library/VoiceTrigger/SAT"
|
|
||||||
RE+=" --exclude=/Library/iTunes/iPad?Software?Updates/"
|
|
||||||
RE+=" --exclude=/Library/iTunes/iPhone?Software?Updates/"
|
|
||||||
RE+=" --exclude=/Movies/CacheClip/"
|
|
||||||
RE+=" --exclude=/Movies/ProxyMedia/"
|
|
||||||
RE+=" --exclude=/Music/iTunes/Album?Artwork/"
|
|
||||||
RE+=" --exclude=/Pictures/iPod?Photo?Cache/"
|
|
||||||
RE+=" --exclude=/Receivd/"
|
|
||||||
RE+=" --exclude=/.persepolis/"
|
|
||||||
|
|
||||||
# new 2026-06
|
|
||||||
RE+=" --exclude=/.lima/default/"
|
|
||||||
RE+=" --exclude=/.lmstudio/models/"
|
|
||||||
RE+=" --exclude=/.ollama/models/"
|
|
||||||
RE+=" --exclude=/.persepolis/"
|
|
||||||
RE+=" --exclude=/.pyenv/"
|
|
||||||
RE+=" --exclude=/.npm/"
|
|
||||||
RE+=" --exclude=/.opencode/node_modules/"
|
|
||||||
RE+=" --exclude=/.walletwasabi/"
|
|
||||||
|
|
||||||
# new 2026-08
|
|
||||||
RE+=" --exclude=/Library/Metadata/CoreSpotlight/"
|
|
||||||
|
|
||||||
MINRE=""
|
|
||||||
MINRE+=" --exclude=/.fseventsd/"
|
|
||||||
MINRE+=" --exclude=/.Spotlight-V100/"
|
|
||||||
MINRE+=" --exclude=/.Trash/"
|
|
||||||
MINRE+=" --exclude=/.Trashes/"
|
|
||||||
MINRE+=" --exclude=/tmp/"
|
|
||||||
MINRE+=" --exclude=/.TemporaryItems/"
|
|
||||||
MINRE+=" --exclude=/.rnd/"
|
|
||||||
MINRE+=" --exclude=.DS_Store"
|
|
||||||
|
|
||||||
RE+=" ${MINRE}"
|
|
||||||
|
|
||||||
echo "beginning backup to '$RCBACKUPDEST'..."
|
|
||||||
|
|
||||||
RETVAL=255
|
|
||||||
while [ $RETVAL -ne 0 ]; do
|
|
||||||
$RCLONE sync $OPTS $RE ${HOME}/ ${RCBACKUPDEST}/home.$(whoami)/
|
|
||||||
RETVAL=$?
|
|
||||||
sleep 1;
|
|
||||||
done
|
|
||||||
|
|
||||||
|
|
||||||
RE=""
|
|
||||||
RE+=" --exclude=/.DS_Store"
|
|
||||||
RE+=" --exclude=/App?Store.app"
|
|
||||||
RE+=" --exclude=/Automator.app"
|
|
||||||
RE+=" --exclude=/Books.app"
|
|
||||||
RE+=" --exclude=/Calculator.app"
|
|
||||||
RE+=" --exclude=/Calendar.app"
|
|
||||||
RE+=" --exclude=/Chess.app"
|
|
||||||
RE+=" --exclude=/Contacts.app"
|
|
||||||
RE+=" --exclude=/DVD?Player.app"
|
|
||||||
RE+=" --exclude=/Dashboard.app"
|
|
||||||
RE+=" --exclude=/Dictionary.app"
|
|
||||||
RE+=" --exclude=/FaceTime.app"
|
|
||||||
RE+=" --exclude=/Font?Book.app"
|
|
||||||
RE+=" --exclude=/Game?Center.app"
|
|
||||||
RE+=" --exclude=/GarageBand.app"
|
|
||||||
RE+=" --exclude=/Home.app"
|
|
||||||
RE+=" --exclude=/Image?Capture.app"
|
|
||||||
RE+=" --exclude=/Install?OS?X?El?Capitan.app"
|
|
||||||
RE+=" --exclude=/Install?OS?X?Yosemite.app"
|
|
||||||
RE+=" --exclude=/Keynote.app"
|
|
||||||
RE+=" --exclude=/Launchpad.app"
|
|
||||||
RE+=" --exclude=/Mail.app"
|
|
||||||
RE+=" --exclude=/Maps.app"
|
|
||||||
RE+=" --exclude=/Messages.app"
|
|
||||||
RE+=" --exclude=/Mission?Control.app"
|
|
||||||
RE+=" --exclude=/News.app"
|
|
||||||
RE+=" --exclude=/Notes.app"
|
|
||||||
RE+=" --exclude=/Numbers.app"
|
|
||||||
RE+=" --exclude=/Pages.app"
|
|
||||||
RE+=" --exclude=/Photo?Booth.app"
|
|
||||||
RE+=" --exclude=/Photos.app"
|
|
||||||
RE+=" --exclude=/Preview.app"
|
|
||||||
RE+=" --exclude=/QuickTime?Player.app"
|
|
||||||
RE+=" --exclude=/Reminders.app"
|
|
||||||
RE+=" --exclude=/Safari.app"
|
|
||||||
RE+=" --exclude=/Siri.app"
|
|
||||||
RE+=" --exclude=/Spotify.app"
|
|
||||||
RE+=" --exclude=/Steam.app"
|
|
||||||
RE+=" --exclude=/Stocks.app"
|
|
||||||
RE+=" --exclude=/Stickies.app"
|
|
||||||
RE+=" --exclude=/System?Preferences.app"
|
|
||||||
RE+=" --exclude=/TextEdit.app"
|
|
||||||
RE+=" --exclude=/Time?Machine.app"
|
|
||||||
RE+=" --exclude=/Yubikey?Manager.app"
|
|
||||||
RE+=" --exclude=/Utilities/Activity?Monitor.app"
|
|
||||||
RE+=" --exclude=/Utilities/AirPort?Utility.app"
|
|
||||||
RE+=" --exclude=/Utilities/AppleScript?Editor.app"
|
|
||||||
RE+=" --exclude=/Utilities/Audio?MIDI?Setup.app"
|
|
||||||
RE+=" --exclude=/Utilities/Bluetooth?File?Exchange.app"
|
|
||||||
RE+=" --exclude=/Utilities/Boot?Camp?Assistant.app"
|
|
||||||
RE+=" --exclude=/Utilities/ColorSync?Utility.app"
|
|
||||||
RE+=" --exclude=/Utilities/Console.app"
|
|
||||||
RE+=" --exclude=/Utilities/Digital?Color?Meter.app"
|
|
||||||
RE+=" --exclude=/Utilities/DigitalColor?Meter.app"
|
|
||||||
RE+=" --exclude=/Utilities/Disk?Utility.app"
|
|
||||||
RE+=" --exclude=/Utilities/Grab.app"
|
|
||||||
RE+=" --exclude=/Utilities/Grapher.app"
|
|
||||||
RE+=" --exclude=/Utilities/Keychain?Access.app"
|
|
||||||
RE+=" --exclude=/Utilities/Migration?Assistant.app"
|
|
||||||
RE+=" --exclude=/Utilities/Screenshot.app"
|
|
||||||
RE+=" --exclude=/Utilities/Script?Editor.app"
|
|
||||||
RE+=" --exclude=/Utilities/System?Information.app"
|
|
||||||
RE+=" --exclude=/Utilities/Terminal.app"
|
|
||||||
RE+=" --exclude=/Utilities/VoiceOver?Utility.app"
|
|
||||||
RE+=" --exclude=/VirtualBox.app"
|
|
||||||
RE+=" --exclude=/VoiceMemos.app"
|
|
||||||
RE+=" --exclude=/Xcode.app"
|
|
||||||
RE+=" --exclude=/iBooks.app"
|
|
||||||
RE+=" --exclude=/iMovie.app"
|
|
||||||
RE+=" --exclude=/iTunes.app"
|
|
||||||
|
|
||||||
if [[ "$(uname)" = "Darwin" ]]; then
|
|
||||||
RETVAL=255
|
|
||||||
while [ $RETVAL -ne 0 ]; do
|
|
||||||
$RCLONE sync $OPTS $RE $MINRE /Applications/ ${RCBACKUPDEST}/Applications/
|
|
||||||
RETVAL=$?
|
|
||||||
sleep 1;
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
@@ -41,14 +41,15 @@ RE+=" --exclude=.bundle/"
|
|||||||
RE+=" --exclude=.cache/"
|
RE+=" --exclude=.cache/"
|
||||||
RE+=" --exclude=/.cpan/build/"
|
RE+=" --exclude=/.cpan/build/"
|
||||||
RE+=" --exclude=/.cpan/sources/"
|
RE+=" --exclude=/.cpan/sources/"
|
||||||
RE+=" --exclude=/.gradle/caches/"
|
|
||||||
RE+=" --exclude=/.docker/"
|
RE+=" --exclude=/.docker/"
|
||||||
RE+=" --exclude=/.local/share/containers/podman/machine/"
|
RE+=" --exclude=/.local/share/containers/podman/machine/"
|
||||||
RE+=" --exclude=/.dropbox/"
|
RE+=" --exclude=/.dropbox/"
|
||||||
RE+=" --exclude=/.minikube/cache/"
|
RE+=" --exclude=/.minikube/cache/"
|
||||||
RE+=" --exclude=/Applications/Fortnite/"
|
RE+=" --exclude=/Applications/Fortnite/"
|
||||||
|
RE+=" --exclude=/Desktop/" # desktop is like a visible tempdir.
|
||||||
RE+=" --exclude=/Documents/Dropbox/.dropbox.cache/"
|
RE+=" --exclude=/Documents/Dropbox/.dropbox.cache/"
|
||||||
RE+=" --exclude=/Documents/Steam?Content/"
|
RE+=" --exclude=/Documents/Steam?Content/"
|
||||||
|
RE+=" --exclude=/Downloads/"
|
||||||
RE+=" --exclude=/Library/Application?Support/Ableton/"
|
RE+=" --exclude=/Library/Application?Support/Ableton/"
|
||||||
RE+=" --exclude=/Library/Application?Support/Adobe/Adobe?Device?Central?CS4/"
|
RE+=" --exclude=/Library/Application?Support/Adobe/Adobe?Device?Central?CS4/"
|
||||||
RE+=" --exclude=/Library/Application?Support/CrossOver?Games/"
|
RE+=" --exclude=/Library/Application?Support/CrossOver?Games/"
|
||||||
@@ -87,17 +88,6 @@ RE+=" --exclude=/Movies/ProxyMedia/"
|
|||||||
RE+=" --exclude=/Music/iTunes/Album?Artwork/"
|
RE+=" --exclude=/Music/iTunes/Album?Artwork/"
|
||||||
RE+=" --exclude=/Pictures/iPod?Photo?Cache/"
|
RE+=" --exclude=/Pictures/iPod?Photo?Cache/"
|
||||||
RE+=" --exclude=/Receivd/"
|
RE+=" --exclude=/Receivd/"
|
||||||
RE+=" --exclude=/.persepolis/"
|
|
||||||
|
|
||||||
# new 2026-06
|
|
||||||
RE+=" --exclude=/.lima/default/"
|
|
||||||
RE+=" --exclude=/.lmstudio/models/"
|
|
||||||
RE+=" --exclude=/.ollama/models/"
|
|
||||||
RE+=" --exclude=/.persepolis/"
|
|
||||||
RE+=" --exclude=/.pyenv/"
|
|
||||||
RE+=" --exclude=/.npm/"
|
|
||||||
RE+=" --exclude=/.opencode/node_modules/"
|
|
||||||
RE+=" --exclude=/.walletwasabi/"
|
|
||||||
|
|
||||||
MINRE=""
|
MINRE=""
|
||||||
MINRE+=" --exclude=/.fseventsd/"
|
MINRE+=" --exclude=/.fseventsd/"
|
||||||
@@ -165,7 +155,6 @@ RE+=" --exclude=/Stickies.app"
|
|||||||
RE+=" --exclude=/System?Preferences.app"
|
RE+=" --exclude=/System?Preferences.app"
|
||||||
RE+=" --exclude=/TextEdit.app"
|
RE+=" --exclude=/TextEdit.app"
|
||||||
RE+=" --exclude=/Time?Machine.app"
|
RE+=" --exclude=/Time?Machine.app"
|
||||||
RE+=" --exclude=/Yubikey?Manager.app"
|
|
||||||
RE+=" --exclude=/Utilities/Activity?Monitor.app"
|
RE+=" --exclude=/Utilities/Activity?Monitor.app"
|
||||||
RE+=" --exclude=/Utilities/AirPort?Utility.app"
|
RE+=" --exclude=/Utilities/AirPort?Utility.app"
|
||||||
RE+=" --exclude=/Utilities/AppleScript?Editor.app"
|
RE+=" --exclude=/Utilities/AppleScript?Editor.app"
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
Regular → Executable
@@ -1,31 +0,0 @@
|
|||||||
#!/usr/bin/env zsh
|
|
||||||
set -x
|
|
||||||
set -e
|
|
||||||
set -o nullglob
|
|
||||||
|
|
||||||
YYYYMMDD="$(date "+%Y-%m-%d")"
|
|
||||||
RSYNCOPTS="-ah --no-inc-recursive --info=progress2"
|
|
||||||
RDST="root@las1stor1:/srv/pool.2024.04/berlin.sneak.fs.video"
|
|
||||||
|
|
||||||
function amLocalToVideoStore() {
|
|
||||||
ifconfig | grep 10.100.205 | wc -l
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyVideosToVideoStore() {
|
|
||||||
rsync \
|
|
||||||
-ah --no-inc-recursive --info=progress2 \
|
|
||||||
./ "$RDST"/ && \
|
|
||||||
rsync \
|
|
||||||
-ah --no-inc-recursive --info=progress2 \
|
|
||||||
--remove-source-files \
|
|
||||||
./ "$RDST"/ && \
|
|
||||||
rm -rfv ./ && \
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
if [[ $(amLocalToLAS1) -gt 0 ]]; then
|
|
||||||
copyVideosToLAS1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/env zsh
|
|
||||||
set -x
|
|
||||||
set -e
|
|
||||||
set -o nullglob
|
|
||||||
|
|
||||||
YYYYMMDD="$(date "+%Y-%m-%d")"
|
|
||||||
DST="$HOME/_TODO/$YYYYMMDD-video-import"
|
|
||||||
RSYNCOPTS="-ah --no-inc-recursive --info=progress2"
|
|
||||||
RDST="root@las1stor1:/srv/pool.2024.04/berlin.sneak.fs.video"
|
|
||||||
|
|
||||||
function amLocalToLAS1() {
|
|
||||||
ifconfig | grep 10.100.205 | wc -l
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyVideosToLAS1() {
|
|
||||||
rsync \
|
|
||||||
-ah --no-inc-recursive --info=progress2 \
|
|
||||||
"$DST"/ "$RDST"/ && \
|
|
||||||
rsync \
|
|
||||||
-ah --no-inc-recursive --info=progress2 \
|
|
||||||
--remove-source-files \
|
|
||||||
"$DST"/ "$RDST"/ && \
|
|
||||||
rm -rfv "$DST" && \
|
|
||||||
ssh root@las1stor1 "cd /srv/pool.2024.04/berlin.sneak.fs.video && chown -R nobody:nogroup ."
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
if [[ $(amLocalToLAS1) -gt 0 ]]; then
|
|
||||||
copyVideosToLAS1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#!/usr/bin/env zsh
|
|
||||||
set -x
|
|
||||||
set -e
|
|
||||||
set -o nullglob
|
|
||||||
|
|
||||||
YYYYMMDD="$(date "+%Y-%m-%d")"
|
|
||||||
RSYNCOPTS="-ah --no-inc-recursive --info=progress2"
|
|
||||||
RDST="root@las1stor1:/srv/pool.2024.04/berlin.sneak.fs.video"
|
|
||||||
|
|
||||||
function rename_to_format() {
|
|
||||||
f2 -r '{{mtime.YYYY}}/{{mtime.YYYY}}-{{mtime.MM}}/{{mtime.YYYY}}-{{mtime.MM}}-{{mtime.DD}}/{{mtime.YYYY}}-{{mtime.MM}}-{{mtime.DD}}.{{f}}.{{hash.sha256}}{{ext}}' -x --verbose
|
|
||||||
}
|
|
||||||
|
|
||||||
function import() {
|
|
||||||
rename_to_format
|
|
||||||
rsync -avP ./2* $RDST/
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
import
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
Regular → Executable
+18
-1
@@ -5,13 +5,14 @@ set -o nullglob
|
|||||||
|
|
||||||
YYYYMMDD="$(date "+%Y-%m-%d")"
|
YYYYMMDD="$(date "+%Y-%m-%d")"
|
||||||
|
|
||||||
|
|
||||||
function downloadPhotos() {
|
function downloadPhotos() {
|
||||||
SRC=(/Volumes/*/DCIM)
|
SRC=(/Volumes/*/DCIM)
|
||||||
if [[ ! -d "$SRC" ]]; then
|
if [[ ! -d "$SRC" ]]; then
|
||||||
echo "no photos" > /dev/stderr
|
echo "no photos" > /dev/stderr
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
DST="$HOME/Pictures/_ORIG"
|
DST="$HOME/Library/Syncthing/folders/LightroomMasters-CurrentYear/toimport"
|
||||||
du -sh "$SRC"
|
du -sh "$SRC"
|
||||||
rsync -ah --no-inc-recursive --info=progress2 --remove-source-files "$SRC"/ $DST/$YYYYMMDD/
|
rsync -ah --no-inc-recursive --info=progress2 --remove-source-files "$SRC"/ $DST/$YYYYMMDD/
|
||||||
cd "$DST/$YYYYMMDD"
|
cd "$DST/$YYYYMMDD"
|
||||||
@@ -19,10 +20,26 @@ function downloadPhotos() {
|
|||||||
f2 -r '../{{mtime.YYYY}}/{{mtime.YYYY}}-{{mtime.MM}}/{{mtime.YYYY}}-{{mtime.MM}}-{{mtime.DD}}/{{mtime.YYYY}}-{{mtime.MM}}-{{mtime.DD}}.{{x.model}}.{{f}}.{{hash.sha256}}{{ext}}' -x --verbose
|
f2 -r '../{{mtime.YYYY}}/{{mtime.YYYY}}-{{mtime.MM}}/{{mtime.YYYY}}-{{mtime.MM}}-{{mtime.DD}}/{{mtime.YYYY}}-{{mtime.MM}}-{{mtime.DD}}.{{x.model}}.{{f}}.{{hash.sha256}}{{ext}}' -x --verbose
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadVideos() {
|
||||||
|
SRC=(/Volumes/*/PRIVATE/M4ROOT/CLIP)
|
||||||
|
if [[ ! -d "$SRC" ]]; then
|
||||||
|
echo "no videos" > /dev/stderr
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
DST="$HOME/_TODO/$YYYYMMDD-video-import/"
|
||||||
|
mkdir -p "$DST"
|
||||||
|
du -sh "$SRC"
|
||||||
|
rsync -ah --no-inc-recursive --info=progress2 --remove-source-files "$SRC"/ $DST/
|
||||||
|
cd "$DST"
|
||||||
|
#mv */* .
|
||||||
|
f2 -r '{{mtime.YYYY}}/{{mtime.YYYY}}-{{mtime.MM}}/{{mtime.YYYY}}-{{mtime.MM}}-{{mtime.DD}}/{{mtime.YYYY}}-{{mtime.MM}}-{{mtime.DD}}.{{f}}.{{hash.sha256}}{{ext}}' -x --verbose
|
||||||
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
# FIXME this doesn't work if there are no photos on the card but the
|
# FIXME this doesn't work if there are no photos on the card but the
|
||||||
# DCIM dir exists
|
# DCIM dir exists
|
||||||
downloadPhotos
|
downloadPhotos
|
||||||
|
downloadVideos
|
||||||
}
|
}
|
||||||
|
|
||||||
main
|
main
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/usr/bin/env zsh
|
|
||||||
set -x
|
|
||||||
set -e
|
|
||||||
set -o nullglob
|
|
||||||
|
|
||||||
YYYYMMDD="$(date "+%Y-%m-%d")"
|
|
||||||
DST="$HOME/_TODO/$YYYYMMDD-video-import"
|
|
||||||
RSYNCOPTS="-ah --no-inc-recursive --info=progress2"
|
|
||||||
LASDST="root@las1:/srv/las1/video"
|
|
||||||
|
|
||||||
function downloadVideos() {
|
|
||||||
SRC=(/Volumes/*/PRIVATE/M4ROOT/CLIP)
|
|
||||||
if [[ ! -d "$SRC" ]]; then
|
|
||||||
echo "no videos" > /dev/stderr
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -n "$(ls -A "$SRC" 2>/dev/null)" ]]; then
|
|
||||||
echo "no videos" > /dev/stderr
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$DST"
|
|
||||||
du -sh "$SRC"
|
|
||||||
rsync \
|
|
||||||
-ah --no-inc-recursive --info=progress2 \
|
|
||||||
--remove-source-files "$SRC"/ $DST/
|
|
||||||
cd "$DST"
|
|
||||||
#mv */* .
|
|
||||||
f2 -r '{{mtime.YYYY}}/{{mtime.YYYY}}-{{mtime.MM}}/{{mtime.YYYY}}-{{mtime.MM}}-{{mtime.DD}}/{{mtime.YYYY}}-{{mtime.MM}}-{{mtime.DD}}.{{f}}.{{hash.sha256}}{{ext}}' -x --verbose
|
|
||||||
}
|
|
||||||
|
|
||||||
function amLocalToLAS1() {
|
|
||||||
ifconfig | grep 10.100.205 | wc -l
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
# FIXME this doesn't work if there are no photos on the card but the
|
|
||||||
# DCIM dir exists
|
|
||||||
downloadVideos
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#!/usr/bin/env zsh
|
|
||||||
|
|
||||||
LOGFILE="$HOME/tmp/photocopylog.txt"
|
|
||||||
|
|
||||||
exec &> >(tee -a "$LOGFILE")
|
|
||||||
|
|
||||||
set -x
|
|
||||||
|
|
||||||
DST="user@nostromo2.local:/Volumes/berlin.sneak.fs.photostore"
|
|
||||||
|
|
||||||
date
|
|
||||||
date -u
|
|
||||||
|
|
||||||
time rsync \
|
|
||||||
-avvh --no-inc-recursive --info=progress2 \
|
|
||||||
"$HOME/Library/Syncthing/folders/LightroomMasters-PastYears/" \
|
|
||||||
"$DST/berlin.sneak.fs.photostore-PastYears/"
|
|
||||||
|
|
||||||
date
|
|
||||||
date -u
|
|
||||||
|
|
||||||
time rsync \
|
|
||||||
-avvh --no-inc-recursive --info=progress2 \
|
|
||||||
"$HOME/Library/Syncthing/folders/LightroomMasters-CurrentYear/" \
|
|
||||||
"$DST/berlin.sneak.fs.photostore-CurrentYear/"
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
|
|
||||||
date
|
|
||||||
date -u
|
|
||||||
|
|
||||||
time rsync \
|
|
||||||
-acvvh --no-inc-recursive --info=progress2 \
|
|
||||||
"$HOME/Library/Syncthing/folders/LightroomMasters-PastYears/" \
|
|
||||||
"$DST/berlin.sneak.fs.photostore-PastYears/"
|
|
||||||
|
|
||||||
|
|
||||||
date
|
|
||||||
date -u
|
|
||||||
|
|
||||||
time rsync \
|
|
||||||
-acvvh --no-inc-recursive --info=progress2 \
|
|
||||||
"$HOME/Library/Syncthing/folders/LightroomMasters-CurrentYear/" \
|
|
||||||
"$DST/berlin.sneak.fs.photostore-CurrentYear/"
|
|
||||||
|
|
||||||
|
|
||||||
date
|
|
||||||
date -u
|
|
||||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user