Initial Contractless source release

This commit is contained in:
viraladmin 2026-05-24 11:56:57 -06:00
commit 118466d9cd
340 changed files with 45973 additions and 0 deletions

2661
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

63
Cargo.toml Normal file
View File

@ -0,0 +1,63 @@
[package]
name = "blockchain"
version = "0.1.0"
edition = "2021"
[features]
default = ["testnet"]
mainnet = []
testnet = []
[[bin]]
name = "contractless-testnet"
path = "src/main.rs"
required-features = ["testnet"]
[[bin]]
name = "contractless-mainnet"
path = "src/main.rs"
required-features = ["mainnet"]
[profile.release]
opt-level = "z"
lto = true
panic = "abort"
codegen-units = 1
[dependencies]
rpassword = "7.1.0"
encrypted_images = "1.4.0"
skein = "0.1.0"
chrono = "0.4"
serde = { version = "1.0.108", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
base64 = "0.22.1"
hex = "0.4"
falcon-rs = "0.2.4"
fn-dsa = "0.3.0"
tokio = { version = "1.45.1", features = ["full", "test-util"] }
tokio-postgres = "0.7"
rand = "0.8"
sled = "0.34"
lazy_static = "1.4"
rust-ini = "0.19"
shellexpand = "3.1.0"
ipnetwork = "0.20.0"
rayon = "1.8.0"
once_cell = "1.8.0"
anyhow = "1.0"
ripemd = { version = "0.2.0-rc.0" }
colored = "2"
cid = "0.11"
flexi_logger = "0.31.8"
log = "0.4"
rustyline = { version = "14.0.0", features = ["derive"] }
rustyline-derive = "0.10.0"
[target.'cfg(unix)'.dependencies]
nix = { version = "0.27", features = ["user", "process", "signal"] }
[target.'cfg(windows)'.dependencies]
windows-service = "0.8"
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization"] }
winreg = "0.52"

312
README.md Normal file
View File

@ -0,0 +1,312 @@
# Contractless
Contractless is a peer-to-peer Fair-Proof-of-Work blockchain with native multi-asset transactions, torrent-based block synchronization and deterministic orphan correction. This README contains the minimum information needed to build, configure and start a node.
For the protocol design, mining rules, balance-sheet model and orphan correction process, read the [Contractless whitepaper](https://contractless.community/contractless_whitepaper.pdf).
## Table of Contents
- [Network Requirements](#network-requirements)
- [Build Instructions](#build-instructions)
- [PostgreSQL Setup](#postgresql-setup)
- [Settings Configuration](#settings-configuration)
- [Runtime Paths](#runtime-paths)
- [Startup](#startup)
- [Additional Documentation](#additional-documentation)
## Network Requirements
A public node needs to be reachable by other peers.
- Open the active RPC port in the local firewall.
- Forward the active RPC port through the router or host firewall when behind NAT.
- Set `IP` in `settings.ini` to the reachable public IP or reachable domain name for the node.
- Keep system time synchronized. Any reliable NTP/time server should work because Contractless only requires second-level timestamp agreement. A common default is `pool.ntp.org`.
- Keep PostgreSQL reachable locally by the node process.
Loopback and private addresses are useful for local testing, but they should not be announced by a public node.
## Build Instructions
Install Rust and build from the repository root.
```bash
cargo build --release
```
The default build target is testnet.
### Build Flags
Testnet is the default feature:
```bash
cargo build --release
```
or explicitly:
```bash
cargo build --release --features testnet
```
Mainnet has a feature flag, but mainnet builds are intentionally disabled during the testnet phase:
```bash
cargo build --release --no-default-features --features mainnet
```
That command will fail until mainnet is enabled for launch.
## PostgreSQL Setup
Contractless uses PostgreSQL for transaction lookup and mempool-style records. PostgreSQL only needs a database, user, password and permissions before the node starts. On startup, the node creates or migrates the required tables and indexes automatically. The node also uses local file and sled storage for chain state, wallets, balance sheets and torrents.
### Linux PostgreSQL CLI Tool
Build the release binaries, then run the installer as root so it can install PostgreSQL if needed and create the database, user, password and permissions:
```bash
sudo ./target/release/postgres_installer
```
After the tool completes, copy the printed PostgreSQL values into the active PostgreSQL section of `settings.ini`.
### Windows PostgreSQL CLI Tool
Open PowerShell as Administrator, copy the built installer into the install folder, then run it:
```powershell
& "C:\Program Files\Contractless\postgres_installer.exe"
```
The Windows installer downloads and installs PostgreSQL when needed, then creates the configured database, user, password and permissions.
### Manual PostgreSQL Setup
Manual PostgreSQL instructions should live in [docs/POSTGRES.md](docs/POSTGRES.md). Until that file is fully written, use the CLI installer unless you already know how to create the database, user and permissions manually.
## Settings Configuration
The node loads `settings.ini` in this order:
1. `--config <path>`
2. `SETTINGS_PATH` environment variable
3. `./settings.ini`
4. `settings.ini` beside the executable
5. platform fallback path
On Linux, the fallback path is:
```text
/etc/contractless/settings.ini
```
On Windows, place `settings.ini` beside the executable:
```text
C:\Program Files\Contractless\settings.ini
```
### Basic `settings.ini` Shape
```ini
[Paths]
BLOCK_PATH = "./blocks"
TORRENT_PATH = "./torrents"
DB_PATH = "./state"
WALLET_PATH = "./wallets"
WALLET_NAME = "contractless.wallet"
BALANCE_SHEET = "./balance_sheet"
[Settings]
LOG_PATH = "./logs"
LOG_LEVEL = "info"
IP = "127.0.0.1"
RPC_PORT = "2006"
TESTNET_RPC_PORT = "50053"
INCOMING_CONNECTIONS = "10"
OUTGOING_CONNECTIONS = "0"
THREADS = "8"
[Piggyback]
PIGGYBACK_1 = "127.0.0.1:50050"
PIGGYBACK_2 = "127.0.0.1:50054"
PIGGYBACK_3 = "127.0.0.1:50051"
PIGGYBACK_4 = "127.0.0.1:50055"
PIGGYBACK_5 = "127.0.0.1:50052"
[Postgres-Testnet]
host = 127.0.0.1
port = 5432
user = contractless
password = your_postgres_password_here
dbname = contractless_db
[Postgres]
host = 127.0.0.1
port = 5432
user = contractless
password = your_postgres_password_here
dbname = contractless_db
```
`THREADS` must be `1`, `2` or a multiple of `4`, and may not be greater than `256`.
`LOG_LEVEL` follows normal logging filters:
- `info` shows info, warning and error logs.
- `warn` shows warning and error logs.
- `error` shows only error logs.
- `off` disables log output.
Detailed settings notes should live in [docs/SETTINGS.md](docs/SETTINGS.md).
## Runtime Paths
Relative paths in `settings.ini` are resolved relative to the location of that `settings.ini` file. The node scopes runtime data by active network internally, so testnet and mainnet paths do not collide when using the same base folders.
The main runtime folders are:
- `BLOCK_PATH`: saved block files
- `TORRENT_PATH`: torrent metadata and staged torrents
- `DB_PATH`: sled state
- `WALLET_PATH`: encrypted wallet files
- `BALANCE_SHEET`: balance sheet files
- `LOG_PATH`: runtime logs
### Linux Copy Instructions
Create the config folder and copy the sample settings file:
```bash
sudo mkdir -p /etc/contractless
sudo cp ./settings.ini /etc/contractless/settings.ini
```
Copy the node binary:
```bash
sudo cp ./target/release/contractless-testnet /usr/bin/
```
Copy any CLI tools you want available system-wide:
```bash
sudo cp ./target/release/postgres_installer /usr/bin/
sudo cp ./target/release/create_new_wallet /usr/bin/
sudo cp ./target/release/register_wallet /usr/bin/
```
Repeat the same pattern for any other tools from `target/release`.
### Windows Copy Instructions
Open PowerShell as Administrator and create the install folder:
```powershell
New-Item -ItemType Directory -Force "C:\Program Files\Contractless" | Out-Null
```
Copy the node, key-submit tool, PostgreSQL installer and settings file:
```powershell
Copy-Item ".\target\release\contractless-testnet.exe" "C:\Program Files\Contractless\"
Copy-Item ".\target\release\contractless-submit-key.exe" "C:\Program Files\Contractless\"
Copy-Item ".\target\release\postgres_installer.exe" "C:\Program Files\Contractless\"
Copy-Item ".\settings-windows.ini" "C:\Program Files\Contractless\settings.ini"
```
Copy any additional CLI tools the same way.
## Startup
Create or restore a wallet before starting a public node, then make sure the wallet is registered and the wallet path/name in `settings.ini` matches the runtime wallet.
### Linux Startup
Start the testnet node:
```bash
contractless-testnet
```
Linux prompts for the wallet decryption key, then detaches into the background automatically.
Run in the foreground for debugging:
```bash
contractless-testnet --foreground
```
Check daemon status:
```bash
contractless-testnet --status
```
Stop the daemon:
```bash
contractless-testnet --stop
```
Use a specific config file:
```bash
contractless-testnet --config /path/to/settings.ini
```
### Windows Startup
Open PowerShell as Administrator and install the service:
```powershell
& "C:\Program Files\Contractless\contractless-testnet.exe" --install-service
```
Start the service:
```powershell
& "C:\Program Files\Contractless\contractless-testnet.exe" --start-service
```
Submit the wallet decryption key from a normal user shell:
```powershell
& "C:\Program Files\Contractless\contractless-submit-key.exe"
```
Stop the service:
```powershell
& "C:\Program Files\Contractless\contractless-testnet.exe" --stop-service
```
Uninstall the service:
```powershell
& "C:\Program Files\Contractless\contractless-testnet.exe" --uninstall-service
```
### Startup Flags
Node flags:
- `--config <path>`: load a specific `settings.ini`.
- `--foreground`: Linux only; keep the process attached to the terminal.
- `--status`: Linux only; check daemon status.
- `--stop`: Linux only; stop the daemon.
- `--install-service`: Windows only; install the Windows service.
- `--start-service`: Windows only; start the Windows service.
- `--stop-service`: Windows only; stop the Windows service.
- `--uninstall-service`: Windows only; uninstall the Windows service.
## Additional Documentation
- [Whitepaper](https://contractless.community/contractless_whitepaper.pdf)
- [Manual PostgreSQL setup](docs/POSTGRES.md)
- [Settings reference](docs/SETTINGS.md)
- [CLI tools reference](docs/CLI_TOOLS.md)
- [Transaction reference](docs/TRANSACTIONS.md)
- [Developer guide](docs/DEV.md)

Binary file not shown.

Binary file not shown.

1335
docs/CLI_TOOLS.md Normal file

File diff suppressed because it is too large Load Diff

250
docs/DEV.md Normal file
View File

@ -0,0 +1,250 @@
# Contractless Developer Guide
Contractless builds as both executable tools and a Rust library crate named `blockchain`. The library artifact is `libblockchain.rlib`, and it exposes the same major modules used by the node, CLI tools and internal tests.
This file is a developer map. It is not meant to replace generated Rust documentation. Many functions are public because the crate is split across modules, not because every public function is a stable external API.
## Build Targets
The package builds node binaries, standalone CLI tools and the `blockchain` library crate.
Default testnet build:
```bash
cargo build --release
```
Explicit testnet build:
```bash
cargo build --release --features testnet
```
Mainnet has a feature flag, but mainnet builds are disabled during the testnet phase:
```bash
cargo build --release --no-default-features --features mainnet
```
## Generated API Reference
Use Rustdoc for the complete list of public modules, structs, enums, constants and functions:
```bash
cargo doc --no-deps
```
Open the generated reference:
```text
target/doc/blockchain/index.html
```
The generated docs are the best way to inspect every exposed item in `libblockchain.rlib`.
## Public Surface
The crate root exposes these local modules:
| Module | What it contains |
| --- | --- |
| `blocks` | Block structs, transaction structs, byte encoding and transaction balance effects |
| `common` | Shared constants, hashing, settings paths, CLI prompts, network startup helpers and shared utility code |
| `config` | `settings.ini` loading and `Settings` structure |
| `miner` | Mining loop, nonce search, block reward logic, fairness checks and mining structs |
| `orphans` | Orphan correction, rollback, staged torrent replay and transaction undo logic |
| `records` | Chain records, balance sheets, wallet registry, memory records, PostgreSQL helpers and network mappings |
| `rpc` | RPC command IDs, client helpers, server loop, command handlers and response types |
| `standalone_tools` | Shared connection and request helpers used by CLI tools |
| `startup` | Node startup, daemon/service support, logging, paths and runtime initialization |
| `torrent` | Torrent metadata, torrent cache, torrent staging, block download and torrent validation |
| `verifications` | Transaction, block, torrent and rule validation logic |
| `wallets` | Wallet structs, address conversion, key generation, signing, verification and vanity lookup |
The crate root also re-exports many dependency types and functions used throughout the project. This keeps internal modules consistent, but external developers should prefer the stable public modules above instead of relying on dependency re-exports as an API contract.
## Useful Entry Points
### Wallets
Useful for tools that need to load wallets, derive addresses or sign data.
Common areas:
- `wallets::structures::Wallet`
- `wallets::structures::SavedWallet`
- address conversion functions in the wallet modules
- transaction signing and verification functions
Typical uses:
- create or load encrypted wallets
- convert long addresses to short addresses
- validate short or vanity addresses
- sign transaction hashes
- verify signatures
### Blocks and Transactions
Useful for tools that need to construct, decode or inspect transactions.
Common areas:
- `blocks::block`
- `blocks::transfer`
- `blocks::token`
- `blocks::nft`
- `blocks::swap`
- `blocks::loans`
- `blocks::loan_payment`
- `blocks::collateral`
- `blocks::vanity`
Typical uses:
- create transaction structs
- encode transactions into bytes
- decode transactions from bytes
- inspect transaction fields
- apply or undo balance-sheet effects
For user-facing transaction behavior, see [TRANSACTIONS.md](TRANSACTIONS.md).
### Common Hashing and Types
Useful for tools that need protocol constants or hashing compatible with the node.
Common areas:
- `common::types`
- `common::skein`
- `common::network_paths_and_settings`
- `common::cli_prompts`
Typical uses:
- access transaction type IDs
- access fee constants
- hash text or bytes with the same Skein helpers used by the node
- resolve network-scoped paths
- share CLI prompt behavior across tools
### RPC
Useful for network clients, node tools and server command development.
Common areas:
- `rpc::command_maps`
- `rpc::commands`
- `rpc::client`
- `rpc::server`
- `rpc::structs`
Typical uses:
- map command IDs to RPC behavior
- implement a new RPC command handler
- inspect client request helpers
- inspect response structures
For raw client handshake notes, see [DEV_CLIENTS.md](DEV_CLIENTS.md).
### Standalone Tool Connections
Useful for CLI tools that need to connect to a peer without acting like a mining node.
Common areas:
- `standalone_tools::connections::handshake`
- `standalone_tools::connections::sending_request`
- `standalone_tools::connections::transaction_creator`
Typical uses:
- perform the authenticated client handshake
- send a binary RPC request
- decode reply payloads for CLI output
### Torrent and Orphan Handling
Useful for tools that inspect block synchronization behavior.
Common areas:
- `torrent`
- `orphans`
- `records::staged_torrents`
Typical uses:
- read and decode torrent metadata
- validate torrent data before block download
- inspect staged torrent candidates
- understand orphan correction and replay behavior
### Records and Storage
Useful for tools that inspect local chain state or database-backed records.
Common areas:
- `records::record_chain`
- `records::balance_sheet`
- `records::wallet_registry`
- `records::postgres`
- `records::memory`
- `records::network_mapping`
Typical uses:
- read saved chain records
- inspect balance sheets
- resolve registered wallets or vanity addresses
- query or maintain PostgreSQL-backed lookup records
- inspect in-memory connection state
## Adding New Public API
Before making a new function public, check whether it is public for one of these reasons:
- another module must call it
- a CLI tool needs it
- an external developer should reasonably use it
- Rustdoc should expose it as part of a stable integration surface
If a function only wraps one other function and adds no validation, conversion, ownership boundary or domain meaning, prefer calling the original function directly.
## Adding New CLI Tools
Standalone CLI tools live in `src/bin`.
When adding a tool:
- use `common::cli_prompts` for user prompts
- use `standalone_tools::connections` for RPC client requests
- keep RPC payloads binary across the TCP stream
- print human-readable or JSON-decoded output only after the binary response is received
- add the tool to [CLI_TOOLS.md](CLI_TOOLS.md)
## Adding New RPC Commands
RPC command handlers should live under `src/rpc/commands`.
When adding a command:
- add or update the command ID in `rpc::command_maps`
- put the command handler in `rpc::commands`
- keep inline code in the server loop minimal
- send and receive binary payloads across the stream
- return through `RpcResponse` directly
- add or update any matching CLI tool documentation
## Documentation Map
- [README.md](../README.md): build, configure and start a node
- [CLI_TOOLS.md](CLI_TOOLS.md): command-line tool reference
- [TRANSACTIONS.md](TRANSACTIONS.md): transaction behavior reference
- [POSTGRES.md](POSTGRES.md): manual PostgreSQL setup
- [SETTINGS.md](SETTINGS.md): `settings.ini` field reference
- [DEV_CLIENTS.md](DEV_CLIENTS.md): low-level client handshake and request notes

98
docs/DEV_CLIENTS.md Normal file
View File

@ -0,0 +1,98 @@
# Developer Client Notes
This file contains low-level notes for building external clients or debugging the Contractless RPC client flow. It is not required for normal node setup.
## RPC Clients
RPC clients are not treated the same as mining nodes. A client must follow the handshake format exactly, or the server may classify it as a node/miner instead of a client. When that happens, client tools can hang, be rejected or behave unexpectedly.
### Required Client Handshake Rules
1. Use a valid wallet address and signature in the handshake.
- The client must load a real wallet locally.
- The wallet encryption key is used only locally to open the wallet and sign the handshake.
- The wallet encryption key is not sent over the stream.
- The wallet address is sent as part of the handshake identity.
2. The opening handshake message must be `aced`.
- The client signs the hash of `aced`.
- The server verifies that signature against the wallet address supplied in the handshake.
3. A client must advertise its IP with port `0`.
- Example: `127.0.0.1:0`
- This is the rule that tells the server the connection is a client-style RPC connection rather than a mining node connection.
- The server checks whether the submitted `ip:port` is externally reachable.
- If the submitted port is open, the connection is treated as a miner/node.
- If the submitted port is `0` or not reachable, the connection is treated as a client.
4. The client timestamp must be within 1 second of the server timestamp.
- If the timestamp is too far off, the handshake is rejected.
5. The submitted IP must match the caller IP seen by the server.
- The server compares the claimed IP in the handshake against the actual remote peer IP.
### Handshake Byte Layout
The client sends:
- `2` bytes: message (`aced`)
- `65` bytes: recoverable signature
- `21` bytes: wallet address
- `4` bytes: timestamp
- `18` bytes: `ip:port`
Total client handshake size:
- `110` bytes
The server returns:
- `2` bytes: message
- `65` bytes: recoverable signature
- `21` bytes: wallet address
Total server handshake size:
- `88` bytes
### Server Return Messages
After the client handshake is accepted:
- miner/node connections receive `face`
- client connections receive `ecaf`
RPC clients should expect `ecaf`.
If you do not follow the client rules above, especially the `ip:0` rule, the server can classify your connection as a mining/node connection instead of a client connection. That will cause errors for standalone RPC tools.
### RPC Request Format After Handshake
After the handshake succeeds, the client sends:
- `1` byte: command
- `3` bytes: request uid
- command-specific payload bytes if required
Normal RPC replies are returned as:
- `1` byte: reply command (`111`)
- `3` bytes: request uid
- `2` bytes: payload length
- payload bytes
### Practical Note For Standalone Tools
Standalone tools in `src/bin` should behave like clients, not mining nodes. Their handshake should therefore:
- use a real wallet
- send `aced`
- sign it correctly
- advertise `your_ip:0`
If a standalone tool advertises a real RPC/mining port instead of `0`, the server may treat it as a node connection and the tool can fail in confusing ways.

111
docs/POSTGRES.md Normal file
View File

@ -0,0 +1,111 @@
# PostgreSQL Setup
Contractless uses PostgreSQL for transaction lookup and mempool-style records. The node creates or migrates its required tables and indexes on startup, so manual setup only needs PostgreSQL itself, a database, a login user, a password and permissions for that user.
PostgreSQL 16 is the tested version. Newer PostgreSQL versions should work, but they have not been tested for launch.
Download PostgreSQL from the official project downloads page:
<https://www.postgresql.org/download/>
## Required Database Access
The database user configured in `settings.ini` must be able to:
- connect to the configured database
- create tables and indexes
- read, insert, update and delete rows in the tables it creates
- use sequences created for `BIGSERIAL` primary keys
The simplest way to provide the required permissions is to make the Contractless user the owner of the Contractless database.
## Linux Manual Setup
Run the SQL as the PostgreSQL superuser. On many Linux installs this means using the `postgres` system user:
```bash
sudo -u postgres psql
```
Create the database user:
```sql
CREATE USER contractless WITH PASSWORD 'change_this_password';
```
Create the database and make the Contractless user the owner:
```sql
CREATE DATABASE contractless_db OWNER contractless;
```
If local password login is not already enabled, update `pg_hba.conf` so local TCP connections can authenticate with a password:
```text
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
```
Reload PostgreSQL after changing `pg_hba.conf`:
```bash
sudo systemctl reload postgresql
```
Then add the matching values to the active PostgreSQL section of `settings.ini`:
```ini
[Postgres-Testnet]
host = 127.0.0.1
port = 5432
user = contractless
password = change_this_password
dbname = contractless_db
```
## Windows Manual Setup
Install PostgreSQL from the official PostgreSQL download page. PostgreSQL 16 is the tested version.
Open the PostgreSQL SQL Shell, pgAdmin query tool or another PostgreSQL client as a superuser, then run:
```sql
CREATE USER contractless WITH PASSWORD 'change_this_password';
CREATE DATABASE contractless_db OWNER contractless;
```
Add the matching values to the active PostgreSQL section of `settings.ini`:
```ini
[Postgres-Testnet]
host = 127.0.0.1
port = 5432
user = contractless
password = change_this_password
dbname = contractless_db
```
## Existing Database
If the database already exists and is not owned by the Contractless user, either change the owner:
```sql
ALTER DATABASE contractless_db OWNER TO contractless;
```
or grant the user enough rights to create and manage objects in the public schema:
```sql
GRANT CONNECT ON DATABASE contractless_db TO contractless;
\c contractless_db
GRANT USAGE, CREATE ON SCHEMA public TO contractless;
ALTER SCHEMA public OWNER TO contractless;
```
Database ownership is preferred because Contractless creates and updates its schema automatically.
## Startup Behavior
When the node starts, it connects to PostgreSQL using the values in `settings.ini`, then runs the mempool schema setup. This creates the required tables, applies compatible migrations, creates indexes and starts from an empty live mempool.
If PostgreSQL login succeeds but the user lacks ownership or create permissions, node startup will fail during the automatic table or index setup.

191
docs/SETTINGS.md Normal file
View File

@ -0,0 +1,191 @@
# Settings Reference
Contractless reads `settings.ini` during startup. Relative paths are resolved relative to the location of the loaded `settings.ini` file. Runtime storage paths are also scoped by active network, so testnet and mainnet data are kept in separate subfolders.
The node loads `settings.ini` in this order:
1. `--config <path>`
2. `SETTINGS_PATH` environment variable
3. `./settings.ini`
4. `settings.ini` beside the executable
5. platform fallback path
On Linux, the fallback path is `/etc/contractless/settings.ini`.
## `[Paths]`
### `BLOCK_PATH`
Base folder where block files are stored. The active network name is appended automatically.
Example:
```ini
BLOCK_PATH = "./blocks"
```
Testnet will store blocks under:
```text
./blocks/testnet
```
Mainnet will store blocks under:
```text
./blocks/mainnet
```
### `TORRENT_PATH`
Base folder where torrent metadata, staged torrents and torrent-related files are stored. The active network name is appended automatically.
### `DB_PATH`
Base folder for the internal sled database used for non-PostgreSQL node state. The active network name is appended automatically.
### `WALLET_PATH`
Base folder where wallet files are stored. The active network name and `WALLET_NAME` are appended automatically.
Example:
```ini
WALLET_PATH = "./wallets"
WALLET_NAME = "contractless.wallet"
```
Testnet will load the wallet from:
```text
./wallets/testnet/contractless.wallet
```
### `WALLET_NAME`
Name of the encrypted wallet file to load at startup.
### `BALANCE_SHEET`
Base folder where balance sheet files are stored. The active network name is appended automatically.
## `[Settings]`
### `LOG_PATH`
Base folder where node log files are written. The active network name is appended automatically so testnet and mainnet nodes can run side by side without writing to the same rotating log files.
Example:
```ini
LOG_PATH = "./logs"
```
Testnet will write logs under:
```text
./logs/testnet
```
### `LOG_LEVEL`
Runtime log filter.
Valid common values:
- `info`: show info, warning and error logs.
- `warn`: show warning and error logs.
- `error`: show only error logs.
- `off`: disable log output.
The logger also supports more advanced module-level filters through `flexi_logger`, but normal nodes should use one of the simple values above.
### `IP`
Reachable IP address or domain name announced by this node during handshakes.
Use `127.0.0.1` only for local testing. A public node should use a public IP address or a domain name that resolves to the node and reaches the configured RPC port.
### `RPC_PORT`
Mainnet RPC port. This value is used when the node is built with the `mainnet` feature.
Mainnet builds are currently disabled during the testnet phase, but the setting exists for launch.
### `TESTNET_RPC_PORT`
Testnet RPC port. This value is used when the node is built with the `testnet` feature.
Public testnet nodes must allow inbound traffic to this port.
### `INCOMING_CONNECTIONS`
Maximum number of incoming peer connections allowed by the node.
This should be greater than `0`. A recommended value is `50`.
If the limit is reached, new incoming peers are rejected during handshake.
### `OUTGOING_CONNECTIONS`
Maximum number of outgoing peer connections the node tries to discover and maintain.
This should be greater than `0`. A recommended value is `10`.
### `THREADS`
Number of async worker tasks used during each mining nonce round.
Contractless mining only searches one byte of nonce space per timestamp, so all workers split the fixed `0..255` nonce range. More threads do not increase the total nonce space; they only split the fixed search range across more tasks.
Allowed values:
- `1`
- `2`
- any multiple of `4`
The value must be between `1` and `256`.
## `[Piggyback]`
Piggyback entries are startup peers. The node tries these peers when it first starts so it can join the network and discover other nodes.
```ini
[Piggyback]
PIGGYBACK_1 = "1.2.3.4:50053"
PIGGYBACK_2 = "5.6.7.8:50053"
```
The node only needs one live piggyback peer to begin discovery. Additional entries are backups if earlier entries are offline or unreachable.
Piggyback entries should be public, routable peers. Private, loopback and invalid addresses are filtered before startup connections begin.
## `[Postgres-Testnet]`
PostgreSQL connection settings used by testnet builds.
```ini
[Postgres-Testnet]
host = 127.0.0.1
port = 5432
user = contractless
password = change_this_password
dbname = contractless_db
```
The configured database must already exist and the configured user must be able to create tables and indexes. The node creates or migrates its own tables on startup.
## `[Postgres]`
PostgreSQL connection settings used by mainnet builds.
```ini
[Postgres]
host = 127.0.0.1
port = 5432
user = contractless
password = change_this_password
dbname = contractless_db
```
Mainnet builds are currently disabled during the testnet phase, but this section is kept so the same settings file can be prepared for launch.

533
docs/TRANSACTIONS.md Normal file
View File

@ -0,0 +1,533 @@
# Contractless Transaction Reference
Contractless transactions are native protocol operations. They are not smart contracts. Each transaction is validated before block inclusion, then the balance-sheet changes are applied when the block is saved. If an orphan correction rolls a block back, the matching balance-sheet operations are undone before replacement blocks are replayed.
This reference explains what each transaction does, which tool creates it, what fee or tip rules apply and how it changes balances.
## Table of Contents
- [Shared Concepts](#shared-concepts)
- [Transaction Type IDs](#transaction-type-ids)
- [Fee Summary](#fee-summary)
- [Transaction Flow](#transaction-flow)
- [Wallet Registration](#wallet-registration)
- [Transaction Types](#transaction-types)
- [Loan Transactions](#loan-transactions)
- [Validation Failures](#validation-failures)
## Shared Concepts
### Addresses
User-facing addresses are network-specific. Mainnet short addresses end with `.clc`; testnet short addresses end with `.cltc`.
Vanity addresses are display addresses. When a normal transaction accepts a vanity address, the node resolves it to the real short address before the transaction is saved in blocks, mempool records or balance-sheet records. The only transaction that stores a vanity address as its own transaction field is the vanity-address registration transaction.
### Timestamps
Each transaction carries its own timestamp. The transaction timestamp records when the transaction was created, not when it was mined. Blocks also carry their own timestamp, which records when the miner discovered and assembled the valid block.
### Fees
Most transactions include a `txfee` field paid in the base network currency. On mainnet that is CLC. On testnet that is CLTC. The fee is removed from the sender and credited to the miner when the transaction is included in a saved block.
Base-currency transfers use a percentage minimum fee:
```text
minimum fee = ceil(value * 1 / 100)
```
Token and NFT transfers use a fixed non-base transfer minimum of 1 CLC or CLTC.
Users may pay more than the minimum fee. Higher fees can improve the chance that a transaction is selected when miners have more valid transactions available than will fit in the next block.
### Token Tips
Some transaction types also include a token tip. A token tip is different from the base-currency transaction fee. It is paid in the asset involved in the transaction and credited to the miner balance sheet for that asset.
Token tips are used by swap transactions and loan payment transactions.
### Signatures
Transactions are signed by the wallet private key. Multi-party transactions require more than one signature. For example, a swap is created by one party, then validated and signed by the second party before broadcast.
### Transaction Hashes
Transaction hashes identify the signed transaction. They are used for lookup, mempool records, duplicate checks and block transaction records.
### Mempool and Block Validation
Broadcast transactions are checked before they are accepted into peer records. They are checked again when a miner attempts to include them in a block. This keeps invalid, stale or already-spent transaction state from being accepted just because it passed an earlier network request.
## Transaction Type IDs
Transaction type bytes are the canonical IDs used in blocks, mempool routing and verification dispatch.
| ID | Transaction | Purpose |
| --- | --- | --- |
| 0 | Genesis | Creates the genesis network record |
| 1 | Rewards | Pays the miner reward |
| 2 | Transfer | Moves CLC, CLTC, tokens or NFTs |
| 3 | Create token | Creates a new token definition |
| 4 | Create NFT | Creates a new NFT definition |
| 5 | Marketing | Creates a marketing record |
| 6 | Swap | Exchanges assets between two parties |
| 7 | Loan | Creates a collateralized loan contract |
| 8 | Loan payment | Pays against an existing loan |
| 9 | Collateral claim | Claims collateral from a defaulted loan |
| 10 | Burn | Burns a token or NFT |
| 11 | Issue token | Issues more supply for an existing token |
| 12 | Vanity address | Registers or updates a vanity address |
## Fee Summary
Fixed fees are stored in the smallest coin unit. The values below are shown as human-readable CLC or CLTC.
| Transaction | Minimum fee |
| --- | --- |
| Base transfer | 1% of transfer value, rounded up |
| Token or NFT transfer | 1 |
| Create token | 500 |
| Create NFT | 0.5 |
| Marketing | 1 |
| Swap | 1 from each party |
| Loan | 3 from lender |
| Loan payment | 0.01 from payer |
| Collateral claim | 3 |
| Burn | 0.0001 |
| Issue token | 100 |
| Vanity address | 5 |
Swap transactions and loan payment transactions may also include token tips paid to the miner in the related asset.
## Transaction Flow
The normal user flow is:
```text
create_* tool -> signed JSON -> broadcast_transaction -> mempool checks -> block validation -> balance-sheet update
```
For two-party transactions:
```text
create_swap_tx/create_loan_tx -> verify_sign_swap_tx/verify_sign_loan_tx -> broadcast_transaction
```
Wallet registration uses `register_wallet` directly because it submits the wallet mapping to a peer instead of creating a normal block transaction JSON file.
## Wallet Registration
Wallet registration is a network registry operation, not a block transaction type. It submits a signed short-address to long-address mapping so peers can resolve and verify wallet identities during later network operations.
Created with:
```text
register_wallet
```
Requires tip:
No.
Minimum fee:
None.
Balance-sheet effect:
None.
Notes:
The command returns `Wallet registered: <short address>` when the peer accepts the mapping.
## Transaction Types
### Genesis
Purpose:
Creates the genesis transaction for the chain.
Created with:
```text
node startup / genesis creation path
```
Requires tip:
No.
Minimum fee:
None.
Balance-sheet effect:
Creates the initial chain state required for block zero.
Notes:
Users do not normally create genesis transactions with a CLI tool.
### Rewards
Purpose:
Pays the miner reward for a saved block.
Created with:
```text
mining process
```
Requires tip:
No.
Minimum fee:
None.
Balance-sheet effect:
Credits the miner base-currency balance sheet with the block reward. During the first 100 blocks mined by a registered miner, the reward transaction is valid but has zero value.
Notes:
Reward transactions are created by the miner, not by a standalone user transaction tool.
### Transfer
Purpose:
Moves base currency, tokens or NFTs from one wallet to another.
Created with:
```text
create_transfer_tx
```
Requires tip:
No.
Minimum fee:
Base-currency transfers pay 1% of the transferred value, rounded up. Token and NFT transfers pay the fixed non-base minimum fee of 1 CLC or CLTC.
Balance-sheet effect:
Removes the transferred value and transaction fee from the sender. Adds the transferred value to the receiver. Adds the transaction fee to the miner.
Notes:
Vanity receiver addresses are resolved to the real short address before the transaction is stored.
### Create Token
Purpose:
Creates a new token definition.
Created with:
```text
create_tokens_tx
```
Requires tip:
No.
Minimum fee:
500 CLC or CLTC.
Balance-sheet effect:
Removes the transaction fee from the creator and credits the fee to the miner. Creates the token record used by later issue-token and transfer transactions.
Notes:
Token names are fixed-length protocol fields. Duplicate token names are rejected.
### Issue Token
Purpose:
Issues additional supply for an existing token.
Created with:
```text
create_issue_token_tx
```
Requires tip:
No.
Minimum fee:
100 CLC or CLTC.
Balance-sheet effect:
Removes the transaction fee from the issuer and credits the fee to the miner. Credits the issued token amount to the issuer if the token rules allow more supply.
Notes:
Hard-limited tokens cannot be issued past their allowed supply.
### Create NFT
Purpose:
Creates a new NFT definition. This can be a 1/1 NFT or a series NFT.
Created with:
```text
create_nft_tx
```
Requires tip:
No.
Minimum fee:
0.5 CLC or CLTC.
Balance-sheet effect:
Removes the transaction fee from the creator and credits the fee to the miner. Creates the NFT record and credits the NFT ownership state to the creator.
Notes:
Use series number `0` for a 1/1 NFT in lookup tooling.
### Burn
Purpose:
Destroys a token or NFT balance.
Created with:
```text
create_burn_tx
```
Requires tip:
No.
Minimum fee:
0.0001 CLC or CLTC.
Balance-sheet effect:
Removes the burned asset from the burner. Removes the base-currency transaction fee from the burner and credits the fee to the miner.
Notes:
Base currency cannot be burned with this transaction type.
### Marketing
Purpose:
Creates a marketing record with campaign metadata, impression value and click value.
Created with:
```text
create_marketing_tx
```
Requires tip:
No.
Minimum fee:
1 CLC or CLTC.
Balance-sheet effect:
Removes the transaction fee from the advertiser and credits the fee to the miner. Stores the marketing record for later lookup.
Notes:
The marketing record stores its own transaction data. It does not move a token or NFT balance to another wallet.
### Vanity Address
Purpose:
Registers or updates the wallet vanity address mapping.
Created with:
```text
create_vanity_tx
```
Requires tip:
No.
Minimum fee:
5 CLC or CLTC.
Balance-sheet effect:
Removes the transaction fee from the registering wallet and credits the fee to the miner. Registers the vanity address mapping so future user-facing input can resolve to the wallet short address.
Notes:
The vanity-address transaction is the only normal transaction type that stores the vanity address itself. Other transactions store the resolved short address.
### Swap
Purpose:
Exchanges assets between two wallets.
Created with:
```text
create_swap_tx
verify_sign_swap_tx <path/to/file.json>
```
Requires tip:
Yes. Swap transactions include `tip1` and `tip2`, paid in the assets being swapped.
Minimum fee:
1 CLC or CLTC from each party.
Balance-sheet effect:
Removes each offered asset from its sender and credits it to the other party. Removes each party's base-currency transaction fee and credits those fees to the miner. Removes token tips from the parties and credits the tips to the miner in the related assets.
Notes:
The first party creates the swap JSON. The second party validates and signs it before broadcast.
## Loan Transactions
Loan, loan payment and collateral claim transactions are related. The loan transaction creates the contract terms. Loan payment transactions pay against that contract. Collateral claim transactions enforce the collateral path when the contract rules allow it.
### Loan
Purpose:
Creates a collateralized loan contract between lender and borrower.
Created with:
```text
create_loan_tx
verify_sign_loan_tx <path/to/file.json>
```
Requires tip:
No.
Minimum fee:
3 CLC or CLTC paid by the lender.
Balance-sheet effect:
Moves or locks the relevant loan and collateral values according to the contract terms. Removes the lender transaction fee and credits it to the miner.
Notes:
The first party creates the loan JSON. The second party validates and signs it before broadcast.
### Loan Payment
Purpose:
Pays against an existing loan contract.
Created with:
```text
create_loan_payment_tx
```
Requires tip:
Yes. Loan payment transactions include a miner tip in the loan token.
Minimum fee:
0.01 CLC or CLTC.
Balance-sheet effect:
Removes the payment amount, token tip and base-currency transaction fee from the payer. Credits the payment to the loan receiver according to the contract. Credits the base fee and token tip to the miner.
Notes:
Loan payments must match the active loan contract state.
### Collateral Claim
Purpose:
Claims collateral from a loan contract when the contract rules allow it.
Created with:
```text
create_collateral_claim_tx
```
Requires tip:
No.
Minimum fee:
3 CLC or CLTC.
Balance-sheet effect:
Moves eligible collateral to the claimant according to the contract. Removes the base-currency transaction fee from the claimant and credits the fee to the miner.
Notes:
Collateral claims fail when the loan state does not allow collateral to be claimed.
## Validation Failures
Common validation failures include:
- Invalid network address.
- Invalid vanity address mapping.
- Invalid transaction signature.
- Missing second signature on a two-party transaction.
- Transaction fee below the required minimum.
- Insufficient base-currency balance for fees.
- Insufficient token or NFT balance for the asset being moved.
- Duplicate token, NFT or vanity registration.
- Token supply rules reject the requested issue amount.
- Loan contract state does not match the payment or collateral claim.
- Transaction has already been accepted or conflicts with current mempool state.
When a transaction fails validation, fix the transaction fields or wallet state before broadcasting again.

39
settings.ini Normal file
View File

@ -0,0 +1,39 @@
; settings.ini
[Paths]
BLOCK_PATH = "./blocks"
TORRENT_PATH = "./torrents"
DB_PATH = "./state"
BALANCE_SHEET = "./balance_sheet"
LOG_PATH = "./logs"
WALLET_PATH = "/home/viraladmin/chatgpt/wallets"
WALLET_NAME = "contractless.wallet"
[Settings]
LOG_LEVEL = "disabled"
PUBLIC_IP = "your_public_ip_address"
LISTEN_IP = "0.0.0.0"
RPC_PORT = "50055"
TESTNET_RPC_PORT = "50050"
INCOMING_CONNECTIONS = "100"
OUTGOING_CONNECTIONS = "10"
; THREADS must 1, 2, or a multple of 4
THREADS = "8"
[Piggyback]
PIGGYBACK_1 = "contractless.dev:50050"
[Postgres-Testnet]
host = 127.0.0.1
port = 5432
user = contractless2
password = your_paddword
dbname = contractless_db2
[Postgres]
host = 127.0.0.1
port = 5432
user = contractless
password = your_password
dbname = contractless_db

View File

@ -0,0 +1,94 @@
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::env;
use blockchain::fs;
use blockchain::Duration;
use blockchain::Error;
fn calculate_average_time_between_timestamps(
start_block: u32,
stop_block: u32,
directory: &str,
) -> Result<Option<Duration>, Box<dyn Error>> {
let mut timestamps: Vec<u32> = Vec::new();
// Iterate over all files in the directory
for entry in fs::read_dir(directory)? {
let entry = entry?;
let path = entry.path();
let (
_network_name,
_padded_base_coin,
extension,
_torrentpath,
_wallet_path,
_blockpath,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
if path.is_file() && path.extension().is_some_and(|ext| *ext == *extension) {
if let Some(file_name) = path.file_stem() {
if let Some(file_name_str) = file_name.to_str() {
if let Ok(block_number) = file_name_str.parse::<u32>() {
if block_number >= start_block && block_number <= stop_block {
let file_content = fs::read(&path)?;
if file_content.len() >= 4 {
let timestamp_bytes = &file_content[0..4];
let timestamp = u32::from_le_bytes(timestamp_bytes.try_into()?);
timestamps.push(timestamp);
}
}
}
}
}
}
}
// Sort the timestamps
timestamps.sort();
// Calculate the differences between consecutive timestamps
let mut time_differences: Vec<Duration> = Vec::new();
for i in 1..timestamps.len() {
let time_difference = timestamps[i] - timestamps[i - 1];
time_differences.push(Duration::from_secs(time_difference as u64));
}
// Calculate the average time difference
if !time_differences.is_empty() {
let total_duration: Duration = time_differences.iter().sum();
let average_time = total_duration / (time_differences.len() as u32);
Ok(Some(average_time))
} else {
Ok(None)
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Collect command-line arguments
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
println!("Usage: {} <start_block> <stop_block> <directory>", args[0]);
return Ok(());
}
let start_block: u32 = args[1].parse()?;
let stop_block: u32 = args[2].parse()?;
let directory = &args[3];
match calculate_average_time_between_timestamps(start_block, stop_block, directory)? {
Some(average_time) => {
println!(
"The average time between timestamps is {:.2} seconds.",
average_time.as_secs_f64()
);
}
None => {
println!("Not enough data to calculate an average time difference.");
}
}
Ok(())
}

View File

@ -0,0 +1,149 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::from_slice;
use blockchain::read;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::wallets::structures::{SavedWallet, Wallet};
use blockchain::Path;
use serde_json::Value;
fn display_vanity_address(short_address: &str) -> Option<String> {
// Wallet display should hide the left padding used by vanity address bytes.
let (payload, network_suffix) = short_address.rsplit_once('.')?;
let trimmed_payload = payload.trim_start();
if trimmed_payload.is_empty() {
return None;
}
Some(format!("{trimmed_payload}.{network_suffix}"))
}
async fn persist_local_vanity_address(encryption_key: &str, tx_json: &str) {
// Only successful vanity transactions update the local saved wallet display field.
let Ok(value) = serde_json::from_str::<Value>(tx_json) else {
return;
};
let txtype = value
.get("txtype")
.and_then(|v| v.as_u64())
.unwrap_or_default() as u8;
if txtype != 12 {
return;
}
let Some(vanity_address) = value.get("vanity_address").and_then(|v| v.as_str()) else {
return;
};
let Some(sender_address) = value.get("address").and_then(|v| v.as_str()) else {
return;
};
// Read the saved wallet and only update it if it matches the vanity transaction sender.
let wallet_path = Wallet::get_wallet_path().await;
let wallet_bytes = match read(Path::new(&wallet_path)).await {
Ok(bytes) => bytes,
Err(_) => return,
};
let mut saved_wallet: SavedWallet = match from_slice(&wallet_bytes) {
Ok(wallet) => wallet,
Err(_) => return,
};
let _ = encryption_key;
if saved_wallet.short_address.trim() != sender_address.trim() {
return;
}
let Some(display_vanity) = display_vanity_address(vanity_address) else {
return;
};
saved_wallet.vanity_address = Some(display_vanity);
saved_wallet.save_the_wallet(Path::new(&wallet_path)).await;
}
#[tokio::main]
async fn main() {
// Broadcast takes one signed transaction JSON file and submits it to a peer.
let hashmap_key = generate_uid();
let args: Vec<String> = env::args().collect();
let filename = match args.get(1) {
Some(filename) => filename,
None => {
eprintln!("Usage: {} <hash.json>", args[0]);
return;
}
};
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Read the signed transaction JSON exactly as it will be serialized for broadcast.
let json = match tokio::fs::read_to_string(filename).await {
Ok(content) => content,
Err(err) => {
eprintln!("Error reading file: {err}");
return;
}
};
let txid = serde_json::from_str::<Value>(&json).ok().and_then(|value| {
value
.get("hash")
.and_then(|hash| hash.as_str())
.map(|hash| hash.to_string())
});
// Command 8 tells sending_request to build a transaction broadcast request.
let rpc_command = 8;
// Try each configured peer until one accepts or returns a response.
let connections = get_connections().await;
let mut connected: bool = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
// When a vanity tx broadcasts successfully, keep the local wallet display in sync.
if trimmed == "successful_broadcast: true" {
persist_local_vanity_address(&encryption_key, &json).await;
if let Some(hash) = &txid {
println!("{hash}");
}
}
connected = true;
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,86 @@
#[cfg(windows)]
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
#[cfg(windows)]
use blockchain::startup::unlock_pipe::pipe_name;
#[cfg(windows)]
use blockchain::startup::unlock_structs::{UnlockPipeRequest, UnlockPipeResponse};
#[cfg(windows)]
use blockchain::{env, from_slice, to_string};
#[cfg(windows)]
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(windows)]
use tokio::net::windows::named_pipe::ClientOptions;
#[cfg(windows)]
#[tokio::main]
async fn main() {
// Keep the Windows-only service helper small: run the pipe request and print any failure.
if let Err(err) = run().await {
eprintln!("{err}");
std::process::exit(1);
}
}
#[cfg(windows)]
async fn run() -> Result<(), Box<dyn std::error::Error>> {
// The helper supports simple service checks as well as submitting the wallet key.
let command = env::args().nth(1).unwrap_or_else(|| "submit".to_string());
let request = match command.as_str() {
"ping" => UnlockPipeRequest::Ping,
"status" => UnlockPipeRequest::Status,
"submit" => {
// Wallet keys are hidden at the terminal and rejected if the user enters nothing.
let wallet_key = prompt_hidden_nonempty(
"Please enter your wallet key: ",
"Wallet key cannot be empty. Please try again.",
)
.await;
UnlockPipeRequest::SubmitKey { wallet_key }
}
other => {
return Err(format!(
"Unsupported command '{other}'. Use 'submit', 'ping', or 'status'."
)
.into())
}
};
let pipe = pipe_name();
let payload = to_string(&request)?;
// The service unlock pipe uses a length-prefixed JSON payload.
let mut client = ClientOptions::new().open(&pipe)?;
client.write_u32_le(payload.len() as u32).await?;
client.write_all(payload.as_bytes()).await?;
// Read the length-prefixed service reply and decode the response enum.
let response_len = client.read_u32_le().await? as usize;
let mut response_bytes = vec![0u8; response_len];
client.read_exact(&mut response_bytes).await?;
let response: UnlockPipeResponse = from_slice(&response_bytes)?;
match response {
UnlockPipeResponse::Pong => {
println!("Pong");
}
UnlockPipeResponse::Status { state } => {
println!("Service state: {state:?}");
}
UnlockPipeResponse::KeyAccepted => {
println!("Wallet key accepted.");
}
UnlockPipeResponse::Error { message } => {
return Err(message.into());
}
}
Ok(())
}
#[cfg(not(windows))]
fn main() {
// Non-Windows builds keep the binary present but clearly report that the pipe does not exist.
eprintln!("contractless-submit-key is only supported on Windows.");
std::process::exit(1);
}

147
src/bin/create_burn_tx.rs Normal file
View File

@ -0,0 +1,147 @@
use blockchain::blocks::burn::{BurnTransaction, UnsignedBurnTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::common::types::BURN_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
// Pad the asset name so it matches the 15-byte on-chain asset format.
fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width);
let _ = std::fmt::write(
&mut result,
format_args!("{input:<width$}"),
);
result
}
fn display_fee(value: u64) -> f64 {
// Fees are stored as atomic units and displayed as whole-coin decimals for the CLI.
value as f64 / 100_000_000.0
}
#[tokio::main]
async fn main() {
// Burn transactions use type 10 and the current local timestamp.
let txtype = 10;
let timestamp = Utc::now().timestamp() as u32;
// Burnable assets are tokens or NFTs, never the base coin for this transaction type.
let coin_name = prompt_visible("Please enter the token or NFT name you wish to burn: ").await;
let coin = pad_to_width(&coin_name.trim().to_lowercase(), 15);
if coin.trim().to_lowercase() == block_extension_and_paths().1.trim().to_lowercase() {
println!("Base coin cannot be burned with this transaction type.");
return;
}
// Series 0 is used for fungible tokens and 1/1 NFTs; series NFTs use their item number.
let nft_series_string = prompt_visible(
"Please enter the NFT series number to burn, or 0 for standard tokens / 1of1 NFTs: ",
)
.await;
let nft_series: u32 = nft_series_string
.trim()
.parse()
.expect("Please enter a valid NFT series number.");
// Values are entered as display units and saved as atomic units.
let value_string = prompt_visible("Please enter the burn amount: (use 1.0 for NFTs) ").await;
let value_f64: f64 = value_string
.trim()
.parse()
.expect("Please enter a valid amount.");
let value = (value_f64 * 100_000_000.0).round() as u64;
let txfee_prompt = format!(
"Please enter the amount for the fee: (eg. 0.0001, minimum fee {:.8})",
display_fee(BURN_FEE)
);
let txfee_string = prompt_visible(&format!("{txfee_prompt}: ")).await;
let txfee_f64: f64 = txfee_string
.trim()
.parse()
.expect("Please enter a valid fee.");
let txfee = (txfee_f64 * 100_000_000.0).round() as u64;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Load the wallet that owns the asset being burned.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let address = &wallet.saved.short_address;
// The burn address must be a valid current-network short address.
if !Wallet::short_address_validation(address.trim_matches('"')) {
println!("burner wallet invalid: {}", &address);
return;
}
// Build and sign the burn transaction before writing the broadcast JSON.
let unsigned_burn = UnsignedBurnTransaction::new(
txtype,
timestamp,
address.trim_matches('"'),
&coin,
nft_series,
value,
txfee,
)
.await;
let burn = match BurnTransaction::new(unsigned_burn, private_key).await {
Ok(tx) => tx,
Err(err) => {
eprintln!("Failed to sign burn transaction: {err}");
return;
}
};
let hashed_data = burn.unsigned_burn.hash().await;
let signature = burn.signature.clone();
// Save the signed transaction as JSON so broadcast_transaction can submit it later.
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"address": address.trim_matches('"'),
"coin": coin,
"nft_series": nft_series,
"value": value,
"txfee": txfee,
"hash": hashed_data,
"signature": signature,
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Save the transaction JSON into the standard transactions folder for broadcasting.
let dir_path = "./transactions";
if let Err(error) = create_dir_all(&dir_path).await {
eprintln!("Failed to create directory: {error}");
return;
}
let file_path = format!("{dir_path}/{hashed_data}.json");
let mut file = File::create(&file_path)
.await
.expect("Failed to create file");
if let Err(error) = file.write_all(output_str.as_bytes()).await {
eprintln!("Failed to write file: {error}");
return;
}
println!("transaction: {output_str}");
}

View File

@ -0,0 +1,117 @@
use blockchain::blocks::collateral::{
CollateralClaimTransaction, UnsignedCollateralClaimTransaction,
};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::types::COLLATERAL_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
fn display_fee(value: u64) -> f64 {
// Fees are stored as atomic units and displayed as whole-coin decimals for the CLI.
value as f64 / 100_000_000.0
}
#[tokio::main]
async fn main() {
// Collateral claims use transaction type 9 and the current local timestamp.
let txtype = 9;
let timestamp = Utc::now().timestamp() as u32;
// The contract hash identifies which loan contract collateral is being claimed.
let contract_hash = prompt_visible("Please enter the loan contract hash: ").await;
let contract_hash = contract_hash.trim().to_string();
// Store the fee in atomic units so the JSON can be signed and broadcast directly.
let txfee_prompt = format!(
"Please enter the amount for the fee: (eg. 1.0, minimum fee {:.8})",
display_fee(COLLATERAL_FEE)
);
let txfee_string = prompt_visible(&format!("{txfee_prompt}: ")).await;
let txfee_f64: f64 = txfee_string
.trim()
.parse()
.expect("Please enter a valid fee");
let txfee = (txfee_f64 * 100_000_000.0).round() as u64;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Load the wallet so the transaction can use the saved short address and private key.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let address = &wallet.saved.short_address;
// Only a valid current-network short address should be written into the transaction.
if !Wallet::short_address_validation(address.trim_matches('"')) {
println!("wallet invalid: {address}");
return;
}
// Build the unsigned body first so the signed transaction hash matches validation logic.
let unsigned_claim = UnsignedCollateralClaimTransaction::new(
txtype,
timestamp,
&contract_hash,
address.trim_matches('"'),
txfee,
)
.await;
// Signing attaches the wallet signature that miners verify before accepting the claim.
let claim = match CollateralClaimTransaction::new(unsigned_claim, private_key).await {
Ok(tx) => tx,
Err(err) => {
eprintln!("Failed to sign collateral claim transaction: {err}");
return;
}
};
let hash = claim.unsigned_collateral_claim.hash().await;
let signature = claim.signature.clone();
// Save the signed transaction as JSON so broadcast_transaction can submit it later.
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"contract_hash": contract_hash,
"address": address.trim_matches('"'),
"txfee": txfee,
"hash": hash,
"signature": signature,
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Transaction creation tools all write into ./transactions using the transaction hash.
let dir_path = "./transactions";
if let Err(e) = create_dir_all(&dir_path).await {
eprintln!("Failed to create directory: {e}");
return;
}
let file_path = format!(
"{}/{}.json",
dir_path,
output["hash"].as_str().unwrap_or_default()
);
let mut file = File::create(&file_path)
.await
.expect("Failed to create file");
if let Err(e) = file.write_all(output_str.as_bytes()).await {
eprintln!("Failed to write file: {e}");
return;
}
println!("transaction: {output_str}");
}

View File

@ -0,0 +1,131 @@
use blockchain::blocks::issue_token::{IssueTokenTransaction, UnsignedIssueTokenTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::types::ISSUE_TOKEN_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
// Pad the ticker so it matches the fixed-width 15-byte on-chain format.
fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width);
let _ = std::fmt::write(
&mut result,
format_args!("{input:<width$}"),
);
result
}
fn display_fee(value: u64) -> f64 {
// Fees are stored as atomic units and displayed as whole-coin decimals for the CLI.
value as f64 / 100_000_000.0
}
#[tokio::main]
async fn main() {
// Issue-token transactions use type 11 and the current local timestamp.
let txtype = 11;
let timestamp = Utc::now().timestamp() as u32;
// The ticker is stored as a fixed-width 15-byte asset field.
let ticker_name =
prompt_visible("Please enter the ticker / token name you wish to issue more of: ").await;
let ticker = pad_to_width(&ticker_name.trim().to_lowercase(), 15);
// Token amounts are entered as whole tokens and stored as atomic units.
let number_string =
prompt_visible("Please enter the amount of additional tokens to issue: ").await;
let number_u64: u64 = number_string
.trim()
.parse()
.expect("Please enter a valid token amount.");
let number = ((number_u64 as f64) * 100_000_000.0).round() as u64;
let txfee_prompt = format!(
"Please enter the amount for the fee: (eg. 0.0001, minimum fee {:.8})",
display_fee(ISSUE_TOKEN_FEE)
);
let txfee_string = prompt_visible(&format!("{txfee_prompt}: ")).await;
let txfee_f64: f64 = txfee_string
.trim()
.parse()
.expect("Please enter a valid fee.");
let txfee = (txfee_f64 * 100_000_000.0).round() as u64;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Load the creator wallet that signs the additional token issuance.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let creator = &wallet.saved.short_address;
// The creator address must be a valid current-network short address.
if !Wallet::short_address_validation(creator.trim_matches('"')) {
println!("creator wallet invalid: {creator}");
return;
}
// Build and sign the issue-token transaction before writing the broadcast JSON.
let unsigned_issue_token = UnsignedIssueTokenTransaction::new(
txtype,
timestamp,
creator.trim_matches('"'),
&ticker,
number,
txfee,
)
.await;
let issue_token = match IssueTokenTransaction::new(unsigned_issue_token, private_key).await {
Ok(tx) => tx,
Err(err) => {
eprintln!("Failed to sign issue-token transaction: {err}");
return;
}
};
let hashed_data = issue_token.unsigned_issue_token.hash().await;
let signature = issue_token.signature.clone();
// Save the signed transaction as JSON so broadcast_transaction can submit it later.
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"creator": creator.trim_matches('"'),
"ticker": ticker,
"number": number,
"txfee": txfee,
"hash": hashed_data,
"signature": signature,
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Save the transaction JSON into the standard transactions folder for broadcasting.
let dir_path = "./transactions";
if let Err(error) = create_dir_all(&dir_path).await {
eprintln!("Failed to create directory: {error}");
return;
}
let file_path = format!("{dir_path}/{hashed_data}.json");
let mut file = File::create(&file_path)
.await
.expect("Failed to create file");
if let Err(error) = file.write_all(output_str.as_bytes()).await {
eprintln!("Failed to write file: {error}");
return;
}
println!("transaction: {output_str}");
}

View File

@ -0,0 +1,139 @@
use blockchain::blocks::loan_payment::{
ContractPaymentTransaction, UnsignedContractPaymentTransaction,
};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::types::BORROWER_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
fn display_fee(value: u64) -> f64 {
// Fees are stored as atomic units and displayed as whole-coin decimals for the CLI.
value as f64 / 100_000_000.0
}
#[tokio::main]
async fn main() {
// Loan payments use transaction type 8 and the current local timestamp.
let txtype = 8;
let timestamp = Utc::now().timestamp() as u32;
// The contract hash ties this payment to the loan contract being repaid.
let contract_hash = prompt_visible("Please enter the loan contract hash: ").await;
let contract_hash = contract_hash.trim().to_string();
// Payment amount and miner tip are entered as coin values and stored as atomic units.
let payback_amount_string =
prompt_visible("Please enter the payment amount: (eg. 12.5): ").await;
let payback_amount_f64: f64 = payback_amount_string
.trim()
.parse()
.expect("Please enter a valid amount");
let payback_amount = (payback_amount_f64 * 100_000_000.0).round() as u64;
let tip_string =
prompt_visible("Please enter the miner tip: (must be at least 1% of payment amount): ")
.await;
let tip_f64: f64 = tip_string
.trim()
.parse()
.expect("Please enter a valid amount");
let tip = (tip_f64 * 100_000_000.0).round() as u64;
// Borrower fee is also stored in atomic units for signing and block serialization.
let txfee_prompt = format!(
"Please enter the amount for the fee: (eg. 0.001, minimum fee {:.8})",
display_fee(BORROWER_FEE)
);
let txfee_string = prompt_visible(&format!("{txfee_prompt}: ")).await;
let txfee_f64: f64 = txfee_string
.trim()
.parse()
.expect("Please enter a valid fee");
let txfee = (txfee_f64 * 100_000_000.0).round() as u64;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Load the wallet so the transaction can use the saved short address and private key.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let address = &wallet.saved.short_address;
// Payments must use a valid current-network short address.
if !Wallet::short_address_validation(address.trim_matches('"')) {
println!("wallet invalid: {address}");
return;
}
// Build the unsigned payment body before signing it with the wallet private key.
let unsigned_payment = UnsignedContractPaymentTransaction::new(
txtype,
timestamp,
payback_amount,
&contract_hash,
address.trim_matches('"'),
tip,
txfee,
)
.await;
// The signed form adds the transaction hash and signature required by verification.
let payment = match ContractPaymentTransaction::new(unsigned_payment, private_key).await {
Ok(tx) => tx,
Err(err) => {
eprintln!("Failed to sign loan payment transaction: {err}");
return;
}
};
let hash = payment.hash.clone();
let signature = payment.signature.clone();
// Save the signed transaction as JSON so broadcast_transaction can submit it later.
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"payback_amount": payback_amount,
"contract_hash": contract_hash,
"address": address.trim_matches('"'),
"tip": tip,
"txfee": txfee,
"hash": hash,
"signature": signature,
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Transaction creation tools all write into ./transactions using the transaction hash.
let dir_path = "./transactions";
if let Err(e) = create_dir_all(&dir_path).await {
eprintln!("Failed to create directory: {e}");
return;
}
let file_path = format!(
"{}/{}.json",
dir_path,
output["hash"].as_str().unwrap_or_default()
);
let mut file = File::create(&file_path)
.await
.expect("Failed to create file");
if let Err(e) = file.write_all(output_str.as_bytes()).await {
eprintln!("Failed to write file: {e}");
return;
}
println!("transaction: {output_str}");
}

250
src/bin/create_loan_tx.rs Normal file
View File

@ -0,0 +1,250 @@
use blockchain::blocks::loans::UnsignedLoanContractTransaction;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::types::LENDER_FEE;
use blockchain::json;
use blockchain::records::wallet_registry::resolve_local_input_short_address;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::{create_dir_all, AsyncWriteExt};
use blockchain::{Local, LocalResult, NaiveDate, NaiveTime, TimeZone};
fn pad_to_width(input: &str, width: usize) -> String {
// Asset names are fixed-width fields in the loan transaction bytes.
let mut result = String::with_capacity(width);
let _ = std::fmt::write(
&mut result,
format_args!("{input:<width$}"),
);
result
}
fn display_fee(value: u64) -> f64 {
// Fees are stored as atomic units and displayed as whole-coin decimals for the CLI.
value as f64 / 100_000_000.0
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
// Accept local vanity/short input and resolve it into the real short address.
resolve_local_input_short_address(address.trim())
}
fn parse_start_date(input: &str) -> Result<u32, String> {
// Loan start dates are entered as calendar dates and stored as local midnight timestamps.
let date = NaiveDate::parse_from_str(input.trim(), "%Y-%m-%d")
.map_err(|_| "Please enter the date in YYYY-MM-DD format.".to_string())?;
let datetime = date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap());
let timestamp = match Local.from_local_datetime(&datetime) {
LocalResult::Single(dt) => dt.timestamp(),
LocalResult::Ambiguous(dt, _) => dt.timestamp(),
LocalResult::None => {
return Err("Loan start date could not be resolved in local time.".to_string());
}
};
if timestamp < 0 || timestamp > u32::MAX as i64 {
return Err("Loan start date is out of range.".to_string());
}
Ok(timestamp as u32)
}
#[tokio::main]
async fn main() {
// Loan contracts use transaction type 7 and are first signed by the lender.
let txtype = 7;
// Coin/token and collateral names are padded to fixed-width transaction fields.
let loan_coin_name = prompt_visible("What coin or token will you be lending out? ").await;
let loan_coin_name = loan_coin_name.trim().to_lowercase();
let loan_coin = pad_to_width(&loan_coin_name, 15);
let loan_amount_string = prompt_visible("How many coins or tokens will you be lending? ").await;
let loan_amount_f64: f64 = loan_amount_string
.trim()
.parse()
.expect("Please enter a valid amount");
let loan_amount = (loan_amount_f64 * 100_000_000.0).round() as u64;
// Payment cadence is stored as a compact one-letter code.
let payment_period =
prompt_visible("Would you like each payment period to be daily, weekly, or monthly? ")
.await;
let payment_period = match payment_period.trim().to_lowercase().as_str() {
"daily" | "d" => "d".to_string(),
"weekly" | "w" => "w".to_string(),
"monthly" | "m" => "m".to_string(),
_ => {
println!("Payment period must be daily, weekly, or monthly.");
return;
}
};
let payment_number_string =
prompt_visible("How many payments do you expect until the loan is considered paid? ").await;
let payment_number: u8 = payment_number_string
.trim()
.parse()
.expect("Please enter a valid number");
let payment_amount_string = prompt_visible("What should be the amount of each payment? ").await;
let payment_amount_f64: f64 = payment_amount_string
.trim()
.parse()
.expect("Please enter a valid amount");
let payment_amount = (payment_amount_f64 * 100_000_000.0).round() as u64;
let grace_period_string =
prompt_visible("How many payments can be missed before you will claim the collateral? ")
.await;
let grace_period: u8 = grace_period_string
.trim()
.parse()
.expect("Please enter a valid number");
let max_late_value_string = prompt_visible(
"How far behind in value can the borrower be before you will claim the collateral? ",
)
.await;
let max_late_value_f64: f64 = max_late_value_string
.trim()
.parse()
.expect("Please enter a valid amount");
let max_late_value = (max_late_value_f64 * 100_000_000.0).round() as u64;
// Collateral can be a coin, token, or NFT name; NFT collateral uses amount 1.
let collateral_name =
prompt_visible("What collateral coin, token, or NFT will you be accepting for this loan? ")
.await;
let collateral_name = collateral_name.trim().to_lowercase();
let collateral = pad_to_width(&collateral_name, 15);
let collateral_amount_string = prompt_visible("How many collateral coins or tokens will you be accepting for this loan (enter 1 for NFT collateral)? ").await;
let collateral_amount_f64: f64 = collateral_amount_string
.trim()
.parse()
.expect("Please enter a valid amount");
let collateral_amount = (collateral_amount_f64 * 100_000_000.0).round() as u64;
let start_date =
prompt_visible("What is the date this loan should begin? (YYYY-MM-DD): ").await;
let timestamp = match parse_start_date(&start_date) {
Ok(timestamp) => timestamp,
Err(err) => {
println!("{err}");
return;
}
};
// Resolve the borrower input before writing it into the loan offer.
let borrower = prompt_visible("What is the wallet address of the borrower? ").await;
let borrower = match normalize_short_address_input(borrower.trim()) {
Ok(address) => address,
Err(_) => {
println!("borrower wallet invalid");
return;
}
};
let txfee_prompt = format!(
"Enter the txfee you will be paying for this transaction (minimum: {:.8})",
display_fee(LENDER_FEE)
);
let txfee_string = prompt_visible(&format!("{txfee_prompt}: ")).await;
let txfee_f64: f64 = txfee_string
.trim()
.parse()
.expect("Please enter a valid fee");
let txfee = (txfee_f64 * 100_000_000.0).round() as u64;
// Load the lender wallet that creates the first loan-contract signature.
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let lender = &wallet.saved.short_address;
// The lender must sign with a valid current-network short address.
if !Wallet::short_address_validation(lender.trim_matches('"')) {
println!("lender wallet invalid: {lender}");
return;
}
// Build the unsigned loan so both lender and borrower sign the exact same hash.
let unsigned_loan = UnsignedLoanContractTransaction::new(
txtype,
timestamp,
&loan_coin,
loan_amount,
lender.trim_matches('"'),
&collateral,
collateral_amount,
&borrower,
&payment_period,
payment_number,
payment_amount,
grace_period,
max_late_value,
txfee,
)
.await;
// This first signature is the lender's offer; the borrower adds signature2 later.
let (hash, signature1) = match unsigned_loan.hash_and_sign(private_key).await {
Ok(value) => value,
Err(err) => {
eprintln!("Failed to sign loan contract: {err}");
return;
}
};
// Save the partially signed loan JSON for the borrower to review and sign.
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"loan_coin": loan_coin,
"loan_amount": loan_amount,
"lender": lender.trim_matches('"'),
"collateral": collateral,
"collateral_amount": collateral_amount,
"borrower": borrower,
"payment_period": payment_period,
"payment_number": payment_number,
"payment_amount": payment_amount,
"grace_period": grace_period,
"max_late_value": max_late_value,
"txfee": txfee,
"hash": hash,
"signature1": signature1,
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Transaction creation tools all write into ./transactions using the transaction hash.
let dir_path = "./transactions";
if let Err(e) = create_dir_all(&dir_path).await {
eprintln!("Failed to create directory: {e}");
return;
}
let file_path = format!(
"{}/{}.json",
dir_path,
output["hash"].as_str().unwrap_or_default()
);
let mut file = File::create(&file_path)
.await
.expect("Failed to create file");
if let Err(e) = file.write_all(output_str.as_bytes()).await {
eprintln!("Failed to write file: {e}");
return;
}
println!("transaction: {output_str}");
}

View File

@ -0,0 +1,179 @@
use blockchain::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::types::MARKETING_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write(
&mut result,
format_args!("{input:<width$}"),
);
result
}
fn display_fee(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
#[tokio::main]
async fn main() {
let campaign_data = prompt_visible("What is the id number of your campaign? ").await;
let campaign: u64 = campaign_data
.trim()
.parse()
.expect("Please enter a valid number");
let ad_type_input =
prompt_visible("What type of campaign is this? banner, social, or text: ").await;
let ad_type = pad_to_width(ad_type_input.trim(), 6);
let keyword_input = prompt_visible("Enter a keyword, can be up to 40 characters: ").await;
let keyword = pad_to_width(keyword_input.trim(), 40);
let displayed_input =
prompt_visible("Enter the location the ad was displayed, can be up to 100 characters: ")
.await;
let displayed = pad_to_width(displayed_input.trim(), 100);
let impression_input = prompt_visible("How many times was the ad viewed? (0 to 255): ").await;
let impression: u8 = impression_input
.trim()
.parse()
.expect("Please enter a valid number");
let click_input = prompt_visible("How many times was the ad clicked? (0 to 255): ").await;
let click: u8 = click_input
.trim()
.parse()
.expect("Please enter a valid number");
let impression_value_data =
prompt_visible("What is the value of each impression? (1.25): ").await;
let impression_value: f32 = impression_value_data
.trim()
.parse()
.expect("Please enter a valid number");
let impression_value = (impression_value * 100.0) as u16;
let click_value_data = prompt_visible("What is the value of each click? (1.25): ").await;
let click_value: f32 = click_value_data
.trim()
.parse()
.expect("Please enter a valid number");
let click_value = (click_value * 100.0) as u16;
let txfee_prompt = format!(
"Please enter the amount for the fee: (e.g., 0.001, minimum fee {:.8})",
display_fee(MARKETING_FEE)
);
let txfee_string = prompt_visible(&format!("{txfee_prompt}: ")).await;
let txfee_f32: f32 = txfee_string
.trim()
.parse()
.expect("Please enter a valid value.");
let txfee = ((txfee_f32 as f64) * (100000000_f64)).round() as u64;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let address = &wallet.saved.short_address;
// Set type and timestamp
let txtype = 5;
let timestamp = Utc::now().timestamp() as u32;
// Validate wallets
if !Wallet::short_address_validation(address.trim_matches('"')) {
println!("creator wallet invalid: {}", &address);
return;
}
let unsigned_marketing = UnsignedMarketingTransaction::new(
txtype,
timestamp,
campaign,
&ad_type,
&keyword,
&displayed,
impression,
click,
impression_value,
click_value,
address.trim_matches('"'),
txfee,
)
.await;
let marketing = match MarketingTransaction::new(unsigned_marketing, private_key).await {
Ok(tx) => tx,
Err(err) => {
eprintln!("Failed to sign marketing transaction: {err}");
return;
}
};
let hashed_data = marketing.unsigned_marketing.hash().await;
let signature = marketing.signature.clone();
// Add the signature and hash
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"campaign": campaign,
"ad_type": ad_type,
"keyword": keyword,
"displayed": displayed,
"impression": impression,
"click": click,
"impression_value": impression_value,
"click_value": click_value,
"advertiser": address.trim_matches('"'),
"txfee": txfee,
"hash": hashed_data,
"signature": signature
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Define the directory path
let dir_path = "./transactions";
// Create the directory if it doesn't exist
if let Err(e) = create_dir_all(&dir_path).await {
eprintln!("Failed to create directory: {e}");
return;
}
// Define the file path
let file_path = format!("{dir_path}/{hashed_data}.json");
// Open the file for writing asynchronously
let mut file = File::create(&file_path)
.await
.expect("Failed to create file");
// Write the JSON string to the file asynchronously
if let Err(e) = file.write_all(output_str.as_bytes()).await {
eprintln!("Failed to write file: {e}");
return;
}
println!("transaction: {output_str}");
}

View File

@ -0,0 +1,59 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::env;
use blockchain::metadata;
use blockchain::wallets::structures::Wallet;
use std::path::PathBuf;
#[tokio::main]
async fn main() {
// Accept wallet path/name as args, otherwise prompt interactively.
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args.len() != 3 {
println!("Usage: ./create_new_wallet <wallet_path> <wallet_filename>");
return;
}
let mut wallet_key: String;
let (wallet_path, wallet_filename) = if args.len() > 2 {
(args[1].clone(), args[2].clone())
} else {
(
prompt_visible("Please enter the path to your wallet file: ").await,
prompt_visible("Please enter wallet filename: ").await,
)
};
wallet_key = prompt_hidden_nonempty(
"Please enter your wallet encryption key, if you do not have a wallet yet, enter a new encryption key here: ",
"Wallet key cannot be empty. Please try again.",
).await;
wallet_key = wallet_key.trim().to_string();
let wallet_path = wallet_path.trim().to_string();
let wallet_filename = wallet_filename.trim().to_string();
let wallet_path = PathBuf::from(&wallet_path)
.join(&wallet_filename)
.to_string_lossy()
.into_owned();
// Refuse to overwrite an existing wallet file.
if let Ok(metadata) = metadata(&wallet_path).await {
if metadata.is_file() {
eprintln!(
"Error: Wallet already exists at the specified path: {wallet_path:?}"
);
std::process::exit(1);
}
}
// try_obtain_wallet creates a new wallet at this path when no saved wallet exists.
let wallet = match Wallet::try_obtain_wallet(wallet_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet creation/loading failed: {err}");
return;
}
};
println!("{}", wallet.display_wallet());
}

157
src/bin/create_nft_tx.rs Normal file
View File

@ -0,0 +1,157 @@
use blockchain::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::types::CREATE_NFT_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write(
&mut result,
format_args!("{input:<width$}"),
);
result
}
fn display_fee(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
#[tokio::main]
async fn main() {
// get user input series
let series_data = prompt_visible("Are you creating a 1/1 or a collection? ").await;
let series = if series_data.trim() == "collection" {
1
} else {
0
};
// get user input ticker
let nft_ticker_name = prompt_visible("Please enter the name for your NFT. This can be 3 to 15 characters and must be 100% unique. First come first serve: ").await;
let nft_name = pad_to_width(nft_ticker_name.trim(), 15);
// get the padded CID string
let ipfs_hash = prompt_visible("Enter your IPFS CID for your NFT. This will be stored in a fixed 100 character field, so shorter CIDs are padded with spaces: ").await;
let item_ipfs = pad_to_width(ipfs_hash.trim(), 100);
// how many items in the collection or 1 for 1/1s
let count_string =
prompt_visible("Please enter the number of items in your collection, enter 1 for 1/1s: ")
.await;
let count: u32 = count_string
.trim()
.parse()
.expect("Please enter a valid number");
// get the nft desc
let nft_desc = prompt_visible("Enter your nft description, A max of 100 characters: ").await;
let desc = pad_to_width(nft_desc.trim(), 100);
// get user input txfee
let txfee_prompt = format!(
"Please enter the amount for the fee: (eg. 0.005, minimum fee {:.8})",
display_fee(CREATE_NFT_FEE)
);
let txfee_string = prompt_visible(&format!("{txfee_prompt}: ")).await;
let txfee_f32: f32 = txfee_string
.trim()
.parse()
.expect("Please enter a valid value.");
let txfee = ((txfee_f32 as f64) * (100000000_f64)).round() as u64;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let address = &wallet.saved.short_address;
// set type and timestampe
let txtype = 4;
let timestamp = Utc::now().timestamp() as u32;
// validate wallets
if !Wallet::short_address_validation(address.trim_matches('"')) {
println!("creator wallet invalid: {}", &address);
return;
}
let unsigned_create_nft = UnsignedCreateNftTransaction::new(
txtype,
timestamp,
address.trim_matches('"'),
series,
&nft_name,
&item_ipfs,
count,
&desc,
txfee,
)
.await;
let create_nft = match CreateNftTransaction::new(unsigned_create_nft, private_key).await {
Ok(tx) => tx,
Err(err) => {
eprintln!("Failed to sign NFT transaction: {err}");
return;
}
};
let hashed_data = create_nft.unsigned_create_nft.hash().await;
let signature = create_nft.signature.clone();
//add the signature and hash
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"creator": address.trim_matches('"'),
"series": series,
"nft_name": nft_name,
"item_ipfs": item_ipfs,
"count": count,
"desc": desc,
"txfee": txfee,
"hash": hashed_data,
"signature": signature
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Define the directory path
let dir_path = "./transactions";
// Create the directory if it doesn't exist
if let Err(e) = create_dir_all(&dir_path).await {
eprintln!("Failed to create directory: {e}");
return;
}
// Define the file path
let file_path = format!("{dir_path}/{hashed_data}.json");
// Open the file for writing asynchronously
let mut file = File::create(&file_path)
.await
.expect("Failed to create file");
// Write the JSON string to the file asynchronously
if let Err(e) = file.write_all(output_str.as_bytes()).await {
eprintln!("Failed to write file: {e}");
return;
}
println!("transaction: {output_str}");
}

237
src/bin/create_swap_tx.rs Normal file
View File

@ -0,0 +1,237 @@
use blockchain::blocks::swap::UnsignedSwapTransaction;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::types::SWAP_FEE;
use blockchain::json;
use blockchain::records::wallet_registry::resolve_local_input_short_address;
use blockchain::wallets::structures::Wallet;
use blockchain::Duration;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write(
&mut result,
format_args!("{input:<width$}"),
);
result
}
fn display_fee(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
resolve_local_input_short_address(address.trim())
}
#[tokio::main]
async fn main() {
// set type and timestampe
let txtype = 6;
let timestamp = Utc::now().timestamp() as u32;
// get user input tocken1
let ticker1_name =
prompt_visible("Please enter the coin or token you wish to send during the swap: ").await;
let ticker1_name = ticker1_name.trim().to_lowercase();
let ticker1 = pad_to_width(&ticker1_name, 15);
let nft_series1_string = prompt_visible("Please enter the NFT series number for the asset you are sending, or 0 for coins/tokens/1of1 NFTs: ").await;
let nft_series1: u32 = nft_series1_string
.trim()
.parse()
.expect("Please enter a valid number");
// get user input value1
let value1_string =
prompt_visible("Please enter the amount of coins or tokens to send: (eg. 23.4): ").await;
let value1_f64: f64 = value1_string
.trim()
.parse()
.expect("Please enter a valid age");
let value1 = (value1_f64 * (100000000_f64)).round() as u64;
// get user input tocken2
let ticker2_name =
prompt_visible("Please enter the coin or token you wish to receive during the swap: ")
.await;
let ticker2_name = ticker2_name.trim().to_lowercase();
let ticker2 = pad_to_width(&ticker2_name, 15);
let nft_series2_string = prompt_visible("Please enter the NFT series number for the asset you expect to receive, or 0 for coins/tokens/1of1 NFTs: ").await;
let nft_series2: u32 = nft_series2_string
.trim()
.parse()
.expect("Please enter a valid number");
// get user input value2
let value2_string = prompt_visible(
"Please enter the amount of coins or tokens you expect to receive: (eg. 23.4): ",
)
.await;
let value2_f64: f64 = value2_string
.trim()
.parse()
.expect("Please enter a valid age");
let value2 = (value2_f64 * (100000000_f64)).round() as u64;
// get user input swapper address
let sender2 =
prompt_visible("Please enter the wallet address of the account you are swapping with: ")
.await;
let sender2 = match normalize_short_address_input(&sender2) {
Ok(address) => address,
Err(_) => {
println!("reciver wallet is not valid");
return;
}
};
// get user 1 tip
let tip1 = prompt_visible(
"Pleased enter the amount for the first miner tip: (must be at least 1% of token 1): ",
)
.await;
let tip1_f64: f64 = tip1.trim().parse().expect("Please enter a valid value.");
let txtip1 = (tip1_f64 * (100000000_f64)).round() as u64;
// get user user 2 tip
let tip2 = prompt_visible(
"Please enter the amount for the second miner tip: (must be at least 1% of token 2): ",
)
.await;
let tip2_f32: f64 = tip2.trim().parse().expect("Please enter a valid value.");
let txtip2 = (tip2_f32 * (100000000_f64)).round() as u64;
// get user input txfee1
let txfee1_prompt = format!(
"Please enter the amount for the fee you will pay: (eg. 0.001, minimum fee {:.8} each party)",
display_fee(SWAP_FEE)
);
let txfee1_string = prompt_visible(&format!("{txfee1_prompt}: ")).await;
let txfee1_f32: f64 = txfee1_string
.trim()
.parse()
.expect("Please enter a valid value.");
let txfee1 = (txfee1_f32 * (100000000_f64)).round() as u64;
// get user input txfee2
let txfee2_prompt = format!(
"Please enter the amount for the fee other party will pay pay: (eg. 0.001, minimum fee {:.8} each party)",
display_fee(SWAP_FEE)
);
let txfee2_string = prompt_visible(&format!("{txfee2_prompt}: ")).await;
let txfee2_f32: f64 = txfee2_string
.trim()
.parse()
.expect("Please enter a valid value.");
let txfee2 = (txfee2_f32 * (100000000_f64)).round() as u64;
// get expiration time in hours
let expiration_time =
prompt_visible("Please enter how many hours until the offer expires (eg. 72): ").await;
let expiration_time: u64 = expiration_time.trim().parse().expect("Invalid input");
let expiration_duration = Duration::from_secs(expiration_time * 60 * 60);
let expiration_time: u32 = expiration_duration.as_secs() as u32;
let expires = timestamp + expiration_time;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let address = &wallet.saved.short_address;
// validate wallets
if !Wallet::short_address_validation(address.trim_matches('"')) {
println!("sender wallet invalid: {}", &address);
return;
}
let unsigned_swap = UnsignedSwapTransaction::new(
txtype,
timestamp,
expires,
&ticker1,
nft_series1,
value1,
&ticker2,
nft_series2,
value2,
address.trim_matches('"'),
&sender2,
txtip1,
txtip2,
txfee1,
txfee2,
)
.await;
let hashed_data = unsigned_swap.hash().await;
let signature1 = match unsigned_swap.hash_and_sign(private_key).await {
Ok(signature) => signature,
Err(err) => {
eprintln!("Failed to sign swap transaction: {err}");
return;
}
};
//add the signature and hash
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"offer_expiration": expires,
"ticker1": ticker1,
"nft_series1": nft_series1,
"value1": value1,
"ticker2": ticker2,
"nft_series2": nft_series2,
"value2": value2,
"sender1": address.trim_matches('"'),
"sender2": sender2,
"tip1": txtip1,
"tip2": txtip2,
"txfee1": txfee1,
"txfee2": txfee2,
"hash": hashed_data,
"signature1": signature1,
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Define the directory path
let dir_path = "./transactions";
// Create the directory if it doesn't exist
if let Err(e) = create_dir_all(&dir_path).await {
eprintln!("Failed to create directory: {e}");
return;
}
// Define the file path
let file_path = format!("{dir_path}/{hashed_data}.json");
// Open the file for writing asynchronously
let mut file = File::create(&file_path)
.await
.expect("Failed to create file");
// Write the JSON string to the file asynchronously
if let Err(e) = file.write_all(output_str.as_bytes()).await {
eprintln!("Failed to write file: {e}");
return;
}
println!("transaction: {output_str}");
}

151
src/bin/create_tokens_tx.rs Normal file
View File

@ -0,0 +1,151 @@
use blockchain::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::types::CREATE_TOKEN_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write(
&mut result,
format_args!("{input:<width$}"),
);
result
}
fn display_fee(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
#[tokio::main]
async fn main() {
// set type and timestampe
let txtype = 3;
let timestamp = Utc::now().timestamp() as u32;
// get user input ticker
let ticker_name = prompt_visible("Please enter the ticker / token name you wish to create. This can be 3 to 15 characters and must be 100% unique. First come first serve: ").await;
let ticker = pad_to_width(ticker_name.trim(), 15);
// get user input value
let number_string = prompt_visible(
"Please enter the amount of coins or tokens to create: (eg. 1, 1000, 100000, etc): ",
)
.await;
let number_u64: u64 = number_string
.trim()
.parse()
.expect("Please enter a valid age");
let number = ((number_u64 as f64) * (100000000_f64)).round() as u64;
let hard_limit_string =
prompt_visible("Should this token have a hard cap? Enter 1 for yes or 0 for no: ").await;
let hard_limit: u8 = hard_limit_string
.trim()
.parse()
.expect("Please enter 1 or 0.");
if hard_limit > 1 {
println!("hard_limit must be 1 or 0.");
return;
}
// get user input txfee
let txfee_prompt = format!(
"Please enter the amount for the fee: (eg. 500, minimum fee {:.8})",
display_fee(CREATE_TOKEN_FEE)
);
let txfee_string = prompt_visible(&format!("{txfee_prompt}: ")).await;
let txfee_f32: f32 = txfee_string
.trim()
.parse()
.expect("Please enter a valid value.");
let txfee = ((txfee_f32 as f64) * (100000000_f64)).round() as u64;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let address = &wallet.saved.short_address;
// validate wallets
if !Wallet::short_address_validation(address.trim_matches('"')) {
println!("creatpr wallet invalid: {}", &address);
return;
}
let unsigned_create_token = UnsignedCreateTokenTransaction::new(
txtype,
timestamp,
address.trim_matches('"'),
&ticker,
number,
hard_limit,
txfee,
)
.await;
let create_token = match CreateTokenTransaction::new(unsigned_create_token, private_key).await {
Ok(tx) => tx,
Err(err) => {
eprintln!("Failed to sign token transaction: {err}");
return;
}
};
let hashed_data = create_token.unsigned_create_token.hash().await;
let signature = create_token.signature.clone();
//add the signature and hash
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"creator": address.trim_matches('"'),
"ticker": ticker,
"number": number,
"hard_limit": hard_limit,
"txfee": txfee,
"hash": hashed_data,
"signature": signature,
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Define the directory path
let dir_path = "./transactions";
// Create the directory if it doesn't exist
if let Err(e) = create_dir_all(&dir_path).await {
eprintln!("Failed to create directory: {e}");
return;
}
// Define the file path
let file_path = format!("{dir_path}/{hashed_data}.json");
// Open the file for writing asynchronously
let mut file = File::create(&file_path)
.await
.expect("Failed to create file");
// Write the JSON string to the file asynchronously
if let Err(e) = file.write_all(output_str.as_bytes()).await {
eprintln!("Failed to write file: {e}");
return;
}
println!("transaction: {output_str}");
}

View File

@ -0,0 +1,206 @@
use blockchain::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::common::types::{NON_BASE_TRANSFER_MIN_FEE, TRANSFER_FEE};
use blockchain::env;
use blockchain::json;
use blockchain::records::wallet_registry::resolve_local_input_short_address;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write(
&mut result,
format_args!("{input:<width$}"),
);
result
}
fn display_fee(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
resolve_local_input_short_address(address.trim())
}
#[tokio::main]
async fn main() {
let minimum_transfer_fee_percent = TRANSFER_FEE * 100.0;
// set type and timestamp
let txtype = 2;
let timestamp = Utc::now().timestamp() as u32;
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args.len() != 6 {
println!("Usage: ./create_transfer_tx <coin> <value> <receiver> <txfee> <nft_series>");
return;
}
let coin_name = if args.len() > 1 {
args[1].clone()
} else {
prompt_visible("Please enter the coin or token you wish to transfer: ").await
};
let coin = pad_to_width(&coin_name, 15);
let is_base_coin =
coin.trim().to_lowercase() == block_extension_and_paths().1.trim().to_lowercase();
let value_string = if args.len() > 2 {
args[2].clone()
} else {
prompt_visible("Please enter the amount of coins or tokens to send: (eg. 23.4): ").await
};
let value_f32: f32 = value_string
.trim()
.parse()
.expect("Please enter a valid amount.");
let value = ((value_f32 as f64) * 100_000_000.0).round() as u64;
let receiver_input = if args.len() > 3 {
args[3].clone()
} else {
prompt_visible("Please enter the receiver wallet address: ").await
};
let receiver = match normalize_short_address_input(&receiver_input) {
Ok(address) => address,
Err(_) => {
println!("reciver wallet is not valid");
return;
}
};
let txfee_string = if args.len() > 4 {
args[4].clone()
} else {
let prompt = if is_base_coin {
format!(
"Please enter the amount for the fee: (eg. 1.09, minimum fee {minimum_transfer_fee_percent:.0}% of transaction):"
)
} else {
format!(
"Please enter the amount for the fee: (eg. 0.0001, minimum fee {:.8} for tokens/NFTs):",
display_fee(NON_BASE_TRANSFER_MIN_FEE)
)
};
prompt_visible(&format!("{prompt} ")).await
};
let txfee_f32: f32 = txfee_string
.trim()
.parse()
.expect("Please enter a valid fee.");
let txfee = ((txfee_f32 as f64) * 100_000_000.0).round() as u64;
let nft_series_string = if args.len() > 5 {
args[5].clone()
} else {
prompt_visible(
"Please enter the NFT series number to transfer, or 0 for coins/tokens/1of1 NFTs: ",
)
.await
};
let nft_series: u32 = nft_series_string
.trim()
.parse()
.expect("Please enter a valid NFT series number.");
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let short_address = &wallet.saved.short_address;
if !Wallet::short_address_validation(short_address.trim()) {
println!("sender wallet invalid: {short_address}");
return;
}
if short_address.trim() == receiver.trim() {
println!("You cannot send funds to yourself");
return;
}
if nft_series > 0 && value != 100_000_000 {
println!("Series NFT transfers must have a value of exactly 1.0");
return;
}
let unsigned_transfer = UnsignedTransferTransaction::new(
txtype,
timestamp,
value,
&coin,
nft_series,
short_address.trim(),
&receiver,
txfee,
)
.await;
let transfer = match TransferTransaction::new(unsigned_transfer, private_key).await {
Ok(tx) => tx,
Err(err) => {
eprintln!("Failed to sign transfer transaction: {err}");
return;
}
};
let hashed_data = transfer.unsigned_transfer.hash().await;
let signature = transfer.signature.clone();
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"value": value,
"coin": coin,
"nft_series": nft_series,
"sender": short_address.trim(),
"receiver": receiver,
"txfee": txfee,
"hash": hashed_data,
"signature": signature,
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Define the directory path
let dir_path = "./transactions";
// Create the directory if it doesn't exist
if let Err(e) = create_dir_all(&dir_path).await {
eprintln!("Failed to create directory: {e}");
return;
}
// Define the file path
let file_path = format!("{dir_path}/{hashed_data}.json");
// Open the file for writing asynchronously
let mut file = File::create(&file_path)
.await
.expect("Failed to create file");
// Write the JSON string to the file asynchronously
if let Err(e) = file.write_all(output_str.as_bytes()).await {
eprintln!("Failed to write file: {e}");
return;
}
println!("transaction: {output_str}");
}

171
src/bin/create_vanity_tx.rs Normal file
View File

@ -0,0 +1,171 @@
use blockchain::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::common::types::{VANITY_ADDRESS_FEE, VANITY_ADDRESS_TYPE};
use blockchain::env;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
fn display_fee(value: u64) -> f64 {
// Fees are stored as atomic units and displayed as whole-coin decimals for the CLI.
value as f64 / 100_000_000.0
}
fn left_pad_vanity_payload(input: &str) -> Option<String> {
// Vanity names are left padded to the 20-byte short-address payload size.
let trimmed = input.trim();
if trimmed.is_empty() || trimmed.len() > Wallet::SHORT_ADDRESS_HASH_BYTES_LENGTH {
return None;
}
// Only letters are allowed so vanity payload bytes stay unambiguous.
if !trimmed.chars().all(|ch| ch.is_ascii_alphabetic()) {
return None;
}
Some(format!(
"{:>width$}",
trimmed,
width = Wallet::SHORT_ADDRESS_HASH_BYTES_LENGTH
))
}
#[tokio::main]
async fn main() {
// The vanity tool accepts optional CLI args, otherwise it prompts interactively.
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args.len() != 3 {
println!("Usage: ./create_vanity_tx <vanity_name> <txfee>");
return;
}
let vanity_name = if args.len() > 1 {
args[1].clone()
} else {
prompt_visible("Enter the vanity name to register (letters only, up to 20 characters): ")
.await
};
// Normalize the vanity name into the exact payload form saved in the transaction.
let padded_payload = match left_pad_vanity_payload(&vanity_name) {
Some(payload) => payload,
None => {
eprintln!("Vanity names must be 1-20 letters only.");
return;
}
};
// Convert the entered fee into atomic units before signing.
let txfee_string = if args.len() > 2 {
args[2].clone()
} else {
let prompt = format!(
"Please enter the vanity registration fee: (e.g. 1.0, minimum fee {:.8})",
display_fee(VANITY_ADDRESS_FEE)
);
prompt_visible(&format!("{prompt}: ")).await
};
let txfee_f32: f32 = txfee_string
.trim()
.parse()
.expect("Please enter a valid fee.");
let txfee = ((txfee_f32 as f64) * 100_000_000.0).round() as u64;
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Load the wallet that will own and sign the vanity registration.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let address = wallet.saved.short_address.trim().to_string();
// Vanity registration is owned by the current wallet short address.
if !Wallet::short_address_validation(&address) {
eprintln!("sender wallet invalid: {address}");
return;
}
// Use the wallet network byte to attach the matching vanity suffix.
let network_byte =
match Wallet::short_address_to_bytes(&address).and_then(|bytes| bytes.last().copied()) {
Some(byte) => byte,
None => {
eprintln!("Could not determine wallet network.");
return;
}
};
let network_suffix = Wallet::map_byte_to_wallet(network_byte).to_ascii_lowercase();
if network_suffix.is_empty() {
eprintln!("Could not determine wallet network.");
return;
}
// Validate the final vanity address before it is signed into the transaction.
let vanity_address = format!("{}.{}", padded_payload.to_ascii_lowercase(), network_suffix);
if Wallet::vanity_address_to_bytes(&vanity_address).is_none() {
eprintln!("Failed to build vanity address.");
return;
}
// Build and sign the vanity registration transaction.
let timestamp = Utc::now().timestamp() as u32;
let unsigned_vanity = UnsignedVanityAddressTransaction::new(
VANITY_ADDRESS_TYPE,
timestamp,
&address,
&vanity_address,
txfee,
)
.await;
let vanity_transaction = match VanityAddressTransaction::new(unsigned_vanity, private_key).await
{
Ok(tx) => tx,
Err(err) => {
eprintln!("Failed to sign vanity transaction: {err}");
return;
}
};
let hashed_data = vanity_transaction.unsigned_vanity_address.hash().await;
let signature = vanity_transaction.signature.clone();
// Save the signed transaction as JSON so broadcast_transaction can submit it later.
let output = json!({
"txtype": VANITY_ADDRESS_TYPE,
"timestamp": timestamp,
"address": address,
"vanity_address": vanity_address,
"txfee": txfee,
"hash": hashed_data,
"signature": signature,
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Transaction creation tools all write into ./transactions using the transaction hash.
let dir_path = "./transactions";
if let Err(e) = create_dir_all(&dir_path).await {
eprintln!("Failed to create directory: {e}");
return;
}
let file_path = format!("{dir_path}/{hashed_data}.json");
let mut file = File::create(&file_path)
.await
.expect("Failed to create file");
if let Err(e) = file.write_all(output_str.as_bytes()).await {
eprintln!("Failed to write file: {e}");
return;
}
println!("transaction: {output_str}");
}

View File

@ -0,0 +1,83 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::records::unpack_block::load_by_binary_data::load_block_from_binary;
use blockchain::standalone_tools::connections::handshake;
use blockchain::to_string_pretty;
#[tokio::main]
async fn main() {
// Command 10 asks a peer for a block by its 32-byte hash.
let hashmap_key = generate_uid();
let rpc_command = 10;
// The CLI accepts the block hash as a hex string and validates it before connecting.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./lookup_block_by_hash <block_hash>");
return;
}
let block_hash = args[1].trim().to_string();
if block_hash.len() != 64 || !block_hash.chars().all(|c| c.is_ascii_hexdigit()) {
println!("Please enter a valid 64-character block hash");
return;
}
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Try each configured peer until one returns a usable response.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
block_hash.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// A successful block response is binary block data, not text.
if let Ok(block) = load_block_from_binary(&response).await {
match to_string_pretty(&block) {
Ok(block_json) => {
println!("{block_json}");
connected = true;
}
Err(err) => eprintln!("failed to serialize block json: {err}"),
}
} else {
// Error replies may be plain text; unknown binary is shown as hex for debugging.
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
} else if !response.is_empty() {
println!("{}", hex::encode(response));
connected = true;
}
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,87 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::records::unpack_block::load_by_binary_data::load_block_from_binary;
use blockchain::standalone_tools::connections::handshake;
use blockchain::to_string_pretty;
#[tokio::main]
async fn main() {
// Command 9 asks a peer for a block by block height.
let hashmap_key = generate_uid();
let rpc_command = 9;
// Validate the height locally before opening a peer connection.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./lookup_block_by_height <block_number>");
return;
}
let block_number = match args[1].parse::<u32>() {
Ok(num) => num,
Err(_) => {
println!("Please enter a valid number");
return;
}
};
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// sending_request expects command 9 input as a decimal block number string.
let payload = block_number.to_string();
// Try each configured peer until one returns a usable response.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
payload.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// Successful block responses are binary block data.
if let Ok(block) = load_block_from_binary(&response).await {
match to_string_pretty(&block) {
Ok(block_json) => {
println!("{block_json}");
connected = true;
}
Err(err) => eprintln!("failed to serialize block json: {err}"),
}
} else {
// Error replies may be plain text; unknown binary is shown as hex for debugging.
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
} else if !response.is_empty() {
println!("{}", hex::encode(response));
connected = true;
}
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,76 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::encode;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
#[tokio::main]
async fn main() {
// Command 37 asks a peer for contract records tied to a wallet address.
let hashmap_key = generate_uid();
let rpc_command = 37;
// The address can be a long, short, or vanity address; request encoding normalizes it later.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./contract_lookup_by_address <wallet_address>");
return;
}
let wallet_address = match args[1].parse::<String>() {
Ok(address) => address,
Err(_) => {
println!("Please enter a wallet address");
return;
}
};
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Try each configured peer until one returns a response.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
wallet_address.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// Contract lookup replies are usually text; fallback to hex if the peer sends raw bytes.
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
} else if !response.is_empty() {
println!("{}", encode(response));
connected = true;
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,76 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::encode;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
#[tokio::main]
async fn main() {
// Command 33 asks a peer for one loan contract by its 32-byte hash.
let hashmap_key = generate_uid();
let rpc_command = 33;
// The hash is passed as text here and encoded into binary request bytes later.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./contract_lookup_by_hash <contract_hash>");
return;
}
let contract_hash = match args[1].parse::<String>() {
Ok(hash) => hash,
Err(_) => {
println!("Please enter a contract hash");
return;
}
};
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Try each configured peer until one returns a response.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
contract_hash.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// Contract lookup replies are usually text; fallback to hex if the peer sends raw bytes.
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
} else if !response.is_empty() {
println!("{}", encode(response));
connected = true;
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,63 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
#[tokio::main]
async fn main() {
// Command 5 asks a peer for the current network difficulty.
let hashmap_key = generate_uid();
let _args: Vec<String> = env::args().collect();
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let rpc_command = 5;
let json = "".to_string();
// Try each configured peer until one returns difficulty or text error.
let connections = get_connections().await;
let mut connected: bool = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// The difficulty reply carries 4 bytes of header plus an 8-byte u64 difficulty.
if response.len() == 12 {
let difficulty = u64::from_le_bytes(response[4..12].try_into().unwrap());
println!("{difficulty}");
connected = true;
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

71
src/bin/lookup_height.rs Normal file
View File

@ -0,0 +1,71 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
#[tokio::main]
async fn main() {
let hashmap_key = generate_uid();
// Get command-line arguments
let args: Vec<String> = env::args().collect();
// Check if a command-line argument is provided
if args.len() != 1 {
println!("Usage: ./request_height");
return;
}
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// set the rpc command number
let rpc_command = 2;
let json = "".to_string();
let connections = get_connections().await;
let mut connected: bool = false;
// Iterate over the returned Vec and print each connection
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
if response.len() == 4 {
let height = u32::from_le_bytes(response[..4].try_into().unwrap());
println!("{height}");
connected = true;
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,82 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
fn format_balance(value: u64) -> String {
let whole = value / 100_000_000;
let fractional = value % 100_000_000;
format!("{whole}.{fractional:08}")
}
#[tokio::main]
async fn main() {
let hashmap_key = generate_uid();
// Get command-line arguments
let args: Vec<String> = env::args().collect();
// Check if a command-line argument is provided
if args.len() != 1 {
println!("Usage: ./large_tx_fee");
return;
}
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// set the rpc command number
let rpc_command = 13;
let json = "".to_string();
let connections = get_connections().await;
let mut connected: bool = false;
// Iterate over the returned Vec and print each connection
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
if response.is_empty() {
println!("0");
connected = true;
} else if response.len() == 8 {
let fee = u64::from_le_bytes(response[0..8].try_into().unwrap());
if fee == 0 {
println!("0");
} else {
println!("{}", format_balance(fee));
}
connected = true;
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,89 @@
use blockchain::env;
use blockchain::fs;
use blockchain::tilde;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
use rustyline_derive::Completer;
use rustyline_derive::Helper as RustyHelper;
use rustyline_derive::Highlighter as RustyHighlighter;
use rustyline_derive::Hinter as RustyHinter;
use rustyline_derive::Validator as RustyValidator;
fn format_balance(balance: u64) -> String {
// Balance files store atomic units; display as whole coins with 8 decimals.
let whole = balance / 100_000_000;
let fractional = balance % 100_000_000;
format!("{whole}.{fractional:08}")
}
#[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)]
struct PathHelper {
#[rustyline(Completer)]
completer: FilenameCompleter,
}
fn prompt_for_path(prompt: &str) -> Result<String, String> {
// Rustyline provides path completion for interactive balance-file lookup.
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(line) => Ok(line.trim().to_string()),
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(format!("Failed to read balance file path: {err}")),
}
}
#[tokio::main]
async fn main() {
// The tool accepts a balance file path as an arg or prompts with completion.
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args.len() != 2 {
eprintln!("Usage: ./lookup_local_balance <path/to/file.bal>");
return;
}
let balance_file_path = if args.len() == 2 {
args[1].clone()
} else {
match prompt_for_path("Please enter the path to the balance file: ") {
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
}
};
// Allow users to type paths with ~ and read the balance bytes from disk.
let expanded_path = tilde(&balance_file_path).to_string();
let bytes = match fs::read(&expanded_path) {
Ok(bytes) => bytes,
Err(err) => {
eprintln!("Error reading file: {err}");
return;
}
};
if bytes.len() < 8 {
eprintln!("Error: File should have at least 8 bytes of data");
return;
}
// The first 8 bytes are the little-endian u64 balance value.
let mut buffer = [0u8; 8];
buffer.copy_from_slice(&bytes[..8]);
let value = u64::from_le_bytes(buffer);
println!("{}", format_balance(value));
}

View File

@ -0,0 +1,130 @@
use blockchain::blocks::burn::BurnTransaction;
use blockchain::blocks::collateral::CollateralClaimTransaction;
use blockchain::blocks::issue_token::IssueTokenTransaction;
use blockchain::blocks::loan_payment::ContractPaymentTransaction;
use blockchain::blocks::loans::LoanContractTransaction;
use blockchain::blocks::marketing::MarketingTransaction;
use blockchain::blocks::nft::CreateNftTransaction;
use blockchain::blocks::swap::SwapTransaction;
use blockchain::blocks::token::CreateTokenTransaction;
use blockchain::blocks::transfer::TransferTransaction;
use blockchain::blocks::vanity::VanityAddressTransaction;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE,
ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE,
};
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::rpc::command_maps;
use blockchain::standalone_tools::connections::handshake;
use blockchain::to_string_pretty;
async fn decode_one_transaction(tx_bytes: &[u8]) -> Option<String> {
let txtype = *tx_bytes.first()?;
let body = &tx_bytes[1..];
match txtype {
TRANSFER_TYPE => to_string_pretty(&TransferTransaction::from_bytes(txtype, body).await.ok()?).ok(),
CREATE_TOKEN_TYPE => to_string_pretty(&CreateTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(),
CREATE_NFT_TYPE => to_string_pretty(&CreateNftTransaction::from_bytes(txtype, body).await.ok()?).ok(),
MARKETING_TYPE => to_string_pretty(&MarketingTransaction::from_bytes(txtype, body).await.ok()?).ok(),
SWAP_TYPE => to_string_pretty(&SwapTransaction::from_bytes(txtype, body).await.ok()?).ok(),
LENDER_TYPE => to_string_pretty(&LoanContractTransaction::from_bytes(txtype, body).await.ok()?).ok(),
BORROWER_TYPE => to_string_pretty(&ContractPaymentTransaction::from_bytes(txtype, body).await.ok()?).ok(),
COLLATERAL_TYPE => to_string_pretty(&CollateralClaimTransaction::from_bytes(txtype, body).await.ok()?).ok(),
BURN_TYPE => to_string_pretty(&BurnTransaction::from_bytes(txtype, body).await.ok()?).ok(),
ISSUE_TOKEN_TYPE => to_string_pretty(&IssueTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(),
VANITY_ADDRESS_TYPE => to_string_pretty(&VanityAddressTransaction::from_bytes(txtype, body).await.ok()?).ok(),
_ => None,
}
}
async fn decode_mempool_transactions(response: &[u8]) -> Option<Vec<String>> {
// Address lookup can return many concatenated original transaction byte records.
let mut offset = 0;
let mut transactions = Vec::new();
while offset < response.len() {
let txtype = *response.get(offset)?;
let tx_len = command_maps::get_bytes(txtype);
if tx_len == 0 || offset + tx_len > response.len() {
return None;
}
let tx_bytes = &response[offset..offset + tx_len];
transactions.push(decode_one_transaction(tx_bytes).await?);
offset += tx_len;
}
Some(transactions)
}
#[tokio::main]
async fn main() {
// Command 16 asks a peer for unprocessed mempool transactions tied to an address.
let rpc_command = 16;
// The address can be long, short, or vanity; sending_request normalizes it to short bytes.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./lookup_mempool_tx_by_address <wallet_address>");
return;
}
let address = args[1].trim().to_string();
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
address.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
generate_uid(),
)
.await;
match result {
Ok(response) => {
if response.is_empty() {
println!("[]");
connected = true;
} else if let Some(transactions) = decode_mempool_transactions(&response).await {
for transaction in transactions {
println!("{transaction}");
}
connected = true;
} else {
// Text errors are printed directly; unknown binary is shown as hex for inspection.
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
} else {
println!("{}", hex::encode(response));
}
connected = true;
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,109 @@
use blockchain::blocks::burn::BurnTransaction;
use blockchain::blocks::collateral::CollateralClaimTransaction;
use blockchain::blocks::issue_token::IssueTokenTransaction;
use blockchain::blocks::loan_payment::ContractPaymentTransaction;
use blockchain::blocks::loans::LoanContractTransaction;
use blockchain::blocks::marketing::MarketingTransaction;
use blockchain::blocks::nft::CreateNftTransaction;
use blockchain::blocks::swap::SwapTransaction;
use blockchain::blocks::token::CreateTokenTransaction;
use blockchain::blocks::transfer::TransferTransaction;
use blockchain::blocks::vanity::VanityAddressTransaction;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE,
ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE,
};
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::to_string_pretty;
async fn decode_mempool_transaction(response: &[u8]) -> Option<String> {
// Mempool transaction replies are the original transaction bytes,
// starting with the transaction type byte and no block-height prefix.
let txtype = *response.first()?;
let body = &response[1..];
match txtype {
TRANSFER_TYPE => to_string_pretty(&TransferTransaction::from_bytes(txtype, body).await.ok()?).ok(),
CREATE_TOKEN_TYPE => to_string_pretty(&CreateTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(),
CREATE_NFT_TYPE => to_string_pretty(&CreateNftTransaction::from_bytes(txtype, body).await.ok()?).ok(),
MARKETING_TYPE => to_string_pretty(&MarketingTransaction::from_bytes(txtype, body).await.ok()?).ok(),
SWAP_TYPE => to_string_pretty(&SwapTransaction::from_bytes(txtype, body).await.ok()?).ok(),
LENDER_TYPE => to_string_pretty(&LoanContractTransaction::from_bytes(txtype, body).await.ok()?).ok(),
BORROWER_TYPE => to_string_pretty(&ContractPaymentTransaction::from_bytes(txtype, body).await.ok()?).ok(),
COLLATERAL_TYPE => to_string_pretty(&CollateralClaimTransaction::from_bytes(txtype, body).await.ok()?).ok(),
BURN_TYPE => to_string_pretty(&BurnTransaction::from_bytes(txtype, body).await.ok()?).ok(),
ISSUE_TOKEN_TYPE => to_string_pretty(&IssueTokenTransaction::from_bytes(txtype, body).await.ok()?).ok(),
VANITY_ADDRESS_TYPE => to_string_pretty(&VanityAddressTransaction::from_bytes(txtype, body).await.ok()?).ok(),
_ => None,
}
}
#[tokio::main]
async fn main() {
// Command 14 asks a peer for one unprocessed mempool transaction by signature.
let rpc_command = 14;
// The signature is passed as hex and converted into raw signature bytes by sending_request.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./lookup_mempool_tx_by_signature <signature>");
return;
}
let signature = args[1].trim().to_string();
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
signature.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
generate_uid(),
)
.await;
match result {
Ok(response) => {
if response.is_empty() {
println!("Transaction not found in mempool.");
connected = true;
} else if let Some(transaction_json) = decode_mempool_transaction(&response).await {
println!("{transaction_json}");
connected = true;
} else {
// Text errors are printed directly; unknown binary is shown as hex for inspection.
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
} else {
println!("{}", hex::encode(response));
}
connected = true;
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,66 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
#[tokio::main]
async fn main() {
// Command 15 asks a peer for the current unprocessed mempool transaction count.
let rpc_command = 15;
// This lookup takes no arguments beyond the wallet key used for the handshake.
let args: Vec<String> = env::args().collect();
if args.len() != 1 {
println!("Usage: ./lookup_mempool_tx_count");
return;
}
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Try each configured peer until one returns a count or text error.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
"".to_string(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
generate_uid(),
)
.await;
match result {
Ok(response) if response.len() >= 4 => {
// The mempool count command returns a 4-byte little-endian integer.
let count = u32::from_le_bytes(response[0..4].try_into().unwrap());
println!("{count}");
connected = true;
}
Ok(response) => {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,142 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::json;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::to_string_pretty;
const NETWORK_NAME_BYTES: usize = 7;
const NETWORK_INFO_FIXED_BYTES_WITHOUT_PREFIX: usize = 40;
fn read_u32(response: &[u8], offset: &mut usize) -> Option<u32> {
let bytes: [u8; 4] = response.get(*offset..*offset + 4)?.try_into().ok()?;
*offset += 4;
Some(u32::from_le_bytes(bytes))
}
fn read_u64(response: &[u8], offset: &mut usize) -> Option<u64> {
let bytes: [u8; 8] = response.get(*offset..*offset + 8)?.try_into().ok()?;
*offset += 8;
Some(u64::from_le_bytes(bytes))
}
fn decode_network_info(response: &[u8]) -> Option<String> {
// Network-info responses are binary, with one variable-width field:
// the wallet prefix is whatever remains after the fixed fields.
if response.len() <= NETWORK_INFO_FIXED_BYTES_WITHOUT_PREFIX {
return None;
}
let wallet_prefix_len = response.len() - NETWORK_INFO_FIXED_BYTES_WITHOUT_PREFIX;
let mut offset = 0;
let version = *response.get(offset)?;
offset += 1;
// The network name is the fixed 7-byte value from network settings:
// "mainnet", "testnet", or "Invalid".
let network = String::from_utf8_lossy(response.get(offset..offset + NETWORK_NAME_BYTES)?)
.trim()
.to_string();
offset += NETWORK_NAME_BYTES;
let time = read_u32(response, &mut offset)?;
// Mainnet uses CLC and testnet uses CLTC, so the prefix length is
// inferred from the total payload size instead of hard-coded.
let wallet_prefix =
String::from_utf8_lossy(response.get(offset..offset + wallet_prefix_len)?)
.trim()
.to_string();
offset += wallet_prefix_len;
let height = read_u32(response, &mut offset)?;
let next_block_difficulty = read_u64(response, &mut offset)?;
let total_block_transactions = read_u32(response, &mut offset)?;
let total_mempool_transactions = read_u32(response, &mut offset)?;
let largest_tx_fee = read_u64(response, &mut offset)?;
// Print JSON for the CLI user, but this is decoded from the binary
// RPC payload and no JSON crossed the TCP stream.
let output = json!({
"version": version,
"network": network,
"time": time,
"wallet_prefix": wallet_prefix,
"height": height,
"next_block_difficulty": next_block_difficulty,
"total_block_transactions": total_block_transactions,
"total_mempool_transactions": total_mempool_transactions,
"largest_tx_fee": largest_tx_fee,
});
to_string_pretty(&output).ok()
}
#[tokio::main]
async fn main() {
// Command 1 asks a peer for its current network-info snapshot.
let hashmap_key = generate_uid();
let rpc_command = 1;
// This lookup takes no arguments; the wallet key is only used for
// the authenticated handshake before the RPC command is sent.
let args: Vec<String> = env::args().collect();
if args.len() != 1 {
println!("Usage: ./lookup_network_info");
return;
}
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Try configured peers in order and stop at the first readable
// network-info response or text error.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
"".to_string(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// Prefer binary network-info decoding; if that fails,
// print a plain text error returned by the peer.
if let Some(output) = decode_network_info(&response) {
println!("{output}");
connected = true;
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

233
src/bin/lookup_nft.rs Normal file
View File

@ -0,0 +1,233 @@
use blockchain::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::encode;
use blockchain::env;
use blockchain::json;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::wallets::structures::Wallet;
use blockchain::{Map, Value};
const NFT_NAME_BYTES: usize = 15;
const SERIES_BYTES: usize = 4;
const GENESIS_HASH_BYTES: usize = 32;
const CREATOR_BYTES: usize = Wallet::SHORT_ADDRESS_BYTES_LENGTH;
const IPFS_BYTES: usize = 100;
const HOLDER_BYTES: usize = Wallet::SHORT_ADDRESS_BYTES_LENGTH;
const HISTORY_COUNT_BYTES: usize = 4;
const HISTORY_ENTRY_SIZE: usize =
32 + 4 + 1 + 1 + (2 * Wallet::SHORT_ADDRESS_BYTES_LENGTH) + 15 + 4 + 8;
const NFT_LOOKUP_FIXED_BYTES: usize = NFT_NAME_BYTES
+ SERIES_BYTES
+ GENESIS_HASH_BYTES
+ CREATOR_BYTES
+ IPFS_BYTES
+ HOLDER_BYTES
+ HISTORY_COUNT_BYTES;
fn action_name(action: u8) -> &'static str {
// History action bytes are stored compactly on the wire and expanded for display.
match action {
1 => "create_nft",
2 => "transfer",
3 => "swap",
4 => "loan_locked",
5 => "loan_payment",
6 => "collateral_claimed",
7 => "loan_issued",
_ => "unknown",
}
}
fn decode_wallet(bytes: &[u8]) -> String {
// Empty wallet fields are encoded as all zeroes; otherwise decode a short address.
if bytes.iter().all(|b| *b == 0) {
String::new()
} else {
Wallet::bytes_to_short_address(bytes).unwrap_or_default()
}
}
fn decode_history_entry(entry: &[u8]) -> Option<Value> {
// Each history row has a fixed binary size so bad lengths mean an invalid response.
if entry.len() != HISTORY_ENTRY_SIZE {
return None;
}
// Decode the history fields in the same order the RPC command writes them.
let txid = encode(&entry[0..32]);
let block = u32::from_le_bytes(entry[32..36].try_into().ok()?);
let txtype = entry[36];
let action = entry[37];
let from = decode_wallet(&entry[38..60]);
let to = decode_wallet(&entry[60..82]);
let received_asset = binary_to_string(entry[82..97].to_vec()).trim().to_string();
let received_series = u32::from_le_bytes(entry[97..101].try_into().ok()?);
let received_value_raw = u64::from_le_bytes(entry[101..109].try_into().ok()?);
let received_value = received_value_raw as f64 / 100_000_000.0;
// Omit empty optional fields so the CLI output stays readable.
let mut obj = Map::new();
obj.insert("txid".to_string(), json!(txid));
obj.insert("block".to_string(), json!(block));
obj.insert("txtype".to_string(), json!(txtype));
obj.insert("action".to_string(), json!(action_name(action)));
if !from.is_empty() {
obj.insert("from".to_string(), json!(from));
}
if !to.is_empty() {
obj.insert("to".to_string(), json!(to));
}
if !received_asset.is_empty() {
obj.insert("received_asset".to_string(), json!(received_asset));
if received_series > 0 {
obj.insert("received_series".to_string(), json!(received_series));
}
obj.insert("received_value".to_string(), json!(received_value));
}
Some(Value::Object(obj))
}
fn decode_nft_lookup(response: &[u8]) -> Option<String> {
// The fixed NFT fields must exist before any history entries can be decoded.
if response.len() < NFT_LOOKUP_FIXED_BYTES {
return None;
}
// Decode the fixed-width NFT metadata fields in wire order.
let mut cursor = 0usize;
let nft_name = binary_to_string(response[cursor..cursor + NFT_NAME_BYTES].to_vec())
.trim()
.to_string();
cursor += NFT_NAME_BYTES;
let series = u32::from_le_bytes(response[cursor..cursor + SERIES_BYTES].try_into().ok()?);
cursor += SERIES_BYTES;
let genesis = encode(&response[cursor..cursor + GENESIS_HASH_BYTES]);
cursor += GENESIS_HASH_BYTES;
let creator = Wallet::bytes_to_short_address(&response[cursor..cursor + CREATOR_BYTES])?;
cursor += CREATOR_BYTES;
let ipfs_cid = binary_to_string(response[cursor..cursor + IPFS_BYTES].to_vec())
.trim()
.to_string();
cursor += IPFS_BYTES;
let current_holder = decode_wallet(&response[cursor..cursor + HOLDER_BYTES]);
cursor += HOLDER_BYTES;
let history_count = u32::from_le_bytes(
response[cursor..cursor + HISTORY_COUNT_BYTES]
.try_into()
.ok()?,
);
cursor += HISTORY_COUNT_BYTES;
// Any remaining bytes are fixed-size history entries.
let mut history = Vec::new();
let mut offset = cursor;
while offset + HISTORY_ENTRY_SIZE <= response.len() {
if let Some(entry) = decode_history_entry(&response[offset..offset + HISTORY_ENTRY_SIZE]) {
history.push(entry);
}
offset += HISTORY_ENTRY_SIZE;
}
// Convert the binary record into user-facing JSON.
let mut output = Map::new();
output.insert("nft_name".to_string(), json!(nft_name));
output.insert("series".to_string(), json!(series));
if series == 0 {
output.insert("ipfs".to_string(), json!(format!("ipfs://{ipfs_cid}")));
} else {
output.insert(
"ipfs".to_string(),
json!(format!("ipfs://{ipfs_cid}/{series}.json")),
);
}
if series > 0 {
output.insert(
"asset_name".to_string(),
json!(format!("{nft_name}_{series}")),
);
}
output.insert("genesis".to_string(), json!(genesis));
output.insert("creator".to_string(), json!(creator));
output.insert("current_holder".to_string(), json!(current_holder));
output.insert("history_count".to_string(), json!(history_count));
output.insert("history".to_string(), Value::Array(history));
serde_json::to_string_pretty(&Value::Object(output)).ok()
}
#[tokio::main]
async fn main() {
// Command 36 asks a peer for NFT metadata by name and series/item number.
let hashmap_key = generate_uid();
let rpc_command = 36;
// Item number 0 means a 1/1 NFT; nonzero values identify a series item.
let args: Vec<String> = env::args().collect();
if args.len() != 3 {
println!("Usage: ./lookup_nft <nft_name> <item_number>");
println!("Use item_number 0 for a 1/1 NFT, or the specific item number for a series NFT.");
return;
}
let nft_name = args[1].clone();
let item_number = args[2].clone();
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// sending_request expects command 36 lookup input as "name|series".
let payload = format!("{nft_name}|{item_number}");
// Try each configured peer until one returns a parsable NFT record or text error.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
payload.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// Successful NFT lookups are binary records; errors are returned as text.
if let Some(output) = decode_nft_lookup(&response) {
println!("{output}");
connected = true;
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}

119
src/bin/lookup_nft_list.rs Normal file
View File

@ -0,0 +1,119 @@
use blockchain::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::json;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::to_string_pretty;
use blockchain::{Map, Value};
fn decode_nft_list(response: &[u8]) -> Option<String> {
// An empty binary payload means the peer has no NFT index entries.
if response.is_empty() {
return Some("{\n \"nfts\": {}\n}".to_string());
}
// Each NFT-list row is 32 bytes hash, 15 bytes name, and 4 bytes series.
if response.len() % 51 != 0 {
return None;
}
let mut grouped = Map::new();
let mut offset = 0;
while offset + 51 <= response.len() {
// Group entries under their genesis hash so a collection can contain multiple series items.
let hash = binary_to_string(response[offset..offset + 32].to_vec())
.trim()
.to_string();
let nft_name = binary_to_string(response[offset + 32..offset + 47].to_vec())
.trim()
.to_string();
let series = u32::from_le_bytes(response[offset + 47..offset + 51].try_into().ok()?);
if !hash.is_empty() && !nft_name.is_empty() {
let entry = grouped
.entry(hash)
.or_insert_with(|| Value::Array(Vec::new()));
if let Value::Array(items) = entry {
items.push(json!({
"nft_name": nft_name,
"series": series,
}));
}
}
offset += 51;
}
let output = json!({
"nfts": Value::Object(grouped),
});
to_string_pretty(&output).ok()
}
#[tokio::main]
async fn main() {
// Command 32 asks a peer for the full NFT list.
let hashmap_key = generate_uid();
let rpc_command = 32;
// This lookup takes no user arguments beyond the wallet key used for handshake auth.
let args: Vec<String> = env::args().collect();
if args.len() != 1 {
println!("Usage: ./nft_list");
return;
}
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Try each configured peer until one returns a parsable NFT list or text error.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
"".to_string(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// Prefer binary NFT-list decoding; otherwise print a text error if one was returned.
if let Some(output) = decode_nft_list(&response) {
println!("{output}");
connected = true;
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
} else {
connected = false;
}
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,73 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::{TimeZone, Utc};
#[tokio::main]
async fn main() {
// Command 4 asks a peer for its current UTC timestamp.
let rpc_command = 4;
// This lookup takes no arguments beyond the wallet key used for the handshake.
let args: Vec<String> = env::args().collect();
if args.len() != 1 {
println!("Usage: ./lookup_node_time");
return;
}
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Try each configured peer until one returns a 4-byte timestamp.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
"".to_string(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
generate_uid(),
)
.await;
match result {
Ok(response) if response.len() == 4 => {
let timestamp = u32::from_le_bytes(response[0..4].try_into().unwrap());
let time = Utc
.timestamp_opt(timestamp as i64, 0)
.single()
.map(|datetime| datetime.format("%H:%M:%S UTC").to_string())
.unwrap_or_else(|| "invalid UTC timestamp".to_string());
println!("timestamp: {timestamp}");
println!("time: {time}");
connected = true;
}
Ok(response) => {
// Text errors are printed directly; unknown binary is ignored so another peer can be tried.
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,177 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::from_str;
use blockchain::read_to_string;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::tilde;
use blockchain::Value;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
use rustyline_derive::Completer;
use rustyline_derive::Helper as RustyHelper;
use rustyline_derive::Highlighter as RustyHighlighter;
use rustyline_derive::Hinter as RustyHinter;
use rustyline_derive::Validator as RustyValidator;
fn format_balance(balance: u64) -> String {
// Balance values are atomic units and display with 8 decimal places.
let whole = balance / 100_000_000;
let fractional = balance % 100_000_000;
format!("{whole}.{fractional:08}")
}
fn extract_address(contents: &str) -> Result<String, String> {
// Address files may be plain text or saved wallet JSON.
let trimmed = contents.trim();
if trimmed.is_empty() {
return Err("Address file is empty".to_string());
}
if trimmed.starts_with('{') {
// Prefer short_address for balance lookups, but accept long_address too.
let value: Value =
from_str(trimmed).map_err(|e| format!("Failed to parse wallet JSON: {e}"))?;
let address = value
.get("short_address")
.and_then(|v| v.as_str())
.or_else(|| value.get("long_address").and_then(|v| v.as_str()))
.ok_or_else(|| "Wallet JSON does not contain a usable address field".to_string())?;
return Ok(address.trim().to_string());
}
Ok(trimmed.to_string())
}
#[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)]
struct PathHelper {
#[rustyline(Completer)]
completer: FilenameCompleter,
}
fn prompt_for_path(prompt: &str) -> Result<String, String> {
// Rustyline gives the interactive prompt filesystem completion.
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(line) => Ok(line.trim().to_string()),
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(format!("Failed to read address file path: {err}")),
}
}
#[tokio::main]
async fn main() {
// Command 23 asks a peer for all asset balances for one wallet address.
let hashmap_key = generate_uid();
let rpc_command = 23;
// Accept an address file path as an arg or prompt interactively.
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args.len() != 2 {
println!("Usage: ./address_balance_lookup <address_file>");
return;
}
let address_file_path = if args.len() == 2 {
args[1].clone()
} else {
match prompt_for_path(
"Please enter the path to the file containing the wallet address: ",
) {
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
}
};
// Extract the lookup address from the selected file before opening a peer connection.
let expanded_path = tilde(&address_file_path).to_string();
let address_contents = read_to_string(&expanded_path)
.await
.expect("Failed to read address file");
let wallet_address = match extract_address(&address_contents) {
Ok(address) => address,
Err(err) => {
eprintln!("{err}");
return;
}
};
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let json = wallet_address;
// Try each configured peer until one returns a parsable balance response or text error.
let connections = get_connections().await;
let mut connected: bool = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
if response.is_empty() {
connected = true;
println!("[]");
break;
}
// Balance rows are 15 bytes asset name, 4 bytes NFT series, and 8 bytes balance.
if response.len() % 27 == 0 {
for entry in response.chunks_exact(27) {
let name_bytes = &entry[..15];
let nft_series = u32::from_le_bytes(entry[15..19].try_into().unwrap());
let balance = u64::from_le_bytes(entry[19..27].try_into().unwrap());
let asset_name = String::from_utf8_lossy(name_bytes).trim_end().to_string();
let formatted_balance = format_balance(balance);
if nft_series > 0 {
println!("{asset_name}:{nft_series} = {formatted_balance}");
} else {
println!("{asset_name} = {formatted_balance}");
}
}
connected = true;
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}

172
src/bin/lookup_token.rs Normal file
View File

@ -0,0 +1,172 @@
use blockchain::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::json;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::wallets::structures::Wallet;
const TOKEN_NAME_BYTES: usize = 15;
const HASH_BYTES: usize = 32;
const CREATOR_BYTES: usize = Wallet::SHORT_ADDRESS_BYTES_LENGTH;
const TOKEN_COUNT_BYTES: usize = 8;
const TOKEN_SPREAD_BYTES: usize = 4;
const HARD_LIMIT_BYTES: usize = 1;
const ISSUE_COUNT_BYTES: usize = 4;
const TOKEN_LOOKUP_FIXED_BYTES: usize = TOKEN_NAME_BYTES
+ HASH_BYTES
+ CREATOR_BYTES
+ TOKEN_COUNT_BYTES
+ TOKEN_SPREAD_BYTES
+ HARD_LIMIT_BYTES
+ ISSUE_COUNT_BYTES;
#[tokio::main]
async fn main() {
// Command 35 asks a peer for token metadata by token name.
let hashmap_key = generate_uid();
let rpc_command = 35;
// The request encoder pads/truncates the token name to the fixed 15-byte field.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./lookup_token <token_name>");
return;
}
let token_name = args[1].clone();
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Try each configured peer until one returns a parsable token record or text error.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
token_name.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// Successful token lookups are binary records with variable issue/burn hash lists.
if let Some(token_json) = decode_token_lookup(&response) {
println!("{token_json}");
connected = true;
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}
fn decode_token_lookup(response: &[u8]) -> Option<String> {
// The fixed portion must be present before reading variable-length issue/burn lists.
if response.len() < TOKEN_LOOKUP_FIXED_BYTES {
return None;
}
// Decode the fixed-width token fields in the same order the RPC command writes them.
let mut cursor = 0usize;
let token_name = binary_to_string(response[cursor..cursor + TOKEN_NAME_BYTES].to_vec())
.trim()
.to_string();
cursor += TOKEN_NAME_BYTES;
let contract = hex::encode(&response[cursor..cursor + HASH_BYTES]);
cursor += HASH_BYTES;
let creator = Wallet::bytes_to_short_address(&response[cursor..cursor + CREATOR_BYTES])?;
cursor += CREATOR_BYTES;
let token_count = u64::from_le_bytes(
response[cursor..cursor + TOKEN_COUNT_BYTES]
.try_into()
.ok()?,
);
cursor += TOKEN_COUNT_BYTES;
let token_spread = u32::from_le_bytes(
response[cursor..cursor + TOKEN_SPREAD_BYTES]
.try_into()
.ok()?,
);
cursor += TOKEN_SPREAD_BYTES;
let hard_limit = response[cursor];
cursor += HARD_LIMIT_BYTES;
let issue_count = u32::from_le_bytes(
response[cursor..cursor + ISSUE_COUNT_BYTES]
.try_into()
.ok()?,
);
cursor += ISSUE_COUNT_BYTES;
// After the issue count, each issue hash is 32 bytes.
let issue_bytes = issue_count as usize * 32;
if response.len() < cursor + issue_bytes + 4 {
return None;
}
let mut issued_hashes = Vec::with_capacity(issue_count as usize);
for chunk in response[cursor..cursor + issue_bytes].chunks(32) {
issued_hashes.push(hex::encode(chunk));
}
cursor += issue_bytes;
let burn_count = u32::from_le_bytes(response[cursor..cursor + 4].try_into().ok()?);
cursor += 4;
// The final section is a fixed number of 32-byte burn hashes.
let burn_bytes = burn_count as usize * 32;
if response.len() != cursor + burn_bytes {
return None;
}
let mut burned_hashes = Vec::with_capacity(burn_count as usize);
for chunk in response[cursor..cursor + burn_bytes].chunks(32) {
burned_hashes.push(hex::encode(chunk));
}
// Convert atomic token units and counters into user-facing JSON.
let display_token_count = token_count as f64 / 100_000_000.0;
let display_token_spread = format!("{token_spread} addresses");
let output = json!({
"token_name": token_name,
"genesis": contract,
"creator": creator,
"token_count": display_token_count,
"token_spread": display_token_spread,
"hard_limit": hard_limit == 1,
"issued_hashes": issued_hashes,
"burned_hashes": burned_hashes
});
serde_json::to_string_pretty(&output).ok()
}

View File

@ -0,0 +1,109 @@
use blockchain::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::json;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::to_string_pretty;
fn decode_token_list(response: &[u8]) -> Option<String> {
// An empty binary payload means the peer has no token index entries.
if response.is_empty() {
return Some("{\n \"tokens\": []\n}".to_string());
}
// Each token-list row is 15 bytes token name and 64 bytes token hash text.
if response.len() % 79 != 0 {
return None;
}
let mut tokens = Vec::new();
let mut offset = 0;
while offset + 79 <= response.len() {
// Convert fixed-width padded fields back into display strings.
let token = binary_to_string(response[offset..offset + 15].to_vec())
.trim()
.to_string();
let hash = binary_to_string(response[offset + 15..offset + 79].to_vec())
.trim()
.to_string();
if !token.is_empty() && !hash.is_empty() {
tokens.push(json!({
"token": token,
"hash": hash,
}));
}
offset += 79;
}
let output = json!({
"tokens": tokens,
});
to_string_pretty(&output).ok()
}
#[tokio::main]
async fn main() {
// Command 31 asks a peer for the full token list.
let hashmap_key = generate_uid();
let rpc_command = 31;
// This lookup takes no user arguments beyond the wallet key used for handshake auth.
let args: Vec<String> = env::args().collect();
if args.len() != 1 {
println!("Usage: ./token_list");
return;
}
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Try each configured peer until one returns a parsable token list or text error.
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
"".to_string(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// Prefer binary token-list decoding; otherwise print a text error if one was returned.
if let Some(output) = decode_token_list(&response) {
println!("{output}");
connected = true;
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
} else {
connected = false;
}
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

91
src/bin/lookup_torrent.rs Normal file
View File

@ -0,0 +1,91 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::to_string_pretty;
use blockchain::torrent::structs::Torrent;
#[tokio::main]
async fn main() {
let hashmap_key = generate_uid();
// set the rpc command number
let rpc_command = 12;
// Get command-line arguments
let args: Vec<String> = env::args().collect();
// Check if a command-line argument is provided
if args.len() != 2 {
println!("Usage: ./request_torrent <block_number>");
return;
}
// Extract the block number from the command-line argument
let block_number = match args[1].parse::<u32>() {
Ok(num) => num,
Err(_) => {
println!("Please enter a valid number");
return;
}
};
// Extract the encryption key from the command-line arguments
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let json = block_number.to_string();
let connections = get_connections().await;
let mut connected: bool = false;
// Iterate over the returned Vec and print each connection
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
if let Ok(torrent) = Torrent::from_bytes(&response).await {
match to_string_pretty(&torrent) {
Ok(torrent_json) => {
println!("{torrent_json}");
connected = true;
}
Err(err) => {
eprintln!("failed to serialize torrent json: {err}");
}
}
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
} else if !response.is_empty() {
println!("{}", hex::encode(response));
connected = true;
}
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,120 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE,
ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, REWARDS_TYPE, SWAP_TYPE, TRANSFER_TYPE,
VANITY_ADDRESS_TYPE,
};
use blockchain::env;
use blockchain::json;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
#[tokio::main]
async fn main() {
// Get command-line arguments
let args: Vec<String> = env::args().collect();
// Check if a command-line argument is provided
if args.len() != 1 {
println!("Usage: ./total_transactions");
return;
}
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// set the rpc command number
let rpc_command = 18;
let json = "".to_string();
let connections = get_connections().await;
let mut connected: bool = false;
let mut saw_empty_response = false;
// Iterate over the returned Vec and print each connection
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let hashmap_key = generate_uid();
let result = handshake::connect_and_handshake(
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
if response.is_empty() {
saw_empty_response = true;
println!("[]");
} else {
let mut output = Vec::new();
for chunk in response.chunks_exact(17) {
let txtype = chunk[0];
let mut total_bytes = [0u8; 8];
total_bytes.copy_from_slice(&chunk[1..9]);
let total = u64::from_le_bytes(total_bytes);
let mut non_zero_bytes = [0u8; 8];
non_zero_bytes.copy_from_slice(&chunk[9..17]);
let non_zero = u64::from_le_bytes(non_zero_bytes);
let tx_name = match txtype {
REWARDS_TYPE => "rewards",
TRANSFER_TYPE => "transfer",
CREATE_TOKEN_TYPE => "token",
CREATE_NFT_TYPE => "nft",
MARKETING_TYPE => "marketing",
SWAP_TYPE => "swap",
LENDER_TYPE => "loan",
BORROWER_TYPE => "loan_payment",
COLLATERAL_TYPE => "collateral_claim",
BURN_TYPE => "burn",
ISSUE_TOKEN_TYPE => "issue_token",
VANITY_ADDRESS_TYPE => "vanity_address",
_ => "unknown",
};
if txtype == REWARDS_TYPE {
output.push(json!({
"txtype": txtype,
"type": tx_name,
"total": total,
"non_zero": non_zero
}));
} else {
output.push(json!({
"txtype": txtype,
"type": tx_name,
"total": total
}));
}
}
println!("{}", serde_json::to_string_pretty(&output).unwrap());
}
connected = true;
}
Err(_) => {
connected = false;
}
}
}
if !connected {
if saw_empty_response {
println!("[]");
} else {
eprintln!("Unable to reach any node or retrieve a transaction count.");
}
}
}

View File

@ -0,0 +1,162 @@
use blockchain::blocks::collateral::CollateralClaimTransaction;
use blockchain::blocks::genesis::GenesisTransaction;
use blockchain::blocks::loan_payment::ContractPaymentTransaction;
use blockchain::blocks::loans::LoanContractTransaction;
use blockchain::blocks::marketing::MarketingTransaction;
use blockchain::blocks::nft::CreateNftTransaction;
use blockchain::blocks::rewards::RewardsTransaction;
use blockchain::blocks::swap::SwapTransaction;
use blockchain::blocks::token::CreateTokenTransaction;
use blockchain::blocks::transfer::TransferTransaction;
use blockchain::blocks::vanity::VanityAddressTransaction;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
BORROWER_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, GENESIS_TYPE, LENDER_TYPE,
MARKETING_TYPE, REWARDS_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE,
};
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::to_string_pretty;
#[tokio::main]
async fn main() {
let hashmap_key = generate_uid();
// set the rpc command number
let rpc_command = 17;
// Get command-line arguments
let args: Vec<String> = env::args().collect();
// Check if a command-line argument is provided
if args.len() != 2 {
println!("Usage: ./lookup_transaction <transaction_hash>");
return;
}
// Extract the block number from the command-line argument
let block_hash = match args[1].parse::<String>() {
Ok(num) => num,
Err(_) => {
println!("Please enter a hash");
return;
}
};
// Extract the encryption ley from the command-line argument
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let json = block_hash.to_string();
let connections = get_connections().await;
let mut connected: bool = false;
// Iterate over the returned Vec and print each connection
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
if let Some(transaction_json) = decode_transaction_to_json(&response).await {
println!("{transaction_json}");
connected = true;
} else {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
} else if !response.is_empty() {
println!("{}", hex::encode(response));
connected = true;
}
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}
async fn decode_transaction_to_json(response: &[u8]) -> Option<String> {
if response.len() < 5 {
return None;
}
let block_number = u32::from_le_bytes(response[0..4].try_into().ok()?);
let txtype = response[4];
let body = &response[5..];
let json = match txtype {
GENESIS_TYPE => {
to_string_pretty(&GenesisTransaction::from_bytes(txtype, body).await.ok()?).ok()?
}
REWARDS_TYPE => {
to_string_pretty(&RewardsTransaction::from_bytes(txtype, body).await.ok()?).ok()?
}
TRANSFER_TYPE => {
to_string_pretty(&TransferTransaction::from_bytes(txtype, body).await.ok()?).ok()?
}
CREATE_TOKEN_TYPE => to_string_pretty(
&CreateTokenTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok()?,
CREATE_NFT_TYPE => {
to_string_pretty(&CreateNftTransaction::from_bytes(txtype, body).await.ok()?).ok()?
}
MARKETING_TYPE => {
to_string_pretty(&MarketingTransaction::from_bytes(txtype, body).await.ok()?).ok()?
}
SWAP_TYPE => {
to_string_pretty(&SwapTransaction::from_bytes(txtype, body).await.ok()?).ok()?
}
LENDER_TYPE => to_string_pretty(
&LoanContractTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok()?,
BORROWER_TYPE => to_string_pretty(
&ContractPaymentTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok()?,
COLLATERAL_TYPE => to_string_pretty(
&CollateralClaimTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok()?,
VANITY_ADDRESS_TYPE => to_string_pretty(
&VanityAddressTransaction::from_bytes(txtype, body)
.await
.ok()?,
)
.ok()?,
_ => return None,
};
Some(format!("block: {block_number}\n{json}"))
}

View File

@ -0,0 +1,531 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible_with_default};
#[cfg(windows)]
use std::env;
#[cfg(unix)]
use std::error::Error;
#[cfg(windows)]
use std::error::Error;
#[cfg(unix)]
use std::fs;
#[cfg(windows)]
use std::fs;
#[cfg(windows)]
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::process::Command;
#[cfg(unix)]
use std::process::{Command, Stdio};
#[cfg(windows)]
const DEFAULT_PG_VERSION: &str = "16.4-1";
#[cfg(unix)]
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
println!("\n=== Contractless Postgres Installer ===\n");
// Collect the settings values that will be printed for settings.ini after setup.
let host = prompt_visible_with_default("Enter Postgres host", "127.0.0.1").await;
let port = prompt_visible_with_default("Enter Postgres port", "5432").await;
let dbname =
prompt_visible_with_default("Enter new database name for blockchain", "contractless_db")
.await;
let user =
prompt_visible_with_default("Enter new username for blockchain database", "contractless")
.await;
let user_pass = prompt_hidden_nonempty(
"Enter password for new database user: ",
"Password cannot be empty. Please try again.",
)
.await;
// Linux setup edits Postgres config and may install packages, so it requires root.
if !nix::unistd::Uid::effective().is_root() {
eprintln!("\n[!] This installer requires root privileges. Please run with sudo.\n");
std::process::exit(1);
}
// Install PostgreSQL when psql is not already available.
let psql_installed = Command::new("which").arg("psql").output()?.status.success();
if !psql_installed {
println!("\n[+] PostgreSQL not found. Installing...");
Command::new("apt-get").args(["update"]).status()?;
Command::new("apt-get")
.args(["install", "-y", "postgresql"])
.status()?;
} else {
println!("[✓] PostgreSQL already installed");
}
// Ensure pg_hba.conf allows local password auth for the blockchain database user.
println!("\n[~] Configuring password-based login...");
let pg_hba_path = find_pg_hba_path()?;
let hba_contents = fs::read_to_string(&pg_hba_path)?;
if !hba_contents.contains("host all all 127.0.0.1/32 md5") {
let mut lines: Vec<String> = hba_contents.lines().map(String::from).collect();
lines.push(String::from("host all all 127.0.0.1/32 md5"));
lines.push(String::from("host all all ::1/128 md5"));
fs::write(&pg_hba_path, lines.join("\n"))?;
println!("[+] Updated pg_hba.conf to enable md5 authentication");
// Reload Postgres so pg_hba.conf changes are applied without a reboot.
Command::new("systemctl")
.args(["reload", "postgresql"])
.status()?;
println!("[✓] PostgreSQL reloaded");
} else {
println!("[✓] Password auth already enabled");
}
// Create the dedicated blockchain database user.
let create_user_cmd = format!("CREATE USER {user} WITH PASSWORD '{user_pass}';");
let user_status = Command::new("sudo")
.arg("-u")
.arg("postgres")
.arg("psql")
.arg("-c")
.arg(&create_user_cmd)
.status()?;
// Create the blockchain database owned by that dedicated user.
let create_db_cmd = format!("CREATE DATABASE {dbname} OWNER {user};");
let db_status = Command::new("sudo")
.arg("-u")
.arg("postgres")
.arg("psql")
.arg("-c")
.arg(&create_db_cmd)
.status()?;
if !db_status.success() {
return Err("❌ Failed to create database (maybe it already exists?)".into());
}
if !user_status.success() {
println!("❌ Failed to create user (maybe it already exists?)");
}
println!("\n[✓] Database and user created successfully!");
println!("\nPaste the following into your settings.ini:\n");
println!("[Postgres]");
println!("host = {host}");
println!("port = {port}");
println!("user = {user}");
println!("password = {user_pass}");
println!("dbname = {dbname}");
Ok(())
}
#[cfg(not(unix))]
#[cfg(windows)]
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
println!("\n=== Contractless Postgres Installer (Windows) ===\n");
// Windows installation writes into Program Files and manages a service.
ensure_administrator()?;
// Collect install settings and the database credentials to print at the end.
let host = prompt_visible_with_default("Enter Postgres host", "127.0.0.1").await;
let port = prompt_visible_with_default("Enter Postgres port", "5432").await;
let dbname =
prompt_visible_with_default("Enter new database name for blockchain", "contractless_db")
.await;
let user =
prompt_visible_with_default("Enter new username for blockchain database", "contractless")
.await;
let user_pass = prompt_hidden_nonempty(
"Enter password for new database user: ",
"Password cannot be empty. Please try again.",
)
.await;
let postgres_pass = prompt_hidden_nonempty(
"Enter password for the postgres superuser/service account: ",
"Password cannot be empty. Please try again.",
)
.await;
let install_dir = prompt_visible_with_default(
"Enter PostgreSQL install directory",
r"C:\Program Files\PostgreSQL\16",
)
.await;
let data_dir = prompt_visible_with_default(
"Enter PostgreSQL data directory",
r"C:\Program Files\PostgreSQL\16\data",
)
.await;
let service_name = prompt_visible_with_default(
"Enter PostgreSQL Windows service name",
"postgresql-contractless",
)
.await;
validate_identifier("database name", &dbname)?;
validate_identifier("database user", &user)?;
// The installer may create PostgreSQL, but existing installs are reused.
let prefix = PathBuf::from(&install_dir);
let datadir = PathBuf::from(&data_dir);
let psql_path = prefix.join("bin").join("psql.exe");
if !psql_path.exists() {
let installer_path = download_installer(DEFAULT_PG_VERSION)?;
run_unattended_installer(
&installer_path,
&prefix,
&datadir,
&port,
&service_name,
&postgres_pass,
)?;
} else {
println!("[✓] PostgreSQL already installed at {}", prefix.display());
}
if !psql_path.exists() {
return Err(format!(
"psql.exe was not found after installation at {}",
psql_path.display()
)
.into());
}
wait_for_psql_ready(&psql_path, &host, &port, &postgres_pass)?;
ensure_database_user(&psql_path, &host, &port, &user, &user_pass, &postgres_pass)?;
ensure_database(&psql_path, &host, &port, &dbname, &user, &postgres_pass)?;
test_blockchain_login(&psql_path, &host, &port, &dbname, &user, &user_pass)?;
println!("\n[✓] PostgreSQL installed and configured successfully!");
println!("\nPaste the following into your settings-windows.ini or settings.ini:\n");
println!("[Postgres]");
println!("host = {host}");
println!("port = {port}");
println!("user = {user}");
println!("password = {user_pass}");
println!("dbname = {dbname}");
println!("\n[Postgres-Testnet]");
println!("host = {host}");
println!("port = {port}");
println!("user = {user}");
println!("password = {user_pass}");
println!("dbname = {dbname}");
Ok(())
}
#[cfg(not(any(unix, windows)))]
fn main() {
eprintln!("postgres_installer is only supported on Unix-like systems and Windows.");
std::process::exit(1);
}
#[cfg(unix)]
fn find_pg_hba_path() -> Result<String, Box<dyn Error>> {
// Ask PostgreSQL for the active pg_hba.conf path instead of guessing distro paths.
let output = Command::new("sudo")
.arg("-u")
.arg("postgres")
.arg("psql")
.arg("-t")
.arg("-P")
.arg("format=unaligned")
.arg("-c")
.arg("SHOW hba_file;")
.stdout(Stdio::piped())
.output()?;
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if path.is_empty() {
return Err("❌ Could not determine pg_hba.conf path".into());
}
Ok(path)
}
#[cfg(windows)]
fn ensure_administrator() -> Result<(), Box<dyn Error>> {
// "net session" succeeds only from an elevated Windows terminal.
let status = Command::new("net").arg("session").status()?;
if status.success() {
Ok(())
} else {
Err("This installer requires Administrator privileges. Please run it from an elevated terminal.".into())
}
}
#[cfg(windows)]
fn download_installer(version: &str) -> Result<PathBuf, Box<dyn Error>> {
// Download the EDB installer once into temp and reuse it on repeated runs.
let installer_name = format!("postgresql-{version}-windows-x64.exe");
let url = format!("https://get.enterprisedb.com/postgresql/{installer_name}");
let temp_dir = env::temp_dir().join("contractless-postgres-installer");
fs::create_dir_all(&temp_dir)?;
let installer_path = temp_dir.join(&installer_name);
if installer_path.exists() {
println!(
"[✓] Reusing downloaded installer at {}",
installer_path.display()
);
return Ok(installer_path);
}
println!("[~] Downloading PostgreSQL installer from {url}");
let status = Command::new("powershell")
.args([
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
&format!(
"Invoke-WebRequest -Uri '{}' -OutFile '{}'",
url,
installer_path.display()
),
])
.status()?;
if !status.success() {
return Err("Failed to download the PostgreSQL installer.".into());
}
Ok(installer_path)
}
#[cfg(windows)]
fn run_unattended_installer(
installer_path: &Path,
prefix: &Path,
datadir: &Path,
port: &str,
service_name: &str,
postgres_pass: &str,
) -> Result<(), Box<dyn Error>> {
println!("[~] Running unattended PostgreSQL installer...");
// Use the command-line installer so the tool can run without GUI interaction.
let prefix_string = prefix.to_string_lossy().into_owned();
let datadir_string = datadir.to_string_lossy().into_owned();
let status = Command::new(installer_path)
.arg("--mode")
.arg("unattended")
.arg("--unattendedmodeui")
.arg("minimal")
.arg("--superaccount")
.arg("postgres")
.arg("--superpassword")
.arg(postgres_pass)
.arg("--serviceaccount")
.arg("postgres")
.arg("--servicepassword")
.arg(postgres_pass)
.arg("--servicename")
.arg(service_name)
.arg("--serverport")
.arg(port)
.arg("--prefix")
.arg(&prefix_string)
.arg("--datadir")
.arg(&datadir_string)
.arg("--enable-components")
.arg("server,commandlinetools")
.arg("--disable-components")
.arg("pgAdmin,stackbuilder")
.status()?;
if !status.success() {
return Err("PostgreSQL installer failed.".into());
}
Ok(())
}
#[cfg(windows)]
fn run_psql(
psql_path: &Path,
host: &str,
port: &str,
database: &str,
password: &str,
sql: &str,
) -> Result<std::process::Output, Box<dyn Error>> {
// PGPASSWORD avoids interactive password prompts while still using normal psql auth.
let output = Command::new(psql_path)
.env("PGPASSWORD", password)
.args([
"-h",
host,
"-p",
port,
"-U",
"postgres",
"-d",
database,
"-v",
"ON_ERROR_STOP=1",
"-t",
"-A",
"-c",
sql,
])
.output()?;
Ok(output)
}
#[cfg(windows)]
fn wait_for_psql_ready(
psql_path: &Path,
host: &str,
port: &str,
postgres_pass: &str,
) -> Result<(), Box<dyn Error>> {
println!("[~] Waiting for PostgreSQL service to accept connections...");
// The Windows service can take a few seconds after install before psql accepts logins.
for _ in 0..30 {
let output = run_psql(
psql_path,
host,
port,
"postgres",
postgres_pass,
"SELECT 1;",
)?;
if output.status.success() {
return Ok(());
}
std::thread::sleep(std::time::Duration::from_secs(2));
}
Err("PostgreSQL did not become ready in time.".into())
}
#[cfg(windows)]
fn ensure_database_user(
psql_path: &Path,
host: &str,
port: &str,
user: &str,
user_pass: &str,
postgres_pass: &str,
) -> Result<(), Box<dyn Error>> {
// Create the blockchain login if missing, otherwise refresh its password.
let escaped_pass = user_pass.replace('\'', "''");
let sql = format!(
"DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '{user}') THEN CREATE ROLE {user} LOGIN PASSWORD '{escaped_pass}'; ELSE ALTER ROLE {user} WITH LOGIN PASSWORD '{escaped_pass}'; END IF; END $$;"
);
let output = run_psql(psql_path, host, port, "postgres", postgres_pass, &sql)?;
if output.status.success() {
Ok(())
} else {
Err(format!(
"Failed to create or update database user: {}",
String::from_utf8_lossy(&output.stderr)
)
.into())
}
}
#[cfg(windows)]
fn ensure_database(
psql_path: &Path,
host: &str,
port: &str,
dbname: &str,
user: &str,
postgres_pass: &str,
) -> Result<(), Box<dyn Error>> {
// Check first because CREATE DATABASE cannot run inside the DO block used for roles.
let check_sql = format!("SELECT 1 FROM pg_database WHERE datname = '{dbname}';");
let output = run_psql(psql_path, host, port, "postgres", postgres_pass, &check_sql)?;
if !output.status.success() {
return Err(format!(
"Failed to check database existence: {}",
String::from_utf8_lossy(&output.stderr)
)
.into());
}
if String::from_utf8_lossy(&output.stdout).trim() == "1" {
println!("[✓] Database {dbname} already exists");
return Ok(());
}
let create_sql = format!("CREATE DATABASE {dbname} OWNER {user};");
let create_output = run_psql(
psql_path,
host,
port,
"postgres",
postgres_pass,
&create_sql,
)?;
if create_output.status.success() {
Ok(())
} else {
Err(format!(
"Failed to create database: {}",
String::from_utf8_lossy(&create_output.stderr)
)
.into())
}
}
#[cfg(windows)]
fn test_blockchain_login(
psql_path: &Path,
host: &str,
port: &str,
dbname: &str,
user: &str,
user_pass: &str,
) -> Result<(), Box<dyn Error>> {
// Verify the credentials that will be placed into settings.ini actually work.
let output = Command::new(psql_path)
.env("PGPASSWORD", user_pass)
.args([
"-h",
host,
"-p",
port,
"-U",
user,
"-d",
dbname,
"-v",
"ON_ERROR_STOP=1",
"-t",
"-A",
"-c",
"SELECT 1;",
])
.output()?;
if output.status.success() {
Ok(())
} else {
Err(format!(
"Failed to verify blockchain database login: {}",
String::from_utf8_lossy(&output.stderr)
)
.into())
}
}
#[cfg(windows)]
fn validate_identifier(label: &str, value: &str) -> Result<(), Box<dyn Error>> {
// Database identifiers are inserted into SQL statements, so keep them simple and safe.
if value.is_empty() || !value.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(format!(
"Invalid {label}. Only letters, numbers, and underscores are allowed."
)
.into());
}
Ok(())
}

View File

@ -0,0 +1,127 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::decode_image_and_extract_text;
use blockchain::decrypts;
use blockchain::env;
use blockchain::from_str;
use blockchain::fs;
use blockchain::tilde;
use blockchain::Value;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
use rustyline_derive::Completer;
use rustyline_derive::Helper as RustyHelper;
use rustyline_derive::Highlighter as RustyHighlighter;
use rustyline_derive::Hinter as RustyHinter;
use rustyline_derive::Validator as RustyValidator;
pub async fn decode_private_key(base64_image: String, wallet_key: String) -> String {
let private_key: String;
if let Some(encrypted_text) = decode_image_and_extract_text(&base64_image) {
// Decrypt the encrypted text to get the real private key
if let Some(decrypted_private_key) = decrypts(&encrypted_text, Some(&wallet_key)) {
private_key = decrypted_private_key;
} else {
eprintln!("Decryption of private key failed.");
panic!();
}
} else {
eprintln!("Failed to decode the image and extract text.");
panic!();
}
private_key
}
fn extract_private_key(contents: &str) -> Result<String, String> {
let trimmed = contents.trim();
if trimmed.is_empty() {
return Err("Wallet/image file is empty".to_string());
}
if trimmed.starts_with('{') {
let value: Value =
from_str(trimmed).map_err(|e| format!("Failed to parse wallet JSON: {e}"))?;
let private_key = value
.get("private_key")
.or_else(|| value.get("privkey"))
.and_then(|v| v.as_str())
.ok_or_else(|| {
"Wallet JSON does not contain a \"private_key\" or \"privkey\" field".to_string()
})?;
return Ok(private_key.trim().to_string());
}
Ok(trimmed.to_string())
}
#[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)]
struct PathHelper {
#[rustyline(Completer)]
completer: FilenameCompleter,
}
fn prompt_for_path(prompt: &str) -> Result<String, String> {
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(line) => Ok(line.trim().to_string()),
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(format!("Failed to read file path: {err}")),
}
}
#[tokio::main]
async fn main() {
// Collect command-line arguments
let args: Vec<String> = env::args().collect();
let base64_file_path = if args.len() > 1 {
args[1].clone()
} else {
// Prompt the user for file path and encryption key
match prompt_for_path(
"Please enter the path to the file containing the wallet/image data: ",
) {
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
}
};
let wallet_key = prompt_hidden_nonempty(
"Please enter your encryption key: ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Read the Base64 string from the specified file
let expanded_path = tilde(&base64_file_path).to_string();
let file_contents = fs::read_to_string(&expanded_path)
.expect("Failed to read Base64 string file")
.to_string();
let base64_string = match extract_private_key(&file_contents) {
Ok(value) => value,
Err(err) => {
eprintln!("{err}");
return;
}
};
// Decode the private key from Base64 string
let private_key = decode_private_key(base64_string, wallet_key).await;
// Print the private key in text format
println!("{private_key}");
}

235
src/bin/recreate_wallet.rs Normal file
View File

@ -0,0 +1,235 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::create_img;
use blockchain::encrypts;
use blockchain::env;
use blockchain::fs;
use blockchain::read_to_string;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::tilde;
use blockchain::wallets::structures::{SavedWallet, Wallet};
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
use rustyline_derive::Completer;
use rustyline_derive::Helper as RustyHelper;
use rustyline_derive::Highlighter as RustyHighlighter;
use rustyline_derive::Hinter as RustyHinter;
use rustyline_derive::Validator as RustyValidator;
use std::path::PathBuf;
#[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)]
struct PathHelper {
#[rustyline(Completer)]
completer: FilenameCompleter,
}
fn display_vanity_address(short_address: &str) -> Option<String> {
let (payload, network_suffix) = short_address.rsplit_once('.')?;
let trimmed_payload = payload.trim_start();
if trimmed_payload.is_empty() {
return None;
}
Some(format!("{trimmed_payload}.{network_suffix}"))
}
async fn lookup_registered_vanity_address(
short_address: &str,
long_address: &str,
private_key: &str,
) -> Option<String> {
let hashmap_key = generate_uid();
let rpc_command = 40;
let connections = get_connections().await;
for conn in connections {
let Ok(socket_address) = conn.parse() else {
continue;
};
let result = handshake::connect_and_handshake(
socket_address,
short_address.to_string(),
rpc_command,
handshake::HandshakeWallet::WalletParts {
long_address: long_address.to_string(),
private_key: private_key.to_string(),
},
hashmap_key,
)
.await;
if let Ok(response) = result {
if response.is_empty() {
return None;
}
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if trimmed.is_empty() || trimmed.starts_with("error:") {
return None;
}
return display_vanity_address(trimmed);
}
}
None
}
fn prompt_for_path(prompt: &str) -> Result<String, String> {
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(line) => Ok(line.trim().to_string()),
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(format!("Failed to read file path: {err}")),
}
}
fn prompt_for_filename(prompt: &str) -> Result<String, String> {
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
Err("Filename cannot be empty".to_string())
} else {
Ok(trimmed.to_string())
}
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(format!("Failed to read filename: {err}")),
}
}
#[tokio::main]
async fn main() {
let args: Vec<String> = env::args().collect();
let private_key_path: String;
let output_path: String;
if args.len() > 1 && args.len() != 3 && args.len() != 4 {
println!("Usage: ./recreate_wallet <private_key_file> <output_wallet_file>");
println!(" or: ./recreate_wallet <private_key_file> <output_wallet_dir> <output_wallet_filename>");
return;
}
if args.len() == 4 {
output_path = PathBuf::from(&args[2])
.join(&args[3])
.to_string_lossy()
.to_string();
private_key_path = args[1].clone();
} else if args.len() == 3 {
private_key_path = args[1].clone();
output_path = args[2].clone();
} else {
private_key_path = match prompt_for_path(
"Please enter the path to the file containing your wallet private key: ",
) {
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
};
let output_dir_path = match prompt_for_path(
"Please enter the directory path where the rebuilt wallet JSON should be written: ",
) {
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
};
let output_filename =
match prompt_for_filename("Please enter the filename for the rebuilt wallet JSON: ") {
Ok(filename) => filename,
Err(err) => {
eprintln!("{err}");
return;
}
};
let output_dir = tilde(&output_dir_path).to_string();
output_path = PathBuf::from(output_dir)
.join(output_filename)
.to_string_lossy()
.to_string();
}
let wallet_key = prompt_hidden_nonempty(
"Please enter your encryption key: ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let expanded_private_key_path = tilde(&private_key_path).to_string();
let expanded_output_path = tilde(&output_path).to_string();
let private_key = read_to_string(&expanded_private_key_path)
.await
.expect("Failed to read private key file")
.trim()
.to_string();
// encrypt the private key for generated a secure image from it
let encrypted = encrypts(&private_key.clone(), Some(&wallet_key), None).unwrap();
// don't set a watermark
let watermark = "empty";
// create an image from the encrypted private key
let image_data = create_img(
&encrypted, "h", watermark, None, None, None, None, None, None,
)
.expect("Failed to create image");
match Wallet::regenerate_public_key(&private_key) {
Ok(public_key_bytes) => {
let public_key = blockchain::encode(public_key_bytes.clone());
let long_address = Wallet::generate_address(&public_key);
let long_address_bytes = Wallet::long_address_to_bytes(long_address.clone());
let short_address_bytes =
Wallet::long_address_bytes_to_short_address_bytes(&long_address_bytes)
.expect("Failed to derive short address bytes");
let short_address = Wallet::bytes_to_short_address(&short_address_bytes)
.expect("Failed to encode short address");
let vanity_address =
lookup_registered_vanity_address(&short_address, &long_address, &private_key).await;
let saved_wallet = SavedWallet {
long_address,
short_address,
vanity_address,
public_key,
private_key: image_data,
};
let output_str = serde_json::to_string_pretty(&saved_wallet).unwrap();
fs::write(&expanded_output_path, output_str)
.expect("Failed to write rebuilt wallet file");
println!("Wallet written to {expanded_output_path}");
}
Err(e) => println!("Error: {e}"),
}
}

View File

@ -0,0 +1,272 @@
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::decode_image_and_extract_text;
use blockchain::decrypts;
use blockchain::env;
use blockchain::fs;
use blockchain::read_to_string;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::stdout;
use blockchain::tilde;
use blockchain::wallets::structures::{SavedWallet, Wallet};
use blockchain::AsyncWriteExt;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
use rustyline_derive::Completer;
use rustyline_derive::Helper as RustyHelper;
use rustyline_derive::Highlighter as RustyHighlighter;
use rustyline_derive::Hinter as RustyHinter;
use rustyline_derive::Validator as RustyValidator;
use std::path::PathBuf;
fn display_vanity_address(short_address: &str) -> Option<String> {
let (payload, network_suffix) = short_address.rsplit_once('.')?;
let trimmed_payload = payload.trim_start();
if trimmed_payload.is_empty() {
return None;
}
Some(format!("{trimmed_payload}.{network_suffix}"))
}
async fn lookup_registered_vanity_address(
short_address: &str,
long_address: &str,
private_key: &str,
) -> Option<String> {
let hashmap_key = generate_uid();
let rpc_command = 40;
let connections = get_connections().await;
for conn in connections {
let Ok(socket_address) = conn.parse() else {
continue;
};
let result = handshake::connect_and_handshake(
socket_address,
short_address.to_string(),
rpc_command,
handshake::HandshakeWallet::WalletParts {
long_address: long_address.to_string(),
private_key: private_key.to_string(),
},
hashmap_key,
)
.await;
if let Ok(response) = result {
if response.is_empty() {
return None;
}
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if trimmed.is_empty() || trimmed.starts_with("error:") {
return None;
}
return display_vanity_address(trimmed);
}
}
None
}
pub fn load_wallet(private_key_image: String, wallet_key: String) -> String {
let private_key: String;
if let Some(encrypted_text) = decode_image_and_extract_text(&private_key_image) {
// Decrypt the encrypted text to get the real private key
if let Some(decrypted_private_key) = decrypts(&encrypted_text, Some(&wallet_key)) {
// Update the wallet's private key with the decrypted private key
private_key = decrypted_private_key;
} else {
eprintln!("Decryption of private key failed.");
panic!();
}
} else {
eprintln!("Failed to decode the image and extract text.");
panic!();
}
private_key
}
#[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)]
struct PathHelper {
#[rustyline(Completer)]
completer: FilenameCompleter,
}
fn prompt_for_path(prompt: &str) -> Result<String, String> {
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(line) => Ok(line.trim().to_string()),
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(format!("Failed to read file path: {err}")),
}
}
fn prompt_for_filename(prompt: &str) -> Result<String, String> {
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
Err("Filename cannot be empty".to_string())
} else {
Ok(trimmed.to_string())
}
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(format!("Failed to read filename: {err}")),
}
}
async fn load_private_key_image_input(path: &str) -> Result<String, String> {
let expanded_path = tilde(path).to_string();
if expanded_path.to_ascii_lowercase().ends_with(".png") {
let image_bytes =
fs::read(&expanded_path).map_err(|e| format!("Failed to read PNG file: {e}"))?;
return Ok(STANDARD.encode(image_bytes));
}
let file_contents = read_to_string(&expanded_path)
.await
.map_err(|e| format!("Failed to read Base64 string file: {e}"))?;
Ok(file_contents.trim().to_string())
}
#[tokio::main]
async fn main() {
let mut stdout = stdout(); // Get Tokio's stdout
// Collect command-line arguments
let args: Vec<String> = env::args().collect();
let base64_file_path: String;
let output_path: String;
if args.len() > 1 && args.len() != 3 && args.len() != 4 {
println!(
"Usage: ./recreate_wallet_from_image <base64_or_png_file_path> <output_wallet_file>"
);
println!(" or: ./recreate_wallet_from_image <base64_or_png_file_path> <output_wallet_dir> <output_wallet_filename>");
return;
}
if args.len() == 4 {
base64_file_path = args[1].clone();
output_path = PathBuf::from(&args[2])
.join(&args[3])
.to_string_lossy()
.to_string();
} else if args.len() == 3 {
base64_file_path = args[1].clone();
output_path = args[2].clone();
} else {
stdout.flush().await.expect("Failed to flush stdout");
base64_file_path =
match prompt_for_path("Please enter the path to the Base64 or PNG image file: ") {
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
};
let output_dir_path = match prompt_for_path(
"Please enter the directory path where the rebuilt wallet JSON should be written: ",
) {
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
};
let output_filename =
match prompt_for_filename("Please enter the filename for the rebuilt wallet JSON: ") {
Ok(filename) => filename,
Err(err) => {
eprintln!("{err}");
return;
}
};
let output_dir = tilde(&output_dir_path).to_string();
output_path = PathBuf::from(output_dir)
.join(output_filename)
.to_string_lossy()
.to_string();
}
let wallet_key = prompt_hidden_nonempty(
"Please enter your encryption key: ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let base64_string = match load_private_key_image_input(&base64_file_path).await {
Ok(contents) => contents,
Err(err) => {
eprintln!("{err}");
return;
}
};
let expanded_output_path = tilde(&output_path).to_string();
// Generate private key from Base64 string
let private_key = load_wallet(base64_string.clone(), wallet_key);
match Wallet::regenerate_public_key(&private_key) {
Ok(public_key_bytes) => {
let public_key = blockchain::encode(public_key_bytes.clone());
let long_address = Wallet::generate_address(&public_key);
let long_address_bytes = Wallet::long_address_to_bytes(long_address.clone());
let short_address_bytes =
Wallet::long_address_bytes_to_short_address_bytes(&long_address_bytes)
.expect("Failed to derive short address bytes");
let short_address = Wallet::bytes_to_short_address(&short_address_bytes)
.expect("Failed to encode short address");
let vanity_address =
lookup_registered_vanity_address(&short_address, &long_address, &private_key).await;
let saved_wallet = SavedWallet {
long_address,
short_address,
vanity_address,
public_key,
private_key: base64_string,
};
let output_str = serde_json::to_string_pretty(&saved_wallet).unwrap();
fs::write(&expanded_output_path, output_str)
.expect("Failed to write rebuilt wallet file");
println!("Wallet written to {expanded_output_path}");
}
Err(e) => println!("Error: {e}"),
}
}

104
src/bin/register_wallet.rs Normal file
View File

@ -0,0 +1,104 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::common::skein::skein_256_hash_bytes;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::rpc::command_maps::RPC_REGISTER_WALLET;
use blockchain::standalone_tools::connections::handshake;
use blockchain::wallets::structures::Wallet;
#[tokio::main]
async fn main() {
// Wallet registration submits a signed short-address -> long-address mapping to a peer.
let hashmap_key = generate_uid();
// This command works from the saved wallet only and takes no extra CLI arguments.
let args: Vec<String> = env::args().collect();
if args.len() != 1 {
println!("Usage: ./register_wallet");
return;
}
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Load the wallet so both address forms and the signing key come from the same saved file.
let wallet = match Wallet::try_obtain_wallet(decryption_key.clone(), None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
// The peer receives both addresses, but the signature proves they belong together.
let short_address = wallet.saved.short_address.clone();
let long_address = wallet.saved.long_address.clone();
let short_address_bytes = match Wallet::short_address_to_bytes(&short_address) {
Some(bytes) => bytes,
None => {
eprintln!("Failed to derive short address bytes from the wallet.");
return;
}
};
let long_address_bytes = Wallet::long_address_to_bytes(long_address.clone());
// The signed payload mirrors the binary request body: command byte, short address, long address.
let mut signed_payload =
Vec::with_capacity(1 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::ADDRESS_BYTES_LENGTH);
signed_payload.push(RPC_REGISTER_WALLET);
signed_payload.extend_from_slice(&short_address_bytes);
signed_payload.extend_from_slice(&long_address_bytes);
let payload_hash = skein_256_hash_bytes(&signed_payload);
let signature = Wallet::sign_transaction(&payload_hash, &wallet.saved.private_key).await;
// sending_request encodes command 38 from this pipe-delimited payload.
let json = format!("{short_address}|{long_address}|{signature}");
let rpc_command = 38;
let connections = get_connections().await;
// Try each configured peer until the registration is accepted or every peer fails.
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
json.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(decryption_key.clone()),
hashmap_key,
)
.await;
match result {
Ok(response) => {
// The peer returns "1" on successful wallet registry insert.
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
if trimmed == "1" {
println!("Wallet registered: {short_address}");
} else {
println!("Wallet registration failed.");
}
connected = true;
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,206 @@
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use blockchain::env;
use blockchain::from_str;
use blockchain::fs;
use blockchain::read_to_string;
use blockchain::tilde;
use blockchain::Value;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
use rustyline_derive::Completer;
use rustyline_derive::Helper as RustyHelper;
use rustyline_derive::Highlighter as RustyHighlighter;
use rustyline_derive::Hinter as RustyHinter;
use rustyline_derive::Validator as RustyValidator;
use std::path::PathBuf;
#[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)]
struct PathHelper {
#[rustyline(Completer)]
completer: FilenameCompleter,
}
fn prompt_for_path(prompt: &str) -> Result<String, String> {
// Rustyline gives interactive path prompts filesystem completion.
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(line) => Ok(line.trim().to_string()),
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(format!("Failed to read file path: {err}")),
}
}
fn prompt_for_filename(prompt: &str) -> Result<String, String> {
// The output filename is kept separate when the user chooses a directory interactively.
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
Err("Filename cannot be empty".to_string())
} else {
Ok(trimmed.to_string())
}
}
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(format!("Failed to read filename: {err}")),
}
}
fn extract_private_key_image(contents: &str) -> Result<String, String> {
// Input may be a saved wallet JSON object or a raw base64 image string.
let trimmed = contents.trim();
if trimmed.is_empty() {
return Err("Wallet/image file is empty".to_string());
}
if trimmed.starts_with('{') {
// Accept both field names used by wallet/private-key image exports.
let value: Value =
from_str(trimmed).map_err(|e| format!("Failed to parse wallet JSON: {e}"))?;
let private_key = value
.get("private_key")
.or_else(|| value.get("privkey"))
.and_then(|v| v.as_str())
.ok_or_else(|| {
"Wallet JSON does not contain a \"private_key\" or \"privkey\" field".to_string()
})?;
return Ok(private_key.trim().to_string());
}
Ok(trimmed.to_string())
}
fn decode_image_bytes(encoded: &str) -> Result<Vec<u8>, String> {
// Support both plain base64 and data:image/...;base64,... payloads.
let trimmed = encoded.trim();
let base64_payload = if let Some((_, payload)) = trimmed.split_once(',') {
if trimmed.starts_with("data:") {
payload
} else {
trimmed
}
} else {
trimmed
};
STANDARD
.decode(base64_payload)
.map_err(|e| format!("Failed to decode image base64: {e}"))
}
#[tokio::main]
async fn main() {
// Accept direct args or prompt for input/output paths interactively.
let args: Vec<String> = env::args().collect();
let wallet_file_path: String;
let output_png_path: String;
if args.len() > 1 && args.len() != 3 && args.len() != 4 {
eprintln!("Usage: ./save_private_key_image <wallet_or_image_file> <output_png_path>");
eprintln!(" or: ./save_private_key_image <wallet_or_image_file> <output_png_dir> <output_png_filename>");
return;
}
if args.len() == 4 {
wallet_file_path = args[1].clone();
output_png_path = PathBuf::from(&args[2])
.join(&args[3])
.to_string_lossy()
.to_string();
} else if args.len() == 3 {
wallet_file_path = args[1].clone();
output_png_path = args[2].clone();
} else {
wallet_file_path = match prompt_for_path("Please enter the path to the wallet/image file: ")
{
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
};
let output_png_dir_path = match prompt_for_path(
"Please enter the directory path where the PNG should be written: ",
) {
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
};
let output_filename = match prompt_for_filename("Please enter the filename for the PNG: ") {
Ok(filename) => filename,
Err(err) => {
eprintln!("{err}");
return;
}
};
let output_dir = tilde(&output_png_dir_path).to_string();
output_png_path = PathBuf::from(output_dir)
.join(output_filename)
.to_string_lossy()
.to_string();
}
// Expand ~ for both source and destination before reading/writing.
let expanded_wallet_path = tilde(&wallet_file_path).to_string();
let expanded_output_path = tilde(&output_png_path).to_string();
let file_contents = match read_to_string(&expanded_wallet_path).await {
Ok(contents) => contents,
Err(err) => {
eprintln!("Failed to read wallet/image file: {err}");
return;
}
};
let private_key_image = match extract_private_key_image(&file_contents) {
Ok(value) => value,
Err(err) => {
eprintln!("{err}");
return;
}
};
// Decode and write the recovered PNG bytes to the requested output path.
let image_bytes = match decode_image_bytes(&private_key_image) {
Ok(bytes) => bytes,
Err(err) => {
eprintln!("{err}");
return;
}
};
if let Err(err) = fs::write(&expanded_output_path, image_bytes) {
eprintln!("Failed to write PNG file: {err}");
return;
}
println!("Private key image written to {expanded_output_path}");
}

View File

@ -0,0 +1,72 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::wallets::structures::Wallet;
#[tokio::main]
async fn main() {
// Command 26 is currently handled by the server as the block-IP command.
let rpc_command = 26;
// The server owner supplies the IP to block; the wallet key signs the request.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./server_owner_block_ip <ip>");
return;
}
let ip = args[1].trim().to_string();
let wallet_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Server-side verification expects a signature over the exact IP string.
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let signature = Wallet::sign_transaction(&ip, &wallet.saved.private_key).await;
let payload = format!("{ip}|{signature}");
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
payload.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(wallet_key.clone()),
generate_uid(),
)
.await;
match result {
Ok(response) => {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -0,0 +1,72 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::wallets::structures::Wallet;
#[tokio::main]
async fn main() {
// Command 27 is currently handled by the server as the unblock-IP command.
let rpc_command = 27;
// The server owner supplies the IP to unblock; the wallet key signs the request.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./server_owner_unblock_ip <ip>");
return;
}
let ip = args[1].trim().to_string();
let wallet_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Server-side verification expects a signature over the exact IP string.
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let signature = Wallet::sign_transaction(&ip, &wallet.saved.private_key).await;
let payload = format!("{ip}|{signature}");
let connections = get_connections().await;
let mut connected = false;
for conn in connections {
if connected {
break;
}
let socket_address = conn.parse().expect("Failed to parse the socket address");
let result = handshake::connect_and_handshake(
socket_address,
payload.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey(wallet_key.clone()),
generate_uid(),
)
.await;
match result {
Ok(response) => {
let response_text = String::from_utf8_lossy(&response);
let trimmed = response_text.trim();
if !trimmed.is_empty() {
println!("{trimmed}");
connected = true;
}
}
Err(_) => connected = false,
}
}
if !connected {
eprintln!("failed to connect");
}
}

45
src/bin/sign_message.rs Normal file
View File

@ -0,0 +1,45 @@
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use blockchain::common::skein::skein_256_hash_data;
use blockchain::env;
use blockchain::wallets::structures::Wallet;
#[tokio::main]
async fn main() {
// The message to sign is supplied directly as a command-line argument.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./sign_message \"<message to sign>\"");
return;
}
let message = match args[1].parse::<String>() {
Ok(num) => num,
Err(_) => {
println!("Please enter a message");
return;
}
};
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
// Load the wallet whose private key will create the detached signature.
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
// Message signing hashes the UTF-8 message text before applying the wallet signature.
let message_hash = skein_256_hash_data(message.as_str());
let signature = Wallet::sign_transaction(&message_hash, &wallet.saved.private_key).await;
println!("message: {}, signature: {}", message.as_str(), signature);
}

139
src/bin/skein_hasher.rs Normal file
View File

@ -0,0 +1,139 @@
use blockchain::common::skein::{
skein_256_hash_data, skein_256_hash_bytes, skein_128_hash_bytes, skein_128_hash_data,
};
use blockchain::env;
use blockchain::File;
use blockchain::SeekFrom;
use blockchain::{AsyncReadExt, AsyncSeekExt};
#[tokio::main]
async fn main() {
// Collect command-line arguments
let args: Vec<String> = env::args().collect();
// Get the mode from the arguments
let mode = if args.len() > 1 { &args[1] } else { "" };
match mode {
"text" => {
// Ensure the correct number of arguments for "text" mode
if args.len() != 4 {
eprintln!("Usage: ./skein_hasher text large|small <value>");
return;
}
// Get the value and hash it
let size = &args[2];
let value = &args[3];
if size == "small" {
let hash = skein_128_hash_data(value);
println!("Hash of text '{value}': {hash}");
}
if size == "large" {
let hash = skein_256_hash_data(value);
println!("Hash of text '{value}': {hash}");
}
}
"file" => {
// Ensure the correct number of arguments for "file" mode
if args.len() != 4 {
eprintln!("Usage: ./skein_hasher file large|small <file_path>");
return;
}
// Get the file path and hash its contents
let file_path = &args[3];
match File::open(file_path).await {
Ok(mut file) => {
let mut data = Vec::new();
if let Err(err) = file.read_to_end(&mut data).await {
eprintln!("Error reading file '{file_path}': {err}");
return;
}
let hash = if &args[2] == "small" {
skein_128_hash_bytes(&data)
} else {
skein_256_hash_bytes(&data)
};
println!("Hash of file {hash}");
}
Err(err) => {
eprintln!("Error opening file '{file_path}': {err}");
return;
}
}
}
"bytes" => {
// Ensure the correct number of arguments for "bytes" mode
if args.len() != 6 {
eprintln!(
"Usage: ./skein_hasher bytes large|small <start_byte> <stop_byte> <file_path>"
);
return;
}
// Parse the start and stop byte positions
let start_byte: usize = match args[3].parse() {
Ok(num) => num,
Err(_) => {
eprintln!("Invalid start byte: '{}'", args[3]);
return;
}
};
let stop_byte: usize = match args[4].parse() {
Ok(num) => num,
Err(_) => {
eprintln!("Invalid stop byte: '{}'", args[4]);
return;
}
};
if stop_byte <= start_byte {
eprintln!("Stop byte must be greater than start byte.");
return;
}
let number_of_bytes = stop_byte - start_byte;
let file_path = &args[5];
// Open the file and read the specified range of bytes
match File::open(file_path).await {
Ok(mut file) => {
// Seek to the start byte position
if let Err(err) = file.seek(SeekFrom::Start(start_byte as u64)).await {
eprintln!("Error seeking to start byte {start_byte}: {err}");
return;
}
let mut buffer = vec![0; number_of_bytes];
match file.read_exact(&mut buffer).await {
Ok(_) => {
let hash = if &args[2] == "large" {
skein_256_hash_bytes(&buffer)
} else {
skein_128_hash_bytes(&buffer)
};
println!(
"Hash of bytes {start_byte} through {stop_byte} of file '{file_path}': {hash}"
);
}
Err(err) => {
eprintln!("Error reading file '{file_path}': {err}");
return;
}
}
}
Err(err) => {
eprintln!("Error opening file '{file_path}': {err}");
return;
}
}
}
_ => {
eprintln!("Invalid mode. Use 'text', 'file', or 'bytes'.");
return;
}
}
}

View File

@ -0,0 +1,35 @@
use blockchain::common::binary_conversions::hex_to_u64;
use blockchain::env;
use blockchain::io;
use blockchain::records::unpack_block::unpack_header::load_block_header;
use blockchain::to_string_pretty;
#[tokio::main]
async fn main() -> io::Result<()> {
// Load a local block header by height and print both decoded fields and hash-derived difficulty.
let args: Vec<String> = env::args().collect();
if args.len() > 4 || args.len() < 2 {
eprintln!("Usage: {} <block_number>", args[0]);
std::process::exit(1);
}
let block_number: u32 = match args[1].parse() {
Ok(num) => num,
Err(_) => {
eprintln!("Error: The provided block number must be a valid unsigned 32-bit integer.");
std::process::exit(1);
}
};
// Header loading uses the active network block path internally.
let header = load_block_header(block_number).await.unwrap();
let hash = header.hash().await;
let json_pretty = to_string_pretty(&header)?;
// The displayed difficulty is the numeric value derived from the header hash.
let difficulty = hex_to_u64(&hash).await.unwrap();
println!("difficulty: {difficulty:?}");
println!("hash_value: {hash}");
println!("{json_pretty}");
Ok(())
}

56
src/bin/unpack_torrent.rs Normal file
View File

@ -0,0 +1,56 @@
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::env;
use blockchain::exit;
use blockchain::io;
use blockchain::to_string_pretty;
use blockchain::torrent::structs::Torrent;
use blockchain::AsyncReadExt;
use blockchain::File;
use blockchain::Path;
#[tokio::main]
async fn main() -> io::Result<()> {
// This utility loads one local torrent file by block number and prints the decoded struct.
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <block_number>", args[0]);
exit(1);
}
let block_number = &args[1];
let (
_network_name,
_padded_base_coin,
_block_ext,
torrent_path,
_wallet_path,
_block_path,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
// Torrent files are scoped by the active network path returned from settings.
let torrent_filename = format!("{torrent_path}/{block_number}.torrent");
if !Path::new(&torrent_filename).exists() {
eprintln!("Torrent file not found: {torrent_filename}");
return Ok(());
}
let mut torrent_file = File::open(&torrent_filename).await?;
let mut torrent_contents = Vec::new();
torrent_file.read_to_end(&mut torrent_contents).await?;
// The torrent parser validates the binary layout before the result is printed as JSON.
match Torrent::from_bytes(&torrent_contents).await {
Ok(torrent) => {
let json_pretty = to_string_pretty(&torrent)?;
println!("{json_pretty}");
}
Err(e) => {
eprintln!("Failed to parse torrent: {e}");
}
}
Ok(())
}

View File

@ -0,0 +1,189 @@
use blockchain::common::binary_conversions::hex_to_u64;
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::common::skein::{skein_256_hash_data, skein_128_hash_bytes};
use blockchain::encode;
use blockchain::env;
use blockchain::records::unpack_block::unpack_header::load_block_header;
use blockchain::records::wallet_registry::resolve_pubkey_from_short_address;
use blockchain::torrent::structs::Torrent;
use blockchain::wallets::structures::Wallet;
use blockchain::{AsyncReadExt, File};
use colored::*;
use std::process;
#[tokio::main]
async fn main() {
// Validate that a local block file, block header, and torrent metadata agree.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <block_number>", args[0]);
process::exit(1);
}
let block_number: u32 = match args[1].parse() {
Ok(n) => n,
Err(_) => {
eprintln!("Block number must be an integer.");
process::exit(1);
}
};
let (
_network_name,
_padded_base_coin,
block_ext,
torrent_path,
_wallet_path,
block_path,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
let block_filename = format!("{block_path}/{block_number}.{block_ext}");
let torrent_filename = format!("{torrent_path}/{block_number}.torrent");
// Load and decode the torrent metadata first because later checks compare against it.
let mut torrent_bytes = Vec::new();
let mut torrent_file = File::open(&torrent_filename).await.unwrap_or_else(|_| {
eprintln!("Error: cannot open torrent file '{torrent_filename}'");
process::exit(1);
});
torrent_file.read_to_end(&mut torrent_bytes).await.unwrap();
let torrent = Torrent::from_bytes(&torrent_bytes).await.unwrap();
// Load the local header and resolve the miner public key for signature/VRF checks.
let header = load_block_header(block_number).await.unwrap();
let block_hash = header.hash().await;
let block_difficulty = hex_to_u64(&block_hash).await.unwrap();
let miner_pubkey = resolve_pubkey_from_short_address(
&blockchain::startup::initialize_startup::open_chain_state().await,
&header.unmined_block.miner,
)
.unwrap_or(None)
.unwrap_or_default();
let miner_pubkey_hex = encode(&miner_pubkey);
// Load the raw block bytes for file-size, info-hash, and piece-hash checks.
let mut block_data = Vec::new();
let mut block_file = File::open(&block_filename).await.unwrap_or_else(|_| {
eprintln!("Error: cannot open block file '{block_filename}'");
process::exit(1);
});
block_file.read_to_end(&mut block_data).await.unwrap();
let info_hash_computed = skein_128_hash_bytes(&block_data);
// Rebuild the unmined-block hash used by the miner proof signature.
let unmined_json = serde_json::to_string(&header.unmined_block).unwrap();
let unmined_hash = skein_256_hash_data(&unmined_json);
let signature_ok =
Wallet::verify_transaction_with_public_key(&unmined_hash, &header.proof, &miner_pubkey_hex)
.await;
let passed = "[PASSED]".green();
let failed = "[FAILED]".red();
// Compare every header field that is duplicated inside the torrent metadata.
if header.unmined_block.timestamp == torrent.info.timestamp {
println!("timestamp match: {:>90}", format!("{passed}"));
} else {
println!("timestamp match: {:>90}", format!("{failed}"));
}
if header.unmined_block.nonce == torrent.info.nonce {
println!("nonce match: {:>94}", format!("{passed}"));
} else {
println!("nonce match: {:>94}", format!("{failed}"));
}
if header.unmined_block.miner == torrent.mined_by {
println!("wallet address match: {:>85}", format!("{passed}"));
} else {
println!("wallet address match: {:>85}", format!("{failed}"));
}
if block_difficulty < torrent.info.this_block_difficulty {
println!("block difficulty check: {:>83}", format!("{passed}"));
} else {
println!("block difficulty check: {:>83}", format!("{failed}"));
}
if header.vrf == torrent.info.vrf {
println!("VRF match: {:>96}", format!("{passed}"));
} else {
println!("VRF match: {:>96}", format!("{failed}"));
}
if block_data.len() == torrent.info.length as usize {
println!("file size check: {:>90}", format!("{passed}"));
} else {
println!("file size check: {:>90}", format!("{failed}"));
}
if block_hash == torrent.info.block_hash {
println!("block header hash check: {:>82}", format!("{passed}"));
} else {
println!("block header hash check: {:>82}", format!("{failed}"));
}
let vrf_ok = Wallet::vrf_verify_with_public_key(
header.vrf,
&unmined_hash,
&miner_pubkey_hex,
&header.proof,
)
.await;
if vrf_ok {
println!("VRF validation check: {:>85}", format!("{passed}"));
} else {
println!("VRF validation check: {:>85}", format!("{failed}"));
}
if signature_ok {
println!("VRF Proof check: {:>90}", format!("{passed}"));
} else {
println!("VRF Proof check: {:>90}", format!("{failed}"));
}
let mut all_pieces_passed = true;
// Piece hashes prove the torrent metadata still matches every block-file chunk.
for piece in &torrent.info.pieces {
for (index, expected_hash) in piece.iter() {
let idx = *index as usize;
let start = (idx - 1) * torrent.info.piece_length as usize;
let end = std::cmp::min(start + torrent.info.piece_length as usize, block_data.len());
let slice = &block_data[start..end];
let hash = skein_128_hash_bytes(slice);
if hash == *expected_hash {
println!("piece {} hash check: {:>87}", idx, format!("{passed}"));
} else {
all_pieces_passed = false;
println!("piece {} hash check: {:>87}", idx, format!("{failed}"));
}
}
}
if info_hash_computed == torrent.info.info_hash {
println!("block hash check: {:>89}", format!("{passed}"));
} else {
println!("block hash check: {:>89}", format!("{failed}"));
}
// The final pass/fail summary requires every individual validation to pass.
if header.unmined_block.nonce == torrent.info.nonce
&& header.unmined_block.miner == torrent.mined_by
&& header.unmined_block.timestamp == torrent.info.timestamp
&& block_difficulty < torrent.info.this_block_difficulty
&& header.vrf == torrent.info.vrf
&& block_data.len() == torrent.info.length as usize
&& all_pieces_passed
&& block_hash == torrent.info.block_hash
&& signature_ok
&& vrf_ok
{
println!("\nBlock {block_number} fully validated.");
} else {
println!("\nBlock {block_number} FAILED validation.");
}
}

110
src/bin/verify_address.rs Normal file
View File

@ -0,0 +1,110 @@
use blockchain::env;
use blockchain::from_str;
use blockchain::read_to_string;
use blockchain::tilde;
use blockchain::wallets::structures::Wallet;
use blockchain::Value;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
use rustyline_derive::Completer;
use rustyline_derive::Helper as RustyHelper;
use rustyline_derive::Highlighter as RustyHighlighter;
use rustyline_derive::Hinter as RustyHinter;
use rustyline_derive::Validator as RustyValidator;
async fn read_address_file(path: &str) -> Result<String, String> {
// Allow ~ in paths and then extract either a raw address or an address from wallet JSON.
let expanded_path = tilde(path).to_string();
read_to_string(&expanded_path)
.await
.map_err(|e| format!("Failed to read {expanded_path}: {e}"))
.and_then(|contents| extract_address(&contents))
}
fn extract_address(contents: &str) -> Result<String, String> {
// Address files may be a plain address string or a saved wallet JSON object.
let trimmed = contents.trim();
if trimmed.is_empty() {
return Err("Address file is empty".to_string());
}
if trimmed.starts_with('{') {
// Prefer the short address when present, but accept long_address for wallet validation too.
let value: Value =
from_str(trimmed).map_err(|e| format!("Failed to parse wallet JSON: {e}"))?;
let address = value
.get("short_address")
.and_then(|v| v.as_str())
.or_else(|| value.get("long_address").and_then(|v| v.as_str()))
.ok_or_else(|| "Wallet JSON does not contain a usable address field".to_string())?;
return Ok(address.trim().to_string());
}
Ok(trimmed.to_string())
}
#[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)]
struct PathHelper {
#[rustyline(Completer)]
completer: FilenameCompleter,
}
fn prompt_for_path(prompt: &str) -> Result<String, String> {
// Rustyline gives the interactive prompt filesystem completion.
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(line) => Ok(line.trim().to_string()),
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(format!("Failed to read address file path: {err}")),
}
}
#[tokio::main]
async fn main() {
// Accept the address file path as an arg or prompt interactively.
let args: Vec<String> = env::args().collect();
if args.len() > 1 && args.len() != 2 {
println!("Usage: ./verify_address <address_file>");
return;
}
let address_path = if args.len() == 2 {
args[1].clone()
} else {
match prompt_for_path(
"Please enter the path to the file containing the wallet address: ",
) {
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
}
};
let address = match read_address_file(&address_path).await {
Ok(address) => address,
Err(err) => {
println!("{err}");
return;
}
};
// Verify either a long wallet address or a current-network short address.
let message_hash = Wallet::wallet_validation(address.trim()).await
|| Wallet::short_address_validation(address.trim());
println!("{message_hash}");
}

150
src/bin/verify_message.rs Normal file
View File

@ -0,0 +1,150 @@
use blockchain::common::cli_prompts::prompt_visible;
use blockchain::common::skein::skein_256_hash_data;
use blockchain::env;
use blockchain::from_str;
use blockchain::read_to_string;
use blockchain::tilde;
use blockchain::wallets::structures::Wallet;
use blockchain::Value;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
use rustyline_derive::Completer;
use rustyline_derive::Helper as RustyHelper;
use rustyline_derive::Highlighter as RustyHighlighter;
use rustyline_derive::Hinter as RustyHinter;
use rustyline_derive::Validator as RustyValidator;
async fn read_input_file(path: &str) -> Result<String, String> {
// Signature input files are plain text and are trimmed before verification.
let expanded_path = tilde(path).to_string();
read_to_string(&expanded_path)
.await
.map(|s| s.trim().to_string())
.map_err(|e| format!("Failed to read {expanded_path}: {e}"))
}
async fn read_address_file(path: &str) -> Result<String, String> {
// Allow ~ in paths and then extract either a raw address or an address from wallet JSON.
let expanded_path = tilde(path).to_string();
read_to_string(&expanded_path)
.await
.map_err(|e| format!("Failed to read {expanded_path}: {e}"))
.and_then(|contents| extract_address(&contents))
}
fn extract_address(contents: &str) -> Result<String, String> {
// Address files may be a plain address string or a saved wallet JSON object.
let trimmed = contents.trim();
if trimmed.is_empty() {
return Err("Address file is empty".to_string());
}
if trimmed.starts_with('{') {
// Signature verification prefers the long address but can also verify with a short address.
let value: Value =
from_str(trimmed).map_err(|e| format!("Failed to parse wallet JSON: {e}"))?;
let address = value
.get("long_address")
.and_then(|v| v.as_str())
.or_else(|| value.get("short_address").and_then(|v| v.as_str()))
.ok_or_else(|| "Wallet JSON does not contain a usable address field".to_string())?;
return Ok(address.trim().to_string());
}
Ok(trimmed.to_string())
}
#[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)]
struct PathHelper {
#[rustyline(Completer)]
completer: FilenameCompleter,
}
fn prompt_for_path(prompt: &str) -> Result<String, String> {
// Rustyline gives the interactive prompt filesystem completion.
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::with_config(config).map_err(|e| e.to_string())?;
editor.set_helper(Some(PathHelper {
completer: FilenameCompleter::new(),
}));
match editor.readline(prompt) {
Ok(line) => Ok(line.trim().to_string()),
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
Err("Input cancelled".to_string())
}
Err(err) => Err(format!("Failed to read file path: {err}")),
}
}
#[tokio::main]
async fn main() {
// Accept all verification inputs as args or prompt interactively.
let args: Vec<String> = env::args().collect();
let address_path: String;
let signature_path: String;
if args.len() > 1 && args.len() != 4 {
println!("Usage: ./verify_message \"<message to verify>\" <address_file> <signature_file>");
return;
}
let message = if args.len() == 4 {
args[1].clone()
} else {
prompt_visible("Please enter the message to verify: ").await
};
if args.len() == 4 {
address_path = args[2].clone();
signature_path = args[3].clone();
} else {
address_path = match prompt_for_path(
"Please enter the path to the file containing the wallet address: ",
) {
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
};
signature_path =
match prompt_for_path("Please enter the path to the file containing the signature: ") {
Ok(path) => path,
Err(err) => {
eprintln!("{err}");
return;
}
};
}
let address = match read_address_file(&address_path).await {
Ok(address) => address,
Err(err) => {
println!("{err}");
return;
}
};
let signature = match read_input_file(&signature_path).await {
Ok(signature) => signature,
Err(err) => {
println!("{err}");
return;
}
};
// Hash the message exactly as sign_message does before verifying the wallet signature.
let message_hash = skein_256_hash_data(message.as_str());
let signature = Wallet::verify_transaction(&message_hash, &signature, address.trim()).await;
if signature {
println!("valid signature");
} else {
println!("invalid signature");
}
}

View File

@ -0,0 +1,282 @@
use blockchain::blocks::loans::UnsignedLoanContractTransaction;
use blockchain::common::cli_prompts::{ask_yes_no_question, prompt_hidden_nonempty};
use blockchain::env;
use blockchain::from_str;
use blockchain::fs;
use blockchain::json;
use blockchain::read_to_string;
use blockchain::records::wallet_registry::resolve_local_input_short_address;
use blockchain::to_string_pretty;
use blockchain::wallets::structures::Wallet;
use blockchain::Value;
use blockchain::{Local, TimeZone};
fn display_amount(value: u64) -> f64 {
// Transaction JSON stores atomic units; prompts show whole-coin decimals.
value as f64 / 100_000_000.0
}
fn display_payment_period(value: &str) -> &'static str {
// Loan transactions store the payment period as a compact one-letter code.
match value.trim().to_lowercase().as_str() {
"d" => "daily",
"w" => "weekly",
"m" => "monthly",
_ => "unknown",
}
}
fn display_start_date(timestamp: u32) -> String {
// Show the stored timestamp as a local calendar date for borrower review.
match Local.timestamp_opt(timestamp as i64, 0).single() {
Some(datetime) => datetime.format("%Y-%m-%d").to_string(),
None => "invalid date".to_string(),
}
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
// Accept local vanity/short input and resolve it into the real short address.
resolve_local_input_short_address(address.trim())
}
#[tokio::main]
async fn main() {
// Borrowers use this tool to review a lender-signed loan and add signature2.
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./validate_sign_loan_tx <path/to/file.json>");
return;
}
let filename = &args[1];
let contents = match read_to_string(filename).await {
Ok(contents) => contents,
Err(_) => {
println!("Error reading file: {filename}");
return;
}
};
let json: Result<Value, _> = from_str(&contents);
let json = match json {
Ok(json) => json,
Err(_) => {
println!("Error parsing JSON in file: {filename}");
return;
}
};
// Extract every field that participates in the hash so the borrower signs the same bytes.
let txtype = 7;
let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32;
let loan_coin = json["loan_coin"].as_str().unwrap_or_default().to_string();
let loan_amount = json["loan_amount"].as_u64().unwrap_or_default();
let lender = match normalize_short_address_input(json["lender"].as_str().unwrap_or_default()) {
Ok(address) => address,
Err(_) => {
println!("Transaction is not valid. Lender address is not a valid short address.");
return;
}
};
let collateral = json["collateral"].as_str().unwrap_or_default().to_string();
let collateral_amount = json["collateral_amount"].as_u64().unwrap_or_default();
let borrower =
match normalize_short_address_input(json["borrower"].as_str().unwrap_or_default()) {
Ok(address) => address,
Err(_) => {
println!(
"Transaction is not valid. Borrower address is not a valid short address."
);
return;
}
};
let payment_period = json["payment_period"]
.as_str()
.unwrap_or_default()
.to_string();
let payment_number = json["payment_number"].as_u64().unwrap_or_default() as u8;
let payment_amount = json["payment_amount"].as_u64().unwrap_or_default();
let grace_period = json["grace_period"].as_u64().unwrap_or_default() as u8;
let max_late_value = json["max_late_value"].as_u64().unwrap_or_default();
let txfee = json["txfee"].as_u64().unwrap_or_default();
let original_hash = json["hash"].as_str().unwrap_or_default().to_string();
let signature1 = json["signature1"].as_str().unwrap_or_default().to_string();
// The borrower must explicitly confirm the human-readable loan terms before signing.
let question1 = format!(
"Are you expecting to receive {} {}?",
display_amount(loan_amount),
loan_coin.trim()
);
let question2 = format!(
"Are you willing to lock {} {} as collateral?",
display_amount(collateral_amount),
collateral.trim()
);
let question3 = format!(
"Do you agree that payments should be made {}?",
display_payment_period(&payment_period)
);
let question4 = format!(
"Do you agree that this loan requires {payment_number} total payments?"
);
let question5 = format!(
"Do you agree that each payment should be {} {}?",
display_amount(payment_amount),
loan_coin.trim()
);
let question6 = format!(
"Do you agree that {grace_period} missed payments allows the lender to claim the collateral?"
);
let question7 = format!(
"Do you agree that being {} {} behind allows the lender to claim the collateral?",
display_amount(max_late_value),
loan_coin.trim()
);
let question8 = format!(
"Do you agree that this loan begins on {}?",
display_start_date(timestamp)
);
let question9 = format!("Do you agree that the lender wallet is {}?", lender.trim());
if !ask_yes_no_question(&question1).await {
println!("Transaction is not valid");
return;
}
if !ask_yes_no_question(&question2).await {
println!("Transaction is not valid");
return;
}
if !ask_yes_no_question(&question3).await {
println!("Transaction is not valid");
return;
}
if !ask_yes_no_question(&question4).await {
println!("Transaction is not valid");
return;
}
if !ask_yes_no_question(&question5).await {
println!("Transaction is not valid");
return;
}
if !ask_yes_no_question(&question6).await {
println!("Transaction is not valid");
return;
}
if !ask_yes_no_question(&question7).await {
println!("Transaction is not valid");
return;
}
if !ask_yes_no_question(&question8).await {
println!("Transaction is not valid");
return;
}
if !ask_yes_no_question(&question9).await {
println!("Transaction is not valid");
return;
}
// Load the borrower wallet and ensure it matches the borrower address in the offer.
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let address = &wallet.saved.short_address;
if borrower.trim() != address.trim() {
println!(
"Transaction is not valid for your wallet address. Expected {borrower} found {address}"
);
return;
}
if !Wallet::short_address_validation(address.trim()) {
println!("Your wallet short address is not valid: {address}");
return;
}
// Rebuild the unsigned loan and sign it with the borrower wallet.
let unsigned_loan = UnsignedLoanContractTransaction::new(
txtype,
timestamp,
&loan_coin,
loan_amount,
&lender,
&collateral,
collateral_amount,
&borrower,
&payment_period,
payment_number,
payment_amount,
grace_period,
max_late_value,
txfee,
)
.await;
let (hash, signature2) = match unsigned_loan.hash_and_sign(private_key).await {
Ok(value) => value,
Err(err) => {
println!("Signing transaction failed: {err}");
return;
}
};
// If the rebuilt hash differs, the JSON was changed after the lender signed it.
if hash != original_hash {
println!("Signing transaction failed. The included hash was incorrect.");
return;
}
// Save the fully signed loan contract JSON for broadcast.
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"loan_coin": loan_coin,
"loan_amount": loan_amount,
"lender": lender,
"collateral": collateral,
"collateral_amount": collateral_amount,
"borrower": borrower,
"payment_period": payment_period,
"payment_number": payment_number,
"payment_amount": payment_amount,
"grace_period": grace_period,
"max_late_value": max_late_value,
"txfee": txfee,
"hash": hash,
"signature1": signature1,
"signature2": signature2,
});
let output_str = to_string_pretty(&output).expect("Failed to serialize JSON");
// Transaction creation tools all write into ./transactions using the transaction hash.
let dir_path = "./transactions";
if let Err(e) = fs::create_dir_all(dir_path) {
eprintln!("Failed to create directory: {e}");
return;
}
let file_path = format!(
"{}/{}.json",
dir_path,
output["hash"].as_str().unwrap_or_default()
);
if let Err(e) = fs::write(&file_path, &output_str) {
eprintln!("Failed to write file: {e}");
return;
}
println!("Transaction: {output_str}");
}

View File

@ -0,0 +1,249 @@
use blockchain::blocks::swap::UnsignedSwapTransaction;
use blockchain::common::cli_prompts::{ask_yes_no_question, prompt_hidden_nonempty};
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::env;
use blockchain::fs;
use blockchain::json;
use blockchain::read_to_string;
use blockchain::records::wallet_registry::resolve_local_input_short_address;
use blockchain::wallets::structures::Wallet;
use blockchain::Value;
// padd the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String {
let mut result = String::with_capacity(width); // Pre-allocate string with capacity
let _ = std::fmt::write(
&mut result,
format_args!("{input:<width$}"),
);
result
}
fn normalize_short_address_input(address: &str) -> Result<String, String> {
resolve_local_input_short_address(address.trim())
}
#[tokio::main]
async fn main() {
// Get the filename from the command line arguments
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage:./sign_swap <path/to/file.json>");
return;
}
let filename = &args[1];
let decryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let private_key = &wallet.saved.private_key;
let address = &wallet.saved.short_address;
// Read the contents of the file
let contents = match read_to_string(filename).await {
Ok(contents) => contents,
Err(_) => {
println!("Error reading file: {filename}");
return;
}
};
// Parse the JSON
let json: Result<Value, _> = serde_json::from_str(&contents);
let json = match json {
Ok(json) => json,
Err(_) => {
println!("Error parsing JSON in file: {filename}");
return;
}
};
let txtype = 6;
let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32;
let offer_expiration = json["offer_expiration"].as_u64().unwrap_or_default() as u32;
// get values from transaction json
let token_name1 = json["ticker1"]
.as_str()
.unwrap_or_default()
.trim()
.to_lowercase();
let nft_series1: u32 = json["nft_series1"].as_u64().unwrap_or_default() as u32;
let value1: u64 = json["value1"].as_u64().unwrap_or_default();
let receive_amount = value1 as f64 / 100000000.0;
let token_name2 = json["ticker2"]
.as_str()
.unwrap_or_default()
.trim()
.to_lowercase();
let nft_series2: u32 = json["nft_series2"].as_u64().unwrap_or_default() as u32;
let value2: u64 = json["value2"].as_u64().unwrap_or_default();
let send_amount = value2 as f64 / 100000000.0;
let txfee1_value: u64 = json["txfee1"].as_u64().unwrap_or_default();
let tip1_value: u64 = json["tip1"].as_u64().unwrap_or_default();
let sender1 = match normalize_short_address_input(json["sender1"].as_str().unwrap_or_default())
{
Ok(address) => address,
Err(_) => {
println!("sender1 wallet invalid");
return;
}
};
let txfee2_value: u64 = json["txfee2"].as_u64().unwrap_or_default();
let txfee2 = txfee2_value as f64 / 100000000.0;
let tip2_value: u64 = json["tip2"].as_u64().unwrap_or_default();
let tip2 = tip2_value as f64 / 100000000.0;
let sender2 = match normalize_short_address_input(json["sender2"].as_str().unwrap_or_default())
{
Ok(address) => address,
Err(_) => {
println!("sender2 wallet invalid");
return;
}
};
// ensure wallet and sender2 match
if sender2 != address.trim() {
println!(
"Transaction is not valid for your wallet address. Expected {sender2} found {address}"
);
return;
}
let (
_network_name,
network_coin,
_suffix,
_torrent_path,
_wallet_path,
_blockpath,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
// setup validation questions
let question1 = format!(
"Are you expecting to receive {receive_amount} {token_name1}?"
);
let question2 = format!("Are you expecting to send {send_amount} {token_name2}?");
let question3 = format!(
"Are you willing to spend {txfee2} {network_coin} in fees?"
);
let question4 = format!("Are you willing to tip {tip2} {token_name2}?");
// ask validation questions
if !ask_yes_no_question(&question1).await {
println!("Transaction is not valid");
return;
}
if !ask_yes_no_question(&question2).await {
println!("Transaction is not valid");
return;
}
if !ask_yes_no_question(&question3).await {
println!("Transaction is not valid");
return;
}
if !ask_yes_no_question(&question4).await {
println!("Transaction is not valid");
return;
}
let padded_token_name1 = pad_to_width(&token_name1, 15);
let padded_token_name2 = pad_to_width(&token_name2, 15);
let unsigned_swap = UnsignedSwapTransaction::new(
txtype,
timestamp,
offer_expiration,
&padded_token_name1,
nft_series1,
value1,
&padded_token_name2,
nft_series2,
value2,
&sender1,
address.trim(),
tip1_value,
tip2_value,
txfee1_value,
txfee2_value,
)
.await;
let hashed_data = unsigned_swap.hash().await;
let original_hash = &json["hash"].as_str().unwrap_or_default().trim();
let signature1 = &json["signature1"].as_str().unwrap_or_default().trim();
let signature2 = if hashed_data == *original_hash.to_string() {
match unsigned_swap.hash_and_sign(&private_key.to_string()).await {
Ok(signature) => signature,
Err(err) => {
println!("Signing transaction failed: {err}");
return;
}
}
} else {
println!("Signing transaction failed. The included hash was incorrect.");
return;
};
let output = json!({
"txtype": txtype,
"timestamp": timestamp,
"offer_expiration": offer_expiration,
"ticker1": padded_token_name1,
"nft_series1": nft_series1,
"value1": value1,
"ticker2": padded_token_name2,
"nft_series2": nft_series2,
"value2": value2,
"sender1": sender1,
"sender2": address.trim(),
"tip1": tip1_value,
"tip2": tip2_value,
"txfee1": txfee1_value,
"txfee2": txfee2_value,
"hash": hashed_data,
"signature1": signature1,
"signature2": signature2
});
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
// Define the directory path
let dir_path = "./transactions";
// Create the directory if it doesn't exist
if let Err(e) = fs::create_dir_all(dir_path) {
eprintln!("Failed to create directory: {e}");
return;
}
// Define the file path
let file_path = format!("{dir_path}/{hashed_data}.json");
// Write the JSON string to the file
if let Err(e) = fs::write(&file_path, &output_str) {
eprintln!("Failed to write file: {e}");
return;
}
println!("Transaction: {output_str}");
}

323
src/blocks/block.rs Normal file
View File

@ -0,0 +1,323 @@
use crate::common::skein::{skein_256_hash_data, skein_512_hash_data};
use crate::common::types::Transaction;
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::averages::{calculate_averages, update_block_data};
use crate::sled::Db;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Duration;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
pub const TIMESTAMP_OFFSET: usize = 0;
pub const MINER_OFFSET: usize = TIMESTAMP_OFFSET + 4;
pub const PREVIOUS_HASH_OFFSET: usize = MINER_OFFSET + Wallet::SHORT_ADDRESS_BYTES_LENGTH;
pub const DIFFICULTY_OFFSET: usize = PREVIOUS_HASH_OFFSET + 32;
pub const NONCE_OFFSET: usize = DIFFICULTY_OFFSET + 8;
pub const VRF_OFFSET: usize = NONCE_OFFSET + 1;
pub const PROOF_OFFSET: usize = VRF_OFFSET + 16;
pub const UNMINED_BLOCK_BYTES: usize = 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 32 + 8 + 1;
pub const VRF_BLOCK_BYTES: usize = UNMINED_BLOCK_BYTES + 16 + Wallet::SIGNATURE_LENGTH;
// UnminedBlock is the deterministic header data that exists before
// the miner wallet adds the VRF proof.
#[derive(Debug, Serialize, Clone)] // 67 bytes
pub struct UnminedBlock {
pub timestamp: u32, // 4 bytes block timestamp
pub miner: String, // 22 bytes miner short address
pub previous_hash: String, // 32 bytes parent block hash
pub next_block_difficulty: u64, // 8 bytes difficulty for this block
pub nonce: u8, // 1 byte nonce searched by mining workers
}
// VrfBlock adds the miner's signed proof and derived VRF number to the header.
#[derive(Debug, Serialize, Clone)] // 749 bytes
pub struct VrfBlock {
pub unmined_block: UnminedBlock, // 67 bytes unsigned block header fields
pub vrf: u128, // 16 bytes random number derived from proof
pub proof: String, // 666 bytes miner signature proof
}
// Block stores the VRF header plus the ordered transaction list.
#[derive(Debug, Serialize)] // header is 749 bytes plus transactions
pub struct Block {
pub vrf_block: VrfBlock,
pub transactions: Vec<Transaction>,
}
impl UnminedBlock {
// Create the unmined block header fields.
pub async fn new(
timestamp: u32,
miner: &str,
previous_hash: &str,
next_block_difficulty: u64,
nonce: u8,
) -> Self {
Self {
timestamp,
miner: miner.to_string(),
previous_hash: previous_hash.to_string(),
next_block_difficulty,
nonce,
}
}
pub async fn generate_random_number(input: &str) -> u128 {
// Hash the proof with Skein512, then fold the 64-byte result
// into one u128 value by XORing four 16-byte chunks.
let hash = skein_512_hash_data(input);
let hash_bytes = decode(&hash).expect("Failed to decode hash");
if hash_bytes.len() != 64 {
panic!("Hash must be exactly 64 bytes long.");
}
let a = u128::from_le_bytes(
hash_bytes[0..16]
.try_into()
.expect("Chunk A must be 16 bytes"),
);
let b = u128::from_le_bytes(
hash_bytes[16..32]
.try_into()
.expect("Chunk B must be 16 bytes"),
);
let c = u128::from_le_bytes(
hash_bytes[32..48]
.try_into()
.expect("Chunk C must be 16 bytes"),
);
let d = u128::from_le_bytes(
hash_bytes[48..64]
.try_into()
.expect("Chunk D must be 16 bytes"),
);
a ^ b ^ c ^ d
}
pub async fn vrf_generate(self, wallet_key: String) -> VrfBlock {
// Sign the unmined header hash with the miner wallet and derive
// the VRF number from that signature.
let hash = self.hash().await;
let wallet = Wallet::try_obtain_wallet(wallet_key, None)
.await
.unwrap_or_else(|err| panic!("Wallet decryption failed: {err}"));
let privkey = &wallet.saved.private_key;
let proof = Wallet::sign_transaction(&hash, privkey).await;
let vrf = Self::generate_random_number(&proof).await;
VrfBlock {
unmined_block: self,
vrf,
proof,
}
}
// Hash the unmined block header for VRF signing.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
// Calculate the next difficulty using the rolling average and target block time.
fn calculate_new_difficulty(
current_difficulty: u64,
difficulty_average: u64,
average_duration: Duration,
) -> u64 {
let lower_bound = Duration::from_secs(14);
let upper_bound = Duration::from_secs(16);
// When the rolling average is already within the target window,
// use the cached mean difficulty exactly.
if difficulty_average > 0
&& average_duration >= lower_bound
&& average_duration <= upper_bound
{
return difficulty_average;
}
// Outside the target window, apply the capped 30% adjustment
// with integer math to keep the result stable.
let adjustment = current_difficulty.saturating_mul(30).saturating_div(100);
if average_duration > upper_bound {
current_difficulty.saturating_add(adjustment)
} else if average_duration < lower_bound {
current_difficulty.saturating_sub(adjustment)
} else {
current_difficulty
}
}
// Adjust difficulty based on the latest saved block averages.
pub async fn adjust_difficulty(
current_timestamp: u32,
db: &Db,
current_difficulty: u64,
) -> u64 {
let block_number = get_height(db);
// Refresh rolling block data before reading averages.
update_block_data(block_number).await;
// Get the current rolling difficulty and duration averages.
let (difficulty_average, average_duration) = calculate_averages(current_timestamp).await;
// Apply the bounded difficulty adjustment.
Self::calculate_new_difficulty(current_difficulty, difficulty_average, average_duration)
}
}
impl VrfBlock {
pub async fn hash(&self) -> String {
// Hash the full VRF header for indexing and validation.
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize the fixed-width VRF header layout.
let mut buffer = Vec::with_capacity(VRF_BLOCK_BYTES);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unmined_block.timestamp.to_le_bytes())
.await?;
let miner_bytes =
Wallet::short_address_to_bytes(&self.unmined_block.miner).ok_or_else(|| {
tokio::io::Error::other("Invalid short miner address")
})?;
cursor.write_all(&miner_bytes).await?;
cursor
.write_all(&decode(&self.unmined_block.previous_hash).unwrap())
.await?;
cursor
.write_all(&self.unmined_block.next_block_difficulty.to_le_bytes())
.await?;
cursor
.write_all(&self.unmined_block.nonce.to_le_bytes())
.await?;
cursor.write_all(&self.vrf.to_le_bytes()).await?;
cursor.write_all(&decode(&self.proof).unwrap()).await?;
Ok(buffer)
}
pub async fn from_bytes(bytes: &[u8]) -> tokio::io::Result<Self> {
// A VRF header must be exactly the fixed header byte length.
if bytes.len() != VRF_BLOCK_BYTES {
return Err(tokio::io::Error::other("Invalid Byte Count for Block",
));
}
// Read from the fixed-width VRF header bytes.
let mut cursor = Cursor::new(bytes);
// Decode timestamp and miner short address.
let timestamp = cursor.read_u32_le().await?;
let mut miner_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut miner_bytes).await?;
let miner = Wallet::bytes_to_short_address(&miner_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid short miner address")
})?;
// Decode parent hash, difficulty, nonce, VRF number, and proof.
let mut prev_hash_bytes = vec![0; 32];
cursor.read_exact(&mut prev_hash_bytes).await?;
let previous_hash = encode(&prev_hash_bytes);
let next_block_difficulty = cursor.read_u64_le().await?;
let nonce = cursor.read_u8().await?;
let mut vrf_bytes = [0u8; 16];
cursor.read_exact(&mut vrf_bytes).await?;
let vrf = u128::from_le_bytes(vrf_bytes);
let mut proof_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut proof_bytes).await?;
let proof = encode(&proof_bytes);
let unmined_block = UnminedBlock {
timestamp,
miner,
previous_hash,
next_block_difficulty,
nonce,
};
Ok(VrfBlock {
unmined_block,
vrf,
proof,
})
}
}
impl Block {
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
let mut buffer = Vec::new();
// Serialize the fixed-width VRF header before any transactions.
let vrf_bytes = self.vrf_block.to_bytes().await?;
buffer.extend_from_slice(&vrf_bytes);
// Append each transaction in block order using its own fixed layout.
for transaction in &self.transactions {
match transaction {
Transaction::Genesis(genesis_tx) => {
let tx_bytes = genesis_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::Rewards(rewards_tx) => {
let tx_bytes = rewards_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::Transfer(transfer_tx) => {
let tx_bytes = transfer_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::Token(token_tx) => {
let tx_bytes = token_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::IssueToken(issue_token_tx) => {
let tx_bytes = issue_token_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::Burn(burn_tx) => {
let tx_bytes = burn_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::Nft(nft_tx) => {
let tx_bytes = nft_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::Marketing(marketing_tx) => {
let tx_bytes = marketing_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::Swap(swap_tx) => {
let tx_bytes = swap_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::Lender(lender_tx) => {
let tx_bytes = lender_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::Borrower(borrower_tx) => {
let tx_bytes = borrower_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::Collateral(collateral_tx) => {
let tx_bytes = collateral_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
Transaction::Vanity(vanity_tx) => {
let tx_bytes = vanity_tx.to_bytes().await?;
buffer.extend_from_slice(&tx_bytes);
}
}
}
Ok(buffer)
}
}

222
src/blocks/burn.rs Normal file
View File

@ -0,0 +1,222 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
use anyhow::{anyhow, Result};
// Burn transactions destroy an existing non-base asset without routing it through a receiver.
#[derive(Debug, Serialize, Clone)] // 62 bytes
pub struct UnsignedBurnTransaction {
pub txtype: u8, // 1 byte txtype should be 10
pub time: u32, // 4 bytes timestamp
pub address: String, // 22 bytes wallet address of burner
pub coin: String, // 15 bytes token or NFT base name, padded with spaces
pub nft_series: u32, // 4 bytes 0 for tokens or 1-of-1 NFTs, otherwise specific NFT series item
pub value: u64, // 8 bytes amount of token units or NFT unit value to burn
pub txfee: u64, // 8 bytes fee of transaction
}
#[derive(Debug, Serialize, Clone)] // 728 bytes
pub struct BurnTransaction {
pub unsigned_burn: UnsignedBurnTransaction, // 62 bytes
pub signature: String, // 666 bytes signature of hash
}
impl BurnTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 15 + 4 + 8 + 8 + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedBurnTransaction {
// Create an unsigned burn transaction.
pub async fn new(
txtype: u8,
time: u32,
address: &str,
coin: &str,
nft_series: u32,
value: u64,
txfee: u64,
) -> Self {
Self {
txtype,
time,
address: address.to_string(),
coin: coin.to_string(),
nft_series,
value,
txfee,
}
}
// Hash without signing for verification.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
// Hash the unsigned transaction and sign it.
pub async fn hash_and_sign(
&self,
private_key: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let serialized = to_string(self)?;
let hash = skein_256_hash_data(&serialized);
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok(signature)
}
}
impl BurnTransaction {
// Create a signed burn transaction.
pub async fn new(
unsigned_burn: UnsignedBurnTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
let signature = unsigned_burn.hash_and_sign(private_key).await?;
Ok(Self {
unsigned_burn,
signature,
})
}
// Load an existing burn transaction.
pub async fn load(unsigned_burn: UnsignedBurnTransaction, signature: &str) -> Self {
Self {
unsigned_burn,
signature: signature.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_burn.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_burn.time.to_le_bytes())
.await?;
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_burn.address)
.ok_or_else(|| {
tokio::io::Error::other("Invalid burn short address")
})?;
cursor.write_all(&address_bytes).await?;
cursor.write_all(self.unsigned_burn.coin.as_bytes()).await?;
cursor
.write_all(&self.unsigned_burn.nft_series.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_burn.value.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_burn.txfee.to_le_bytes())
.await?;
cursor.write_all(&decode(&self.signature).unwrap()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
let mut cursor = Cursor::new(bytes);
let time = cursor.read_u32_le().await?;
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut address_bytes).await?;
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid burn short address bytes",
)
})?;
let mut coin_bytes = vec![0; 15];
cursor.read_exact(&mut coin_bytes).await?;
let coin = binary_to_string(coin_bytes);
let nft_series = cursor.read_u32_le().await?;
let value = cursor.read_u64_le().await?;
let txfee = cursor.read_u64_le().await?;
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature_bytes).await?;
let signature = encode(&signature_bytes);
let unsigned_burn = UnsignedBurnTransaction {
txtype,
time,
address,
coin,
nft_series,
value,
txfee,
};
Ok(BurnTransaction {
unsigned_burn,
signature,
})
}
pub async fn add_to_memory(&self) -> Result<()> {
let original_data = self
.to_bytes()
.await
.map_err(|_| anyhow!("Failed to serialize transaction"))?;
let fee = i64::try_from(self.unsigned_burn.txfee)
.map_err(|_| std::io::Error::other("Burn fee exceeds i64 mempool limit"))?;
let time = self.unsigned_burn.time as i32;
let value = i64::try_from(self.unsigned_burn.value)
.map_err(|_| std::io::Error::other("Burn value exceeds i64 mempool limit"))?;
let nft_series = self.unsigned_burn.nft_series as i32;
let address = &self.unsigned_burn.address;
let coin = &self.unsigned_burn.coin;
let hash = &self.unsigned_burn.hash().await;
let signature = &self.signature;
let client = DB.get().expect("DB not initialized");
client
.execute(
r#"
INSERT INTO burn (
time,
fee,
address,
coin,
nft_series,
value,
hash,
signature,
original
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
&[
&time,
&fee,
address,
coin,
&nft_series,
&value,
hash,
signature,
&original_data,
],
)
.await?;
Ok(())
}
}

215
src/blocks/collateral.rs Normal file
View File

@ -0,0 +1,215 @@
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 67 bytes
pub struct UnsignedCollateralClaimTransaction {
pub txtype: u8, // 1 byte transaction type, should be 9
pub time: u32, // 4 bytes transaction timestamp
pub contract_hash: String, // 32 bytes hash of the loan contract
pub address: String, // 22 bytes claimant short address
pub txfee: u64, // 8 bytes transaction fee
}
#[derive(Debug, Serialize, Clone)] // 733 bytes
pub struct CollateralClaimTransaction {
pub unsigned_collateral_claim: UnsignedCollateralClaimTransaction, // 67 bytes
pub signature: String, // 666 bytes signature of the transaction hash
}
impl CollateralClaimTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + 32 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedCollateralClaimTransaction {
// Create an unsigned collateral-claim transaction.
pub async fn new(
txtype: u8,
time: u32,
contract_hash: &str,
address: &str,
txfee: u64,
) -> Self {
Self {
txtype,
time,
contract_hash: contract_hash.to_string(),
address: address.to_string(),
txfee,
}
}
// Hash without signing for verification.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
// Hash the unsigned transaction and sign it.
pub async fn hash_and_sign(
&self,
private_key: &str,
) -> Result<String, Box<dyn std::error::Error>> {
// Serialize the unsigned transaction before hashing.
let serialized = to_string(self)?;
// Hash the serialized unsigned payload.
let hash = skein_256_hash_data(&serialized);
// Sign the transaction hash with the claimant wallet.
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok(signature)
}
}
impl CollateralClaimTransaction {
pub async fn new(
unsigned_collateral_claim: UnsignedCollateralClaimTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
// Hash and sign the unsigned transaction.
let signature = unsigned_collateral_claim.hash_and_sign(private_key).await?;
// Return the complete collateral-claim transaction.
Ok(Self {
unsigned_collateral_claim,
signature,
})
}
// Load an existing collateral-claim transaction.
pub async fn load(
unsigned_collateral_claim: UnsignedCollateralClaimTransaction,
signature: &str,
) -> Self {
Self {
unsigned_collateral_claim,
signature: signature.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed collateral-claim transaction byte layout.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_collateral_claim.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_collateral_claim.time.to_le_bytes())
.await?;
cursor
.write_all(&decode(&self.unsigned_collateral_claim.contract_hash).unwrap())
.await?;
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_collateral_claim.address)
.ok_or_else(|| {
tokio::io::Error::other("Invalid collateral claimant short address",
)
})?;
cursor.write_all(&address_bytes).await?;
cursor
.write_all(&self.unsigned_collateral_claim.txfee.to_le_bytes())
.await?;
cursor.write_all(&decode(&self.signature).unwrap()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
// Read the remaining fixed-width collateral-claim bytes.
let mut cursor = Cursor::new(bytes);
// Decode timestamp and contract hash.
let time = cursor.read_u32_le().await?;
let mut contract_hash_bytes = vec![0; 32];
cursor.read_exact(&mut contract_hash_bytes).await?;
let contract_hash = encode(&contract_hash_bytes);
// Decode claimant short address, fee, and signature.
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut address_bytes).await?;
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid collateral claimant short address bytes",
)
})?;
let txfee = cursor.read_u64_le().await?;
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature_bytes).await?;
let signature = encode(&signature_bytes);
let unsigned_collateral_claim = UnsignedCollateralClaimTransaction {
txtype,
time,
contract_hash,
address,
txfee,
};
Ok(CollateralClaimTransaction {
unsigned_collateral_claim,
signature,
})
}
pub async fn add_to_memory(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Store original bytes so the mempool can rebuild the exact
// transaction submitted by the wallet.
let original_data = self.to_bytes().await?;
// PostgreSQL uses signed integer types for these persisted fields.
let time = self.unsigned_collateral_claim.time as i32;
let fee = i64::try_from(self.unsigned_collateral_claim.txfee)
.map_err(|_| std::io::Error::other("Collateral fee exceeds i64 mempool limit"))?;
let address = &self.unsigned_collateral_claim.address;
let contract_hash = &self.unsigned_collateral_claim.contract_hash;
let hash = &self.unsigned_collateral_claim.hash().await;
let signature = &self.signature;
// Collateral-claim transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
client
.execute(
r#"
INSERT INTO collateral_claim (
time,
fee,
address,
contract_hash,
hash,
signature,
original
) VALUES (
$1, $2, $3, $4, $5, $6, $7
)
"#,
&[
&time,
&fee,
address,
contract_hash,
hash,
signature,
&original_data,
],
)
.await?;
Ok(())
}
}

83
src/blocks/genesis.rs Normal file
View File

@ -0,0 +1,83 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::to_string;
use crate::Cursor;
use crate::Serialize;
use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 49 bytes
pub struct UnsignedGenesisTransaction {
pub txtype: u8, // 1 byte transaction type, should be 0
pub message: String, // 48 bytes fixed genesis block message
}
#[derive(Debug, Serialize, Clone)] // 49 bytes
pub struct GenesisTransaction {
pub unsigned: UnsignedGenesisTransaction, // 49 bytes
}
impl UnsignedGenesisTransaction {
// Create the unsigned genesis transaction.
pub async fn new(txtype: u8, message: &str) -> Self {
Self {
txtype,
message: message.to_string(),
}
}
pub async fn hash(&self) -> String {
// Genesis hashes the serialized unsigned payload like other
// transaction types, even though it has no wallet signature.
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
}
impl GenesisTransaction {
const MESSAGE_LENGTH: usize = 48;
pub const BYTE_LENGTH: usize = 1 + Self::MESSAGE_LENGTH;
pub async fn new(unsigned: UnsignedGenesisTransaction) -> Self {
// Wrap a freshly created unsigned genesis payload.
Self { unsigned }
}
pub async fn load(unsigned: UnsignedGenesisTransaction) -> Self {
// Rebuild a genesis transaction already read from storage.
Self { unsigned }
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed genesis byte layout:
// type byte followed by the fixed-length launch message.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned.txtype.to_le_bytes())
.await?;
cursor.write_all(self.unsigned.message.as_bytes()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The transaction type is read by the block parser, so this
// function receives only the remaining genesis bytes.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
// Read from the remaining fixed-width transaction bytes.
let mut cursor = Cursor::new(bytes);
// Decode the fixed 48-byte genesis message.
let mut message_bytes = vec![0; Self::MESSAGE_LENGTH];
cursor.read_exact(&mut message_bytes).await?;
let message = binary_to_string(message_bytes);
let unsigned = UnsignedGenesisTransaction { txtype, message };
Ok(GenesisTransaction { unsigned })
}
}

221
src/blocks/issue_token.rs Normal file
View File

@ -0,0 +1,221 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 58 bytes
pub struct UnsignedIssueTokenTransaction {
pub txtype: u8, // 1 byte txtype should be 11
pub time: u32, // 4 bytes timestamp
pub creator: String, // 22 bytes wallet address of token creator
pub ticker: String, // 15 bytes token ticker padded with spaces
pub number: u64, // 8 bytes number of new tokens to issue
pub txfee: u64, // 8 bytes fee of transaction
}
#[derive(Debug, Serialize, Clone)] // 724 bytes
pub struct IssueTokenTransaction {
pub unsigned_issue_token: UnsignedIssueTokenTransaction, // 58 bytes
pub signature: String, // 666 bytes signature of hash
}
impl IssueTokenTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 15 + 8 + 8 + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedIssueTokenTransaction {
// Create an unsigned issue-token transaction.
pub async fn new(
txtype: u8,
time: u32,
creator: &str,
ticker: &str,
number: u64,
txfee: u64,
) -> Self {
Self {
txtype,
time,
creator: creator.to_string(),
ticker: ticker.to_string(),
number,
txfee,
}
}
// Hash without signing for verification.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
// Hash the unsigned transaction and sign it.
pub async fn hash_and_sign(
&self,
private_key: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let serialized = to_string(self)?;
let hash = skein_256_hash_data(&serialized);
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok(signature)
}
}
impl IssueTokenTransaction {
// Create a signed issue-token transaction.
pub async fn new(
unsigned_issue_token: UnsignedIssueTokenTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
let signature = unsigned_issue_token.hash_and_sign(private_key).await?;
Ok(Self {
unsigned_issue_token,
signature,
})
}
// Load an existing issue-token transaction.
pub async fn load(
unsigned_issue_token: UnsignedIssueTokenTransaction,
signature: &str,
) -> Self {
Self {
unsigned_issue_token,
signature: signature.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed issue-token transaction byte layout.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_issue_token.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_issue_token.time.to_le_bytes())
.await?;
let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_issue_token.creator)
.ok_or_else(|| {
tokio::io::Error::other("Invalid issue-token creator short address",
)
})?;
cursor.write_all(&creator_bytes).await?;
cursor
.write_all(self.unsigned_issue_token.ticker.as_bytes())
.await?;
cursor
.write_all(&self.unsigned_issue_token.number.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_issue_token.txfee.to_le_bytes())
.await?;
cursor.write_all(&decode(&self.signature).unwrap()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
let mut cursor = Cursor::new(bytes);
// Decode timestamp and creator short address.
let time = cursor.read_u32_le().await?;
let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut creator_bytes).await?;
let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid issue-token creator short address bytes",
)
})?;
// Decode token ticker, issued amount, fee, and signature.
let mut ticker_bytes = vec![0; 15];
cursor.read_exact(&mut ticker_bytes).await?;
let ticker = binary_to_string(ticker_bytes);
let number = cursor.read_u64_le().await?;
let txfee = cursor.read_u64_le().await?;
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature_bytes).await?;
let signature = encode(&signature_bytes);
let unsigned_issue_token = UnsignedIssueTokenTransaction {
txtype,
time,
creator,
ticker,
number,
txfee,
};
Ok(IssueTokenTransaction {
unsigned_issue_token,
signature,
})
}
pub async fn add_to_memory(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Store original bytes so the mempool can rebuild the exact
// transaction submitted by the wallet.
let original_data = self.to_bytes().await?;
// PostgreSQL uses signed integer types for these persisted fields.
let time = self.unsigned_issue_token.time as i32;
let fee = i64::try_from(self.unsigned_issue_token.txfee)
.map_err(|_| std::io::Error::other("Issue-token fee exceeds i64 mempool limit"))?;
let number = i64::try_from(self.unsigned_issue_token.number)
.map_err(|_| std::io::Error::other("Issue-token amount exceeds i64 mempool limit"))?;
let creator = &self.unsigned_issue_token.creator;
let ticker = &self.unsigned_issue_token.ticker;
let hash = &self.unsigned_issue_token.hash().await;
let signature = &self.signature;
// Issue-token transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
client
.execute(
r#"
INSERT INTO issue_token (
time,
fee,
creator,
number,
ticker,
hash,
signature,
original
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
&[
&time,
&fee,
creator,
&number,
ticker,
hash,
signature,
&original_data,
],
)
.await?;
Ok(())
}
}

253
src/blocks/loan_payment.rs Normal file
View File

@ -0,0 +1,253 @@
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 83 bytes
pub struct UnsignedContractPaymentTransaction {
pub txtype: u8, // 1 byte transaction type, should be 8
pub timestamp: u32, // 4 bytes payment timestamp
pub payback_amount: u64, // 8 bytes amount of loaned token to pay back
pub contract_hash: String, // 32 bytes hash of the loan contract
pub address: String, // 22 bytes payer short address
pub tip: u64, // 8 bytes miner tip paid in the loan token
pub txfee: u64, // 8 bytes transaction fee
}
#[derive(Debug, Serialize, Clone)] // 781 bytes
pub struct ContractPaymentTransaction {
pub unsigned_contract_payment: UnsignedContractPaymentTransaction, // 83 bytes
pub hash: String, // 32 bytes The hash of the transaction
pub signature: String, // 666 bytes The signature of the transaction hash
}
impl ContractPaymentTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + 8 + 32 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + 8 + 32 + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedContractPaymentTransaction {
// Create an unsigned loan-payment transaction.
pub async fn new(
txtype: u8,
timestamp: u32,
payback_amount: u64,
contract_hash: &str,
address: &str,
tip: u64,
txfee: u64,
) -> Self {
Self {
txtype,
timestamp,
payback_amount,
contract_hash: contract_hash.to_string(),
address: address.to_string(),
tip,
txfee,
}
}
// Hash without signing for verification.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
// Hash the unsigned transaction and sign it.
pub async fn hash_and_sign(
&self,
private_key: &str,
) -> Result<(String, String), Box<dyn std::error::Error>> {
// Serialize the unsigned transaction before hashing.
let serialized = to_string(self)?;
// Hash the serialized unsigned payload.
let hash = skein_256_hash_data(&serialized);
// Sign the transaction hash with the payer wallet.
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok((hash, signature))
}
}
impl ContractPaymentTransaction {
pub async fn new(
unsigned_contract_payment: UnsignedContractPaymentTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
// Hash and sign the unsigned transaction.
let (hash, signature) = unsigned_contract_payment.hash_and_sign(private_key).await?;
// Return the complete loan-payment transaction with its txid hash.
Ok(Self {
unsigned_contract_payment,
hash,
signature,
})
}
// Load an existing loan-payment transaction.
pub async fn load(
unsigned_contract_payment: UnsignedContractPaymentTransaction,
hash: &str,
signature: &str,
) -> Self {
Self {
unsigned_contract_payment,
hash: hash.to_string(),
signature: signature.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed loan-payment transaction byte layout.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_contract_payment.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_contract_payment.timestamp.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_contract_payment.payback_amount.to_le_bytes())
.await?;
cursor
.write_all(&decode(&self.unsigned_contract_payment.contract_hash).unwrap())
.await?;
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_contract_payment.address)
.ok_or_else(|| {
tokio::io::Error::other("Invalid payer short address")
})?;
cursor.write_all(&address_bytes).await?;
cursor
.write_all(&self.unsigned_contract_payment.tip.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_contract_payment.txfee.to_le_bytes())
.await?;
cursor.write_all(&decode(&self.hash).unwrap()).await?;
cursor.write_all(&decode(&self.signature).unwrap()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
// Read the remaining fixed-width loan-payment bytes.
let mut cursor = Cursor::new(bytes);
// Decode timestamp, payment amount, and contract hash.
let timestamp = cursor.read_u32_le().await?;
let payback_amount = cursor.read_u64_le().await?;
let mut contract_hash_bytes = vec![0; 32];
cursor.read_exact(&mut contract_hash_bytes).await?;
let contract_hash = encode(&contract_hash_bytes);
// Decode payer short address, miner tip, fee, txid hash, and signature.
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut address_bytes).await?;
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid payer short address bytes",
)
})?;
let tip = cursor.read_u64_le().await?;
let txfee = cursor.read_u64_le().await?;
let mut hash_bytes = vec![0; 32];
cursor.read_exact(&mut hash_bytes).await?;
let hash = encode(&hash_bytes);
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature_bytes).await?;
let signature = encode(&signature_bytes);
let unsigned_contract_payment = UnsignedContractPaymentTransaction {
txtype,
timestamp,
payback_amount,
contract_hash,
address,
tip,
txfee,
};
Ok(ContractPaymentTransaction {
unsigned_contract_payment,
hash,
signature,
})
}
pub async fn add_to_memory(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Store original bytes so the mempool can rebuild the exact
// transaction submitted by the wallet.
let original_data = self.to_bytes().await?;
// PostgreSQL uses signed integer types for these persisted fields.
let fee = i64::try_from(self.unsigned_contract_payment.txfee)
.map_err(|_| std::io::Error::other("Loan payment fee exceeds i64 mempool limit"))?;
let time = self.unsigned_contract_payment.timestamp as i32;
let payback_amount = i64::try_from(self.unsigned_contract_payment.payback_amount)
.map_err(|_| std::io::Error::other("Loan payment amount exceeds i64 mempool limit"))?;
let tip = i64::try_from(self.unsigned_contract_payment.tip)
.map_err(|_| std::io::Error::other("Loan payment tip exceeds i64 mempool limit"))?;
let contract_hash = &self.unsigned_contract_payment.contract_hash;
let address = &self.unsigned_contract_payment.address;
let txid = &self.hash;
let hash = &self.unsigned_contract_payment.hash().await;
let signature = &self.signature;
// Loan-payment transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
client
.execute(
r#"
INSERT INTO loan_payment (
fee,
time,
payback_amount,
contract_hash,
address,
tip,
txid,
hash,
signature,
original
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
)
"#,
&[
&fee,
&time,
&payback_amount,
contract_hash,
address,
&tip,
txid,
hash,
signature,
&original_data,
],
)
.await?;
Ok(())
}
}

369
src/blocks/loans.rs Normal file
View File

@ -0,0 +1,369 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 122 bytes
pub struct UnsignedLoanContractTransaction {
pub txtype: u8, // 1 byte transaction type, should be 7
pub timestamp: u32, // 4 bytes contract creation timestamp
pub loan_coin: String, // 15 bytes coin or token being loaned
pub loan_amount: u64, // 8 bytes amount being loaned
pub lender: String, // 22 bytes lender short address
pub collateral: String, // 15 bytes token or NFT used as collateral
pub collateral_amount: u64, // 8 bytes collateral token amount or 1 for NFT collateral
pub borrower: String, // 22 bytes borrower short address
pub payment_period: String, // 1 byte d, w, or m for days, weeks, or months
pub payment_number: u8, // 1 byte total number of payments
pub payment_amount: u64, // 8 bytes amount due for each payment
pub grace_period: u8, // 1 byte missed payments before collateral claim is allowed
pub max_late_value: u64, // 8 bytes max overdue value before collateral claim is allowed
pub txfee: u64, // 8 bytes transaction fee paid by the lender
}
#[derive(Debug, Serialize, Clone)] // 1486 bytes
pub struct LoanContractTransaction {
pub unsigned_loan_contract: UnsignedLoanContractTransaction, // 122 bytes
pub hash: String, // 32 bytes The hash of the transaction
pub signature1: String, // 666 bytes The signature of lender for the hash
pub signature2: String, // 666 bytes The signature of borrower for the hash
}
impl LoanContractTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + 15 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 15 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ 1 + 1 + 8 + 1 + 8 + 8 + 32 + Wallet::SIGNATURE_LENGTH + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedLoanContractTransaction {
// Create an unsigned loan-contract transaction.
#[allow(clippy::too_many_arguments)]
pub async fn new(
txtype: u8,
timestamp: u32,
loan_coin: &str,
loan_amount: u64,
lender: &str,
collateral: &str,
collateral_amount: u64,
borrower: &str,
payment_period: &str,
payment_number: u8,
payment_amount: u64,
grace_period: u8,
max_late_value: u64,
txfee: u64,
) -> Self {
Self {
txtype,
timestamp,
loan_coin: loan_coin.to_string(),
loan_amount,
lender: lender.to_string(),
collateral: collateral.to_string(),
collateral_amount,
borrower: borrower.to_string(),
payment_period: payment_period.to_string(),
payment_number,
payment_amount,
grace_period,
max_late_value,
txfee,
}
}
// Hash without signing for verification.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
// Hash the unsigned transaction and sign it.
pub async fn hash_and_sign(
&self,
private_key: &str,
) -> Result<(String, String), Box<dyn std::error::Error>> {
// Serialize the unsigned transaction before hashing.
let serialized = to_string(self)?;
// Hash the serialized unsigned payload.
let hash = skein_256_hash_data(&serialized);
// Sign the transaction hash with the current signer wallet.
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok((hash, signature))
}
}
impl LoanContractTransaction {
// Add the next required signature to a loan-contract transaction.
pub async fn new(
&self,
unsigned_loan_contract: UnsignedLoanContractTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
// Hash and sign the unsigned transaction.
let (hash, signature) = unsigned_loan_contract.hash_and_sign(private_key).await?;
// The first signer fills signature1, and the second signer fills signature2.
let mut signature1 = "".to_string();
let mut signature2 = "".to_string();
if self.signature1.is_empty() {
signature1 = signature;
} else {
signature2 = signature;
}
// Return the loan contract with the newest signature included.
Ok(Self {
unsigned_loan_contract,
hash,
signature1,
signature2,
})
}
// Load an existing loan-contract transaction.
pub async fn load(
unsigned_loan_contract: UnsignedLoanContractTransaction,
hash: &str,
signature1: &str,
signature2: &str,
) -> Self {
Self {
unsigned_loan_contract,
hash: hash.to_string(),
signature1: signature1.to_string(),
signature2: signature2.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed loan-contract transaction byte layout.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_loan_contract.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_loan_contract.timestamp.to_le_bytes())
.await?;
cursor
.write_all(self.unsigned_loan_contract.loan_coin.as_bytes())
.await?;
cursor
.write_all(&self.unsigned_loan_contract.loan_amount.to_le_bytes())
.await?;
let lender_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.lender)
.ok_or_else(|| {
tokio::io::Error::other("Invalid lender short address")
})?;
cursor.write_all(&lender_bytes).await?;
cursor
.write_all(self.unsigned_loan_contract.collateral.as_bytes())
.await?;
cursor
.write_all(&self.unsigned_loan_contract.collateral_amount.to_le_bytes())
.await?;
let borrower_bytes = Wallet::short_address_to_bytes(&self.unsigned_loan_contract.borrower)
.ok_or_else(|| {
tokio::io::Error::other("Invalid borrower short address",
)
})?;
cursor.write_all(&borrower_bytes).await?;
cursor
.write_all(self.unsigned_loan_contract.payment_period.as_bytes())
.await?;
cursor
.write_all(&self.unsigned_loan_contract.payment_number.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_loan_contract.payment_amount.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_loan_contract.grace_period.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_loan_contract.max_late_value.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_loan_contract.txfee.to_le_bytes())
.await?;
cursor.write_all(&decode(&self.hash).unwrap()).await?;
cursor.write_all(&decode(&self.signature1).unwrap()).await?;
cursor.write_all(&decode(&self.signature2).unwrap()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
// Read the remaining fixed-width loan-contract bytes.
let mut cursor = Cursor::new(bytes);
// Decode timestamp and loan terms.
let timestamp = cursor.read_u32_le().await?;
let mut loan_coin_bytes = vec![0; 15];
cursor.read_exact(&mut loan_coin_bytes).await?;
let loan_coin = binary_to_string(loan_coin_bytes);
let loan_amount = cursor.read_u64_le().await?;
// Decode lender short address.
let mut lender_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut lender_bytes).await?;
let lender = Wallet::bytes_to_short_address(&lender_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid lender short address bytes",
)
})?;
// Decode collateral asset and amount.
let mut collateral_bytes = vec![0; 15];
cursor.read_exact(&mut collateral_bytes).await?;
let collateral = binary_to_string(collateral_bytes);
let collateral_amount = cursor.read_u64_le().await?;
// Decode borrower short address.
let mut borrower_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut borrower_bytes).await?;
let borrower = Wallet::bytes_to_short_address(&borrower_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid borrower short address bytes",
)
})?;
// Decode payment schedule and late-payment rules.
let mut payment_period_bytes = vec![0; 1];
cursor.read_exact(&mut payment_period_bytes).await?;
let payment_period = binary_to_string(payment_period_bytes);
let payment_number = cursor.read_u8().await?;
let payment_amount = cursor.read_u64_le().await?;
let grace_period = cursor.read_u8().await?;
let max_late_value = cursor.read_u64_le().await?;
// Decode fee, txid hash, and both signatures.
let txfee = cursor.read_u64_le().await?;
let mut hash_bytes = vec![0; 32];
cursor.read_exact(&mut hash_bytes).await?;
let hash = encode(&hash_bytes);
let mut signature1_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature1_bytes).await?;
let signature1 = encode(&signature1_bytes);
let mut signature2_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature2_bytes).await?;
let signature2 = encode(&signature2_bytes);
// Rebuild the unsigned loan-contract payload.
let unsigned_loan_contract = UnsignedLoanContractTransaction {
txtype,
timestamp,
loan_coin,
loan_amount,
lender,
collateral,
collateral_amount,
borrower,
payment_period,
payment_number,
payment_amount,
grace_period,
max_late_value,
txfee,
};
// Wrap the rebuilt unsigned payload, txid hash, and signatures.
Ok(LoanContractTransaction {
unsigned_loan_contract,
hash,
signature1,
signature2,
})
}
pub async fn add_to_memory(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Store original bytes so the mempool can rebuild the exact
// transaction submitted by the wallets.
let original_data = self.to_bytes().await?;
// PostgreSQL uses signed integer types for these persisted fields.
let fee = i64::try_from(self.unsigned_loan_contract.txfee)
.map_err(|_| std::io::Error::other("Loan contract fee exceeds i64 mempool limit"))?;
let time = self.unsigned_loan_contract.timestamp as i32;
let loan_amount = i64::try_from(self.unsigned_loan_contract.loan_amount)
.map_err(|_| std::io::Error::other("Loan amount exceeds i64 mempool limit"))?;
let collateral_amount = i64::try_from(self.unsigned_loan_contract.collateral_amount)
.map_err(|_| std::io::Error::other("Collateral amount exceeds i64 mempool limit"))?;
let loan_coin = &self.unsigned_loan_contract.loan_coin;
let lender = &self.unsigned_loan_contract.lender;
let collateral = &self.unsigned_loan_contract.collateral;
let borrower = &self.unsigned_loan_contract.borrower;
let txid = &self.hash;
let hash = self.unsigned_loan_contract.hash().await;
let signature1 = &self.signature1;
let signature2 = &self.signature2;
// Loan contracts remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
client
.execute(
r#"
INSERT INTO loan_contract (
fee,
time,
loan_coin,
loan_amount,
lender,
collateral,
collateral_amount,
borrower,
txid,
hash,
signature1,
signature2,
original
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10, $11, $12, $13
)
"#,
&[
&fee,
&time,
loan_coin,
&loan_amount,
lender,
collateral,
&collateral_amount,
borrower,
txid,
&hash,
signature1,
signature2,
&original_data,
],
)
.await?;
Ok(())
}
}

271
src/blocks/marketing.rs Normal file
View File

@ -0,0 +1,271 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 195 bytes
pub struct UnsignedMarketingTransaction {
pub txtype: u8, // 1 byte transaction type, should be 5
pub time: u32, // 4 bytes transaction timestamp
pub campaign: u64, // 8 bytes campaign identifier
pub ad_type: String, // 6 bytes banner, social, or text padded with spaces
pub keyword: String, // 40 bytes targeted keyword padded with spaces
pub displayed: String, // 100 bytes display location padded or truncated to fit
pub impression: u8, // 1 byte impression count update, 0 to 255
pub click: u8, // 1 byte click count update, 0 to 255
pub impression_value: u16, // 2 bytes impression value multiplied by 100
pub click_value: u16, // 2 bytes click value multiplied by 100
pub advertiser: String, // 22 bytes ad agency short address, not the client
pub txfee: u64, // 8 bytes transaction fee
}
#[derive(Debug, Serialize, Clone)] // 861 bytes
pub struct MarketingTransaction {
pub unsigned_marketing: UnsignedMarketingTransaction, // 195 bytes
pub signature: String, // 666 bytes signature of hash
}
impl MarketingTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + 8 + 6 + 40 + 100 + 1 + 1 + 2 + 2 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedMarketingTransaction {
// Create an unsigned marketing transaction.
#[allow(clippy::too_many_arguments)]
pub async fn new(
txtype: u8,
time: u32,
campaign: u64,
ad_type: &str,
keyword: &str,
displayed: &str,
impression: u8,
click: u8,
impression_value: u16,
click_value: u16,
advertiser: &str,
txfee: u64,
) -> Self {
Self {
txtype,
time,
campaign,
ad_type: ad_type.to_string(),
keyword: keyword.to_string(),
displayed: displayed.to_string(),
impression,
click,
impression_value,
click_value,
advertiser: advertiser.to_string(),
txfee,
}
}
// Hash without signing for verification.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
// Hash the unsigned transaction and sign it.
pub async fn hash_and_sign(
&self,
private_key: &str,
) -> Result<String, Box<dyn std::error::Error>> {
// Serialize the unsigned transaction before hashing.
let serialized = to_string(self)?;
// Hash the serialized unsigned payload.
let hash = skein_256_hash_data(&serialized);
// Sign the transaction hash with the advertiser wallet.
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok(signature)
}
}
impl MarketingTransaction {
// Create a signed marketing transaction.
pub async fn new(
unsigned_marketing: UnsignedMarketingTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
// Hash and sign the unsigned transaction.
let signature = unsigned_marketing.hash_and_sign(private_key).await?;
// Return the complete marketing transaction.
Ok(Self {
unsigned_marketing,
signature,
})
}
// Load an existing marketing transaction.
pub async fn load(unsigned_marketing: UnsignedMarketingTransaction, signature: &str) -> Self {
Self {
unsigned_marketing,
signature: signature.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed marketing transaction byte layout.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_marketing.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_marketing.time.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_marketing.campaign.to_le_bytes())
.await?;
cursor
.write_all(self.unsigned_marketing.ad_type.as_bytes())
.await?;
cursor
.write_all(self.unsigned_marketing.keyword.as_bytes())
.await?;
cursor
.write_all(self.unsigned_marketing.displayed.as_bytes())
.await?;
cursor
.write_all(&self.unsigned_marketing.impression.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_marketing.click.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_marketing.impression_value.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_marketing.click_value.to_le_bytes())
.await?;
let advertiser_bytes = Wallet::short_address_to_bytes(&self.unsigned_marketing.advertiser)
.ok_or_else(|| {
tokio::io::Error::other("Invalid advertiser short address",
)
})?;
cursor.write_all(&advertiser_bytes).await?;
cursor
.write_all(&self.unsigned_marketing.txfee.to_le_bytes())
.await?;
cursor.write_all(&decode(&self.signature).unwrap()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
// Read the remaining fixed-width marketing bytes.
let mut cursor = Cursor::new(bytes);
// Decode timestamp and campaign identifier.
let time = cursor.read_u32_le().await?;
let campaign = cursor.read_u64_le().await?;
// Decode fixed-width string fields.
let mut ad_type_bytes = vec![0; 6];
cursor.read_exact(&mut ad_type_bytes).await?;
let ad_type = binary_to_string(ad_type_bytes);
let mut keyword_bytes = vec![0; 40];
cursor.read_exact(&mut keyword_bytes).await?;
let keyword = binary_to_string(keyword_bytes);
let mut displayed_bytes = vec![0; 100];
cursor.read_exact(&mut displayed_bytes).await?;
let displayed = binary_to_string(displayed_bytes);
// Decode impression/click counts and their configured values.
let impression = cursor.read_u8().await?;
let click = cursor.read_u8().await?;
let impression_value = cursor.read_u16_le().await?;
let click_value = cursor.read_u16_le().await?;
// Decode advertiser short address, fee, and signature.
let mut advertiser_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut advertiser_bytes).await?;
let advertiser = Wallet::bytes_to_short_address(&advertiser_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid advertiser short address bytes",
)
})?;
let txfee = cursor.read_u64_le().await?;
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature_bytes).await?;
let signature = encode(&signature_bytes);
// Rebuild the unsigned marketing payload.
let unsigned_marketing = UnsignedMarketingTransaction {
txtype,
time,
campaign,
ad_type,
keyword,
displayed,
impression,
click,
impression_value,
click_value,
advertiser,
txfee,
};
// Wrap the rebuilt unsigned payload and signature.
Ok(MarketingTransaction {
unsigned_marketing,
signature,
})
}
pub async fn add_to_memory(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Store original bytes so the mempool can rebuild the exact
// transaction submitted by the wallet.
let original_data = self.to_bytes().await?;
// PostgreSQL uses signed integer types for these persisted fields.
let time = self.unsigned_marketing.time as i32;
let fee = i64::try_from(self.unsigned_marketing.txfee)
.map_err(|_| std::io::Error::other("Marketing fee exceeds i64 mempool limit"))?;
let advertiser = &self.unsigned_marketing.advertiser;
let hash = &self.unsigned_marketing.hash().await;
let signature = &self.signature;
// Marketing transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
client
.execute(
r#"
INSERT INTO marketing (
time,
fee,
advertiser,
hash,
signature,
original
) VALUES ($1, $2, $3, $4, $5, $6)
"#,
&[&time, &fee, advertiser, hash, signature, &original_data],
)
.await?;
Ok(())
}
}

14
src/blocks/mod.rs Normal file
View File

@ -0,0 +1,14 @@
pub mod block;
pub mod burn;
pub mod collateral;
pub mod genesis;
pub mod issue_token;
pub mod loan_payment;
pub mod loans;
pub mod marketing;
pub mod nft;
pub mod rewards;
pub mod swap;
pub mod token;
pub mod transfer;
pub mod vanity;

263
src/blocks/nft.rs Normal file
View File

@ -0,0 +1,263 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 255 bytes
pub struct UnsignedCreateNftTransaction {
pub txtype: u8, // 1 byte transaction type, should be 4
pub time: u32, // 4 bytes transaction timestamp
pub creator: String, // 22 bytes creator short address
pub series: u8, // 1 byte 0 for single NFT, 1 for series
pub nft_name: String, // 15 bytes NFT or collection name padded with spaces
pub item_ipfs: String, // 100 bytes padded CID string
pub count: u32, // 4 bytes 1 for single NFT, otherwise series item count
pub desc: String, // 100 bytes description padded with spaces
pub txfee: u64, // 8 bytes transaction fee
}
#[derive(Debug, Serialize, Clone)] // 921 bytes
pub struct CreateNftTransaction {
pub unsigned_create_nft: UnsignedCreateNftTransaction, // 255 bytes
pub signature: String, // 666 bytes signature of hash
}
impl CreateNftTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 1 + 15 + 100 + 4 + 100 + 8 + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedCreateNftTransaction {
// Create an unsigned NFT-creation transaction.
#[allow(clippy::too_many_arguments)]
pub async fn new(
txtype: u8,
time: u32,
creator: &str,
series: u8,
nft_name: &str,
item_ipfs: &str,
count: u32,
desc: &str,
txfee: u64,
) -> Self {
Self {
txtype,
time,
creator: creator.to_string(),
series,
nft_name: nft_name.to_string(),
item_ipfs: item_ipfs.to_string(),
count,
desc: desc.to_string(),
txfee,
}
}
// Hash without signing for verification.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
// Hash the unsigned transaction and sign it.
pub async fn hash_and_sign(
&self,
private_key: &str,
) -> Result<String, Box<dyn std::error::Error>> {
// Serialize the unsigned transaction before hashing.
let serialized = to_string(self)?;
// Hash the serialized unsigned payload.
let hash = skein_256_hash_data(&serialized);
// Sign the transaction hash with the creator wallet.
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok(signature)
}
}
impl CreateNftTransaction {
// Create a signed NFT-creation transaction.
pub async fn new(
unsigned_create_nft: UnsignedCreateNftTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
// Hash and sign the unsigned transaction.
let signature = unsigned_create_nft.hash_and_sign(private_key).await?;
// Return the complete NFT-creation transaction.
Ok(Self {
unsigned_create_nft,
signature,
})
}
// Load an existing NFT-creation transaction.
pub async fn load(unsigned_create_nft: UnsignedCreateNftTransaction, signature: &str) -> Self {
Self {
unsigned_create_nft,
signature: signature.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed NFT-creation transaction byte layout.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_create_nft.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_create_nft.time.to_le_bytes())
.await?;
let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_create_nft.creator)
.ok_or_else(|| {
tokio::io::Error::other("Invalid creator short address")
})?;
cursor.write_all(&creator_bytes).await?;
cursor
.write_all(&self.unsigned_create_nft.series.to_le_bytes())
.await?;
cursor
.write_all(self.unsigned_create_nft.nft_name.as_bytes())
.await?;
cursor
.write_all(self.unsigned_create_nft.item_ipfs.as_bytes())
.await?;
cursor
.write_all(&self.unsigned_create_nft.count.to_le_bytes())
.await?;
cursor
.write_all(self.unsigned_create_nft.desc.as_bytes())
.await?;
cursor
.write_all(&self.unsigned_create_nft.txfee.to_le_bytes())
.await?;
cursor.write_all(&decode(&self.signature).unwrap()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
// Read the remaining fixed-width NFT-creation bytes.
let mut cursor = Cursor::new(bytes);
// Decode timestamp and creator short address.
let time = cursor.read_u32_le().await?;
let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut creator_bytes).await?;
let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid creator short address bytes",
)
})?;
// Decode series flag, name, CID, count, description, fee, and signature.
let series = cursor.read_u8().await?;
let mut nft_name_bytes = vec![0; 15];
cursor.read_exact(&mut nft_name_bytes).await?;
let nft_name = binary_to_string(nft_name_bytes);
let mut item_ipfs_bytes = vec![0; 100];
cursor.read_exact(&mut item_ipfs_bytes).await?;
let item_ipfs = binary_to_string(item_ipfs_bytes);
let count = cursor.read_u32_le().await?;
let mut desc_bytes = vec![0; 100];
cursor.read_exact(&mut desc_bytes).await?;
let desc = binary_to_string(desc_bytes);
let txfee = cursor.read_u64_le().await?;
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature_bytes).await?;
let signature = encode(&signature_bytes);
// Rebuild the unsigned NFT-creation payload.
let unsigned_create_nft = UnsignedCreateNftTransaction {
txtype,
time,
creator,
series,
nft_name,
item_ipfs,
count,
desc,
txfee,
};
// Wrap the rebuilt unsigned payload and signature.
Ok(CreateNftTransaction {
unsigned_create_nft,
signature,
})
}
pub async fn add_to_memory(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Store original bytes so the mempool can rebuild the exact
// transaction submitted by the wallet.
let original_data = self.to_bytes().await?;
// PostgreSQL uses signed integer types for these persisted fields.
let time = self.unsigned_create_nft.time as i32;
let fee = i64::try_from(self.unsigned_create_nft.txfee)
.map_err(|_| std::io::Error::other("NFT fee exceeds i64 mempool limit"))?;
let series = self.unsigned_create_nft.series as i16;
let count = i64::from(self.unsigned_create_nft.count);
let creator = &self.unsigned_create_nft.creator;
let nft_name = &self.unsigned_create_nft.nft_name;
let hash = &self.unsigned_create_nft.hash().await;
let signature = &self.signature;
// NFT-creation transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
client
.execute(
r#"
INSERT INTO nft (
fee,
time,
creator,
nft_name,
series,
count,
hash,
signature,
original
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
&[
&fee,
&time,
creator,
nft_name,
&series,
&count,
hash,
signature,
&original_data,
],
)
.await?;
Ok(())
}
}

89
src/blocks/rewards.rs Normal file
View File

@ -0,0 +1,89 @@
use crate::common::skein::skein_256_hash_data;
use crate::to_string;
use crate::Cursor;
use crate::Serialize;
use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 13 bytes
pub struct UnsignedRewardsTransaction {
pub txtype: u8, // 1 byte transaction type, should be 1
pub timestamp: u32, // 4 bytes timestamp used in the reward payload
pub value: u64, // 8 bytes reward value
}
#[derive(Debug, Serialize, Clone)]
pub struct RewardsTransaction {
// 13 bytes
pub unsigned: UnsignedRewardsTransaction, // 13 bytes consensus-created reward data
}
impl UnsignedRewardsTransaction {
// Create an unsigned reward transaction for a mined block.
pub async fn new(txtype: u8, timestamp: u32, value: u64) -> Self {
Self {
txtype,
timestamp,
value,
}
}
// Hash the unsigned reward data for transaction indexing.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
}
impl RewardsTransaction {
pub const BYTE_LENGTH: usize = 1 + 4 + 8;
pub async fn new(unsigned: UnsignedRewardsTransaction) -> Self {
// Rewards are not wallet-signed, so the final transaction
// only wraps the consensus-created unsigned data.
Self { unsigned }
}
pub async fn load(unsigned: UnsignedRewardsTransaction) -> Self {
// Rebuild a reward transaction already read from storage.
Self { unsigned }
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed reward layout: type, timestamp, value.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned.timestamp.to_le_bytes())
.await?;
cursor.write_all(&self.unsigned.value.to_le_bytes()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
// Read the remaining fixed reward bytes.
let mut cursor = Cursor::new(bytes);
// Decode timestamp and reward value in the same order they were written.
let timestamp = cursor.read_u32_le().await?;
let value = cursor.read_u64_le().await?;
// Rebuild the unsigned reward payload.
let unsigned = UnsignedRewardsTransaction {
txtype,
timestamp,
value,
};
// Wrap the rebuilt unsigned reward payload.
Ok(RewardsTransaction { unsigned })
}
}

377
src/blocks/swap.rs Normal file
View File

@ -0,0 +1,377 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 139 bytes
pub struct UnsignedSwapTransaction {
pub txtype: u8, // 1 byte transaction type, should be 6
pub timestamp: u32, // 4 bytes offer creation timestamp
pub offer_expiration: u32, // 4 bytes offer expiration timestamp
pub ticker1: String, // 15 bytes asset offered by sender1
pub nft_series1: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item
pub value1: u64, // 8 bytes amount offered by sender1
pub ticker2: String, // 15 bytes asset offered by sender2
pub nft_series2: u32, // 4 bytes 0 for fungible assets, otherwise NFT series item
pub value2: u64, // 8 bytes amount offered by sender2
pub sender1: String, // 22 bytes sender1 short address
pub sender2: String, // 22 bytes sender2 short address
pub tip1: u64, // 8 bytes miner tip paid in ticker1
pub tip2: u64, // 8 bytes miner tip paid in ticker2
pub txfee1: u64, // 8 bytes sender1 fee
pub txfee2: u64, // 8 bytes sender2 fee
}
#[derive(Debug, Serialize, Clone)] // 1471 bytes
pub struct SwapTransaction {
pub unsigned_swap: UnsignedSwapTransaction, // 139 bytes
pub signature1: String, // 666 bytes The signature of sender1 for the hash
pub signature2: String, // 666 bytes The signature of sender2 for the hash
}
impl SwapTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + 4 + 15 + 4 + 8 + 15 + 4 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH
+ 8 + 8 + 8 + 8 + Wallet::SIGNATURE_LENGTH + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedSwapTransaction {
// Create an unsigned swap transaction.
#[allow(clippy::too_many_arguments)]
pub async fn new(
txtype: u8,
timestamp: u32,
offer_expiration: u32,
ticker1: &str,
nft_series1: u32,
value1: u64,
ticker2: &str,
nft_series2: u32,
value2: u64,
sender1: &str,
sender2: &str,
tip1: u64,
tip2: u64,
txfee1: u64,
txfee2: u64,
) -> Self {
Self {
txtype,
timestamp,
offer_expiration,
ticker1: ticker1.to_string(),
nft_series1,
value1,
ticker2: ticker2.to_string(),
nft_series2,
value2,
sender1: sender1.to_string(),
sender2: sender2.to_string(),
tip1,
tip2,
txfee1,
txfee2,
}
}
// Hash without signing for verification.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
// Hash the unsigned transaction and sign it.
pub async fn hash_and_sign(
&self,
private_key: &str,
) -> Result<String, Box<dyn std::error::Error>> {
// Serialize the unsigned transaction before hashing.
let serialized = to_string(self)?;
// Hash the serialized unsigned payload.
let hash = skein_256_hash_data(&serialized);
// Sign the transaction hash with the current signer wallet.
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok(signature)
}
}
impl SwapTransaction {
// Add the next required signature to a swap transaction.
pub async fn new(
&self,
unsigned_swap: UnsignedSwapTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
// Hash and sign the unsigned transaction.
let signature = unsigned_swap.hash_and_sign(private_key).await?;
// The first signer fills signature1, and the second signer fills signature2.
let (signed1, signed2) = if self.signature1.is_empty() {
let signed1 = signature;
let signed2 = "".to_string();
(signed1, signed2)
} else {
let signed2 = signature;
let signed1 = self.signature1.clone();
(signed1, signed2)
};
// Return the swap with the newest signature included.
Ok(Self {
unsigned_swap,
signature1: signed1,
signature2: signed2,
})
}
// Load an existing swap transaction.
pub async fn load(
unsigned_swap: UnsignedSwapTransaction,
signature1: &str,
signature2: &str,
) -> Self {
Self {
unsigned_swap,
signature1: signature1.to_string(),
signature2: signature2.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed swap transaction byte layout.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_swap.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_swap.timestamp.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_swap.offer_expiration.to_le_bytes())
.await?;
cursor
.write_all(self.unsigned_swap.ticker1.as_bytes())
.await?;
cursor
.write_all(&self.unsigned_swap.nft_series1.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_swap.value1.to_le_bytes())
.await?;
cursor
.write_all(self.unsigned_swap.ticker2.as_bytes())
.await?;
cursor
.write_all(&self.unsigned_swap.nft_series2.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_swap.value2.to_le_bytes())
.await?;
let sender1_bytes = Wallet::short_address_to_bytes(&self.unsigned_swap.sender1)
.ok_or_else(|| {
tokio::io::Error::other("Invalid sender1 short address")
})?;
let sender2_bytes = Wallet::short_address_to_bytes(&self.unsigned_swap.sender2)
.ok_or_else(|| {
tokio::io::Error::other("Invalid sender2 short address")
})?;
cursor.write_all(&sender1_bytes).await?;
cursor.write_all(&sender2_bytes).await?;
cursor
.write_all(&self.unsigned_swap.tip1.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_swap.tip2.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_swap.txfee1.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_swap.txfee2.to_le_bytes())
.await?;
cursor.write_all(&decode(&self.signature1).unwrap()).await?;
cursor.write_all(&decode(&self.signature2).unwrap()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
// Read the remaining fixed-width swap bytes.
let mut cursor = Cursor::new(bytes);
// Decode offer timing.
let timestamp = cursor.read_u32_le().await?;
let offer_expiration = cursor.read_u32_le().await?;
// Decode sender1 asset, series, and value.
let mut ticker1_bytes = vec![0; 15];
cursor.read_exact(&mut ticker1_bytes).await?;
let ticker1 = binary_to_string(ticker1_bytes);
let nft_series1 = cursor.read_u32_le().await?;
let value1 = cursor.read_u64_le().await?;
// Decode sender2 asset, series, and value.
let mut ticker2_bytes = vec![0; 15];
cursor.read_exact(&mut ticker2_bytes).await?;
let ticker2 = binary_to_string(ticker2_bytes);
let nft_series2 = cursor.read_u32_le().await?;
let value2 = cursor.read_u64_le().await?;
// Decode both sender short addresses.
let mut sender1_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut sender1_bytes).await?;
let sender1 = Wallet::bytes_to_short_address(&sender1_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid sender1 short address bytes",
)
})?;
let mut sender2_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut sender2_bytes).await?;
let sender2 = Wallet::bytes_to_short_address(&sender2_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid sender2 short address bytes",
)
})?;
// Decode miner tips, fees, and both signatures.
let tip1 = cursor.read_u64_le().await?;
let tip2 = cursor.read_u64_le().await?;
let txfee1 = cursor.read_u64_le().await?;
let txfee2 = cursor.read_u64_le().await?;
let mut signature1_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature1_bytes).await?;
let signature1 = encode(&signature1_bytes);
let mut signature2_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature2_bytes).await?;
let signature2 = encode(&signature2_bytes);
// Rebuild the unsigned swap payload.
let unsigned_swap = UnsignedSwapTransaction {
txtype,
timestamp,
offer_expiration,
ticker1,
nft_series1,
value1,
ticker2,
nft_series2,
value2,
sender1,
sender2,
tip1,
tip2,
txfee1,
txfee2,
};
// Wrap the rebuilt unsigned payload and signatures.
Ok(SwapTransaction {
unsigned_swap,
signature1,
signature2,
})
}
pub async fn add_to_memory(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Store original bytes so the mempool can rebuild the exact
// transaction submitted by the wallets.
let original_data = self.to_bytes().await?;
// PostgreSQL uses signed integer types for these persisted fields.
let fee1 = i64::try_from(self.unsigned_swap.txfee1)
.map_err(|_| std::io::Error::other("Swap fee1 exceeds i64 mempool limit"))?;
let fee2 = i64::try_from(self.unsigned_swap.txfee2)
.map_err(|_| std::io::Error::other("Swap fee2 exceeds i64 mempool limit"))?;
let timestamp = self.unsigned_swap.timestamp as i32;
let value1 = i64::try_from(self.unsigned_swap.value1)
.map_err(|_| std::io::Error::other("Swap value1 exceeds i64 mempool limit"))?;
let value2 = i64::try_from(self.unsigned_swap.value2)
.map_err(|_| std::io::Error::other("Swap value2 exceeds i64 mempool limit"))?;
let nft_series1 = self.unsigned_swap.nft_series1 as i32;
let nft_series2 = self.unsigned_swap.nft_series2 as i32;
let tip1 = i64::try_from(self.unsigned_swap.tip1)
.map_err(|_| std::io::Error::other("Swap tip1 exceeds i64 mempool limit"))?;
let tip2 = i64::try_from(self.unsigned_swap.tip2)
.map_err(|_| std::io::Error::other("Swap tip2 exceeds i64 mempool limit"))?;
let ticker1 = &self.unsigned_swap.ticker1;
let ticker2 = &self.unsigned_swap.ticker2;
let hash = &self.unsigned_swap.hash().await;
let sender1 = &self.unsigned_swap.sender1;
let sender2 = &self.unsigned_swap.sender2;
let signature1 = &self.signature1;
let signature2 = &self.signature2;
// Swap transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
client
.execute(
r#"
INSERT INTO swap (
fee1,
fee2,
time,
ticker1,
nft_series1,
ticker2,
nft_series2,
value1,
value2,
sender1,
tip1,
tip2,
sender2,
hash,
signature1,
signature2,
original
) VALUES (
$1, $2, $3, $4, $5, $6, $7,
$8, $9, $10, $11, $12, $13, $14, $15, $16, $17
)
"#,
&[
&fee1,
&fee2,
&timestamp,
ticker1,
&nft_series1,
ticker2,
&nft_series2,
&value1,
&value2,
sender1,
&tip1,
&tip2,
sender2,
hash,
signature1,
signature2,
&original_data,
],
)
.await?;
Ok(())
}
}

245
src/blocks/token.rs Normal file
View File

@ -0,0 +1,245 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 59 bytes
pub struct UnsignedCreateTokenTransaction {
pub txtype: u8, // 1 byte transaction type, should be 3
pub time: u32, // 4 bytes transaction timestamp
pub creator: String, // 22 bytes creator short address
pub ticker: String, // 15 bytes token ticker padded with spaces
pub number: u64, // 8 bytes number of tokens to create
pub hard_limit: u8, // 1 byte 1 means capped forever, 0 allows future issuance
pub txfee: u64, // 8 bytes transaction fee
}
#[derive(Debug, Serialize, Clone)] // 725 bytes
pub struct CreateTokenTransaction {
pub unsigned_create_token: UnsignedCreateTokenTransaction, // 59 bytes
pub signature: String, // 666 bytes signature of hash
}
impl CreateTokenTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 15 + 8 + 1 + 8 + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedCreateTokenTransaction {
// Create an unsigned token-creation transaction.
pub async fn new(
txtype: u8,
time: u32,
creator: &str,
ticker: &str,
number: u64,
hard_limit: u8,
txfee: u64,
) -> Self {
Self {
txtype,
time,
creator: creator.to_string(),
ticker: ticker.to_string(),
number,
hard_limit,
txfee,
}
}
// Hash without signing for verification.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
// Hash the unsigned transaction and sign it.
pub async fn hash_and_sign(
&self,
private_key: &str,
) -> Result<String, Box<dyn std::error::Error>> {
// Serialize the unsigned transaction before hashing.
let serialized = to_string(self)?;
// Hash the serialized unsigned payload.
let hash = skein_256_hash_data(&serialized);
// Sign the transaction hash with the creator wallet.
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok(signature)
}
}
impl CreateTokenTransaction {
// Create a signed token-creation transaction.
pub async fn new(
unsigned_create_token: UnsignedCreateTokenTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
// Hash and sign the unsigned transaction.
let signature = unsigned_create_token.hash_and_sign(private_key).await?;
// Return the complete token-creation transaction.
Ok(Self {
unsigned_create_token,
signature,
})
}
// Load an existing token-creation transaction.
pub async fn load(
unsigned_create_token: UnsignedCreateTokenTransaction,
signature: &str,
) -> Self {
Self {
unsigned_create_token,
signature: signature.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed token-creation transaction byte layout.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_create_token.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_create_token.time.to_le_bytes())
.await?;
let creator_bytes = Wallet::short_address_to_bytes(&self.unsigned_create_token.creator)
.ok_or_else(|| {
tokio::io::Error::other("Invalid creator short address")
})?;
cursor.write_all(&creator_bytes).await?;
cursor
.write_all(self.unsigned_create_token.ticker.as_bytes())
.await?;
cursor
.write_all(&self.unsigned_create_token.number.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_create_token.hard_limit.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_create_token.txfee.to_le_bytes())
.await?;
cursor.write_all(&decode(&self.signature).unwrap()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
// Read the remaining fixed-width token-creation bytes.
let mut cursor = Cursor::new(bytes);
// Decode timestamp.
let time = cursor.read_u32_le().await?;
// Decode the creator short address.
let mut creator_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut creator_bytes).await?;
let creator = Wallet::bytes_to_short_address(&creator_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid creator short address bytes",
)
})?;
// Decode the fixed 15-byte token ticker.
let mut ticker_bytes = vec![0; 15];
cursor.read_exact(&mut ticker_bytes).await?;
let ticker = binary_to_string(ticker_bytes);
// Decode token supply, hard-limit flag, and fee.
let number = cursor.read_u64_le().await?;
let hard_limit = cursor.read_u8().await?;
let txfee = cursor.read_u64_le().await?;
// Decode the Falcon signature.
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature_bytes).await?;
let signature = encode(&signature_bytes);
// Rebuild the unsigned token-creation payload.
let unsigned_create_token = UnsignedCreateTokenTransaction {
txtype,
time,
creator,
ticker,
number,
hard_limit,
txfee,
};
// Wrap the rebuilt unsigned payload and signature.
Ok(CreateTokenTransaction {
unsigned_create_token,
signature,
})
}
pub async fn add_to_memory(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Store original bytes so the mempool can rebuild the exact
// transaction submitted by the wallet.
let original_data = self.to_bytes().await?;
// PostgreSQL uses signed integer types for these persisted fields.
let time = self.unsigned_create_token.time as i32;
let fee = i64::try_from(self.unsigned_create_token.txfee)
.map_err(|_| std::io::Error::other("Create-token fee exceeds i64 mempool limit"))?;
let number = i64::try_from(self.unsigned_create_token.number)
.map_err(|_| std::io::Error::other("Create-token amount exceeds i64 mempool limit"))?;
let hard_limit = self.unsigned_create_token.hard_limit as i16;
let creator = &self.unsigned_create_token.creator;
let ticker = &self.unsigned_create_token.ticker;
let hash = &self.unsigned_create_token.hash().await;
let signature = &self.signature;
// Token-creation transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
client
.execute(
r#"
INSERT INTO token (
time,
fee,
creator,
number,
hard_limit,
ticker,
hash,
signature,
original
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
"#,
&[
&time,
&fee,
creator,
&number,
&hard_limit,
ticker,
hash,
signature,
&original_data,
],
)
.await?;
Ok(())
}
}

267
src/blocks/transfer.rs Normal file
View File

@ -0,0 +1,267 @@
use crate::common::binary_conversions::binary_to_string;
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
use anyhow::{anyhow, Result};
#[derive(Debug, Serialize, Clone)] // 84 bytes
pub struct UnsignedTransferTransaction {
pub txtype: u8, // 1 byte transaction type, should be 2
pub time: u32, // 4 bytes transaction timestamp
pub value: u64, // 8 bytes number of coins or tokens to send
pub coin: String, // 15 bytes base coin, token ticker, or NFT name, padded with spaces
pub nft_series: u32, // 4 bytes 0 for coins/tokens/1-of-1 NFTs, otherwise NFT series item
pub sender: String, // 22 bytes sender short address
pub receiver: String, // 22 bytes receiver short address
pub txfee: u64, // 8 bytes transaction fee
}
#[derive(Debug, Serialize, Clone)] // 750 bytes
pub struct TransferTransaction {
pub unsigned_transfer: UnsignedTransferTransaction, // 84 bytes
pub signature: String, // 666 bytes signature
}
impl TransferTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + 8 + 15 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedTransferTransaction {
// Create an unsigned transfer transaction.
#[allow(clippy::too_many_arguments)]
pub async fn new(
txtype: u8,
time: u32,
value: u64,
coin: &str,
nft_series: u32,
sender: &str,
receiver: &str,
txfee: u64,
) -> Self {
Self {
txtype,
time,
value,
coin: coin.to_string(),
nft_series,
sender: sender.to_string(),
receiver: receiver.to_string(),
txfee,
}
}
// Hash without signing for verification.
pub async fn hash(&self) -> String {
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
// Hash the unsigned transaction and sign it.
pub async fn hash_and_sign(
&self,
private_key: &str,
) -> Result<String, Box<dyn std::error::Error>> {
// Serialize the unsigned transaction before hashing.
let serialized = to_string(self)?;
// Hash the serialized unsigned payload.
let hash = skein_256_hash_data(&serialized);
// Sign the transaction hash with the sender wallet.
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok(signature)
}
}
impl TransferTransaction {
// Create a signed transfer transaction.
pub async fn new(
unsigned_transfer: UnsignedTransferTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
// Hash and sign the unsigned transaction.
let signature = unsigned_transfer.hash_and_sign(private_key).await?;
// Return the complete transfer transaction.
Ok(Self {
unsigned_transfer,
signature,
})
}
// Load an existing transfer transaction.
pub async fn load(unsigned_transfer: UnsignedTransferTransaction, signature: &str) -> Self {
Self {
unsigned_transfer,
signature: signature.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed transfer transaction byte layout.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_transfer.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_transfer.time.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_transfer.value.to_le_bytes())
.await?;
cursor
.write_all(self.unsigned_transfer.coin.as_bytes())
.await?;
cursor
.write_all(&self.unsigned_transfer.nft_series.to_le_bytes())
.await?;
let sender_bytes = Wallet::short_address_to_bytes(&self.unsigned_transfer.sender)
.ok_or_else(|| {
tokio::io::Error::other("Invalid sender short address")
})?;
let receiver_bytes = Wallet::short_address_to_bytes(&self.unsigned_transfer.receiver)
.ok_or_else(|| {
tokio::io::Error::other("Invalid receiver short address",
)
})?;
cursor.write_all(&sender_bytes).await?;
cursor.write_all(&receiver_bytes).await?;
cursor
.write_all(&self.unsigned_transfer.txfee.to_le_bytes())
.await?;
cursor.write_all(&decode(&self.signature).unwrap()).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count",
));
}
// Read the remaining fixed-width transfer bytes.
let mut cursor = Cursor::new(bytes);
// Decode timestamp and transfer amount.
let time = cursor.read_u32_le().await?;
let value = cursor.read_u64_le().await?;
// Decode the fixed 15-byte coin/token/NFT field.
let mut coin_bytes = vec![0; 15];
cursor.read_exact(&mut coin_bytes).await?;
let coin = binary_to_string(coin_bytes);
let nft_series = cursor.read_u32_le().await?;
// Decode the sender short address.
let mut sender_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut sender_bytes).await?;
let sender = Wallet::bytes_to_short_address(&sender_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid sender short address bytes",
)
})?;
// Decode the receiver short address.
let mut receiver_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut receiver_bytes).await?;
let receiver = Wallet::bytes_to_short_address(&receiver_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid receiver short address bytes",
)
})?;
// Decode fee and signature.
let txfee = cursor.read_u64_le().await?;
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature_bytes).await?;
let signature = encode(&signature_bytes);
// Rebuild the unsigned transfer payload.
let unsigned_transfer = UnsignedTransferTransaction {
txtype,
time,
value,
coin,
nft_series,
sender,
receiver,
txfee,
};
// Wrap the rebuilt unsigned payload and signature.
Ok(TransferTransaction {
unsigned_transfer,
signature,
})
}
pub async fn add_to_memory(&self) -> Result<()> {
// Store original bytes so the mempool can rebuild the exact
// transaction submitted by the wallet.
let original_data = self
.to_bytes()
.await
.map_err(|_| anyhow!("Failed to serialize transaction"))?;
// PostgreSQL uses signed integer types, so reject values that cannot
// fit instead of wrapping them into negative mempool amounts.
let fee = i64::try_from(self.unsigned_transfer.txfee)
.map_err(|_| std::io::Error::other("Transfer fee exceeds i64 mempool limit"))?;
let time = self.unsigned_transfer.time as i32;
let value = i64::try_from(self.unsigned_transfer.value)
.map_err(|_| std::io::Error::other("Transfer value exceeds i64 mempool limit"))?;
let nft_series = self.unsigned_transfer.nft_series as i32;
let sender = &self.unsigned_transfer.sender;
let coin = &self.unsigned_transfer.coin;
let receiver = &self.unsigned_transfer.receiver;
let hash = &self.unsigned_transfer.hash().await;
let signature = &self.signature;
// Transfer transactions remain in the mempool table until mined or removed.
let client = DB.get().expect("DB not initialized");
client
.execute(
r#"
INSERT INTO transfer (
time,
fee,
sender,
value,
coin,
nft_series,
receiver,
hash,
signature,
original
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
"#,
&[
&time,
&fee,
sender,
&value,
coin,
&nft_series,
receiver,
&hash,
signature,
&original_data,
],
)
.await?;
Ok(())
}
}

216
src/blocks/vanity.rs Normal file
View File

@ -0,0 +1,216 @@
use crate::common::skein::skein_256_hash_data;
use crate::records::memory::mempool::DB;
use crate::to_string;
use crate::wallets::structures::Wallet;
use crate::Cursor;
use crate::Serialize;
use crate::{decode, encode};
use crate::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Clone)] // 57 bytes
pub struct UnsignedVanityAddressTransaction {
pub txtype: u8, // 1 byte transaction type, should be 12
pub timestamp: u32, // 4 bytes transaction timestamp
pub address: String, // 22 bytes real short address receiving the vanity mapping
pub vanity_address: String, // 22 bytes vanity short address being registered
pub txfee: u64, // 8 bytes fee paid for vanity registration
}
#[derive(Debug, Serialize, Clone)] // 723 bytes
pub struct VanityAddressTransaction {
pub unsigned_vanity_address: UnsignedVanityAddressTransaction, // 57 bytes
pub signature: String, // 666 bytes
}
impl VanityAddressTransaction {
pub const BYTE_LENGTH: usize =
1 + 4 + Wallet::SHORT_ADDRESS_BYTES_LENGTH + Wallet::SHORT_ADDRESS_BYTES_LENGTH + 8 + Wallet::SIGNATURE_LENGTH;
}
impl UnsignedVanityAddressTransaction {
// Create the unsigned vanity-address registration payload.
pub async fn new(
txtype: u8,
timestamp: u32,
address: &str,
vanity_address: &str,
txfee: u64,
) -> Self {
Self {
txtype,
timestamp,
address: address.to_string(),
vanity_address: vanity_address.to_string(),
txfee,
}
}
pub async fn hash(&self) -> String {
// Hash the unsigned payload for verification and mempool indexing.
let serialized = to_string(self).unwrap();
skein_256_hash_data(&serialized)
}
pub async fn hash_and_sign(
&self,
private_key: &str,
) -> Result<String, Box<dyn std::error::Error>> {
// Sign the unsigned vanity transaction with the registering wallet.
let serialized = to_string(self)?;
let hash = skein_256_hash_data(&serialized);
let signature = Wallet::sign_transaction(&hash, private_key).await;
Ok(signature)
}
}
impl VanityAddressTransaction {
pub async fn new(
unsigned_vanity_address: UnsignedVanityAddressTransaction,
private_key: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
// Create a signed vanity-address transaction.
let signature = unsigned_vanity_address.hash_and_sign(private_key).await?;
Ok(Self {
unsigned_vanity_address,
signature,
})
}
pub async fn load(
unsigned_vanity_address: UnsignedVanityAddressTransaction,
signature: &str,
) -> Self {
// Rebuild a vanity transaction already read from storage.
Self {
unsigned_vanity_address,
signature: signature.to_string(),
}
}
pub async fn to_bytes(&self) -> tokio::io::Result<Vec<u8>> {
// Serialize into the fixed vanity transaction byte layout.
let mut buffer = Vec::with_capacity(Self::BYTE_LENGTH);
let mut cursor = Cursor::new(&mut buffer);
cursor
.write_all(&self.unsigned_vanity_address.txtype.to_le_bytes())
.await?;
cursor
.write_all(&self.unsigned_vanity_address.timestamp.to_le_bytes())
.await?;
let address_bytes = Wallet::short_address_to_bytes(&self.unsigned_vanity_address.address)
.ok_or_else(|| {
tokio::io::Error::other("Invalid sender short address")
})?;
// Vanity addresses use the same 22-byte width as short addresses
// but are encoded through the vanity-specific byte conversion.
let vanity_bytes =
Wallet::vanity_address_to_bytes(&self.unsigned_vanity_address.vanity_address)
.ok_or_else(|| tokio::io::Error::other("Invalid vanity short address"))?;
cursor.write_all(&address_bytes).await?;
cursor.write_all(&vanity_bytes).await?;
cursor
.write_all(&self.unsigned_vanity_address.txfee.to_le_bytes())
.await?;
let signature_bytes = decode(&self.signature).map_err(|err| {
tokio::io::Error::other(format!("Invalid vanity signature hex: {err}"))
})?;
cursor.write_all(&signature_bytes).await?;
Ok(buffer)
}
pub async fn from_bytes(txtype: u8, bytes: &[u8]) -> tokio::io::Result<Self> {
// The block parser already consumed the transaction type byte.
if bytes.len() != Self::BYTE_LENGTH - 1 {
return Err(tokio::io::Error::other("Invalid Byte Count"));
}
let mut cursor = Cursor::new(bytes);
// Decode timestamp, real short address, vanity address, fee, and signature.
let timestamp = cursor.read_u32_le().await?;
let mut address_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut address_bytes).await?;
let address = Wallet::bytes_to_short_address(&address_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid sender short address bytes")
})?;
let mut vanity_bytes = vec![0; Wallet::SHORT_ADDRESS_BYTES_LENGTH];
cursor.read_exact(&mut vanity_bytes).await?;
let vanity_address = Wallet::bytes_to_vanity_address(&vanity_bytes).ok_or_else(|| {
tokio::io::Error::other("Invalid vanity short address bytes")
})?;
let txfee = cursor.read_u64_le().await?;
let mut signature_bytes = vec![0; Wallet::SIGNATURE_LENGTH];
cursor.read_exact(&mut signature_bytes).await?;
let signature = encode(&signature_bytes);
let unsigned_vanity_address = UnsignedVanityAddressTransaction {
txtype,
timestamp,
address,
vanity_address,
txfee,
};
Ok(Self {
unsigned_vanity_address,
signature,
})
}
pub async fn add_to_memory(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Store the original bytes so the mempool can later rebuild the
// exact transaction submitted by the wallet.
let original_data = self.to_bytes().await?;
// PostgreSQL uses signed integer types for these persisted fields.
let time = self.unsigned_vanity_address.timestamp as i32;
let fee = i64::try_from(self.unsigned_vanity_address.txfee)
.map_err(|_| std::io::Error::other("Vanity fee exceeds i64 mempool limit"))?;
let address = &self.unsigned_vanity_address.address;
let vanity_address = &self.unsigned_vanity_address.vanity_address;
let hash = &self.unsigned_vanity_address.hash().await;
let signature = &self.signature;
// Vanity transactions are written to the vanity mempool table
// until the transaction is mined or removed.
let client = DB.get().ok_or_else(|| {
Box::new(std::io::Error::other("DB not initialized"))
as Box<dyn std::error::Error + Send + Sync>
})?;
client
.execute(
r#"
INSERT INTO vanity_address (
time,
fee,
address,
vanity_address,
hash,
signature,
original
) VALUES ($1, $2, $3, $4, $5, $6, $7)
"#,
&[
&time,
&fee,
address,
vanity_address,
hash,
signature,
&original_data,
],
)
.await?;
Ok(())
}
}

View File

@ -0,0 +1,136 @@
use crate::IpAddr;
use crate::Ipv6Addr;
// Convert a 256-bit hex hash into the reduced u64 value used by
// the mining difficulty comparison.
pub async fn hex_to_u64(hex_string: &str) -> Result<u64, String> {
// The input must be one full 32-byte hash encoded as 64 hex characters.
if hex_string.len() != 64 {
return Err("Invalid hash length. Must be 64 characters.".to_string());
}
let mut extracted_string = String::new();
// Sample across the whole hash instead of using only the front bytes.
for i in (0..64).step_by(4) {
extracted_string.push(hex_string.chars().nth(i).unwrap());
}
// Interpret the sampled hex characters as the mining comparison value.
if let Ok(value) = u64::from_str_radix(&extracted_string, 16) {
Ok(value)
} else {
Err("Failed to convert to u64.".to_string())
}
}
// Convert raw bytes into UTF-8 text for command payloads that are
// expected to be plain strings.
pub fn binary_to_string(binary_data: Vec<u8>) -> String {
match String::from_utf8(binary_data.clone()) {
Ok(s) => s,
Err(_) => {
"Invalid UTF-8 Data".to_string()
}
}
}
pub fn ip_to_binary(ip_str: &str) -> Vec<u8> {
// Parse the IP string into the standard IP enum before encoding it.
match ip_str.parse::<IpAddr>() {
Ok(IpAddr::V4(ipv4_addr)) => {
// Store IPv4 addresses in the same 16-byte field by
// prefixing the 4-byte address with twelve zero bytes.
let mut bytes = vec![0u8; 12];
bytes.extend_from_slice(&ipv4_addr.octets());
bytes
}
Ok(IpAddr::V6(ipv6_addr)) => {
// IPv6 already fits the fixed 16-byte network layout.
ipv6_addr.octets().to_vec()
}
Err(_) => {
// Invalid IP strings are rejected by returning no bytes.
Vec::new()
}
}
}
// Convert the fixed 16-byte network IP layout back into visible text.
pub fn binary_to_ip(bytes: Vec<u8>) -> String {
if bytes.len() != 16 {
return "Invalid input length".to_string();
}
// Twelve leading zero bytes identify an IPv4 address stored in
// the final four bytes of the fixed-width field.
if bytes[0..12].iter().all(|&b| b == 0) {
let ipv4_addr = (u32::from(bytes[12]) << 24
| u32::from(bytes[13]) << 16
| u32::from(bytes[14]) << 8
| u32::from(bytes[15]))
.into();
format!("{}", IpAddr::V4(ipv4_addr))
} else {
// Non-IPv4 layouts are decoded as a full IPv6 address.
let mut ipv6_bytes = [0u8; 16];
ipv6_bytes.copy_from_slice(&bytes);
format!("{}", IpAddr::V6(Ipv6Addr::from(ipv6_bytes)))
}
}
// Convert the fixed 18-byte network endpoint layout into ip:port text.
pub fn binary_to_ip_port(bytes: &[u8]) -> String {
if bytes.len() != 18 {
return String::from("Error: Invalid binary length");
}
// The first 16 bytes are the IP address and the final two are the port.
let ip_bytes = &bytes[..16];
let port_bytes = &bytes[16..];
let ip = binary_to_ip(ip_bytes.to_vec());
let port_bytes: [u8; 2] = match port_bytes.try_into() {
Ok(bytes) => bytes,
Err(_) => {
return String::from("Error: Invalid port_bytes length");
}
};
let port = u16::from_le_bytes(port_bytes);
format!("{ip}:{port}")
}
// Convert ip:port text into the fixed 18-byte endpoint layout used on the wire.
pub fn ip_port_to_binary(ip_port: &str) -> Result<Vec<u8>, String> {
// Split from the right so IPv6 addresses with colons still parse correctly.
let (ip_part, port_part) = ip_port
.rsplit_once(':')
.ok_or_else(|| String::from("Invalid format"))?;
// Accept bracketed IPv6 endpoint strings such as [::1]:50000.
let normalized_ip = ip_part
.strip_prefix('[')
.and_then(|ip| ip.strip_suffix(']'))
.unwrap_or(ip_part);
let ip: IpAddr = normalized_ip
.parse()
.map_err(|_| String::from("Invalid IP address"))?;
let port: u16 = port_part
.parse()
.map_err(|_| String::from("Invalid port"))?;
// Store IPv4 and IPv6 in the same fixed-width 16-byte address slot.
let ip_bytes = match ip {
IpAddr::V4(ipv4) => {
let mut ipv6_bytes = [0u8; 16];
ipv6_bytes[12..].copy_from_slice(&ipv4.octets());
ipv6_bytes.to_vec()
}
IpAddr::V6(ipv6) => ipv6.octets().to_vec(),
};
// Ports are little-endian because the rest of the binary protocol
// stores integer fields in little-endian order.
let mut result = ip_bytes;
result.extend_from_slice(&port.to_le_bytes());
Ok(result)
}

View File

@ -0,0 +1,24 @@
use crate::common::network_paths_and_settings::block_extension_and_paths;
use crate::metadata;
use crate::PathBuf;
// Check whether the local chain already has the active network's genesis block.
pub async fn genesis_checkup() -> bool {
// Resolve the active network suffix and block directory from the
// shared path helper so mainnet/testnet never duplicate path logic.
let (
_network_name,
_padded_base_coin,
suffix,
_torrent_path,
_wallet_path,
block_path,
_db_path,
_balance_path,
_log_path,
) = block_extension_and_paths();
// Genesis is always block zero using the current network's block suffix.
let genesis_location = PathBuf::from(block_path).join(format!("0.{suffix}"));
(metadata(genesis_location).await).is_ok()
}

90
src/common/cli_prompts.rs Normal file
View File

@ -0,0 +1,90 @@
use crate::read_password;
use crate::stdout;
use crate::AsyncWriteExt;
use tokio::io::{stdin, AsyncBufReadExt, BufReader};
pub async fn prompt_visible_with_default(prompt: &str, default: &str) -> String {
// Show the default in brackets so pressing enter keeps the existing value.
let full_prompt = format!("{prompt} [{default}]: ");
let value = prompt_visible(&full_prompt).await;
if value.is_empty() {
default.to_string()
} else {
value
}
}
pub async fn prompt_visible(prompt: &str) -> String {
// Write the prompt manually so async bin tools can share the same
// visible-input behavior without repeating stdout setup.
let mut stdout = stdout();
stdout
.write_all(prompt.as_bytes())
.await
.expect("Failed to write to stdout");
stdout.flush().await.expect("Failed to flush stdout");
// Read one line from stdin and trim shell newline characters.
let mut input = String::new();
let mut reader = BufReader::new(stdin());
reader
.read_line(&mut input)
.await
.expect("Failed to read visible input");
input.trim().to_string()
}
pub async fn ask_yes_no_question(question: &str) -> bool {
// Keep asking until the user gives a recognizable yes/no answer.
loop {
let answer = prompt_visible(&format!("{question} (yes/no): ")).await;
match answer.to_lowercase().as_str() {
"yes" | "y" => return true,
"no" | "n" => return false,
_ => println!("Invalid input, please enter 'yes' or 'no'"),
}
}
}
pub async fn cli_options() -> String {
// Startup never accepts the wallet key as a positional CLI argument, so
// config overrides like --config still fall through to the same hidden prompt.
prompt_hidden_nonempty(
"Please enter your wallet key. If you do not have a wallet yet, enter a new key here: ",
"Wallet key cannot be empty. Please try again.",
)
.await
}
async fn prompt_hidden(prompt: &str) -> String {
// Hidden prompts are used for secrets such as wallet keys and
// database passwords.
let mut stdout = stdout();
stdout
.write_all(prompt.as_bytes())
.await
.expect("Failed to write to stdout");
stdout.flush().await.expect("Failed to flush stdout");
let value = read_password().expect("Failed to read hidden input");
// read_password does not echo the typed value, so print a newline
// before returning control to the terminal.
println!();
value.trim().to_string()
}
pub async fn prompt_hidden_nonempty(prompt: &str, retry_message: &str) -> String {
// Secret values are required anywhere this helper is used, so empty
// input loops until the caller receives a real value.
loop {
let value = prompt_hidden(prompt).await;
if !value.is_empty() {
return value;
}
println!("{retry_message}\n");
}
}

8
src/common/mod.rs Normal file
View File

@ -0,0 +1,8 @@
pub mod binary_conversions;
pub mod check_genesis;
pub mod cli_prompts;
pub mod network_paths_and_settings;
pub mod network_startup;
pub mod nft_assets;
pub mod skein;
pub mod types;

View File

@ -0,0 +1,130 @@
use crate::PathBuf;
use crate::Settings;
// Return the active network name, padded base coin, file suffix, and
// runtime paths. Every returned path is scoped by the active network
// so mainnet and testnet keep separate runtime data.
pub fn block_extension_and_paths() -> (
&'static str,
String,
String,
String,
String,
String,
String,
String,
String,
) {
#[cfg(feature = "mainnet")]
{
// Mainnet files use the clc suffix and mainnet subdirectories.
(
network_name(),
base_coin(),
"clc".to_string(),
torrent_root_path(),
wallet_root_path(),
block_root_path(),
db_root_path(),
balance_sheet_root_path(),
log_root_path(),
)
}
#[cfg(feature = "testnet")]
{
// Testnet files use the cltc suffix and testnet subdirectories.
(
network_name(),
base_coin(),
"cltc".to_string(),
torrent_root_path(),
wallet_root_path(),
block_root_path(),
db_root_path(),
balance_sheet_root_path(),
log_root_path(),
)
}
}
// Return the base coin ticker padded to the fixed 15-byte asset field.
fn base_coin() -> String {
#[cfg(feature = "mainnet")]
{
"CLC ".to_string()
}
#[cfg(feature = "testnet")]
{
"CLTC ".to_string()
}
}
// Return the network name selected by the active compile feature.
fn network_name() -> &'static str {
#[cfg(feature = "mainnet")]
{
"mainnet"
}
#[cfg(feature = "testnet")]
{
"testnet"
}
}
fn block_root_path() -> String {
// Blocks are stored beneath the configured block root and active network.
let settings = Settings::load().expect("Failed to load settings");
PathBuf::from(settings.block_path)
.join(network_name())
.to_string_lossy()
.into_owned()
}
fn torrent_root_path() -> String {
// Torrent metadata is stored beneath the configured torrent root and active network.
let settings = Settings::load().expect("Failed to load settings");
PathBuf::from(settings.torrent_path)
.join(network_name())
.to_string_lossy()
.into_owned()
}
fn wallet_root_path() -> String {
// The wallet path points at the configured wallet filename inside
// the active network's wallet directory.
let settings = Settings::load().expect("Failed to load settings");
PathBuf::from(settings.wallet_path)
.join(network_name())
.join(settings.wallet_name)
.to_string_lossy()
.into_owned()
}
fn db_root_path() -> String {
// The internal database used for non-mempool data storage.
let settings = Settings::load().expect("Failed to load settings");
PathBuf::from(settings.db_path)
.join(network_name())
.to_string_lossy()
.into_owned()
}
fn balance_sheet_root_path() -> String {
// The balance root is the configured balance-sheet directory scoped
// to the active network name.
let settings = Settings::load().expect("Failed to load settings");
PathBuf::from(settings.balance_sheet)
.join(network_name())
.to_string_lossy()
.into_owned()
}
fn log_root_path() -> String {
// Logs are scoped by network so testnet and mainnet nodes can run
// side by side without writing to the same rotating log files.
let settings = Settings::load().expect("Failed to load settings");
PathBuf::from(settings.log_path)
.join(network_name())
.to_string_lossy()
.into_owned()
}

View File

@ -0,0 +1,165 @@
use crate::Settings;
use ipnetwork::IpNetwork;
use std::net::{IpAddr, SocketAddr};
use tokio::net::lookup_host;
pub fn is_public_network_address(ip_or_endpoint: &str) -> bool {
// These ranges cannot be used as advertised network addresses
// because remote peers cannot route to them over the public network.
let private_ranges: Vec<IpNetwork> = vec![
"127.0.0.0/8".parse().unwrap(),
"10.0.0.0/8".parse().unwrap(),
"172.16.0.0/12".parse().unwrap(),
"192.168.0.0/16".parse().unwrap(),
"100.64.0.0/10".parse().unwrap(),
"fd00::/8".parse().unwrap(),
"fe80::/10".parse().unwrap(),
];
// Accept either a bare IP or an ip:port endpoint. Bare IPv6 addresses
// must be tested before splitting on the final port separator.
let parsed_ip = match ip_or_endpoint.parse::<IpAddr>() {
Ok(ip) => ip,
Err(_) => {
let host = ip_or_endpoint
.strip_prefix('[')
.and_then(|value| value.split_once(']').map(|(ip, _)| ip))
.or_else(|| ip_or_endpoint.rsplit_once(':').map(|(ip, _)| ip))
.unwrap_or(ip_or_endpoint);
let Ok(ip) = host.parse::<IpAddr>() else {
return false;
};
ip
}
};
!private_ranges
.iter()
.any(|range: &IpNetwork| range.contains(parsed_ip))
}
pub async fn get_connections() -> Vec<String> {
// If settings cannot be loaded, startup simply has no piggyback peers.
let settings = match Settings::load() {
Ok(settings) => settings,
Err(_) => return Vec::new(),
};
let default_port = active_rpc_port(&settings);
let connection_limit = settings.piggybacks.len();
let mut resolved_connections = Vec::new();
for server in settings.piggybacks {
if let Some(endpoint) = resolve_bootstrap_peer(&server, &default_port).await {
resolved_connections.push(endpoint);
}
if resolved_connections.len() >= connection_limit {
break;
}
}
resolved_connections
}
fn split_bootstrap_peer(server: &str, default_port: &str) -> Option<(String, u16)> {
let server = server.trim();
if server.is_empty() {
return None;
}
let default_port = default_port.parse::<u16>().ok()?;
if let Ok(ip) = server.parse::<IpAddr>() {
return Some((ip.to_string(), default_port));
}
if let Ok(socket) = server.parse::<SocketAddr>() {
return Some((socket.ip().to_string(), socket.port()));
}
if let Some(host_with_suffix) = server.strip_prefix('[') {
let (host, suffix) = host_with_suffix.split_once(']')?;
let port = suffix
.strip_prefix(':')
.and_then(|value| value.parse::<u16>().ok())
.unwrap_or(default_port);
return Some((host.to_string(), port));
}
if let Some((host, port)) = server.rsplit_once(':') {
if !host.contains(':') {
let port = port.parse::<u16>().ok()?;
return Some((host.to_string(), port));
}
}
Some((server.to_string(), default_port))
}
async fn resolve_bootstrap_peer(server: &str, default_port: &str) -> Option<String> {
let (host, port) = split_bootstrap_peer(server, default_port)?;
if let Ok(ip) = host.parse::<IpAddr>() {
if is_public_network_address(&ip.to_string()) {
return Some(SocketAddr::new(ip, port).to_string());
}
return None;
}
// Hostnames are allowed only here, at the configured bootstrap boundary.
// The resolved endpoint handed to the rest of the protocol is still an IP.
let resolved = lookup_host((host.as_str(), port)).await.ok()?;
for socket in resolved {
if is_public_network_address(&socket.ip().to_string()) {
return Some(socket.to_string());
}
}
None
}
fn active_rpc_port(settings: &Settings) -> String {
// Mainnet and testnet listen on separate configured RPC ports.
#[cfg(feature = "mainnet")]
{
settings.rpc_port.clone()
}
#[cfg(feature = "testnet")]
{
settings.testnet_rpc_port.clone()
}
}
// Return the public node IP, active-network RPC port, and public endpoint.
pub async fn get_ip_and_port() -> (String, String, String) {
let settings = Settings::load().expect("Failed to load settings");
// PUBLIC_IP is the protocol identity announced to peers.
let ip = settings.public_ip.clone();
let port = active_rpc_port(&settings);
// Many handshake and network-map paths need the public ip:port
// string as one value.
let combined_ip_port = format!("{ip}:{port}");
(ip, port, combined_ip_port)
}
pub async fn get_listen_socket() -> String {
let settings = Settings::load().expect("Failed to load settings");
// LISTEN_IP is local-only bind configuration and must never be
// broadcast as the node's public network identity.
let port = active_rpc_port(&settings);
format!("{}:{}", settings.listen_ip, port)
}
pub async fn get_listen_ip() -> String {
let settings = Settings::load().expect("Failed to load settings");
// Outbound sockets may bind to this only when it is a concrete
// local interface address. Wildcard listeners leave outbound source
// selection to the operating system and NAT/router.
settings.listen_ip
}

26
src/common/nft_assets.rs Normal file
View File

@ -0,0 +1,26 @@
// Build the visible NFT asset name, adding the series suffix only
// for numbered series entries.
pub fn nft_asset_name(name: &str, nft_series: u32) -> String {
if nft_series == 0 {
name.to_string()
} else {
format!("{}_{}", name.trim_end(), nft_series)
}
}
// Split a visible NFT asset name back into its fixed-width base
// name and numeric series value.
pub fn nft_asset_parts(asset_name: &str) -> (String, u32) {
// A trailing _number is treated as the series suffix.
if let Some((name, series)) = asset_name.rsplit_once('_') {
if !name.is_empty() && !series.is_empty() && series.chars().all(|c| c.is_ascii_digit()) {
if let Ok(series_number) = series.parse::<u32>() {
// Asset names stored in transactions use the fixed 15-byte field.
return (format!("{name:<15}"), series_number);
}
}
}
// Non-series assets keep their original name and use series zero.
(asset_name.to_string(), 0)
}

56
src/common/skein.rs Normal file
View File

@ -0,0 +1,56 @@
use crate::encode;
use crate::Digest;
use crate::Output;
use crate::Ripemd160;
use crate::Skein256;
use crate::Skein512;
use crate::ripemd::Digest as RipemdDigest;
pub fn skein_128_hash_bytes(data: &[u8]) -> String {
// Contractless 128-bit hashes are Skein256 hashes reduced to 16 bytes.
let mut hasher = Skein256::new();
hasher.update(data);
let result: Output<Skein256> = hasher.finalize();
let reduced_result: Vec<u8> = result.iter().step_by(2).copied().collect();
encode(reduced_result)
}
pub fn skein_128_hash_data(data: &str) -> String {
// Text hashing uses the UTF-8 bytes of the string directly.
let mut hasher = Skein256::new();
hasher.update(data.as_bytes());
let result: Output<Skein256> = hasher.finalize();
let reduced_result: Vec<u8> = result.iter().step_by(2).copied().collect();
encode(reduced_result)
}
pub fn skein_256_hash_bytes(data: &[u8]) -> String {
// Skein256 returns the full 32-byte hash as hex.
let mut hasher = Skein256::new();
hasher.update(data);
let result: Output<Skein256> = hasher.finalize();
encode(result)
}
pub fn skein_256_hash_data(data: &str) -> String {
// Text hashing uses the UTF-8 bytes of the string directly.
let mut hasher = Skein256::new();
hasher.update(data.as_bytes());
let result: Output<Skein256> = hasher.finalize();
encode(result)
}
pub fn skein_512_hash_data(data: &str) -> String {
// Text hashing uses the UTF-8 bytes of the string directly.
let mut hasher = Skein512::new();
hasher.update(data.as_bytes());
let result: Output<Skein512> = hasher.finalize();
encode(result)
}
pub fn ripemd160_hash_bytes(data: &[u8]) -> Vec<u8> {
// RIPEMD-160 is used after Skein256 when deriving short-address payloads.
let mut hasher = Ripemd160::new();
hasher.update(data);
hasher.finalize().to_vec()
}

95
src/common/types.rs Normal file
View File

@ -0,0 +1,95 @@
use crate::blocks::burn::BurnTransaction;
use crate::blocks::collateral::CollateralClaimTransaction;
use crate::blocks::genesis::GenesisTransaction;
use crate::blocks::issue_token::IssueTokenTransaction;
use crate::blocks::loan_payment::ContractPaymentTransaction;
use crate::blocks::loans::LoanContractTransaction;
use crate::blocks::marketing::MarketingTransaction;
use crate::blocks::nft::CreateNftTransaction;
use crate::blocks::rewards::RewardsTransaction;
use crate::blocks::swap::SwapTransaction;
use crate::blocks::token::CreateTokenTransaction;
use crate::blocks::transfer::TransferTransaction;
use crate::blocks::vanity::VanityAddressTransaction;
use crate::Serialize;
// Transaction type bytes are the canonical IDs used in blocks, mempool
// routing, and verification dispatch.
pub const GENESIS_TYPE: u8 = 0;
pub const REWARDS_TYPE: u8 = 1;
pub const TRANSFER_TYPE: u8 = 2;
pub const CREATE_TOKEN_TYPE: u8 = 3;
pub const CREATE_NFT_TYPE: u8 = 4;
pub const MARKETING_TYPE: u8 = 5;
pub const SWAP_TYPE: u8 = 6;
pub const LENDER_TYPE: u8 = 7;
pub const BORROWER_TYPE: u8 = 8;
pub const COLLATERAL_TYPE: u8 = 9;
pub const BURN_TYPE: u8 = 10;
pub const ISSUE_TOKEN_TYPE: u8 = 11;
pub const VANITY_ADDRESS_TYPE: u8 = 12;
// Coin and asset names are stored in fixed 15-byte fields.
pub const COIN_LENGTH: usize = 15;
// The fixed parent hash used only by the genesis block.
pub const GENESIS_BLOCK_HASH: &str =
"03c8e5f02c97a4f4a270dbc13ffe2e3f782f0013674b9b5e960124b89e581f38";
// The built-in genesis network entry uses localhost.
pub const GENESIS_IP: &str = "127.0.0.1";
// Reward halvings are based on this fixed block interval.
pub const BLOCKS_PER_HALVING: u32 = 9600000;
// Decimal display value for the base transfer fee percentage.
pub const TRANSFER_FEE: f64 = 0.01;
// Integer numerator and denominator used for fee math.
// example: 1 / 100 = 1%; 25/1000 = 2.5%
pub const BASE_TRANSFER_FEE_NUMERATOR: u64 = 1;
pub const BASE_TRANSFER_FEE_DENOMINATOR: u64 = 100;
// Fixed transaction fees are stored in the smallest coin unit.
pub const NON_BASE_TRANSFER_MIN_FEE: u64 = 100000000; // 1 CLC
pub const CREATE_TOKEN_FEE: u64 = 50000000000; // cost to create tokens, 500 CLC
pub const CREATE_NFT_FEE: u64 = 50000000; // cost to creat nfts, 0.5 CLC
pub const MARKETING_FEE: u64 = 100000000; // cost to add marketing record, 1 CLC
pub const SWAP_FEE: u64 = 100000000; // cost for a token swap each party must pay, 1 CLC
pub const LENDER_FEE: u64 = 300000000; // cost to create a loan, 3 CLC
pub const BORROWER_FEE: u64 = 1000000; // cost to make loan payment, 0.01 CLC
pub const COLLATERAL_FEE: u64 = 300000000; // cost to clain collateral, 3 CLC
pub const BURN_FEE: u64 = 10000; // cost to burn a token or NFT, 0.0001 CLC
pub const ISSUE_TOKEN_FEE: u64 = 10000000000; // cost to issue more of an existing token, 100 CLC
pub const VANITY_ADDRESS_FEE: u64 = 500000000; // cost to register or update a vanity address, 5 CLC
pub fn minimum_transfer_fee(value: u64, is_base_coin: bool) -> u64 {
// Base-coin transfers pay the percentage fee using integer math.
if is_base_coin {
value
.saturating_mul(BASE_TRANSFER_FEE_NUMERATOR)
.div_ceil(BASE_TRANSFER_FEE_DENOMINATOR)
} else {
// Token and NFT transfers use the fixed non-base minimum fee.
NON_BASE_TRANSFER_MIN_FEE
}
}
// Transaction groups every concrete block transaction type so block
// verification and save paths can dispatch through one enum.
#[derive(Debug, Serialize, Clone)]
pub enum Transaction {
Genesis(GenesisTransaction),
Rewards(RewardsTransaction),
Transfer(TransferTransaction),
Burn(BurnTransaction),
Token(CreateTokenTransaction),
IssueToken(IssueTokenTransaction),
Nft(CreateNftTransaction),
Marketing(MarketingTransaction),
Swap(SwapTransaction),
Lender(LoanContractTransaction),
Borrower(ContractPaymentTransaction),
Collateral(CollateralClaimTransaction),
Vanity(VanityAddressTransaction),
}

210
src/config.rs Normal file
View File

@ -0,0 +1,210 @@
use crate::env;
use crate::tilde;
use crate::Ini;
use crate::Path;
use crate::PathBuf;
lazy_static::lazy_static! {
pub static ref SETTINGS: Settings = Settings::load().expect("Failed to load settings");
}
#[derive(Debug)]
pub struct Settings {
pub block_path: String,
pub torrent_path: String,
pub db_path: String,
pub wallet_path: String,
pub wallet_name: String,
pub balance_sheet: String,
pub log_path: String,
pub log_level: String,
pub public_ip: String,
pub listen_ip: String,
pub rpc_port: String,
pub testnet_rpc_port: String,
pub outgoing_connections: u8,
pub incoming_connections: u8,
pub threads: u16,
pub piggybacks: Vec<String>,
pub pg_host: String,
pub pg_port: u16,
pub pg_user: String,
pub pg_password: Option<String>,
pub pg_dbname: String,
}
impl Settings {
fn expand(path: &str, base_dir: &Path) -> String {
let expanded = PathBuf::from(tilde(path).as_ref());
if expanded.is_relative() {
base_dir.join(expanded).to_string_lossy().into_owned()
} else {
expanded.to_string_lossy().into_owned()
}
}
fn get_config_path() -> PathBuf {
// Priority: CLI arg > ENV var > ./settings.ini > executable directory > platform fallback
let mut args = env::args().skip(1); // Skip the executable name
while let Some(arg) = args.next() {
if arg == "--config" {
if let Some(path) = args.next() {
return PathBuf::from(path);
} else {
eprintln!("Error: --config flag requires a path");
std::process::exit(1);
}
}
}
if let Ok(path) = env::var("SETTINGS_PATH") {
return PathBuf::from(path);
}
let local_settings = env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("settings.ini");
if local_settings.exists() {
return local_settings;
}
if let Ok(exe_path) = env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
let exe_settings = exe_dir.join("settings.ini");
if exe_settings.exists() {
return exe_settings;
}
}
}
#[cfg(unix)]
{
PathBuf::from("/etc/contractless/settings.ini")
}
#[cfg(not(unix))]
{
env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("settings.ini")
}
}
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let config_file_path = Self::get_config_path();
let config_dir = config_file_path.parent().unwrap_or_else(|| Path::new("."));
let conf = Ini::load_from_file(&config_file_path)?;
let section = conf
.section(Some("Piggyback"))
.ok_or("Piggyback section not found")?;
#[cfg(feature = "mainnet")]
let pg_section = conf
.section(Some("Postgres"))
.ok_or("Postgres section not found")?;
#[cfg(feature = "testnet")]
let pg_section = conf
.section(Some("Postgres-Testnet"))
.ok_or("Postgres-Testnet section not found")?;
let mut piggybacks = Vec::new();
for (key, value) in section.iter() {
if key.to_uppercase().starts_with("PIGGYBACK_") {
piggybacks.push(value.to_string());
}
}
let threads = conf
.get_from(Some("Settings"), "THREADS")
.unwrap_or("1")
.parse::<u16>()?;
if threads != 1 && threads != 2 && threads % 4 != 0 {
return Err("THREADS must be 1, 2, or a multiple of 4".into());
}
if threads == 0 || threads > 256 {
return Err("THREADS must be between 1 and 256".into());
}
Ok(Settings {
block_path: Self::expand(
conf.get_from(Some("Paths"), "BLOCK_PATH")
.ok_or("BLOCK_PATH not found")?,
config_dir,
),
torrent_path: Self::expand(
conf.get_from(Some("Paths"), "TORRENT_PATH")
.ok_or("TORRENT_PATH not found")?,
config_dir,
),
db_path: Self::expand(
conf.get_from(Some("Paths"), "DB_PATH")
.ok_or("DB_PATH not found")?,
config_dir,
),
wallet_path: Self::expand(
conf.get_from(Some("Paths"), "WALLET_PATH")
.ok_or("WALLET_PATH not found")?,
config_dir,
),
wallet_name: conf
.get_from(Some("Paths"), "WALLET_NAME")
.ok_or("WALLET_NAME not found")?
.to_string(),
balance_sheet: Self::expand(
conf.get_from(Some("Paths"), "BALANCE_SHEET")
.ok_or("BALANCE_SHEET not found")?,
config_dir,
),
log_path: Self::expand(
conf.get_from(Some("Paths"), "LOG_PATH")
.unwrap_or("./logs"),
config_dir,
),
log_level: conf
.get_from(Some("Settings"), "LOG_LEVEL")
.unwrap_or("info")
.to_string(),
public_ip: conf
.get_from(Some("Settings"), "PUBLIC_IP")
.or_else(|| conf.get_from(Some("Settings"), "IP"))
.ok_or("PUBLIC_IP not found")?
.to_string(),
listen_ip: conf
.get_from(Some("Settings"), "LISTEN_IP")
.unwrap_or("0.0.0.0")
.to_string(),
rpc_port: conf
.get_from(Some("Settings"), "RPC_PORT")
.ok_or("RPC_PORT not found")?
.to_string(),
testnet_rpc_port: conf
.get_from(Some("Settings"), "TESTNET_RPC_PORT")
.ok_or("TESTNET_RPC_PORT not found")?
.to_string(),
outgoing_connections: conf
.get_from(Some("Settings"), "OUTGOING_CONNECTIONS")
.ok_or("OUTGOING_CONNECTIONS not found")?
.parse::<u8>()?,
incoming_connections: conf
.get_from(Some("Settings"), "INCOMING_CONNECTIONS")
.ok_or("INCOMING_CONNECTIONS not found")?
.parse::<u8>()?,
threads,
pg_host: pg_section.get("host").unwrap_or("127.0.0.1").to_string(),
pg_port: pg_section.get("port").unwrap_or("5432").parse::<u16>()?,
pg_user: pg_section
.get("user")
.ok_or("postgres user not set")?
.to_string(),
pg_password: pg_section.get("password").map(|s| s.to_string()),
pg_dbname: pg_section
.get("dbname")
.ok_or("postgres dbname not set")?
.to_string(),
piggybacks,
})
}
}

79
src/lib.rs Normal file
View File

@ -0,0 +1,79 @@
// we user this file to include all external libs
// in our library as well as definitions for local
// mod groups for out lib file. Internally all
// external libs are called from our own crate or
// our own lib in the case of standalone tools.
pub mod blocks;
pub mod common;
pub mod config;
pub mod miner;
pub mod orphans;
pub mod records;
pub mod rpc;
pub mod standalone_tools;
pub mod startup;
pub mod torrent;
pub mod verifications;
pub mod wallets;
pub use chrono::{
DateTime, Datelike, Local, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc,
};
pub use cid::Cid;
pub use config::Settings;
pub use encrypted_images::decryption::images::decode_image_and_extract_text;
pub use encrypted_images::decryption::text::decrypts;
pub use encrypted_images::encryption::images::create_img;
pub use encrypted_images::encryption::text::encrypts;
pub use falcon::{
DomainSeparation, FalconError, FalconKeyPair, FalconSignature, FnDsaExpandedKey, FnDsaKeyPair,
FnDsaSignature, PreHashAlgorithm,
};
pub use flexi_logger;
pub use hex::{decode, encode, encode_upper};
pub use ini::Ini;
pub use ipnetwork::IpNetwork;
pub use lazy_static::lazy_static;
pub use log;
pub use rand::rngs::{OsRng, StdRng};
pub use rand::seq::{IteratorRandom, SliceRandom};
pub use rand::{thread_rng, Rng, RngCore, SeedableRng};
pub use rayon;
pub use rayon::iter::IntoParallelIterator;
pub use rayon::iter::ParallelIterator;
pub use ripemd;
pub use ripemd::Ripemd160;
pub use rpassword::read_password;
pub use serde::{Deserialize, Serialize};
pub use serde_json::{
from_slice, from_str, json, to_string, to_string_pretty, to_value, Map, Value,
};
pub use shellexpand::tilde;
pub use skein::digest::{Digest, Output};
pub use skein::{Skein256, Skein512};
pub use sled;
pub use std::cmp::Ordering;
pub use std::collections::{BinaryHeap, HashMap};
pub use std::convert::TryInto;
pub use std::error::Error;
pub use std::fmt::Write;
pub use std::fs::{self, OpenOptions};
pub use std::io::Cursor;
pub use std::net::{IpAddr, Ipv6Addr, SocketAddr};
pub use std::path::{Path, PathBuf};
pub use std::process::exit;
pub use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
pub use std::sync::{Arc, OnceLock};
pub use std::{env, fmt, io, panic};
pub use tokio;
pub use tokio::fs::{create_dir_all, metadata, read, read_dir, read_to_string, remove_file, File};
pub use tokio::io::{
stdin, stdout, AsyncBufReadExt, AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufReader, Result,
SeekFrom,
};
pub use tokio::net::{TcpListener, TcpStream};
pub use tokio::runtime::{Builder, Runtime};
pub use tokio::sync::{mpsc, oneshot, Mutex, RwLock};
pub use tokio::task;
pub use tokio::time::{sleep, timeout, Duration, Instant};
pub use tokio_postgres::NoTls;

83
src/main.rs Normal file
View File

@ -0,0 +1,83 @@
use blockchain::exit;
use blockchain::log::{error, logger};
use blockchain::startup::daemonize::daemonize_after_wallet_prompt;
use blockchain::startup::daemonize::handle_control_command;
use blockchain::startup::initialize_startup::obtain_startup_wallet_key;
use blockchain::startup::initialize_startup::prepare_pre_wallet_startup;
use blockchain::startup::node_runtime::initialize_node_logging;
use blockchain::startup::node_runtime::install_panic_cleanup;
use blockchain::startup::node_runtime::run_unlocked_node;
use blockchain::startup::windows_service::handle_windows_service_command;
use blockchain::startup::windows_service::try_run_as_windows_service;
use blockchain::Runtime;
use tokio::runtime::Builder;
fn main() {
// Linux-specific control commands are handled before any startup work begins.
match handle_control_command() {
Ok(true) => return,
Ok(false) => {}
Err(e) => {
eprintln!("Control command failed: {e}");
exit(1);
}
}
// Windows service management commands are also handled before normal node startup.
match handle_windows_service_command() {
Ok(true) => return,
Ok(false) => {}
Err(e) => {
eprintln!("Windows service command failed: {e}");
exit(1);
}
}
// If the binary was launched by the Windows Service Control Manager, the service
// entrypoint takes over and the normal console path stops here.
match try_run_as_windows_service() {
Ok(true) => return,
Ok(false) => {}
Err(e) => {
eprintln!("Failed to start Windows service path: {e}");
exit(1);
}
}
// The pre-wallet startup work runs in a temporary runtime so Linux can daemonize
// only after the wallet key is obtained, while Windows console launches still use
// the same shared startup sequence.
let startup_runtime = Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create startup runtime");
startup_runtime.block_on(prepare_pre_wallet_startup());
let wallet_key = startup_runtime.block_on(obtain_startup_wallet_key());
drop(startup_runtime);
// Linux detaches after the wallet prompt unless --foreground is supplied.
if let Err(e) = daemonize_after_wallet_prompt() {
eprintln!("Failed to daemonize: {e}");
exit(1);
}
// Once the platform-specific startup path is settled, the shared node runtime
// takes over for normal unlocked operation.
let runtime = Runtime::new().expect("Failed to create main runtime");
let _log_handle = runtime.block_on(async {
match initialize_node_logging().await {
Ok(handle) => Some(handle),
Err(e) => {
eprintln!("Failed to initialize logging: {e}");
None
}
}
});
install_panic_cleanup();
if let Err(e) = runtime.block_on(run_unlocked_node(wallet_key, true)) {
error!("Failed to start unlocked node runtime: {e}");
logger().flush();
eprintln!("Failed to start unlocked node runtime: {e}");
exit(1);
}
}

View File

@ -0,0 +1,50 @@
use crate::blocks::rewards::{RewardsTransaction, UnsignedRewardsTransaction};
use crate::common::types::BLOCKS_PER_HALVING;
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::network_mapping::NodeInfo;
use crate::sled::Db;
pub async fn calculate_block_reward(block_height: u32) -> u64 {
// Apply the fixed halving schedule based on the block
// height currently being mined or verified.
let halving_count = block_height / BLOCKS_PER_HALVING;
// Rewards are stored in the smallest coin unit, so this represents
// 416.66666666 base coins before the first halving.
let base_reward = 41666666666; // Represents 416.66666666 * 100000000
// The first interval receives the full base reward. Later intervals
// divide by two for each completed halving period.
let reward: u64 = if halving_count == 0 {
base_reward
} else {
base_reward / (2u64.pow(halving_count))
};
reward
}
pub async fn create_rewards_transaction(
short_address: &str,
timestamp: u32,
db: &Db,
) -> RewardsTransaction {
// Rewards are created as the first transaction in every
// mined block using the current reward schedule.
let txtype = 1;
// The reward belongs to the block being created, not the current tip.
let block_height = get_height(db) + 1;
// New miners must first prove participation before receiving
// the block subsidy, so early mined blocks pay a zero reward.
let value = if NodeInfo::get_mined_count(short_address).await < 100 {
0_u64
} else {
calculate_block_reward(block_height).await
};
// Reward transactions are unsigned because they are created by
// consensus rules rather than by a wallet spending funds.
let unsigned_rewards = UnsignedRewardsTransaction::new(txtype, timestamp, value).await;
RewardsTransaction::new(unsigned_rewards).await
}

82
src/miner/fairness.rs Normal file
View File

@ -0,0 +1,82 @@
use crate::log::info;
use crate::miner::flag::{is_mining_stop_requested, is_normal_mode, set_mining_state, MiningState};
use crate::records::block_height::get_block_height::get_height;
use crate::records::unpack_block::unpack_header::load_block_header;
use crate::sled::Db;
use crate::sleep;
use crate::Duration;
pub async fn fairness_difficulty(block_height: u32, miner_wallet: &str) -> bool {
// The fairness window tightens once peri tenth of the first halving interval.
let current_interval = (block_height / 960000) + 1;
// Start by allowing ten consecutive blocks, then reduce the
// allowance over time without ever dropping below one.
let max_consecutive_blocks = u8::max(10 - current_interval as u8, 1) as u32;
// Only the trailing fairness window needs to be checked.
let start_block = block_height.saturating_sub(max_consecutive_blocks);
let mut consecutive_blocks = 0;
// Walk backward through the recent headers and count how many
// consecutive blocks were mined by this same miner.
for i in (start_block..=block_height).rev() {
// Load the saved header for this height.
let block = load_block_header(i).await.unwrap();
// The header stores the miner short address directly.
let mined_by_miner = block.unmined_block.miner;
// A different miner breaks the consecutive streak.
if mined_by_miner == miner_wallet {
consecutive_blocks += 1;
// More than the allowed streak means this miner must wait
// for another miner to advance the chain.
if consecutive_blocks > max_consecutive_blocks {
return false;
}
} else {
consecutive_blocks = 0;
}
}
true
}
pub async fn wait_for_fairness_gate(
db: &Db,
miner_short: &str,
current_block_number: u32,
fairness_paused_height: &mut Option<u32>,
) -> bool {
// The fairness gate temporarily yields this height when the local miner
// should wait for another registered miner to advance the chain.
let previous_block_height = current_block_number.saturating_sub(1);
let fairness_allowed = if previous_block_height > 0 {
fairness_difficulty(previous_block_height, miner_short).await
} else {
true
};
if fairness_allowed {
// Clear the one-shot log guard once this height is mineable again.
*fairness_paused_height = None;
return true;
}
// Log only once per paused height so the miner does not flood logs
// while it waits for another node to produce the next block.
if *fairness_paused_height != Some(current_block_number) {
info!("[mining] fairness gate active for block {current_block_number}, waiting for another miner to advance the chain");
*fairness_paused_height = Some(current_block_number);
}
set_mining_state(MiningState::Idle);
// Stay idle until the chain moves, node mode changes, or mining is stopped.
while is_normal_mode()
&& !is_mining_stop_requested()
&& get_height(db) + 1 == current_block_number
{
sleep(Duration::from_millis(250)).await;
}
false
}

142
src/miner/flag.rs Normal file
View File

@ -0,0 +1,142 @@
use crate::sleep;
use crate::Duration;
use crate::{AtomicBool, AtomicOrdering};
use std::sync::atomic::AtomicU8;
static REORG_GATE: AtomicBool = AtomicBool::new(false);
static MINING_STOP_REQUESTED: AtomicBool = AtomicBool::new(false);
static NODE_MODE: AtomicU8 = AtomicU8::new(NodeMode::Startup as u8);
static MINING_STATE: AtomicU8 = AtomicU8::new(MiningState::Idle as u8);
// NodeMode is stored as a byte so lightweight background tasks can
// coordinate startup, syncing, mining, and reorg work without a mutex.
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NodeMode {
Startup,
Syncing,
Normal,
Reorganizing,
}
fn node_mode_from_bits(value: u8) -> NodeMode {
// Unknown byte values fall back to startup so mining remains disabled.
match value {
1 => NodeMode::Syncing,
2 => NodeMode::Normal,
3 => NodeMode::Reorganizing,
_ => NodeMode::Startup,
}
}
// MiningState is separate from NodeMode because the node can be normal
// while the mining task is temporarily idle between rounds.
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MiningState {
Idle,
Running,
}
fn mining_state_from_bits(value: u8) -> MiningState {
// Unknown byte values are treated as idle so reorg and shutdown code
// can wait safely instead of assuming a worker is active.
match value {
1 => MiningState::Running,
_ => MiningState::Idle,
}
}
pub fn set_node_mode(mode: NodeMode) {
// Store the current node mode for all async tasks that need to
// decide whether mining or reorg work is allowed.
NODE_MODE.store(mode as u8, AtomicOrdering::SeqCst);
}
pub fn is_syncing_mode() -> bool {
// Syncing mode prevents local mining while the node catches up.
node_mode_from_bits(NODE_MODE.load(AtomicOrdering::SeqCst)) == NodeMode::Syncing
}
pub fn is_normal_mode() -> bool {
// Normal mode is the only mode where the miner may actively search.
node_mode_from_bits(NODE_MODE.load(AtomicOrdering::SeqCst)) == NodeMode::Normal
}
pub fn is_reorganizing_mode() -> bool {
// Reorganizing mode is held while chain state is being rewritten.
node_mode_from_bits(NODE_MODE.load(AtomicOrdering::SeqCst)) == NodeMode::Reorganizing
}
pub fn set_mining_state(state: MiningState) {
// Publish whether the mining task is actively running or idle.
MINING_STATE.store(state as u8, AtomicOrdering::SeqCst);
}
pub fn request_mining_stop() {
// Stop requests let reorg, shutdown, and mode transitions ask the
// mining loop to leave its current nonce round.
MINING_STOP_REQUESTED.store(true, AtomicOrdering::SeqCst);
}
pub fn clear_mining_stop_request() {
// Once the blocking operation finishes, mining may resume when
// the node returns to normal mode.
MINING_STOP_REQUESTED.store(false, AtomicOrdering::SeqCst);
}
pub fn is_mining_idle() -> bool {
// Reorg code uses this to wait until no nonce worker is saving a block.
mining_state_from_bits(MINING_STATE.load(AtomicOrdering::SeqCst)) == MiningState::Idle
}
pub fn is_mining_running() -> bool {
// Genesis creation uses this to avoid repeatedly publishing
// the same running state inside its nonce loop.
mining_state_from_bits(MINING_STATE.load(AtomicOrdering::SeqCst)) == MiningState::Running
}
pub fn is_mining_stop_requested() -> bool {
// Mining workers poll this flag between nonce attempts.
MINING_STOP_REQUESTED.load(AtomicOrdering::SeqCst)
}
pub async fn wait_for_mining_idle() {
// Yield until the active mining round has reported idle.
while !is_mining_idle() {
sleep(Duration::from_millis(10)).await;
}
}
fn try_acquire_reorg_gate() -> bool {
// Only one reorg path may own the gate at a time.
REORG_GATE
.compare_exchange(false, true, AtomicOrdering::SeqCst, AtomicOrdering::SeqCst)
.is_ok()
}
pub async fn begin_reorg_lock() {
// Reorg starts by asking mining to stop and waiting for the
// current worker round to leave the save path.
request_mining_stop();
wait_for_mining_idle().await;
if is_reorganizing_mode() {
return;
}
// Wait for any competing reorg attempt to release the gate.
while !try_acquire_reorg_gate() {
sleep(Duration::from_millis(5)).await;
}
// Reorganizing mode blocks new mining rounds until the lock ends.
set_node_mode(NodeMode::Reorganizing);
}
pub fn end_reorg_lock() {
// Restore normal mode only if this task still owns the reorg state.
if is_reorganizing_mode() {
set_node_mode(NodeMode::Normal);
}
// Clear the stop flag last so mining cannot resume before mode is restored.
clear_mining_stop_request();
REORG_GATE.store(false, AtomicOrdering::SeqCst);
}

156
src/miner/genesis.rs Normal file
View File

@ -0,0 +1,156 @@
use crate::blocks::block::{Block, UnminedBlock};
use crate::blocks::genesis::{GenesisTransaction, UnsignedGenesisTransaction};
use crate::common::check_genesis::genesis_checkup;
use crate::common::types::{Transaction, GENESIS_BLOCK_HASH};
use crate::miner::flag::{is_mining_running, is_mining_stop_requested, is_normal_mode, set_mining_state, MiningState};
use crate::records::memory::connections::outgoing_connection_count;
use crate::records::memory::response_channels::Command;
use crate::records::record_chain::save::save_block;
use crate::records::record_chain::structs::{SaveBlockParams, SaveType};
use crate::verifications::verification_service::VerificationService;
use crate::wallets::structures::Wallet;
use crate::log::{error, info};
use crate::sled::Db;
use crate::Arc;
use crate::Duration;
use crate::Error;
use crate::Mutex;
use crate::sleep;
use crate::Utc;
pub async fn create_genesis_transaction(
db: &Db,
verification_service: Arc<VerificationService>,
wallet_key: String,
map: Arc<Mutex<Command>>,
) {
// Load the local wallet so the genesis block records the miner's
// current short address in the header.
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
Ok(wallet) => wallet,
Err(err) => {
error!("Wallet decryption failed: {err}");
return;
}
};
let miner = wallet.saved.short_address;
// The genesis transaction carries the fixed launch message and
// uses transaction type zero.
let txtype = 0;
let message = "We are all Satoshi. In 15 years nothing changed.".to_string();
// Genesis has no spender signature, but it still follows the
// normal unsigned/signed transaction struct flow.
let unsigned_genesis = UnsignedGenesisTransaction::new(txtype, &message).await;
let genesis_transaction = GenesisTransaction::new(unsigned_genesis).await;
let _ = create_genesis_block(
genesis_transaction,
&miner,
wallet_key,
db,
verification_service,
map,
)
.await;
}
async fn create_genesis_block(
signed_genesis_transaction: GenesisTransaction,
miner: &str,
wallet_key: String,
db: &Db,
verification_service: Arc<VerificationService>,
map: Arc<Mutex<Command>>,
) -> Result<(), Box<dyn Error>> {
// Genesis searches the one-byte nonce space until the block verifies
// or another task creates the genesis file first.
let mut nonce: u8 = 0;
let mut genesis_search_logged = false;
let mut disconnected_logged = false;
loop {
// The genesis miner obeys the same mode and stop flags as
// normal mining so startup, sync, reorg, and disconnected
// node states can pause it.
while !is_normal_mode()
|| is_mining_stop_requested()
|| outgoing_connection_count().await == 0
{
if is_normal_mode() && !is_mining_stop_requested() && !disconnected_logged {
info!("[genesis] waiting for an outgoing peer connection before mining genesis");
disconnected_logged = true;
}
set_mining_state(MiningState::Idle);
let _ = sleep(Duration::from_millis(100)).await;
}
disconnected_logged = false;
// If sync or another task already saved genesis, stop before
// publishing any mining state or logging a search that will not run.
if genesis_checkup().await {
set_mining_state(MiningState::Idle);
break Ok(());
}
// Mark the miner running once per active stretch.
if !is_mining_running() {
set_mining_state(MiningState::Running);
}
if !genesis_search_logged {
info!("[genesis] mining genesis block");
genesis_search_logged = true;
}
// Genesis uses the fixed parent hash and launch difficulty.
let timestamp = Utc::now().timestamp() as u32;
let next_block_difficulty = 3000000000000000_u64;
let block_struct = UnminedBlock::new(
timestamp,
miner,
GENESIS_BLOCK_HASH,
next_block_difficulty,
nonce,
)
.await;
// The VRF binds the candidate header to the mining wallet.
let vrf_block = UnminedBlock::vrf_generate(block_struct, wallet_key.clone()).await;
let header_hash = vrf_block.hash().await;
// The genesis block contains exactly one genesis transaction.
let block = Block {
vrf_block,
transactions: vec![Transaction::Genesis(signed_genesis_transaction.clone())],
};
// Reuse normal block verification before saving so genesis
// still passes through consensus validation.
if let Ok(transactions) = block.verify(db, verification_service.clone()).await {
// Save through the chain writer so indexes and in-memory
// state are updated the same way as later mined blocks.
match save_block(SaveBlockParams {
block,
db: db.clone(),
header_hash: header_hash.to_string(),
timestamp,
signatures: transactions,
save_type: SaveType::Mining,
allow_during_reorg: false,
map: map.clone(),
})
.await
{
Ok(()) => {
set_mining_state(MiningState::Idle);
break Ok(());
}
Err(err) => {
error!("[genesis] save_block failed: {err}");
}
}
}
// Try the next nonce; wrapping is intentional because the
// timestamp changes while the loop keeps searching.
nonce = nonce.wrapping_add(1);
}
}

274
src/miner/mining.rs Normal file
View File

@ -0,0 +1,274 @@
use crate::blocks::block::{Block, UnminedBlock};
use crate::common::types::Transaction;
use crate::log::{error, info};
use crate::miner::block_rewards::create_rewards_transaction;
use crate::miner::fairness::wait_for_fairness_gate;
use crate::miner::flag::{is_mining_stop_requested, is_normal_mode, set_mining_state, MiningState};
use crate::miner::nonce::run_nonce_round;
use crate::miner::structs::MiningAttemptContext;
use crate::miner::winner::{handle_mining_winner, verify_and_save_block};
use crate::records::block_height::get_block_height::get_height;
use crate::records::memory::connections::outgoing_connection_count;
use crate::records::memory::network_mapping::NodeInfo;
use crate::records::memory::response_channels::Command;
use crate::records::unpack_block::unpack_header::load_block_header;
use crate::sled::Db;
use crate::sleep;
use crate::verifications::verification_service::VerificationService;
use crate::wallets::structures::Wallet;
use crate::Arc;
use crate::Duration;
use crate::Error;
use crate::Mutex;
use crate::Utc;
pub async fn mine_block(
db: &Db,
verification_service: Arc<VerificationService>,
wallet_key: String,
map: Arc<Mutex<Command>>,
) -> Result<(), Box<dyn Error>> {
// Mining runs continuously, rebuilding its context from the
// latest saved tip before each one-second nonce round.
let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await {
Ok(wallet) => wallet,
Err(err) => {
return Err(std::io::Error::other(format!("Wallet decryption failed: {err}")).into());
}
};
let miner_short = wallet.saved.short_address;
// Track the height this miner expects to produce next so nonce workers
// can stop quickly when another peer advances the chain.
let mut expected_block_height = get_height(db) + 1;
let mut fairness_paused_height: Option<u32> = None;
let mut was_stopped = true;
loop {
// Pause here until the node is in normal mode and no stop
// request is pending from reorg or shutdown logic.
was_stopped = wait_until_mining_allowed(was_stopped).await;
// Re-read height each round because peers may have saved a block
// while this miner was paused or waiting for the next second.
let current_block_number = get_height(db) + 1;
if current_block_number != expected_block_height {
expected_block_height = current_block_number;
}
if !NodeInfo::address_checkup(&miner_short, current_block_number).await {
// Mining requires the local node wallet to be announced in
// the network map. Offline/solo nodes stay idle instead of
// producing private blocks that can later be submitted in bulk.
set_mining_state(MiningState::Idle);
was_stopped = true;
sleep(Duration::from_millis(250)).await;
continue;
}
if !wait_for_fairness_gate(
db,
&miner_short,
current_block_number,
&mut fairness_paused_height,
)
.await
{
was_stopped = true;
continue;
}
if was_stopped {
info!("mining started");
was_stopped = false;
}
set_mining_state(MiningState::Running);
// rebuild the mining context for the current expected height
// before splitting the one-second nonce space across workers
let attempt_context = match prepare_attempt_context(
db,
miner_short.clone(),
wallet_key.clone(),
current_block_number,
verification_service.clone(),
)
.await
{
Some(context) => context,
None => {
was_stopped = true;
continue;
}
};
let round_second = Utc::now().timestamp() as u32;
// Each nonce round searches all 256 nonces for a single timestamp.
let winning_block = run_nonce_round(attempt_context).await;
if handle_mining_winner(winning_block, db, map.clone()).await {
was_stopped = true;
} else if wait_for_next_second_or_chain_change(db, expected_block_height, round_second)
.await
{
continue;
} else {
set_mining_state(MiningState::Idle);
was_stopped = true;
}
}
}
async fn wait_until_mining_allowed(mut was_stopped: bool) -> bool {
// Mining pauses whenever the node is syncing, reorganizing, starting,
// disconnected, or a stop request has been raised by another task.
let mut disconnected_logged = false;
while !is_normal_mode() || is_mining_stop_requested() || outgoing_connection_count().await == 0
{
if is_normal_mode() && !is_mining_stop_requested() && !disconnected_logged {
info!("[mining] waiting for an outgoing peer connection before mining");
disconnected_logged = true;
}
set_mining_state(MiningState::Idle);
was_stopped = true;
sleep(Duration::from_millis(25)).await;
}
was_stopped
}
async fn prepare_attempt_context(
db: &Db,
miner_short: String,
wallet_key: String,
current_block_number: u32,
verification_service: Arc<VerificationService>,
) -> Option<MiningAttemptContext> {
// Before each worker round, capture a single parent header and difficulty
// so all nonce tasks mine against the same chain tip.
if !is_normal_mode() || is_mining_stop_requested() {
set_mining_state(MiningState::Idle);
return None;
}
match build_attempt_context(
db,
miner_short,
wallet_key,
current_block_number,
verification_service,
)
.await
.map_err(|err| err.to_string())
{
Ok(context) => Some(context),
Err(err_string) => {
error!("[mining] unable to build attempt context: {err_string}");
set_mining_state(MiningState::Idle);
sleep(Duration::from_millis(50)).await;
None
}
}
}
async fn wait_for_next_second_or_chain_change(
db: &Db,
expected_block_height: u32,
round_second: u32,
) -> bool {
// If no worker found a block, avoid re-mining the same timestamp/nonce
// space unless the chain tip or node mode changes first.
if !(is_normal_mode()
&& !is_mining_stop_requested()
&& get_height(db) + 1 == expected_block_height)
{
return false;
}
while is_normal_mode()
&& !is_mining_stop_requested()
&& get_height(db) + 1 == expected_block_height
{
let now_second = Utc::now().timestamp() as u32;
if now_second != round_second {
break;
}
sleep(Duration::from_millis(10)).await;
}
true
}
async fn build_attempt_context(
db: &Db,
miner_short: String,
wallet_key: String,
current_block_number: u32,
verification_service: Arc<VerificationService>,
) -> Result<MiningAttemptContext, Box<dyn Error>> {
// Capture the previous header hash and difficulty once per
// round so all worker tasks mine against the same parent.
if !is_normal_mode() || is_mining_stop_requested() {
return Err("Mining paused before loading previous block header".into());
}
let previous_block_height = current_block_number - 1;
let previous_block = load_block_header(previous_block_height).await?;
let previous_hash = previous_block.hash().await;
let previous_difficulty = previous_block.unmined_block.next_block_difficulty;
Ok(MiningAttemptContext {
db: db.clone(),
miner_short,
wallet_key,
current_block_number,
previous_hash,
previous_difficulty,
verification_service,
})
}
pub async fn mine_block_internal(
ctx: &MiningAttemptContext,
nonce: u8,
) -> Result<Option<(Block, String, u32)>, Box<dyn Error>> {
// Build a candidate block for this timestamp/nonce pair and
// return it only if full verification succeeds locally.
if !is_normal_mode() || is_mining_stop_requested() {
return Ok(None);
}
let timestamp = Utc::now().timestamp() as u32;
// Difficulty is calculated from the previous block's difficulty
// and the new candidate timestamp.
let new_difficulty =
UnminedBlock::adjust_difficulty(timestamp, &ctx.db, ctx.previous_difficulty).await;
// Build the unmined header from the captured parent context and
// this worker's nonce.
let unmined_block = UnminedBlock::new(
timestamp,
&ctx.miner_short,
&ctx.previous_hash,
new_difficulty,
nonce,
)
.await;
// Add the wallet VRF proof before hashing and verifying the candidate.
let vrf_block = UnminedBlock::vrf_generate(unmined_block, ctx.wallet_key.clone()).await;
let block_hash = vrf_block.hash().await;
// Every mined block begins with a consensus-created reward transaction.
let rewards_transaction =
create_rewards_transaction(&ctx.miner_short, timestamp, &ctx.db).await;
let new_block = Block {
vrf_block,
transactions: vec![Transaction::Rewards(rewards_transaction)],
};
// Return only candidates that pass local verification.
if let Some(new_block) =
verify_and_save_block(new_block, &ctx.db, ctx.verification_service.clone()).await
{
Ok(Some((new_block, block_hash, timestamp)))
} else {
Ok(None)
}
}

10
src/miner/mod.rs Normal file
View File

@ -0,0 +1,10 @@
// Miner modules cover block production, mining state, fairness gates,
// genesis creation, nonce workers, and mined-block save handling.
pub mod block_rewards;
pub mod fairness;
pub mod flag;
pub mod genesis;
pub mod mining;
pub mod nonce;
pub mod structs;
pub mod winner;

123
src/miner/nonce.rs Normal file
View File

@ -0,0 +1,123 @@
use crate::blocks::block::Block;
use crate::config::SETTINGS;
use crate::log::error;
use crate::miner::flag::{is_mining_stop_requested, is_normal_mode};
use crate::miner::mining::mine_block_internal;
use crate::miner::structs::MiningAttemptContext;
use crate::records::block_height::get_block_height::get_height;
use crate::task;
use crate::Arc;
use crate::AtomicBool;
use crate::AtomicOrdering;
use crate::Mutex;
pub async fn run_nonce_round(
attempt_context: MiningAttemptContext,
) -> Option<(Block, String, u32)> {
// Split one mining timestamp across worker tasks and collect the first
// locally verified block candidate they find.
let stop_flag = Arc::new(AtomicBool::new(false));
// The winning block is shared between workers so only the first
// valid candidate is returned to the save path.
let winner = Arc::new(Mutex::new(None::<(Block, String, u32)>));
let ranges = build_nonce_ranges(SETTINGS.threads);
let mut handles = Vec::with_capacity(ranges.len());
// Each task receives a disjoint nonce range but shares the same
// parent block context and stop flag.
for (start_nonce, stop_nonce) in ranges {
let ctx = attempt_context.clone();
let stop = stop_flag.clone();
let winner_clone = winner.clone();
handles.push(task::spawn(async move {
nonce_range(ctx, start_nonce, stop_nonce, stop, winner_clone).await
}));
}
for handle in handles {
match handle.await {
Ok(Ok(())) => {}
Ok(Err(err)) => {
error!("[mining] worker error: {err}");
}
Err(err) => {
error!("[mining] task join error: {err}");
}
}
}
// Take the winner out of the shared slot after all workers exit.
let mut winner_guard = winner.lock().await;
winner_guard.take()
}
pub fn build_nonce_ranges(threads: u16) -> Vec<(u8, u8)> {
// Split the 0-255 nonce space across the configured worker
// count so each task searches a disjoint slice.
let threads = threads.min(256) as usize;
let base = 256 / threads;
let remainder = 256 % threads;
let mut ranges = Vec::with_capacity(threads);
let mut start = 0usize;
for idx in 0..threads {
// Spread the remainder over the first ranges so all 256 nonce
// values are covered without overlap.
let width = base + if idx < remainder { 1 } else { 0 };
let end = start + width - 1;
ranges.push((start as u8, end as u8));
start = end + 1;
}
ranges
}
async fn nonce_range(
ctx: MiningAttemptContext,
start_nonce: u8,
stop_nonce: u8,
stop_flag: Arc<AtomicBool>,
winner: Arc<Mutex<Option<(Block, String, u32)>>>,
) -> Result<(), String> {
// Scan this worker's nonce slice until a winner is found,
// the tip changes, or the assigned range is exhausted.
let mut nonce = start_nonce;
loop {
// Stop immediately when another worker wins or the node leaves
// normal mining mode.
if stop_flag.load(AtomicOrdering::SeqCst) || !is_normal_mode() || is_mining_stop_requested()
{
return Ok(());
}
// If the chain tip changed, this round is stale for every worker.
if get_height(&ctx.db) + 1 != ctx.current_block_number {
stop_flag.store(true, AtomicOrdering::SeqCst);
return Ok(());
}
// A verified candidate claims the shared winner slot and stops
// the rest of the nonce workers.
if let Some(found) = mine_block_internal(&ctx, nonce)
.await
.map_err(|e| e.to_string())?
{
let mut winner_guard = winner.lock().await;
if winner_guard.is_none() {
*winner_guard = Some(found);
stop_flag.store(true, AtomicOrdering::SeqCst);
}
return Ok(());
}
// The assigned range is inclusive, so stop after checking the
// final nonce value.
if nonce == stop_nonce {
break;
}
nonce = nonce.wrapping_add(1);
}
Ok(())
}

16
src/miner/structs.rs Normal file
View File

@ -0,0 +1,16 @@
use crate::sled::Db;
use crate::verifications::verification_service::VerificationService;
use crate::Arc;
// MiningAttemptContext captures one consistent chain tip for a nonce round.
// Worker tasks clone this instead of reloading the parent header independently.
#[derive(Clone)]
pub struct MiningAttemptContext {
pub db: Db,
pub miner_short: String,
pub wallet_key: String,
pub current_block_number: u32,
pub previous_hash: String,
pub previous_difficulty: u64,
pub verification_service: Arc<VerificationService>,
}

Some files were not shown because too many files have changed in this diff Show More