bug fixes

This commit is contained in:
viraladmin 2026-07-12 16:01:00 -06:00
parent 81f8f6e711
commit 9ed2dee62b
87 changed files with 6401 additions and 3056 deletions

76
Cargo.lock generated
View File

@ -161,44 +161,6 @@ dependencies = [
"hybrid-array",
]
[[package]]
name = "blockchain"
version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"cid",
"colored",
"encrypted_images",
"falcon-rs",
"flexi_logger",
"fn-dsa",
"hex",
"ipnetwork",
"lazy_static",
"log",
"nix 0.27.1",
"once_cell",
"rand 0.8.5",
"rayon",
"ripemd",
"rpassword",
"rust-ini",
"rustyline",
"rustyline-derive",
"serde",
"serde_json",
"shellexpand",
"skein",
"sled",
"tokio",
"tokio-postgres",
"windows-service",
"windows-sys 0.61.2",
"winreg",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -357,6 +319,44 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3"
[[package]]
name = "contractless"
version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"cid",
"colored",
"encrypted_images",
"falcon-rs",
"flexi_logger",
"fn-dsa",
"hex",
"ipnetwork",
"lazy_static",
"log",
"nix 0.27.1",
"once_cell",
"rand 0.8.5",
"rayon",
"ripemd",
"rpassword",
"rust-ini",
"rustyline",
"rustyline-derive",
"serde",
"serde_json",
"shellexpand",
"skein",
"sled",
"tokio",
"tokio-postgres",
"windows-service",
"windows-sys 0.61.2",
"winreg",
]
[[package]]
name = "core-foundation"
version = "0.9.4"

View File

@ -1,5 +1,5 @@
[package]
name = "blockchain"
name = "contractless"
version = "0.1.0"
edition = "2021"

338
README.md
View File

@ -1,319 +1,75 @@
# 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.
Contractless is a new peer-to-peer blockchain built from the ground up around spendable digital money, native transaction types, and a consensus model called Fair Proof of Work.
For the protocol design, mining rules, balance-sheet model and orphan correction process, read the [Contractless whitepaper](https://contractless.community/contractless_whitepaper.pdf).
Instead of rewarding specialized mining hardware with overwhelming advantage, Contractless limits mining attempts so ordinary hardware can participate on more equal footing. The chain includes native support for base-currency transfers, tokens, NFTs, swaps, loans, marketing records, and app-style data storage without requiring a smart-contract VM.
## Table of Contents
Contractless is currently in testnet. Node operators, developers, wallet testers, and application builders are encouraged to review the documentation, run testnet nodes, and help test the network.
- [System Requirements](#system-requirements)
- [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)
For protocol design, mining rules, balance handling, orphan correction, and Fair Proof of Work details, read the [Contractless whitepaper](https://contractless.community/contractless_whitepaper.pdf).
## System Requirements:
## System Requirements
Recommended node requirements:
- Dedicated Internet
- PC / Laptop / Hosted VPS or cloud hostingg
- PC / Laptop / Hosted VPS or cloud hosting
- Minimum 16GB memory 24 - 32 GB recommended
- i5 or higher CPU (or AMD equivalent CPU)
- 64 bit processing OS
## Network Requirements
Contractless uses Falcon signatures and requires 64-bit processing support.
A public node needs to be reachable by other peers.
## Networking Requirements
- 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.
A public Contractless node must be reachable by other peers.
Loopback and private addresses are useful for local testing, but they should not be announced by a public node.
- Dedicated Public IP address
- Open public RPC port
- Router or host firewall port forwarding when behind NAT
- Reliable system time using NTP or another time synchronization service A common default is `pool.ntp.org`.
- One mining node per public IP
## Download Source
## Build Instructions
Install Rust and build from the repository root.
Clone the repository:
```bash
cargo build --release
git clone https://contractless.dev/contractless/Contractless.git
cd Contractless
```
The default build target is testnet.
## Precompiled Binaries
### Build Flags
Precompiled releases are available here:
Testnet is the default feature:
[https://contractless.dev/contractless/Contractless/releases](https://contractless.dev/contractless/Contractless/releases)
```bash
cargo build --release
```
Use the release package that matches your operating system and follow the matching installation guide below.
or explicitly:
## Documentation
```bash
cargo build --release --features testnet
```
| Guide | Description |
| --- | --- |
| [Linux Installation](src/branch/main/docs/LINUX_INSTALLATION.md) | Install, configure, and run a Contractless node on Linux. |
| [Windows Installation](src/branch/main/docs/WINDOWS_INSTALLATION.md) | Install, configure, and run a Contractless node on Windows. |
| [Manual Postgres Setup](src/branch/main/docs/POSTGRES.md) | Manually create the PostgreSQL database, user, permissions, and settings. |
| [Config Settings](src/branch/main/docs/SETTINGS.md) | Full `settings.ini` reference and runtime path behavior. |
| [Wallet Tools](src/branch/main/docs/WALLET_TOOLS.md) | Create, restore, register, validate, and manage wallets. |
| [Validation Tools](src/branch/main/docs/VALIDATION_TOOLS.md) | Validate addresses, messages, blocks, torrents, and chain data. |
| [NFT Transactions](src/branch/main/docs/NFT_TRANSACTIONS.md) | Create, transfer and understand NFTs and RWA metadata. |
| [Tokens and Swaps](src/branch/main/docs/TOKEN_SWAPS.md) | Create tokens, swaps, import swaps, and handle two-party swap signatures. |
| [Loan Transactions](src/branch/main/docs/LOAN_TRANSACTIONS.md) | Create loans, make payments, claim collateral, and understand loan rules. |
| [Marketing Transactions](src/branch/main/docs/MARKETING_TRANSACTIONS.md) | Record campaign data and query marketing activity. |
| [Blocks and Transactions](src/branch/main/docs/BLOCKS_AND_TRANSACTIONS.md) | Look up blocks and transactions, create remaining transaction types, and broadcast signed transactions. |
| [Mempool](src/branch/main/docs/MEMPOOL.md) | Inspect pending transactions and mempool state. |
| [Data Storage](src/branch/main/docs/DATA_STORAGE.md) | Create storage keys, write app data, and use paid storage lookups. |
| [Other Tools](src/branch/main/docs/OTHER_TOOLS.md) | Additional CLI tools and maintenance commands. |
| [Fees](src/branch/main/docs/FEES.md) | Base fees for every native transaction type. |
| [Developers](src/branch/main/docs/DEV.md) | Architecture notes, library structure, RPC behavior, and contribution guidance. |
Mainnet has a feature flag, but mainnet builds are intentionally disabled during the testnet phase:
## Current Status
```bash
cargo build --release --no-default-features --features mainnet
```
Contractless is still under active testnet development. Interfaces, transaction tools, docs, and node behavior may continue to change while the chain is tested.
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](src/branch/main/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"
BALANCE_SHEET = "./balance_sheet"
LOG_PATH = "./logs"
WALLET_PATH = "./wallets"
WALLET_NAME = "contractless.wallet"
[Settings]
LOG_LEVEL = "info"
IP = "YOUR.PUBLIC.IP.ADDRESS"
LISTEN_IP = "0.0.0.0"
RPC_PORT = "50050"
TESTNET_RPC_PORT = "50055"
INCOMING_CONNECTIONS = "100"
OUTGOING_CONNECTIONS = "10"
VALIDATOR = "false"
THREADS = "8"
[Piggyback]
PIGGYBACK_1 = "contractless.dev:50050"
[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](src/branch/main/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](src/branch/main/docs/POSTGRES.md)
- [Settings reference](src/branch/main/docs/SETTINGS.md)
- [CLI tools reference](src/branch/main/docs/CLI_TOOLS.md)
- [Transaction reference](src/branch/main/docs/TRANSACTIONS.md)
- [Developer guide](src/branch/main/docs/DEV.md)
If you are interested in running a testnet node, start with the installation guide for your operating system, then review the configuration and wallet tools documentation.

View File

@ -0,0 +1,418 @@
# Blocks and Transactions
This page covers the general block and transaction CLI tools:
- `create_transfer_tx`
- `broadcast_transaction`
- `lookup_block_by_hash`
- `lookup_block_by_height`
- `lookup_torrent`
- `lookup_total_transactions`
- `lookup_transaction`
Most specialized transaction types have their own guides. Token creation and swaps are covered in `TOKEN_SWAPS.md`, NFTs and RWAs are covered in `NFT_TRANSACTIONS.md`, loans are covered in `LOAN_TRANSACTIONS.md`, marketing records are covered in `MARKETING_TRANSACTIONS.md`, and storage transactions are covered in `DATA_STORAGE.md`.
On Windows, the compiled binaries usually end in `.exe`. For example, `create_transfer_tx` becomes `create_transfer_tx.exe`.
## Amounts and Raw Values
Most CLI prompts ask for normal decimal amounts, such as `1`, `1.25`, or `0.0001`.
Internally, Contractless stores amounts as whole integer units where:
```text
1.00000000 = 100000000
```
That means a transfer entered as `1.25` appears in the saved transaction JSON as:
```json
"value": 125000000
```
The same conversion is used for transaction fees.
## Signed Transaction Files
Transaction creation tools write signed transaction files into:
```text
./transactions/
```
The filename is the transaction hash:
```text
./transactions/<transaction_hash>.json
```
The saved JSON often contains a `hash` field. That field is a CLI convenience used for filenames and user display. The on-chain transaction struct is still verified from the signed transaction payload itself.
## create_transfer_tx
`create_transfer_tx` creates and signs a transfer transaction.
Transfers can send:
- The network base coin.
- A fungible token.
- A 1-of-1 NFT.
- One item from a numbered NFT series.
The tool saves the signed transaction locally. It does not broadcast it. Use `broadcast_transaction` after reviewing the saved file.
### Usage
Interactive:
```bash
./create_transfer_tx
```
With command-line fields:
```bash
./create_transfer_tx <coin> <value> <receiver> <txfee> <nft_series>
```
Example:
```bash
./create_transfer_tx CLTC 10 7ba97ce531937388ef8bca4d8a4ca54cefbefb87.cltc 0.1 0
```
### Fields
`coin`
The base coin, token ticker, or NFT asset name to transfer. The base coin is accepted case-insensitively. Non-base assets must normalize to a canonical 3-15 character alphanumeric asset id.
`value`
The amount to send, entered in decimal form. For numbered NFT series transfers, this must be exactly `1.0`.
`receiver`
The receiving wallet address. The tool accepts a registered short address or vanity address and resolves it to the canonical short address before signing.
`txfee`
The transaction fee, entered in decimal form.
Base coin transfers require a minimum fee of 1% of the transfer amount. Token, NFT, and numbered NFT transfers require the fixed non-base transfer minimum fee.
`nft_series`
Use `0` for base coin transfers, token transfers, and 1-of-1 NFTs.
Use the numbered series item for a collection NFT. For example, if the asset is a numbered NFT and the user wants to transfer item `12`, use:
```text
nft_series = 12
```
### Validation
The network verifies that:
- The transaction is not already in the mempool.
- The sender and receiver are valid canonical registered short addresses.
- The receiver is already registered.
- The receiver is not an all-zero burn address.
- The sender signature matches the sender wallet.
- The asset is the base coin or a canonical padded asset id.
- The fee meets the required minimum.
- Numbered NFT transfers send exactly one item and the numbered NFT exists.
- The sender has enough confirmed plus pending-adjusted balance.
- The transaction hash does not already exist on-chain.
### Example Output
```json
{
"txtype": 2,
"timestamp": 1783657720,
"value": 1000000000,
"coin": "CLTC ",
"nft_series": 0,
"sender": "1439f758f93333734778a6459dc64b8d63a74c06.cltc",
"receiver": "7ba97ce531937388ef8bca4d8a4ca54cefbefb87.cltc",
"txfee": 10000000,
"hash": "8d38f9e9f4f03fb936a6f45e1bcf1a9e706d7f3795e78b7ff16d5b4a4536d73",
"signature": "<signature>"
}
```
In this example, `value` is `10.00000000` and `txfee` is `0.10000000`.
## broadcast_transaction
`broadcast_transaction` submits a signed transaction JSON file to a configured node.
This is the common broadcast path for transaction files created by the CLI tools.
### Usage
```bash
./broadcast_transaction <hash.json>
```
Example:
```bash
./broadcast_transaction ./transactions/8d38f9e9f4f03fb936a6f45e1bcf1a9e706d7f3795e78b7ff16d5b4a4536d73.json
```
The tool asks for the wallet path and wallet decryption key so it can authenticate to the node. The transaction file itself must already contain the required signature or signatures.
### Success Output
```text
successful_broadcast: true
8d38f9e9f4f03fb936a6f45e1bcf1a9e706d7f3795e78b7ff16d5b4a4536d73
```
If every configured node fails or rejects the transaction, the tool prints the node error or:
```text
failed to connect
```
## lookup_block_by_height
`lookup_block_by_height` asks a configured node for a block at a specific height.
### Usage
```bash
./lookup_block_by_height <block_number>
```
Example:
```bash
./lookup_block_by_height 31445
```
The tool asks for the wallet path and wallet decryption key for the node handshake. This lookup does not spend funds.
### Example Return
The exact transaction list depends on the block contents, but the response is a decoded block JSON object:
```json
{
"vrf_block": {
"unmined_block": {
"timestamp": 1783657720,
"miner": "1439f758f93333734778a6459dc64b8d63a74c06.cltc",
"previous_hash": "<previous_block_hash>",
"next_block_difficulty": 1806884765625000,
"nonce": 42
},
"vrf": 123456789,
"proof": "<miner_signature_proof>"
},
"transactions": [
{
"Rewards": {
"unsigned": {
"txtype": 1,
"timestamp": 1783657720,
"value": 41666666666
}
}
}
]
}
```
If the node returns a text error, the text is printed directly. If binary data cannot be decoded as a block, the tool prints the raw response as hex JSON.
## lookup_block_by_hash
`lookup_block_by_hash` asks a configured node for a block by its block hash.
### Usage
```bash
./lookup_block_by_hash <block_hash>
```
Example:
```bash
./lookup_block_by_hash 8d38f9e9f4f03fb936a6f45e1bcf1a9e706d7f3795e78b7ff16d5b4a4536d73
```
The hash must be 64 hexadecimal characters.
The returned JSON has the same decoded block shape as `lookup_block_by_height`.
## lookup_torrent
`lookup_torrent` asks a configured node for the torrent metadata associated with a block height.
Blocks and torrents work together in Contractless. The block file contains the block header and transaction payload. The torrent metadata describes how peers can retrieve and verify the block data used during sync and relay.
### Usage
```bash
./lookup_torrent <block_number>
```
Example:
```bash
./lookup_torrent 31445
```
The tool asks for the wallet path and wallet decryption key for the node handshake. This lookup does not spend funds.
### Example Return
The exact fields depend on the torrent structure, but a successful response is printed as decoded torrent JSON.
```json
{
"announce": "<announce data>",
"info": {
"name": "31445.block",
"piece_length": 16384,
"pieces": "<piece hashes>"
}
}
```
If the torrent is not found, the node may return a text error such as:
```text
error: Block 31445 not found
```
If the response cannot be decoded as a torrent but contains bytes, the tool prints a hex JSON fallback.
## lookup_total_transactions
`lookup_total_transactions` asks a configured node for transaction totals grouped by transaction type.
### Usage
```bash
./lookup_total_transactions
```
The tool asks for the wallet path and wallet decryption key for the node handshake. This lookup does not spend funds.
### Example Return
```json
[
{
"txtype": 1,
"type": "rewards",
"total": 31445,
"non_zero": 31345
},
{
"txtype": 2,
"type": "transfer",
"total": 82
},
{
"txtype": 3,
"type": "token",
"total": 4
},
{
"txtype": 6,
"type": "swap",
"total": 1
}
]
```
For rewards, `total` is the total number of reward transactions and `non_zero` is the number of rewards that paid a non-zero block reward. This lets wallets and explorers show the first 100 free mined blocks while still distinguishing unpaid rewards from paid rewards.
For other transaction types, only `total` is returned.
## lookup_transaction
`lookup_transaction` asks a configured node for one transaction by transaction hash.
### Usage
```bash
./lookup_transaction <transaction_hash>
```
Example:
```bash
./lookup_transaction 8d38f9e9f4f03fb936a6f45e1bcf1a9e706d7f3795e78b7ff16d5b4a4536d73
```
The tool asks for the wallet path and wallet decryption key for the node handshake. This lookup does not spend funds.
### Example Return
For a transfer transaction, the output looks like:
```json
{
"block": 4484,
"transaction": {
"unsigned_transfer": {
"txtype": 2,
"time": 1782760786,
"value": 100000000,
"coin": "CLTC ",
"nft_series": 0,
"sender": "7ba97ce531937388ef8bca4d8a4ca54cefbefb87.cltc",
"receiver": "1439f758f93333734778a6459dc64b8d63a74c06.cltc",
"txfee": 1000000
},
"signature": "<signature>"
}
}
```
`block` is the confirmed block height where the transaction was found.
`transaction` contains the decoded transaction struct. The inner field name depends on the transaction type, such as `unsigned_transfer`, `unsigned_create_token`, `unsigned_nft`, `unsigned_swap`, `unsigned_loan_contract`, or another transaction-specific payload.
If the transaction is not found, the node may return a plain text error such as:
```text
Transaction not found in mempool.
```
## Common Workflow
A normal transfer flow is:
1. Create the signed transaction.
```bash
./create_transfer_tx
```
2. Review the saved JSON in `./transactions/`.
3. Broadcast the signed file.
```bash
./broadcast_transaction ./transactions/<transaction_hash>.json
```
4. Look up the transaction after it is submitted or confirmed.
```bash
./lookup_transaction <transaction_hash>
```
5. Look up the block after the transaction is confirmed.
```bash
./lookup_block_by_height <block_number>
```

File diff suppressed because it is too large Load Diff

397
docs/DATA_STORAGE.md Normal file
View File

@ -0,0 +1,397 @@
# Data Storage
Contractless data storage lets applications record user-owned public data on chain without relying on smart contracts.
Instead of an application contract owning and controlling user data, each wallet writes its own data under an application-style storage key. Other users and applications can read that data, but only the wallet that owns the address can update records under that address.
This is useful for applications that need public, user-controlled records, but it is not a private database and it is not a smart-contract state machine.
## What Data Storage Is For
Data storage is designed for application data that should be public, portable, and controlled by the user.
Good examples include:
- public user profiles
- usernames and display names
- social-network posts or profile fields
- public application preferences
- swap matchmaking listings
- loan matchmaking listings
- public marketplace listings
- public reputation fields
- app-specific public metadata
The key idea is user ownership. An application can define the meaning of fields, but the user signs the transaction that writes those fields. That means the user can update their own data at any time.
## What Data Storage Is Not For
Do not store sensitive data in plaintext.
Bad examples include:
- passwords
- private keys
- seed phrases
- API keys
- authentication tokens
- private customer records
- confidential business data
- anything a user or company would not want permanently public
Data storage should also not be used for data where the user must not be able to change the value themselves.
Bad authoritative-use examples include:
- account balances controlled by an app
- proof that a user paid a fee
- escrow balances
- internal accounting state
- access-control state that depends only on a user-written value
If an app treats the mere presence of user-written data as proof of payment, ownership, or entitlement, that app is probably using storage incorrectly. A user can update their own storage fields, so app designers must not treat those fields like trusted server-side state.
## Storage Keys
A storage key is the application identification key for a group of user data.
The storage key is created by a `StorageKey` transaction. The hash of that signed transaction becomes the storage key hash used by later storage value transactions.
Important details:
- Storage key transaction type: `100`
- Storage key creation fee: `500 CLTC`
- Storage key hash length: `32 bytes`
- Storage key hash display format: `64 hex characters`
- One address can create multiple storage keys.
- A storage key identifies a data namespace, but it is not owned exclusively by an app creator.
That last point matters. A storage key is best thought of as an app namespace. It lets wallets and applications agree on where to look for data, but users still own their own records under their own wallet address.
For example, an app may publish a storage key and say:
```text
Use this storage key for profile records.
Field username stores the public username.
Field bio stores the public biography.
Field active stores whether the profile is active.
```
Each user then writes their own values under that storage key.
## Storage Fields
Storage values are stored under:
```text
storage_key_hash + wallet_address + field_key
```
The `storage_key_hash` identifies the app namespace.
The `wallet_address` identifies the user whose data is being read or written.
The `field_key` identifies the app-defined field.
Field keys:
- must be 1 to 50 bytes
- are padded internally for fixed-size transaction storage
- should be stable and predictable for application use
Examples:
```text
username
bio
active
loan_offer
swap_offer
profile_url
```
## Supported Value Types
The `data_storage_tx` tool supports the following storage value types:
| Type | Transaction Type | Use |
| --- | ---: | --- |
| `bool` | `101` | true or false values |
| `u8` / `8bit` | `102` | small unsigned integer values |
| `u16` / `16bit` | `103` | unsigned 16-bit values |
| `u32` / `32bit` | `104` | unsigned 32-bit values |
| `u64` / `64bit` | `105` | unsigned 64-bit values |
| `u128` / `128bit` | `106` | large unsigned integer values |
| `string` | `107` | public text values up to 180 bytes per chunk |
Storage value transaction fee:
```text
1 CLTC
```
## String Storage
String values are stored in chunks of up to 180 bytes.
For short text, use a single string transaction with previous hash `0`.
For longer text, later string transactions can point to the previous string transaction hash. Contractless stores those chunks as incremented field keys internally:
```text
bio_00
bio_01
bio_02
```
The lookup command joins string chunks back together when returning data.
String chains are limited to 20 chunks:
```text
key_00 through key_19
```
That gives a practical maximum of 3,600 bytes for one logical string field. Larger application content should be split across multiple app-defined fields or handled by an application-specific linking scheme.
## Creating Storage Transactions
The storage transaction tool is:
```bash
./data_storage_tx
```
It creates signed transaction JSON files in the same style as the other transaction creation tools. Those transactions can then be broadcast with `broadcast_transaction`.
### Create a Storage Key
```bash
./data_storage_tx create_storage
```
Optional fee override:
```bash
./data_storage_tx create_storage 500.00000000
```
The tool will ask for:
- wallet file path
- wallet decryption key
The output includes the transaction hash. That hash is the storage key hash used when writing or reading data under this namespace.
### Write a Boolean
```bash
./data_storage_tx bool <storage_key_hash> <field_key> <true|false>
```
Example:
```bash
./data_storage_tx bool fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed active true
```
### Write an Unsigned Integer
```bash
./data_storage_tx u8 <storage_key_hash> <field_key> <value>
./data_storage_tx u16 <storage_key_hash> <field_key> <value>
./data_storage_tx u32 <storage_key_hash> <field_key> <value>
./data_storage_tx u64 <storage_key_hash> <field_key> <value>
./data_storage_tx u128 <storage_key_hash> <field_key> <value>
```
Aliases are also accepted:
```bash
8bit
16bit
32bit
64bit
128bit
```
Example:
```bash
./data_storage_tx u32 fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed reputation_score 120
```
### Write a String
```bash
./data_storage_tx string <storage_key_hash> <field_key> <value> [previous_hash|0]
```
For the first string chunk, use `0` as the previous hash:
```bash
./data_storage_tx string fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed username viraladmin 0
```
For a follow-up chunk, use the previous string transaction hash:
```bash
./data_storage_tx string fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed bio "Second chunk of public profile text" <previous_string_tx_hash>
```
The previous hash must either be:
- `0`, meaning no previous string chunk
- a 64-character hash of a previous storage string transaction for the same storage key, field key, and wallet address
## Broadcasting Storage Transactions
`data_storage_tx` creates the transaction file. It does not automatically place the transaction on chain.
Use:
```bash
./broadcast_transaction
```
Then select the saved storage transaction file.
The node validates that:
- the wallet address exists
- the signer owns the address being updated
- the storage key exists before storage values are written
- the fee is sufficient
- the field key is valid
- the value matches the selected type
- string previous-hash links are valid when used
Only the address owner can write or update records for that address.
## Pricing Storage Lookups
Public RPC nodes may charge a fee for returning storage data. The node operator sets the storage lookup fee per byte in `settings.ini`.
To price a lookup before paying:
```bash
./lookup_storage_cost <storage_key_hash> <data_key_or_all> <wallet_address>
```
Examples:
```bash
./lookup_storage_cost fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed username 1439f758f93333734778a6459dc64b8d63a74c06.cltc
```
```bash
./lookup_storage_cost fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed all 1439f758f93333734778a6459dc64b8d63a74c06.cltc
```
Arguments:
| Argument | Meaning |
| --- | --- |
| `storage_key_hash` | 64-character storage key transaction hash |
| `data_key_or_all` | one field key, or `all` to price every stored field for that wallet under that storage key |
| `wallet_address` | short, long, or vanity address whose stored data will be priced |
Example output:
```json
{
"payment_address": "7ba97ce531937388ef8bca4d8a4ca54cefbefb87.cltc",
"total_cost": 0.0025,
"cost_per_byte": 0.0001,
"total_bytes": 25
}
```
`payment_address` is the node wallet that receives the lookup payment.
`total_bytes` is the size of the compact returned data.
`cost_per_byte` is the node operator's configured fee per returned byte.
`total_cost` is the lookup fee before any transfer transaction fee.
## Looking Up Storage Data
To retrieve storage data:
```bash
./lookup_storage <storage_key_hash> <data_key_or_all> <wallet_address>
```
Examples:
```bash
./lookup_storage fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed username 1439f758f93333734778a6459dc64b8d63a74c06.cltc
```
```bash
./lookup_storage fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed all 1439f758f93333734778a6459dc64b8d63a74c06.cltc
```
The tool first asks the node for a quote. If payment is required, it builds and signs the transfer transaction for the lookup fee, shows the total spend, asks for confirmation, submits the payment, and then prints the returned data.
Example quote shown by `lookup_storage`:
```json
{
"payment_address": "7ba97ce531937388ef8bca4d8a4ca54cefbefb87.cltc",
"lookup_cost": 0.0025,
"txfee": 0.000025,
"total_cost": 0.002525,
"cost_per_byte": 0.0001,
"total_bytes": 25
}
```
Example returned data:
```json
{
"username": "viraladmin"
}
```
When `all` is used, the returned object contains every compact field the node found for that wallet under that storage key.
Example:
```json
{
"username": "viraladmin",
"active": true,
"uid": 12345678
}
```
## Lookup Fees and Node Operators
Storage lookup fees are paid to the node serving the lookup. This allows public RPC nodes to earn fees for application data access, including non-mining nodes that serve public RPC requests.
If the requester wallet is the same as the node wallet serving the request, the lookup does not need to pay itself. This lets app creators and node operators query their own node without setting the public lookup fee to zero.
## Application Design Notes
Apps should treat storage as public, user-owned, user-editable data.
Good app designs:
- publish a storage key for app users
- define stable field names
- read user records by storage key and wallet address
- validate user intent with wallet signatures
- treat user-written fields as claims made by that user
Risky app designs:
- storing private or sensitive data
- treating a storage field as proof of payment
- treating user-written values as trusted app balances
- assuming an app creator owns all data under a storage key
- assuming a user cannot later change their own data
Storage is powerful precisely because the user controls their own records. Application rules should be built around that fact.

View File

@ -108,7 +108,7 @@ Typical uses:
- inspect transaction fields
- apply or undo balance-sheet effects
For user-facing transaction behavior, see [TRANSACTIONS.md](src/branch/main/docs/TRANSACTIONS.md).
For user-facing transaction behavior, see the transaction-specific guides linked from [README.md](../README.md).
### Common Hashing and Types
@ -148,7 +148,7 @@ Typical uses:
- inspect client request helpers
- inspect response structures
For raw client handshake notes, see [DEV_CLIENTS.md](src/branch/main/docs/DEV_CLIENTS.md).
For raw client handshake notes, see [DEV_CLIENTS.md](DEV_CLIENTS.md).
### Standalone Tool Connections
@ -225,7 +225,7 @@ When adding a tool:
- 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](src/branch/main/docs/CLI_TOOLS.md)
- add the tool to the matching user-facing guide linked from [README.md](../README.md)
## Adding New RPC Commands
@ -242,9 +242,10 @@ When adding a command:
## Documentation Map
- [README.md](src/branch/main/README.md): build, configure and start a node
- [CLI_TOOLS.md](src/branch/main/docs/CLI_TOOLS.md): command-line tool reference
- [TRANSACTIONS.md](src/branch/main/docs/TRANSACTIONS.md): transaction behavior reference
- [POSTGRES.md](src/branch/main/docs/POSTGRES.md): manual PostgreSQL setup
- [SETTINGS.md](src/branch/main/docs/SETTINGS.md): `settings.ini` field reference
- [DEV_CLIENTS.md](src/branch/main/docs/DEV_CLIENTS.md): low-level client handshake and request notes
- [README.md](../README.md): build, configure and start a node
- [WALLET_TOOLS.md](WALLET_TOOLS.md): wallet command reference
- [VALIDATION_TOOLS.md](VALIDATION_TOOLS.md): validation command reference
- [BLOCKS_AND_TRANSACTIONS.md](BLOCKS_AND_TRANSACTIONS.md): block and transaction lookup 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

150
docs/FEES.md Normal file
View File

@ -0,0 +1,150 @@
# Fees
Contractless transactions have base fees. These are the minimum fees required for validation.
Wallet software may allow users to increase fees above the base minimum. Higher fees can make a transaction more attractive for miners when they are selecting pending transactions from the mempool.
Fees are paid in the base coin for the network. On testnet this is usually displayed as `CLTC`. On mainnet this is usually displayed as `CLC`.
## Base Fee Chart
| Transaction | Base Fee |
| --- | ---: |
| Base coin transfer | `1%` of transfer amount |
| Token transfer | `1.00000000` |
| NFT transfer | `1.00000000` |
| Numbered NFT transfer | `1.00000000` |
| Create token | `500.00000000` |
| Issue more tokens | `100.00000000` |
| Create NFT | `0.50000000` |
| Marketing record | `1.00000000` |
| Swap | `1.00000000` per signer |
| Create loan | `3.00000000` |
| Loan payment | `0.01000000` |
| Collateral claim | `3.00000000` |
| Burn token or NFT | `0.00010000` |
| Vanity address registration/update | `5.00000000` |
| Create storage key | `500.00000000` |
| Write storage value | `1.00000000` |
## Transfer Fees
Base coin transfers use a percentage fee:
```text
minimum fee = transfer amount * 1%
```
Token and NFT transfers use the fixed non-base transfer minimum:
```text
1.00000000
```
For numbered NFT collection transfers, the transfer amount must be exactly one NFT item, and the transfer fee is still the fixed non-base transfer fee.
## Swap Fees and Tips
Each swap signer pays the fixed swap base fee:
```text
1.00000000 per signer
```
Swaps also include asset-denominated miner tips for the trade sides.
Each side has two separate miner payments:
| Field | Paid By | Paid In | Paid To | Notes |
| --- | --- | --- | --- | --- |
| `txfee1` | `sender1` | Base coin | Miner | Must be at least `1.00000000`. |
| `txfee2` | `sender2` | Base coin | Miner | Must be at least `1.00000000`. |
| `tip1` | `sender1` | `ticker1` / side 1 asset | Miner | For fungible assets, must be at least 1% of `value1`. For NFT sides, must be `0`. |
| `tip2` | `sender2` | `ticker2` / side 2 asset | Miner | For fungible assets, must be at least 1% of `value2`. For NFT sides, must be `0`. |
The tips are not sent to the swap counterparty. They are paid to the miner who includes the swap in a block.
For fungible swap sides, spendability is checked against the offered amount plus the tip:
```text
sender1 must have value1 + tip1 of ticker1
sender2 must have value2 + tip2 of ticker2
```
Each sender must also have enough base coin for their own base transaction fee.
## Loan Payment Fees and Tips
Loan payments have a base fee:
```text
0.01000000
```
Loan payments also include an asset-denominated miner tip.
| Field | Paid By | Paid In | Paid To | Notes |
| --- | --- | --- | --- | --- |
| `txfee` | Borrower / payer | Base coin | Miner | Must be at least `0.01000000`. |
| `payback_amount` | Borrower / payer | Loan asset | Lender | Counts toward the loan balance. |
| `tip` | Borrower / payer | Loan asset | Miner | Must be at least 1% of `payback_amount`. |
The lender receives only the `payback_amount`. The miner receives the base fee and the tip.
Spendability is checked against the payment amount plus the tip:
```text
borrower must have payback_amount + tip of the loan asset
borrower must also have txfee in base coin
```
Pending loan payments in the mempool also reserve the tip amount. This prevents a borrower from creating multiple pending payments that appear valid only because the tips were ignored.
## Storage Lookup Fees
Storage write fees are normal transaction fees and are listed in the base fee chart.
Storage lookups are different. A node operator may charge a per-byte lookup fee for returning app storage data through RPC. That fee is configured by the node operator in `settings.ini` and is not a fixed network-wide transaction fee.
Use `lookup_storage_cost` to quote storage lookup fees before paying for a lookup. See [Data Storage](src/branch/main/docs/DATA_STORAGE.md).
## lookup_largest_txfee
`lookup_largest_txfee` asks a configured node for the largest transaction fee currently known from its mempool data.
This can help users or wallet software understand what base fee level is currently competing for miner selection.
For swaps, the lookup compares both base fee fields, `txfee1` and `txfee2`. It does not include asset-denominated swap tips.
For loan payments, the lookup uses the base `txfee`. It does not include the asset-denominated loan payment tip.
Usage:
```bash
./lookup_largest_txfee
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Expected output:
```json
{
"largest_tx_fee": "1.00000000"
}
```
If the peer has no mempool transactions, the tool returns:
```json
{
"largest_tx_fee": "0.00000000"
}
```
The value is only a snapshot from the peer that answered the request. Different nodes may temporarily report different mempool fee data.

233
docs/LINUX_INSTALLATION.md Normal file
View File

@ -0,0 +1,233 @@
# Linux Installation
This guide explains how to build, install, configure, and start a Contractless testnet node on Linux.
Contractless can be run from the build folder, from a user-owned install folder, or from a system path such as `/usr/bin`. For public testnet nodes, use a stable run path and a stable `settings.ini` location so runtime data does not move unexpectedly between starts.
## Build Flags
The default build target is testnet:
```bash
cargo build --release
```
The same testnet build can be requested explicitly:
```bash
cargo build --release --features testnet
```
Mainnet has its own feature flag:
```bash
cargo build --release --no-default-features --features mainnet
```
Mainnet is not active during the testnet phase. Use the testnet binary unless mainnet has been officially enabled.
After building, release binaries are written to:
```text
target/release/
```
The testnet node binary is:
```text
target/release/contractless-testnet
```
## PostgreSQL Installation
**Do not expose PostgreSQL to the public Internet. The PostgreSQL port should not be publicly accessible.** Contractless needs a public RPC port for peers, but PostgreSQL should stay local or private, usually on `127.0.0.1`. Do not open PostgreSQL port `5432` in your router, cloud firewall, VPS firewall, or host firewall.
The installer only supports Linux systems where this `apt-get` flow is valid. If your Linux distribution does not use `apt-get`, [create the PostgreSQL database manually](src/branch/main/docs/POSTGRES.md).
Contractless uses PostgreSQL for transaction lookup and mempool-style records. The node creates and updates its own tables during startup, but PostgreSQL itself must exist first with a database, user, password, and permissions.
Build the release binaries first:
```bash
cargo build --release
```
Run the PostgreSQL installer with root access:
```bash
sudo ./target/release/postgres_installer
```
The Linux installer:
- checks whether `psql` is installed
- runs `apt-get update` when PostgreSQL is missing
- installs PostgreSQL with `apt-get install -y postgresql`
- updates local password authentication in `pg_hba.conf` when needed
- reloads PostgreSQL
- creates the configured database user
- creates the configured database
- prints the `[Postgres]` settings block to paste into `settings.ini`
For testnet builds, paste the printed values into `[Postgres-Testnet]` in the active `settings.ini`.
Example:
```ini
[Postgres-Testnet]
host = 127.0.0.1
port = 5432
user = contractless
password = your_postgres_password_here
dbname = contractless_db
```
## Run Path and Settings Location
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. `/etc/contractless/settings.ini`
For a stable Linux install, use:
```text
/etc/contractless/settings.ini
```
Create the config directory:
```bash
sudo mkdir -p /etc/contractless
```
Move the repository settings file:
```bash
sudo mv ./settings.ini /etc/contractless/settings.ini
```
The node also scopes runtime folders by network internally, so testnet data and mainnet data do not collide.
Important runtime paths:
| Setting | Purpose |
| --- | --- |
| `BLOCK_PATH` | Saved block files |
| `TORRENT_PATH` | Torrent metadata and staged torrents |
| `DB_PATH` | sled state and Linux PID file |
| `BALANCE_SHEET` | Balance files |
| `LOG_PATH` | Runtime logs |
| `WALLET_PATH` | Wallet directory |
| `WALLET_NAME` | Wallet filename |
## Copy Binaries
Copy the testnet node binary into a system path:
```bash
sudo cp ./target/release/contractless-testnet /usr/bin/
```
Copy the PostgreSQL installer if you want it available system-wide:
```bash
sudo cp ./target/release/postgres_installer /usr/bin/
```
Copy wallet tools:
```bash
sudo cp ./target/release/create_new_wallet /usr/bin/
sudo cp ./target/release/recreate_wallet /usr/bin/
sudo cp ./target/release/recreate_wallet_from_image /usr/bin/
sudo cp ./target/release/register_wallet /usr/bin/
sudo cp ./target/release/verify_address /usr/bin/
```
Copy transaction and lookup tools as needed:
```bash
sudo cp ./target/release/create_transfer_tx /usr/bin/
sudo cp ./target/release/broadcast_transaction /usr/bin/
sudo cp ./target/release/lookup_height /usr/bin/
sudo cp ./target/release/lookup_transaction /usr/bin/
sudo cp ./target/release/lookup_remote_balance /usr/bin/
```
You can copy any additional tools from `target/release` using the same pattern.
## Start the Node
Start the testnet node:
```bash
contractless-testnet
```
On Linux, the node prompts for the wallet decryption key and then detaches into the background by default.
Use a specific settings file:
```bash
contractless-testnet --config /etc/contractless/settings.ini
```
## Startup Flags
Linux node flags:
| Flag | Purpose |
| --- | --- |
| `--config <path>` | Load a specific `settings.ini` file |
| `--status` | Check whether the daemonized node is running |
| `--stop` | Stop the daemonized node |
Check daemon status:
```bash
contractless-testnet --status
```
Stop the daemon:
```bash
contractless-testnet --stop
```
The Linux daemon PID file is stored under the active network's `DB_PATH`.
## Updating a Node
Stop the node:
```bash
contractless-testnet --stop
```
Pull the latest source:
```bash
git pull
```
Rebuild:
```bash
cargo build --release
```
Copy the updated binary:
```bash
sudo cp ./target/release/contractless-testnet /usr/bin/
```
Start the node:
```bash
contractless-testnet
```

588
docs/LOAN_TRANSACTIONS.md Normal file
View File

@ -0,0 +1,588 @@
# Loan Transactions
Contractless loans are native two-party loan contracts. They do not use a smart-contract VM, and they are not simple wallet notes. A confirmed loan contract moves real balances on chain:
- The lender's loan asset moves to the borrower.
- The borrower's collateral moves into a chain-tracked collateral holding balance.
- Later loan payments move repayment value back to the lender.
- A collateral claim closes the contract by moving the collateral either back to the borrower or to the lender.
Loans are one of the most unique transaction categories in Contractless. This guide explains how the loan contract, loan payment, and collateral claim transactions work together.
On Windows, add `.exe` to each command name.
When a tool asks for a wallet path or transaction file path, press `<Tab>` to search and auto-complete files and folders.
## CLI Tools
| Tool | Purpose |
| --- | --- |
| `create_loan_tx` | Creates the lender-signed loan offer. |
| `verify_sign_loan_tx` | Lets the borrower review terms, second-sign the loan, and save the completed contract. |
| `create_loan_payment_tx` | Creates a borrower payment transaction for an active loan. |
| `create_collateral_claim_tx` | Creates a collateral claim transaction. Used by either borrower or lender depending on contract state. |
| `lookup_loan_by_address` | Lists loan contracts involving a wallet address. |
| `lookup_loan_by_hash` | Looks up one loan contract by contract hash. |
Creation tools save signed transaction JSON under:
```text
./transactions/<hash>.json
```
Saving a transaction file does not broadcast it. Submit completed transactions with `broadcast_transaction` or with the GUI wallet import/broadcast flow.
## Three Transaction Types
Loans use three native transaction types:
| Transaction | Type | Who Signs | What It Does |
| --- | ---: | --- | --- |
| Loan contract | `7` | Lender and borrower | Creates the loan, disburses the loaned asset, and locks collateral. |
| Loan payment | `8` | Borrower | Pays some or all of the scheduled repayment balance. |
| Collateral claim | `9` | Borrower or lender | Returns collateral to borrower after full repayment, or transfers collateral to lender when claim rules are met. |
## Loan Lifecycle
1. The lender creates a loan offer with `create_loan_tx`.
2. The lender sends the saved JSON file to the borrower.
3. The borrower reviews and signs the loan with `verify_sign_loan_tx`.
4. The completed loan contract is broadcast.
5. When the loan confirms, the loaned asset moves from lender to borrower.
6. At the same time, collateral moves from borrower to a collateral holding balance.
7. The borrower makes one or more payments with `create_loan_payment_tx`.
8. When fully paid, the borrower uses `create_collateral_claim_tx` to reclaim collateral.
9. If the borrower becomes delinquent enough, the lender uses `create_collateral_claim_tx` to claim collateral.
## Loan Contract Creation
Use `create_loan_tx` to create the lender side of a loan.
Usage:
```text
create_loan_tx
```
The active wallet becomes the lender. The output is only partially signed. The borrower must review and sign it before it can be broadcast.
### Loan Contract Fields
| Field | Size | Meaning |
| --- | ---: | --- |
| `txtype` | 1 byte | Transaction type. For loan contracts this is `7`. |
| `timestamp` | 4 bytes | Loan start timestamp. Created from the entered start date at UTC midnight. |
| `loan_coin` | 15 bytes | Base coin or token being lent, padded to 15 bytes. |
| `loan_amount` | 8 bytes | Amount lent, in atomic units. |
| `lender` | 22 bytes | Lender short address. |
| `collateral` | 21 bytes | Base coin, token, or numbered NFT collateral field. |
| `collateral_amount` | 8 bytes | Collateral amount in atomic units. For NFT collateral, use `1`. |
| `borrower` | 22 bytes | Borrower short address. |
| `payment_period` | 1 byte | `d`, `w`, or `m` for daily, weekly, or monthly. |
| `payment_number` | 1 byte | Total number of scheduled payments. |
| `payment_amount` | 8 bytes | Amount due per scheduled payment, in atomic units. |
| `grace_period` | 1 byte | Number of overdue payments allowed before normal lender claim can become possible. |
| `max_late_value` | 8 bytes | Overdue value threshold for normal lender collateral claim. |
| `txfee` | 8 bytes | Lender base-currency transaction fee. Minimum is `3` CLC or CLTC. |
| `hash` | 32 bytes | Hash of the unsigned loan contract payload. |
| `signature1` | 666 bytes | Lender signature. |
| `signature2` | 666 bytes | Borrower signature. Present only after borrower signs. |
### Prompts
`create_loan_tx` asks for:
| Prompt | What To Enter |
| --- | --- |
| Loan coin/token | Base coin or existing token being lent. |
| Loan amount | Amount lent in display units. |
| Payment period | `daily`, `weekly`, or `monthly`. |
| Payment number | Number of scheduled payments required. |
| Payment amount | Amount due each payment period. |
| Grace period | How many overdue payments are allowed before normal lender claim can become possible. |
| Max late value | How far behind in value the borrower may be before normal lender claim can become possible. |
| Collateral coin/token/NFT | Base coin, token, or NFT collateral. Numbered NFTs use the `name_#####` form. |
| Collateral amount | Amount of collateral. Enter `1` for NFT collateral. |
| Start date | Loan start date as `YYYY-MM-DD`. Stored as UTC midnight for that date. |
| Wallet path | Lender wallet path. |
| Wallet key | Lender wallet decryption key. |
| Borrower address | Borrower short address or vanity address. |
| Transaction fee | Minimum `3` CLC or CLTC. |
### Loan Creation Rules
A loan contract is valid only when:
- Both lender and borrower addresses are registered and valid.
- Lender and borrower both sign the exact same loan hash.
- The lender has enough of the loan asset to fund the loan.
- The lender has enough base currency for the loan creation fee.
- The borrower has enough collateral.
- The loan asset exists as base currency or a token.
- The collateral exists as base currency, a token, or an NFT.
- `payment_period` is `d`, `w`, or `m`.
- `payment_amount` is not greater than `loan_amount`.
- `payment_amount * payment_number` is at least the `loan_amount`.
- `max_late_value` is not greater than `loan_amount`.
- `grace_period` is not greater than `payment_number`.
- The contract is broadcast within 30 days of the stored timestamp.
### What Happens When The Loan Confirms
When a valid loan contract is mined:
- The lender pays the base-currency fee to the miner.
- The loaned asset moves from lender to borrower.
- The collateral moves from borrower to a collateral holding balance named from the contract hash.
- The contract is indexed for both lender and borrower.
- The loan is marked active.
The collateral is not held by the lender while the loan is active. It is held by the chain under a derived collateral holding balance.
## Borrower Review And Second Signature
The borrower signs the lender-created file with `verify_sign_loan_tx`.
Usage:
```text
verify_sign_loan_tx <path/to/file.json>
```
The tool:
1. Loads the lender-signed loan JSON.
2. Loads the borrower wallet.
3. Confirms the active wallet is the borrower in the contract.
4. Rebuilds the loan hash from the JSON fields.
5. Shows the borrower human-readable questions about the loan terms.
6. Signs only if the rebuilt hash matches the lender's hash.
7. Saves the completed two-signature loan contract JSON.
The borrower is asked to confirm:
- Amount and asset they expect to receive.
- Collateral amount and asset they agree to lock.
- Payment period.
- Total number of payments.
- Amount of each payment.
- Grace period.
- Max late value.
- Start date.
- Lender wallet address.
After borrower signing, the output contains both `signature1` and `signature2` and can be broadcast.
## Payment Schedule
Loan schedules are based on the loan `timestamp`, which is created from the start date at UTC midnight.
Payment periods:
| Code | Meaning | First Payment Due |
| --- | --- | --- |
| `d` | Daily | 1 day after start date |
| `w` | Weekly | 7 days after start date |
| `m` | Monthly | 1 calendar month after start date |
A payment is not overdue until the full due date has passed in UTC.
Example:
```text
Start date: March 15
Period: daily
First payment due: March 16
First payment overdue: March 17 at 00:00 UTC
```
Weekly works the same way, but with 7-day intervals.
Monthly payments are anchored to the original calendar day. If a month does not have that day, the due date uses the last day of that month.
Example:
```text
Start date: January 31
First monthly due date: February 28, or February 29 in a leap year
Second monthly due date: March 31
```
## Loan Payments
Use `create_loan_payment_tx` to create a borrower payment.
Usage:
```text
create_loan_payment_tx
```
The transaction type is `8`.
### Loan Payment Fields
| Field | Size | Meaning |
| --- | ---: | --- |
| `txtype` | 1 byte | Transaction type. For loan payments this is `8`. |
| `timestamp` | 4 bytes | Payment creation timestamp. |
| `payback_amount` | 8 bytes | Amount paid toward the contract, in the loan asset atomic units. |
| `contract_hash` | 32 bytes | Hash of the loan contract being paid. |
| `address` | 22 bytes | Payer short address. |
| `tip` | 8 bytes | Miner tip paid in the loan asset. Must be at least 1% of payment amount. |
| `txfee` | 8 bytes | Base-currency transaction fee. Minimum is `0.01` CLC or CLTC. |
| `hash` | 32 bytes | Hash of the payment transaction. |
| `signature` | 666 bytes | Payer signature. |
### Payment Prompts
`create_loan_payment_tx` asks for:
| Prompt | What To Enter |
| --- | --- |
| Loan contract hash | Contract hash of the loan being paid. |
| Payment amount | Amount paid toward the loan in display units. |
| Miner tip | Tip in the loan asset. Must be at least 1% of payment amount. |
| Fee | Minimum `0.01` CLC or CLTC. |
| Wallet path | Borrower wallet path. |
| Wallet key | Borrower wallet decryption key. |
### Payment Rules
Payments are flexible:
- The borrower may make partial payments.
- The borrower may pay more than one scheduled payment at once.
- The borrower may pay the full remaining balance early.
But payments cannot exceed the remaining contract balance. The node also checks pending loan payments in the mempool so multiple pending payments cannot collectively overpay the loan.
Payment totals are tracked by value, not by "number of payment transactions."
Example:
```text
Scheduled payment amount: 210
Borrower sends five payments of 1
Total paid: 5
Completed scheduled payment value: 5 of 210
```
Those five small payments do not count as five full scheduled payments. They only reduce the overdue value by `5`.
When a payment confirms:
- The borrower pays the base-currency fee to the miner.
- The borrower pays `payback_amount` of the loan asset to the lender.
- The borrower pays `tip` of the loan asset to the miner.
- The payment amount is appended to the contract payment history.
## Collateral Claims
Use `create_collateral_claim_tx` to create a collateral claim.
Usage:
```text
create_collateral_claim_tx
```
The transaction type is `9`.
The same transaction type is used by:
- Borrower reclaiming collateral after full repayment.
- Lender claiming collateral after the borrower is sufficiently delinquent.
### Collateral Claim Fields
| Field | Size | Meaning |
| --- | ---: | --- |
| `txtype` | 1 byte | Transaction type. For collateral claims this is `9`. |
| `time` / `timestamp` | 4 bytes | Claim creation timestamp. |
| `contract_hash` | 32 bytes | Hash of the loan contract. |
| `address` | 22 bytes | Claimant short address. Must be lender or borrower. |
| `txfee` | 8 bytes | Base-currency transaction fee. Minimum is `3` CLC or CLTC. |
| `signature` | 666 bytes | Claimant signature. |
### Claim Prompts
`create_collateral_claim_tx` asks for:
| Prompt | What To Enter |
| --- | --- |
| Loan contract hash | Contract hash of the loan being claimed. |
| Fee | Minimum `3` CLC or CLTC. |
| Wallet path | Claimant wallet path. |
| Wallet key | Claimant wallet decryption key. |
Only the lender or borrower can claim collateral.
## Borrower Collateral Reclaim
The borrower can reclaim collateral only after the loan is fully paid.
The full repayment requirement is:
```text
payment_amount * payment_number
```
The borrower cannot reclaim collateral while any part of that scheduled total remains unpaid.
When borrower reclaim confirms:
- The borrower pays the base-currency collateral claim fee to the miner.
- Collateral moves from the collateral holding balance back to the borrower.
- The loan is marked inactive.
## Lender Collateral Claim
Lender collateral claims have two normal conditions:
1. The borrower must be past the allowed missed-payment grace period.
2. The overdue value must be greater than `max_late_value`.
Both normal conditions must be met. One condition alone is not enough.
### Condition 1: Grace Period
The node calculates how many scheduled payments are overdue as of the claim time.
A payment only becomes overdue after the full UTC due date has passed.
The lender cannot claim while:
```text
payments_due <= grace_period
```
The normal grace condition is met only when:
```text
payments_due > grace_period
```
Example:
```text
grace_period = 1
payments_due = 1
claim rejected
grace_period = 1
payments_due = 2
grace condition met
```
### Condition 2: Overdue Value
The node calculates:
```text
should_have_paid = payments_due * payment_amount
overdue_value = should_have_paid - total_paid
```
The normal overdue-value condition is met only when:
```text
overdue_value > max_late_value
```
Equal is not enough.
Example:
```text
max_late_value = 210
overdue_value = 210
claim rejected
max_late_value = 210
overdue_value = 211
claim allowed if grace condition is also met
```
This prevents collateral from being claimed merely because a scheduled due date passed. The borrower must be both late enough in time and behind enough in value.
### Partial Payment Example
Assume:
```text
payment_amount = 210
grace_period = 1
max_late_value = 210
```
If the borrower sends five payments of `1`, total paid is `5`.
After the first payment is overdue:
```text
payments_due = 1
should_have_paid = 210
total_paid = 5
overdue_value = 205
```
The claim is rejected because:
- `payments_due <= grace_period`
- `overdue_value <= max_late_value`
After the second payment is overdue:
```text
payments_due = 2
should_have_paid = 420
total_paid = 5
overdue_value = 415
```
The claim can be allowed because:
- `payments_due > grace_period`
- `overdue_value > max_late_value`
## Final Term Plus Grace Rule
There is one special final closeout rule.
After the full loan term plus the allowed grace period has passed, any unpaid remaining balance allows the lender to claim collateral.
This rule exists to prevent a borrower from leaving a small final balance unpaid forever.
Example:
```text
payment_amount = 100
payment_number = 5
grace_period = 1
max_late_value = 100
remaining_balance = 99
```
Under the normal overdue-value rule, `99` is not greater than `100`, so the lender might never be able to claim collateral.
The final closeout rule fixes that:
```text
full loan term + grace period has passed
remaining_balance > 0
lender can claim collateral
```
This final rule still has a time requirement. The lender must wait until the full schedule plus grace period has passed. What it ignores is the normal `max_late_value` threshold, because any unpaid amount after the final grace window means the loan was not fully repaid.
## Lookup By Hash
Use `lookup_loan_by_hash` to inspect one loan contract.
Usage:
```text
lookup_loan_by_hash <loan_hash>
```
The tool asks for a wallet path and wallet key for authenticated RPC handshake. The lookup does not spend funds.
Expected output includes:
```json
{
"contract": "loan_contract_hash",
"status": "active",
"creation_date": "06-30-26",
"start_timestamp": 1782777600,
"lender": "lender_short_address.cltc",
"borrower": "borrower_short_address.cltc",
"coin_loaned": "CLTC",
"loaned_count": 1000.0,
"loaned_count_atomic": 100000000000,
"collateral": "tokenname",
"collateral_count": 2000.0,
"collateral_count_atomic": 200000000000,
"payment_type": "daily",
"number_of_payments": 5,
"payment_value": 210.0,
"payment_value_atomic": 21000000000,
"max_late_payments": 1,
"max_late_value": 210.0,
"max_late_value_atomic": 21000000000,
"total_payments_made": 1,
"total_value_paid": 210.0,
"total_value_paid_atomic": 21000000000,
"pending_value": 0.0,
"pending_value_atomic": 0,
"remaining_balance": 840.0,
"remaining_balance_atomic": 84000000000,
"remaining_after_pending": 840.0,
"remaining_after_pending_atomic": 84000000000,
"collateral_claimed_by": "",
"payments": [
{
"txid": "payment_txid",
"amount": 210.0,
"payee": "borrower_short_address.cltc",
"date": "07-01-26"
}
]
}
```
Statuses:
| Status | Meaning |
| --- | --- |
| `active` | Loan is active and not currently delinquent by due schedule. |
| `delinquent` | Scheduled expected payments exceed confirmed paid value. |
| `inactive` | Collateral has been claimed or returned. |
`pending_value` is the total of pending mempool payments known to the node. It is shown for wallet/user awareness, but consensus validation of blocks uses confirmed payment history.
In the payment history, `payee` is the field name returned by the current lookup parser for the payment transaction address. In normal borrower payments, this is the borrower/payer address from the payment transaction.
## Lookup By Address
Use `lookup_loan_by_address` to list loan contracts involving a wallet address.
Usage:
```text
lookup_loan_by_address <wallet_address>
```
The address can be a short address or vanity address. The tool asks for a wallet path and wallet key for authenticated RPC handshake. The lookup does not spend funds.
Expected output:
```json
{
"address": "wallet_short_address.cltc",
"contracts": 2,
"loans": [
{
"contract": "loan_contract_hash",
"status": "active",
"lender": "lender_short_address.cltc",
"borrower": "borrower_short_address.cltc",
"payments": []
}
]
}
```
Each loan entry uses the same summary fields as `lookup_loan_by_hash`.
## Common Problems
| Problem | Likely Cause |
| --- | --- |
| Loan contract rejected | Missing second signature, bad hash, invalid asset, insufficient lender balance, insufficient borrower collateral, fee below `3`, bad payment schedule, or broadcast more than 30 days after timestamp. |
| Borrower cannot sign | Active wallet is not the borrower, terms were changed after lender signed, or the borrower rejected one of the review questions. |
| Payment rejected | Contract not active, contract hash is not a loan, payment exceeds remaining balance, pending payments would overpay, tip below 1%, fee below `0.01`, or borrower lacks funds. |
| Borrower collateral reclaim rejected | Loan is not fully paid. |
| Lender collateral claim rejected | Grace period has not passed, overdue value is not greater than `max_late_value`, final closeout window has not passed, collateral already claimed, or fee is below `3`. |
| Lookup returns no loan | The contract is not confirmed, the hash is not a loan contract, or the queried address is not lender or borrower on any indexed contract. |

View File

@ -0,0 +1,263 @@
# Marketing Transactions
Marketing transactions are native Contractless records intended for marketing and advertising agencies. They let an agency record campaign activity on chain so a customer can later verify impressions, clicks, display locations, keywords, campaign identifiers, and related value fields.
There is one CLI tool for creating marketing records:
```text
create_marketing_tx
```
On Windows, add `.exe` to the command name.
When a tool asks for a wallet path, press `<Tab>` to search and auto-complete files and folders.
## Purpose
Marketing records are designed for campaign reporting.
Example uses:
- Record that an ad was displayed on a specific URL.
- Record impressions and clicks for a campaign.
- Record the keyword or targeting term associated with an impression/click record.
- Record impression and click values for later customer reporting.
- Let a customer verify that a marketing agency recorded activity on chain.
The `advertiser` field is the agency or reporting wallet that signs and pays for the record. It is not necessarily the customer wallet.
## Lookup Notes
There is no dedicated marketing lookup CLI tool.
To inspect marketing records from CLI tools, use general transaction lookup flows:
- Look up a known marketing transaction hash.
- Look up transactions for an address and filter for marketing transaction records.
The customer generally needs to know either:
- The transaction hash of the marketing record, or
- The address of the agency/reporting wallet and enough context to filter its marketing transactions.
## Transaction Type
Marketing transactions are transaction type `5`.
## Transaction Fields
| Field | Size | Meaning |
| --- | ---: | --- |
| `txtype` | 1 byte | Transaction type. For marketing records this is `5`. |
| `time` / `timestamp` | 4 bytes | UTC timestamp when the transaction is created. |
| `campaign` | 8 bytes | Numeric campaign identifier. |
| `ad_type` | 6 bytes | Campaign type. Must be `banner`, `social`, or `text`, padded to 6 bytes. |
| `keyword` | 40 bytes | Keyword or targeting term, padded to 40 bytes. |
| `displayed` | 100 bytes | Display location, usually a URL or placement identifier, padded to 100 bytes. |
| `impression` | 1 byte | Impression count update, `0` to `255`. |
| `click` | 1 byte | Click count update, `0` to `255`. |
| `impression_value` | 2 bytes | Value per impression multiplied by `100`. |
| `click_value` | 2 bytes | Value per click multiplied by `100`. |
| `advertiser` | 22 bytes | Agency/reporting wallet short address. |
| `txfee` | 8 bytes | Base-currency transaction fee. Minimum is `1` CLC or CLTC. |
| `signature` | 666 bytes | Advertiser signature. |
## create_marketing_tx
Use `create_marketing_tx` to create and sign a marketing transaction.
Usage:
```text
create_marketing_tx
```
The tool writes the signed transaction JSON under:
```text
./transactions/<hash>.json
```
Saving the file does not broadcast it. Submit the saved transaction with `broadcast_transaction` or another normal transaction broadcast flow.
## Prompts
`create_marketing_tx` asks for:
| Prompt | What To Enter |
| --- | --- |
| Campaign ID | Numeric campaign identifier. |
| Campaign type | `banner`, `social`, or `text`. |
| Keyword | Keyword or targeting term, up to 40 characters. |
| Displayed location | URL, placement, or display location, up to 100 characters. |
| Views | Impression count for this record, `0` to `255`. |
| Clicks | Click count for this record, `0` to `255`. |
| Impression value | Decimal value for each impression, such as `1.25`. |
| Click value | Decimal value for each click, such as `1.25`. |
| Fee | Minimum `1` CLC or CLTC. |
| Wallet path | Agency/reporting wallet path. |
| Wallet key | Wallet decryption key. |
The tool pads fixed-width text fields automatically before signing:
- `ad_type` is padded to 6 bytes.
- `keyword` is padded to 40 bytes.
- `displayed` is padded to 100 bytes.
## Campaign ID
`campaign` is a numeric ID chosen by the advertiser/agency.
The chain does not define campaign meaning. An agency may use:
- A customer account number.
- An internal campaign number.
- A campaign/ad group ID from another system.
- A manually assigned reporting ID.
Customers should confirm with the agency which campaign ID should be tracked.
## Ad Type
`ad_type` must be one of:
```text
banner
social
text
```
The field is fixed at 6 bytes. `banner` and `social` already fill 6 bytes. `text` is padded with two spaces by the tool.
## Keyword
`keyword` is a 40-byte field. It can be a search term, targeting category, content keyword, placement keyword, or another short campaign descriptor.
Examples:
```text
blockchain wallet
testnet node
summer sale
```
## Displayed Location
`displayed` is a 100-byte field for where the ad was displayed.
Common values:
- A URL.
- A website domain.
- A social platform placement.
- An ad slot identifier.
- A newsletter or publisher placement ID.
Example:
```text
https://example.com/article/123
```
If the location is longer than 100 characters, shorten it before creating the transaction.
## Impressions And Clicks
Each marketing transaction records one update, not an entire campaign database.
`impression` is an unsigned byte, so one transaction can record `0` through `255` impressions.
`click` is also an unsigned byte, so one transaction can record `0` through `255` clicks.
Large campaigns should be recorded as multiple marketing transactions over time.
## Impression And Click Values
The CLI asks for decimal values and stores each value multiplied by `100`.
Example:
```text
Entered impression value: 1.25
Stored impression_value: 125
Entered click value: 2.50
Stored click_value: 250
```
This keeps value fields compact while preserving two decimal places.
The chain records the values supplied by the advertiser. It does not independently verify market price, invoice terms, or whether the agency's claimed value is fair. Customers should compare on-chain records to their agreement with the agency.
## Fees And Signing
The advertiser wallet signs the transaction and pays the base-currency fee.
Validation requires:
- Advertiser address is valid.
- Advertiser address is registered.
- Signature matches the advertiser wallet.
- Fee is at least `1` CLC or CLTC.
- Advertiser has enough base currency for the fee.
- `ad_type` is `banner`, `social`, or `text`.
- `keyword` is exactly 40 bytes after padding.
- `displayed` is exactly 100 bytes after padding.
- Transaction hash has not already been saved on chain.
## Output Shape
The saved JSON looks like:
```json
{
"txtype": 5,
"timestamp": 1780000000,
"campaign": 12345,
"ad_type": "banner",
"keyword": "blockchain wallet ",
"displayed": "https://example.com/article/123 ",
"impression": 100,
"click": 5,
"impression_value": 125,
"click_value": 250,
"advertiser": "advertiser_short_address.cltc",
"txfee": 100000000,
"hash": "transaction_hash",
"signature": "advertiser_signature"
}
```
In this example:
- `impression = 100`
- `click = 5`
- `impression_value = 125`, meaning `1.25`
- `click_value = 250`, meaning `2.50`
- `txfee = 100000000`, meaning `1` CLC or CLTC
## Customer Verification
A customer verifying campaign records should ask the agency for:
- Agency wallet address.
- Campaign ID.
- Expected ad types.
- Expected display URLs or placements.
- Expected keywords.
- Relevant transaction hashes, if available.
The customer can then inspect the agency's marketing transactions and compare:
- `campaign`
- `ad_type`
- `keyword`
- `displayed`
- `impression`
- `click`
- `impression_value`
- `click_value`
- transaction timestamp
Because the transaction is signed by the advertiser wallet, the customer can verify that the agency wallet recorded the campaign activity on chain.

260
docs/MEMPOOL.md Normal file
View File

@ -0,0 +1,260 @@
# Mempool
The mempool is the node's pending transaction area. Transactions enter the mempool after they are decoded, verified, stored in PostgreSQL, and relayed to connected peers.
Contractless uses mempool state during transaction validation. Pending balance changes, transaction fees, loan payments, swap tips, storage fees, and other unconfirmed effects are included in the checks so a wallet cannot normally spend the same confirmed balance twice while transactions are still waiting to be mined.
Contractless does not have replace-by-fee. There is no transaction replacement mechanism, no fee bumping mechanism, and no user-facing mempool removal command. A pending transaction either remains pending, becomes processed when included in a block, is restored during a short orphan rollback, or disappears when the node restarts and the live mempool is cleared.
## Tools
This page covers:
- `lookup_mempool_tx_by_address`
- `lookup_mempool_tx_by_signature`
- `lookup_mempool_tx_count`
On Windows, the compiled binaries usually end in `.exe`. For example, `lookup_mempool_tx_count` becomes `lookup_mempool_tx_count.exe`.
## Important Behavior
### Pending Balances
Most transaction verification paths call the shared balance check before accepting a transaction into the mempool.
That check compares the saved wallet balance against already pending mempool usage:
- Base coin transfers reserve the transfer amount plus the fee.
- Token and NFT transfers reserve the token/NFT amount and still reserve the base coin fee.
- Token creation, NFT creation, marketing records, vanity address transactions, storage keys, and storage values reserve their base coin fee.
- Swaps reserve each side's offered asset, tip, and base coin fee.
- Loan contracts reserve the lender's loan amount, the borrower's collateral, and the loan fee.
- Loan payments reserve the payment amount, tip, and fee.
- Collateral claims reserve the claim fee.
This is why a wallet balance in the GUI can differ from a confirmed-chain-only lookup: the wallet tries to show spendable state after pending transactions are considered.
### No Replace-By-Fee
Once a transaction is accepted into a node's mempool, the protocol does not provide a way to replace it with another transaction by paying a higher fee.
If a user broadcasts a second transaction that tries to spend the same funds already reserved by a pending transaction, the normal validation result is rejection for insufficient available funds.
### Race Conditions and Double-Spends
The intended rule is simple:
```text
The first valid transaction to be accepted into the mempool or included in a valid block consumes the spendable balance. Later conflicting transactions fail validation.
```
There are two layers that matter:
1. **Mempool admission**
When a node receives a transaction, it verifies signatures, registered addresses, fees, duplicate transaction hashes, and available balance after pending mempool usage. If the first transaction is already in that node's mempool, a second transaction spending the same funds should fail the pending-balance check.
2. **Block validation**
When transactions are verified inside a block, Contractless uses a block-local balance reservation tracker. That tracker aggregates debits by address and asset across the whole block. If two transactions inside the same block try to spend the same funds, the block is rejected with an insufficient block-local funds error. The tracker also rejects duplicate transaction hashes and duplicate signatures inside the same block.
This means a confirmed double-spend should not be valid even if two conflicting transactions reach different nodes at nearly the same time.
There is one important implementation detail: most transaction submit paths do `verify -> insert into mempool`. That is not a single PostgreSQL transaction that locks the spender's balance across all transaction tables. Loan payments have an explicit submit lock because overpaying the same loan through concurrent pending payments was handled separately. Other transaction types rely on pending-balance checks at admission and the block-local reservation tracker at block validation.
So the practical behavior is:
- If the same node sees transaction A first, transaction B should usually fail when it arrives because A is already reserving the balance.
- If two different nodes receive conflicting transactions at the same time, both may briefly hold different pending transactions.
- Only one conflicting spend can become part of the accepted chain. A block containing both should be rejected, and a block containing the losing spend after the winning spend is already confirmed should fail validation.
If the project later needs stronger mempool convergence before mining, the submit path should be tightened so balance reservation and mempool insertion are atomic per spending address/asset across transaction types.
## lookup_mempool_tx_count
`lookup_mempool_tx_count` asks a configured node for the number of rows currently stored in the mempool tables.
### Usage
```bash
./lookup_mempool_tx_count
```
The tool asks for the wallet path and wallet decryption key for the node handshake. This lookup does not spend funds.
### Example Return
```json
{
"mempool_tx_count": 3
}
```
The count includes rows across all mempool tables. Processed rows may remain briefly after mining so they can be restored during short orphan rollbacks. Because of that, this count is a mempool table count, not always a strict "currently spendable pending transactions only" count.
## lookup_mempool_tx_by_signature
`lookup_mempool_tx_by_signature` asks a configured node for one unprocessed mempool transaction by signature.
### Usage
```bash
./lookup_mempool_tx_by_signature <signature>
```
Example:
```bash
./lookup_mempool_tx_by_signature 39a9b06ec11c9d01e08383f7591f76573afbd456e4a488e59f777f08f7dcd3...
```
The signature is passed as hex. The tool asks for the wallet path and wallet decryption key for the node handshake.
### What It Searches
The lookup checks unprocessed rows in the mempool tables for:
- Transfers
- Token creation
- Token issuance
- Burns
- NFTs
- Marketing records
- Vanity addresses
- Swaps, by either signature
- Loan contracts, by either signature
- Loan payments
- Collateral claims
- Storage keys
- Storage values
### Example Return
For a pending transfer:
```json
{
"unsigned_transfer": {
"txtype": 2,
"time": 1783657720,
"value": 100000000,
"coin": "CLTC ",
"nft_series": 0,
"sender": "1439f758f93333734778a6459dc64b8d63a74c06.cltc",
"receiver": "7ba97ce531937388ef8bca4d8a4ca54cefbefb87.cltc",
"txfee": 1000000
},
"signature": "<signature>"
}
```
If the signature is not found, the tool prints:
```text
Transaction not found in mempool.
```
## lookup_mempool_tx_by_address
`lookup_mempool_tx_by_address` asks a configured node for unprocessed mempool transactions related to an address.
### Usage
```bash
./lookup_mempool_tx_by_address <wallet_address>
```
Example:
```bash
./lookup_mempool_tx_by_address 1439f758f93333734778a6459dc64b8d63a74c06.cltc
```
The address can be a registered short address or vanity address. The tool resolves vanity input to the canonical short address before making the request.
The tool asks for the wallet path and wallet decryption key. It uses the wallet key for the authenticated node handshake and vanity resolution.
### What It Returns
The response is a JSON object containing a `transactions` array.
```json
{
"transactions": []
}
```
If matching pending transactions exist, each transaction is decoded into its transaction-specific JSON shape:
```json
{
"transactions": [
{
"unsigned_transfer": {
"txtype": 2,
"time": 1783657720,
"value": 100000000,
"coin": "CLTC ",
"nft_series": 0,
"sender": "1439f758f93333734778a6459dc64b8d63a74c06.cltc",
"receiver": "7ba97ce531937388ef8bca4d8a4ca54cefbefb87.cltc",
"txfee": 1000000
},
"signature": "<signature>"
}
]
}
```
### Address Matching
The mempool address lookup searches the fields that make sense for each transaction type:
- Transfer receiver
- Token creator
- Issue-token creator
- Burn address
- NFT creator
- Marketing advertiser
- Vanity address owner
- Swap sender1 and sender2
- Loan lender and borrower
- Loan payment address
- Collateral claim address
- Storage key address
- Storage value address
The lookup is mainly intended for pending visibility. For confirmed history, use the normal history and transaction lookup tools.
## Mempool and Block Save Lifecycle
When a node mines a block, it selects unprocessed mempool rows by fee priority and time. Selected rows are streamed into the block payload and their balance effects are applied through the normal block save path.
When a block is accepted:
- Locally selected mempool rows are marked `processed=true`.
- Synced or discovered block transactions are marked processed by signature if matching local mempool rows exist.
- Processed rows are retained briefly so short orphan rollbacks can restore them.
- Cleanup trails the chain tip so recently processed rows are not deleted too early.
When an orphan rollback restores a block, recently processed mempool rows can be unmarked by signature and returned to pending status if they are still available in the local mempool tables.
## Common Usage
Check the total mempool size:
```bash
./lookup_mempool_tx_count
```
Look up one pending transaction by signature:
```bash
./lookup_mempool_tx_by_signature <signature>
```
Look up pending transactions for an address:
```bash
./lookup_mempool_tx_by_address <wallet_address>
```

479
docs/NFT_TRANSACTIONS.md Normal file
View File

@ -0,0 +1,479 @@
# NFT Transactions
Contractless supports native NFT creation, NFT lookup, NFT transfers, NFT burns, and NFT use inside swaps or loan collateral. This guide focuses on NFT creation and lookup tools, metadata requirements, IPFS/RWA preparation, and the difference between standalone NFTs and collections.
Contractless stores the NFT transaction and the metadata CID on chain. It does not store your image, media, document, or full metadata JSON on chain. You must upload and pin those files before creating the NFT transaction.
**Important permanence note:** Anyone who owns an NFT or RWA should run a local IPFS node and pin the metadata and media for the NFTs/RWAs they care about. On other chains, NFT platforms and hosting services have disappeared in the past, causing people to lose access to the images, documents, or metadata tied to assets they still technically owned on chain. Pinning your own NFTs and RWAs helps ensure the data continues to exist even if the seller, marketplace, minting platform, or public gateway goes offline.
## CLI Tools
NFT-related CLI tools:
| Tool | Purpose |
| --- | --- |
| `create_nft_tx` | Creates and signs a new NFT creation transaction JSON. |
| `lookup_nft` | Looks up one NFT by name and item number. |
| `lookup_nft_list` | Lists NFTs known to the connected node. |
On Windows, add `.exe` to each command name.
When a tool asks for a wallet path or file path, press `<Tab>` to search and auto-complete files and folders.
## NFT Creation Transaction Fields
The NFT creation transaction is transaction type `4`.
Struct fields:
| Field | Size | Meaning |
| --- | ---: | --- |
| `txtype` | 1 byte | Transaction type. For NFT creation this is `4`. |
| `time` / `timestamp` | 4 bytes | UTC timestamp when the transaction is created. |
| `creator` | 22 bytes | Creator short address. |
| `series` | 1 byte | `0` for a standalone NFT, `1` for a collection/series. |
| `nft_name` | 15 bytes | Unique NFT or collection name, padded to 15 bytes. |
| `item_ipfs` | 100 bytes | Metadata CID, padded to 100 bytes. |
| `count` | 4 bytes | `1` for standalone NFTs; collection item count for series NFTs. |
| `desc` | 100 bytes | Short on-chain description, padded to 100 bytes. |
| `txfee` | 8 bytes | Fee paid by creator in base currency. Minimum is `0.5` CLC or CLTC. |
| `signature` | 666 bytes | Creator signature. |
Important limits:
- NFT names must normalize to 3 to 15 alphanumeric characters.
- NFT names must be unique. First confirmed creation gets the name.
- `item_ipfs` must be a CID and must fit in the fixed 100 byte field.
- `desc` is only a short on-chain description. Use the off-chain metadata JSON for full descriptions.
- Standalone NFTs must use `series = 0` and `count = 1`.
- Collections must use `series = 1` and `count >= 2`.
## Standalone NFT vs Collection vs RWA
### Standalone NFT
A standalone NFT is one item. It uses:
```text
series = 0
count = 1
```
The wallet and lookup tooling refer to standalone NFTs with item number `0`.
Use a standalone NFT for:
- One-of-one artwork.
- A single certificate.
- A single RWA record.
- A single media item.
### Collection / Series
A collection creates many numbered NFTs from one creation transaction. It uses:
```text
series = 1
count = <number of items>
```
If `count = 100`, the chain creates 100 numbered NFT assets:
```text
collectionname_1
collectionname_2
collectionname_3
...
collectionname_100
```
The creator receives all items at creation. This means if you create a large series, every NFT in that series appears in your wallet until you transfer, swap, burn, or lock those items as collateral.
This matters for wallet usability. A 10,000 item series creates 10,000 owned NFT records immediately. The GUI paginates NFTs, but large collections still mean more wallet data and more metadata to cache over time.
Use a collection for:
- Generative art collections.
- Trading-card style collections.
- Ticket batches.
- Numbered certificate sets.
- Numbered RWA records.
### RWA / Non-Image NFT
An RWA is not a separate on-chain transaction type. It is an NFT whose metadata describes a real-world asset or non-image record.
Use an RWA NFT for:
- Equipment certificates.
- Property or inventory records.
- Authenticity certificates.
- Legal or business documents.
- Membership or access records.
RWA metadata should include normal NFT fields plus a `contractless` object that identifies it as an RWA.
## IPFS Hosting
Contractless does not provide IPFS hosting. Before creating an NFT, upload and pin your metadata and media files with a provider.
Hosting providers that currently offer IPFS or decentralized storage services include:
- [Pinata](https://www.pinata.cloud/)
- [Filebase](https://filebase.com/)
- [thirdweb Storage](https://thirdweb.com/storage)
- [Lighthouse](https://www.lighthouse.storage/)
Public gateways can be checked here:
[https://ipfs.github.io/public-gateway-checker/](https://ipfs.github.io/public-gateway-checker/)
Active gateways can still fail to return specific data if they cannot find or stream a provider. The default GUI gateway is:
```text
https://gateway.pinata.cloud/ipfs/
```
## Metadata Rules
Contractless follows common NFT metadata conventions. The chain does not enforce every JSON field, but the GUI wallet expects common fields so it can display names, descriptions, images, traits, and RWA details.
Recommended common fields:
| Field | Purpose |
| --- | --- |
| `name` | User-facing NFT name. |
| `description` | Full description shown in wallets and marketplaces. |
| `image` | Image URI, usually `ipfs://...`. |
| `image_url` | Optional image URI fallback. |
| `animation_url` | Optional video, audio, HTML, or animation URI. |
| `external_url` | Optional website or public detail page. |
| `attributes` | Trait list. |
| `contractless` | Optional Contractless-specific metadata, especially for RWAs. |
Use `ipfs://` URIs inside metadata. The wallet converts them through the configured IPFS gateway.
## Single Image NFT Metadata
For a standalone NFT, upload the image first. Then upload one metadata JSON file. Use the CID of that metadata JSON in `create_nft_tx`.
Example:
```json
{
"name": "Example Artwork #1",
"description": "A short description of the NFT.",
"image": "ipfs://IMAGE_CID/example.png",
"external_url": "https://example.com/nft/1",
"attributes": [
{ "trait_type": "Background", "value": "Blue" },
{ "trait_type": "Edition", "value": "1 of 1" }
]
}
```
For this format:
- `item_ipfs` should be the CID of the metadata JSON.
- `series` should be `0`.
- `count` should be `1`.
- `lookup_nft <name> 0` is used for lookup.
## Collection / Series Metadata
For a collection, pin one parent folder that contains both a `metadata` folder and an `images` folder. Use the CID of the parent folder in the NFT transaction.
Do not use:
- The CID of one numbered JSON file.
- The CID of the `metadata` folder alone.
- The CID of the `images` folder alone.
Use the CID of this parent folder:
```text
collection-root/ <- use this folder CID in the transaction
metadata/
1.json
2.json
3.json
images/
1.png
2.png
3.png
```
The GUI wallet builds metadata paths like:
```text
ipfs://PARENT_FOLDER_CID/metadata/1.json
ipfs://PARENT_FOLDER_CID/metadata/2.json
ipfs://PARENT_FOLDER_CID/metadata/3.json
```
Each numbered JSON file should describe one item and point to the matching image.
Example `metadata/1.json`:
```json
{
"name": "Example Collection #1",
"description": "Item 1 from Example Collection.",
"image": "ipfs://PARENT_FOLDER_CID/images/1.png",
"attributes": [
{ "trait_type": "Background", "value": "Blue" },
{ "trait_type": "Rarity", "value": "Common" }
]
}
```
For this format:
- `item_ipfs` should be the CID of `collection-root/`.
- `series` should be `1`.
- `count` should match the number of JSON files/items.
- `lookup_nft <name> 1` looks up item 1.
- `lookup_nft <name> 2` looks up item 2.
The current CLI `lookup_nft` output formats series IPFS display as `ipfs://<CID>/<series>.json`. The GUI wallet uses the recommended `metadata/<series>.json` path. For collection creation, follow the parent-folder structure above.
## RWA / Non-Image Metadata
For a real-world asset or non-image NFT, keep the common NFT fields and add structured details under `attributes` and `contractless`.
Example:
```json
{
"name": "Equipment Certificate #42",
"description": "Ownership record for a physical asset.",
"image": "ipfs://IMAGE_CID/certificate-preview.png",
"external_url": "https://example.com/certificates/42",
"attributes": [
{ "trait_type": "Asset Type", "value": "Equipment" },
{ "trait_type": "Serial Number", "value": "EQ-2026-0042" },
{ "trait_type": "Issuer", "value": "Example Agency" }
],
"contractless": {
"metadata_type": "rwa",
"issuer": "Example Agency",
"document": "ipfs://DOCUMENT_CID/asset-record.pdf",
"jurisdiction": "US",
"record_date": "2026-07-12"
}
}
```
For RWA records:
- Use `contractless.metadata_type = "rwa"` so the GUI can label the NFT as an RWA.
- Use `image` for a preview image or certificate preview.
- Use `contractless.document` for the primary document if there is one.
- Avoid putting private personal data in public metadata. IPFS data is public if the CID is shared or discovered.
RWAs can be standalone or collections. For example, one equipment certificate can be standalone, while a batch of numbered tickets or certificates can be a collection.
## create_nft_tx
Creates a signed NFT creation transaction JSON and saves it under:
```text
./transactions/<hash>.json
```
The command prints the signed JSON to the terminal. Creating the file does not guarantee the transaction is mined. Broadcast the transaction with `broadcast_transaction` or through the GUI wallet.
Usage:
```text
create_nft_tx
```
Interactive prompts:
```text
Are you creating a 1/1 or a collection?
Please enter the name for your NFT. This can be 3 to 15 characters and must be 100% unique. First come first serve:
Enter your IPFS CID for your NFT. This will be stored in a fixed 100 character field, so shorter CIDs are padded with spaces:
Please enter the number of items in your collection, enter 1 for 1/1s:
Enter your nft description, A max of 100 characters:
Please enter the amount for the fee: (eg. 0.005, minimum fee 0.50000000):
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Prompt notes:
| Prompt | What To Enter |
| --- | --- |
| `1/1 or collection` | Enter `collection` for a collection. Anything else is treated as standalone. |
| NFT name | Unique 3 to 15 character alphanumeric name. |
| IPFS CID | Standalone: metadata JSON CID. Collection: parent folder CID. |
| Number of items | `1` for standalone. Collection count for series. |
| Description | Short on-chain description, max 100 characters. |
| Fee | Minimum `0.5` CLC or CLTC. |
| Wallet path | Path to the creator wallet file. Use `<Tab>` for completion. |
| Wallet key | Creator wallet decryption key. |
Output JSON shape:
```json
{
"txtype": 4,
"timestamp": 1780000000,
"creator": "creator_short_address.cltc",
"series": 0,
"nft_name": "example ",
"item_ipfs": "bafy...padded...",
"count": 1,
"desc": "short description padded...",
"txfee": 50000000,
"hash": "transaction_hash",
"signature": "creator_signature"
}
```
## lookup_nft
Looks up one NFT by NFT name and item number.
Usage:
```text
lookup_nft <nft_name> <item_number>
```
Use item number `0` for a standalone NFT:
```text
lookup_nft example 0
```
Use item numbers `1` through `count` for collection items:
```text
lookup_nft example 1
lookup_nft example 2
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
The wallet is used for authenticated RPC handshake. The lookup itself does not spend funds.
Expected output shape:
```json
{
"nft_name": "example",
"series": 1,
"ipfs": "ipfs://CID/1.json",
"asset_name": "example_1",
"genesis": "creation_txid",
"creator": "creator_short_address.cltc",
"current_holder": "holder_short_address.cltc",
"history_count": 1,
"history": [
{
"txid": "transaction_id",
"block": 123,
"txtype": 4,
"action": "create_nft",
"to": "creator_short_address.cltc"
}
]
}
```
Common history actions:
| Action | Meaning |
| --- | --- |
| `create_nft` | NFT was created. |
| `transfer` | NFT was transferred. |
| `swap` | NFT moved through a swap. |
| `loan_locked` | NFT was locked as loan collateral. |
| `loan_payment` | NFT moved through loan payment handling. |
| `collateral_claimed` | NFT collateral was claimed. |
| `loan_issued` | NFT moved as part of loan issuance. |
## lookup_nft_list
Returns the NFT list known to the connected peer.
Usage:
```text
lookup_nft_list
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Expected output shape:
```json
{
"nfts": {
"creation_txid": [
{
"nft_name": "example",
"series": 0
},
{
"nft_name": "collection",
"series": 1
}
]
}
}
```
The list is grouped by creation transaction hash. A collection can produce many rows under the same creation hash because each numbered item is its own NFT asset.
## Ownership and Wallet Display
NFT balances are tracked like native assets, but NFTs use a fixed unit internally. Wallets display NFT ownership based on whether the active address has a positive balance for that NFT asset.
Standalone ownership:
```text
asset = example
series = 0
```
Collection ownership:
```text
asset = collection
series = 1
asset = collection
series = 2
asset = collection
series = 3
```
When you create a collection, the creator receives every item from `1` through `count`. If you create a large collection, expect the NFT page to show many items.
## NFT Transfers, Burns, Swaps, and Loans
NFTs can be used by other native transaction types:
- Transfer: move the NFT to another address.
- Burn: destroy the NFT.
- Swap: trade the NFT for another asset.
- Loan collateral: lock the NFT as collateral.
- Collateral claim: transfer locked NFT collateral when claim rules are met.
For numbered collection items, the transaction must include the correct NFT series/item number. For standalone NFTs, the series is `0`.
Details for those transaction flows are covered in the transfer, burn, swap, and loan documentation.

256
docs/OTHER_TOOLS.md Normal file
View File

@ -0,0 +1,256 @@
# Other Tools
This page covers additional Contractless CLI tools that do not fit into the wallet, transaction, mempool, validation, storage, or fee-specific guides.
On Windows, add `.exe` to each command name.
When a tool asks for a wallet path, press `<Tab>` to search and auto-complete files and folders.
## average_block_time_checker
Calculates the average time between block timestamps for a local range of block files.
This is a local file inspection tool. It does not contact a node. It reads block files from the directory you provide, extracts the timestamp from each block file, sorts those timestamps, and calculates the average difference between consecutive timestamps.
Usage:
```bash
./average_block_time_checker <start_block> <stop_block> <directory>
```
Example:
```bash
./average_block_time_checker 30000 35000 ./blocks/testnet
```
Expected output:
```text
The average time between timestamps is 15.21 seconds.
```
If the range does not contain enough block files to compare timestamps, the tool prints:
```text
Not enough data to calculate an average time difference.
```
Use this for local difficulty/block-timing checks, especially when reviewing how close a range of blocks stayed to the intended block target.
## lookup_difficulty
Asks a configured node for the current network difficulty.
Usage:
```bash
./lookup_difficulty
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Expected output:
```json
{
"difficulty": 1806884765625000
}
```
The tool contacts configured peers and stops at the first peer that returns a readable difficulty response or a text error.
## lookup_height
Asks a configured node for its current chain height.
Usage:
```bash
./lookup_height
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Expected output:
```json
{
"height": 31445
}
```
This is a quick way to confirm what height a reachable peer reports.
## lookup_network_info
Asks a configured node for a compact network status snapshot.
Usage:
```bash
./lookup_network_info
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Example output:
```json
{
"version": 1,
"network": "testnet",
"time": 1783657720,
"wallet_prefix": "cltc",
"height": 31445,
"next_block_difficulty": 1806884765625000,
"total_block_transactions": 31540,
"total_mempool_transactions": 2,
"largest_tx_fee": 100000000
}
```
Fields:
`version`
The protocol/network info version returned by the peer.
`network`
The peer's configured network name, such as `testnet` or `mainnet`.
`time`
The peer's current UTC timestamp as a Unix timestamp.
`wallet_prefix`
The wallet suffix/prefix used by that network, such as `cltc`.
`height`
The peer's current chain height.
`next_block_difficulty`
The next block difficulty currently reported by the peer.
`total_block_transactions`
The number of transactions the peer reports from confirmed block data.
`total_mempool_transactions`
The number of transactions currently in the peer's mempool.
`largest_tx_fee`
The largest transaction fee currently known from the peer's mempool data. This value is raw atomic units.
## lookup_node_time
Asks a configured node for its current UTC time.
Usage:
```bash
./lookup_node_time
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Expected output:
```json
{
"timestamp": 1783657720,
"time": "04:28:40 UTC"
}
```
Use this to confirm that a node is responding and to compare node time against expected UTC time. Correct system time matters for block timestamps and transaction validation.
## server_owner_block_ip
Sends a signed request to block an IP address from the server-side connection layer.
This is an administrative tool. It is intended for the node/server owner, not normal wallet users. The request signs the exact IP string with the selected wallet, then sends the signed payload to a configured peer.
Usage:
```bash
./server_owner_block_ip <ip>
```
Example:
```bash
./server_owner_block_ip 203.0.113.10
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
The node returns a text response indicating whether the request was accepted or rejected. If no configured peer can be reached, the tool prints:
```text
failed to connect
```
Use this carefully. Blocking the wrong IP may prevent a valid peer from connecting to the node receiving the request.
## server_owner_unblock_ip
Sends a signed request to remove a previously blocked IP address from the server-side connection layer.
This is the administrative counterpart to `server_owner_block_ip`.
Usage:
```bash
./server_owner_unblock_ip <ip>
```
Example:
```bash
./server_owner_unblock_ip 203.0.113.10
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
The node returns a text response indicating whether the request was accepted or rejected. If no configured peer can be reached, the tool prints:
```text
failed to connect
```

View File

@ -4,6 +4,12 @@ Contractless uses PostgreSQL for transaction lookup and mempool-style records. T
PostgreSQL 16 is the tested version. Newer PostgreSQL versions should work, but they have not been tested for launch.
## PostgreSQL Must Not Be Public
**Do not expose PostgreSQL to the public Internet. The PostgreSQL port should not be publicly accessible.**
Contractless nodes need a public RPC port for peer connections, but PostgreSQL is an internal database dependency and should normally listen only on `127.0.0.1` or another private interface. Do not open PostgreSQL port `5432` to the public Internet in your router, cloud firewall, VPS firewall, or host firewall.
Download PostgreSQL from the official project downloads page:
<https://www.postgresql.org/download/>

View File

@ -1,6 +1,6 @@
# 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.
Contractless reads `settings.ini` during startup. This file controls runtime paths, logging, networking, peer discovery, mining behavior, paid data lookup fees, and PostgreSQL connection settings.
The node loads `settings.ini` in this order:
@ -10,13 +10,81 @@ The node loads `settings.ini` in this order:
4. `settings.ini` beside the executable
5. platform fallback path
On Linux, the fallback path is `/etc/contractless/settings.ini`.
On Linux, the fallback path is:
## `[Paths]`
```text
/etc/contractless/settings.ini
```
### `BLOCK_PATH`
On Windows, there is no `/etc` fallback. If no explicit config is provided, Windows checks the current working directory and then the executable directory.
Base folder where block files are stored. The active network name is appended automatically.
## Default Settings
Example testnet settings:
```ini
[Paths]
BLOCK_PATH = "./blocks"
TORRENT_PATH = "./torrents"
DB_PATH = "./state"
BALANCE_SHEET = "./balance_sheet"
LOG_PATH = "./logs"
WALLET_PATH = "./wallets"
WALLET_NAME = "contractless.wallet"
[Settings]
LOG_LEVEL = "info"
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"
VALIDATOR = "false"
THREADS = "8"
STORAGE_LOOKUP_FEE_PER_BYTE = "0.0001"
[Piggyback]
PIGGYBACK_1 = "contractless.dev:50050"
[Postgres-Testnet]
host = 127.0.0.1
port = 5432
user = contractless
password = change_this_password
dbname = contractless_db
[Postgres]
host = 127.0.0.1
port = 5432
user = contractless
password = change_this_password
dbname = contractless_db
```
## Paths and Network Folders
Relative paths are resolved relative to the loaded `settings.ini` file, not always relative to the terminal's current folder.
If Linux uses:
```text
/etc/contractless/settings.ini
```
then:
```ini
BLOCK_PATH = "./blocks"
```
resolves under:
```text
/etc/contractless/blocks
```
Contractless automatically appends the active network name to runtime storage paths. You do not add `testnet` or `mainnet` yourself.
Example:
@ -24,29 +92,28 @@ Example:
BLOCK_PATH = "./blocks"
```
Testnet will store blocks under:
Testnet stores blocks under:
```text
./blocks/testnet
```
Mainnet will store blocks under:
Mainnet stores blocks under:
```text
./blocks/mainnet
```
### `TORRENT_PATH`
The same automatic network folder is added to:
Base folder where torrent metadata, staged torrents and torrent-related files are stored. The active network name is appended automatically.
- `BLOCK_PATH`
- `TORRENT_PATH`
- `DB_PATH`
- `BALANCE_SHEET`
- `LOG_PATH`
- `WALLET_PATH`
### `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.
For `WALLET_PATH`, the node appends both the network folder and `WALLET_NAME`.
Example:
@ -55,113 +122,266 @@ WALLET_PATH = "./wallets"
WALLET_NAME = "contractless.wallet"
```
Testnet will load the wallet from:
Testnet loads:
```text
./wallets/testnet/contractless.wallet
```
### `WALLET_NAME`
Mainnet loads:
Name of the encrypted wallet file to load at startup.
```text
./wallets/mainnet/contractless.wallet
```
## Windows Paths
Windows accepts forward slashes in paths, and that is the clearest format for `settings.ini`:
```ini
BLOCK_PATH = "C:/Contractless/blocks"
TORRENT_PATH = "C:/Contractless/torrents"
DB_PATH = "C:/Contractless/state"
BALANCE_SHEET = "C:/Contractless/balance_sheet"
LOG_PATH = "C:/Contractless/logs"
WALLET_PATH = "C:/Contractless/wallets"
```
Backslashes can also be used, but avoid ending a quoted Windows path with a single trailing backslash because it can be confused with escaping the closing quote in some INI parsers or editors.
Good:
```ini
BLOCK_PATH = "C:\Contractless\blocks"
```
Avoid:
```ini
BLOCK_PATH = "C:\Contractless\blocks\"
```
If you want a trailing separator, use forward slashes:
```ini
BLOCK_PATH = "C:/Contractless/blocks/"
```
## `[Paths]`
### `BLOCK_PATH`
Base folder where block files are stored. The active network name is appended automatically.
Default:
```ini
BLOCK_PATH = "./blocks"
```
### `TORRENT_PATH`
Base folder where torrent metadata, staged torrents, and torrent-related files are stored. The active network name is appended automatically.
Default:
```ini
TORRENT_PATH = "./torrents"
```
### `DB_PATH`
Base folder for the internal sled database used for non-PostgreSQL node state. The active network name is appended automatically.
On Linux, the daemon PID file is also stored under the active network's `DB_PATH`.
Default:
```ini
DB_PATH = "./state"
```
### `BALANCE_SHEET`
Base folder where balance sheet files are stored. The active network name is appended automatically.
## `[Settings]`
Default:
```ini
BALANCE_SHEET = "./balance_sheet"
```
### `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:
Default:
```ini
LOG_PATH = "./logs"
```
Testnet will write logs under:
Testnet writes logs under:
```text
./logs/testnet
```
Logs are written to rotating `node` log files. The current logger rotates at about 10 MB per file and keeps up to 10 log files.
### `WALLET_PATH`
Base folder where wallet files are stored. The active network name and `WALLET_NAME` are appended automatically.
Default:
```ini
WALLET_PATH = "./wallets"
```
### `WALLET_NAME`
Name of the encrypted wallet file to load at startup. If the wallet file does not exist, the node creates it the first time it starts and asks for a wallet encryption key.
Default:
```ini
WALLET_NAME = "contractless.wallet"
```
## `[Settings]`
### `LOG_LEVEL`
Runtime log filter.
Valid common values:
Default:
- `info`: show info, warning and error logs.
- `warn`: show warning and error logs.
- `error`: show only error logs.
- `off`: disable log output.
```ini
LOG_LEVEL = "info"
```
The logger also supports more advanced module-level filters through `flexi_logger`, but normal nodes should use one of the simple values above.
Log levels are minimum severity filters, not exact categories. For example, `warn` means "write warnings and errors," not "write only warning lines."
Common choices:
| Value | What It Writes |
| --- | --- |
| `error` | Errors only |
| `warn` | Warnings and errors |
| `info` | Info, warnings, and errors |
| `off` | Disables log output |
Normal public nodes should use `info`.
The node writes logs to files under `LOG_PATH`; it does not use `LOG_LEVEL` to print normal runtime logs into the terminal.
### `PUBLIC_IP`
Reachable public IP address announced by this node during handshakes and network-map broadcasts.
Default placeholder:
```ini
PUBLIC_IP = "your.public.ip.address"
```
Use `127.0.0.1` only for local testing. A public node should use the public IP address that outside peers use to reach the configured RPC port.
This value is protocol identity. It is validated against forbidden private or unroutable ranges before the node is accepted by peers.
This value is protocol identity. Peers reject private, loopback, and unroutable public identities for public network operation.
### `LISTEN_IP`
Local bind address used by the RPC server.
Use `0.0.0.0` to listen on all local interfaces, which is usually the correct value for nodes behind NAT or router port forwarding.
Default:
This value is never announced to peers. It may be private, loopback or wildcard because it only controls where the local process listens.
```ini
LISTEN_IP = "0.0.0.0"
```
Use `0.0.0.0` to listen on all local interfaces. This is usually correct for public nodes, VPS nodes, and nodes behind NAT or router port forwarding.
This value is local bind configuration and is not announced as the node identity. It may be private, loopback, or wildcard because it only controls where the local process listens.
### `RPC_PORT`
Mainnet RPC port. This value is used when the node is built with the `mainnet` feature.
Default:
```ini
RPC_PORT = "50055"
```
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.
Default:
```ini
TESTNET_RPC_PORT = "50050"
```
Public testnet nodes must allow inbound traffic to this port. This is the public Contractless port peers use to connect to your node.
Do not confuse this with PostgreSQL port `5432`. The Contractless RPC port may be public; the PostgreSQL port should not be public.
### `INCOMING_CONNECTIONS`
Maximum number of incoming peer connections allowed by the node.
This should be greater than `0`. A recommended value is `50`.
Default:
If the limit is reached, new incoming peers are rejected during handshake.
```ini
INCOMING_CONNECTIONS = "100"
```
If the limit is reached, new incoming peers are rejected during handshake. Public nodes usually want this higher than a private test node, as long as the machine has enough network and memory capacity.
### `OUTGOING_CONNECTIONS`
Maximum number of outgoing peer connections the node tries to discover and maintain.
Maximum number of outgoing peer connections the node tries to maintain.
This should be greater than `0`. A recommended value is `10`.
Default:
```ini
OUTGOING_CONNECTIONS = "10"
```
Outgoing peers are the nodes your node actively connects to. Once a node joins the network, it can use the network map to fill outgoing connections up to this limit.
### `VALIDATOR`
Disables local mining while keeping the node connected to peers, synced and able to validate and rebroadcast network activity.
Disables local mining while keeping the node connected to peers, synced, and able to validate and rebroadcast network activity.
Use this for bootstrap or validator-only nodes:
```ini
VALIDATOR = "true"
```
Mining nodes should use:
Default:
```ini
VALIDATOR = "false"
```
Use validator mode for non-mining infrastructure nodes:
```ini
VALIDATOR = "true"
```
Mining nodes should keep this set to `false`.
### `THREADS`
Number of async worker tasks used during each mining nonce round.
Default:
```ini
THREADS = "8"
```
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:
@ -172,21 +392,48 @@ Allowed values:
The value must be between `1` and `256`.
### `STORAGE_LOOKUP_FEE_PER_BYTE`
Fee charged by this node for paid storage lookups, in decimal CLTC/CLC per byte returned.
Default:
```ini
STORAGE_LOOKUP_FEE_PER_BYTE = "0.0001"
```
Data storage transactions let users store app data on chain. Public RPC nodes can serve lookups for that data. This setting lets a node operator charge for storage lookup bandwidth and processing.
Examples:
```ini
STORAGE_LOOKUP_FEE_PER_BYTE = "0.0001"
```
Charge `0.0001` CLTC/CLC per byte returned.
The lookup tool asks the node for the byte count and total lookup cost before payment. If the requester accepts, the lookup payment is made as a normal transfer transaction to the node's wallet, plus the normal transfer transaction fee.
When the requester is the same wallet as the node operator's wallet, the node verifies the signed lookup request and returns the data without broadcasting a payment transaction. Node operators do not need to set this value to `0` for their own local lookups to be free.
Node operators can set this to `0` during testing or raise it if they are serving dapp data to external users.
## `[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.
Piggyback entries are startup peers. The node tries these peers when it first starts so it can join the network and learn the current network map.
Default:
```ini
[Piggyback]
PIGGYBACK_1 = "contractless.dev"
PIGGYBACK_2 = "5.6.7.8:50053"
PIGGYBACK_1 = "contractless.dev:50050"
```
The node only needs one live piggyback peer to begin discovery. Additional entries are backups if earlier entries are offline or unreachable.
Piggyback entries may use a public IP endpoint or a hostname. If no port is included, the active network RPC port is used. Hostnames are resolved only for these startup entries; nodes must still announce public IP endpoints in handshakes and network mapping.
Piggyback entries may use a public IP endpoint or a hostname. If no port is included, the active network RPC port is used. Hostnames are resolved only for startup entries; nodes must still announce public IP endpoints in handshakes and network mapping.
Piggyback entries should resolve to public, routable peers. Private, loopback and invalid addresses are filtered before startup connections begin.
Piggyback entries should resolve to public, routable peers. Private, loopback, and invalid addresses are filtered before startup connections begin.
## `[Postgres-Testnet]`
@ -201,7 +448,9 @@ 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.
These values are intentionally mostly self-explanatory. 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.
PostgreSQL should not be publicly exposed. In normal node setups, `host = 127.0.0.1` is the correct value.
## `[Postgres]`

503
docs/TOKEN_SWAPS.md Normal file
View File

@ -0,0 +1,503 @@
# Tokens and Swaps
Contractless supports native fungible tokens and native two-party swaps. Token creation, additional issuance, token burns, token lookup, and swap creation are handled by CLI tools that create or inspect native transaction types.
This guide covers:
- Creating tokens with `create_tokens_tx`
- Issuing more tokens with `create_issue_token_tx`
- Burning tokens with `create_burn_tx`
- Looking up tokens with `lookup_token` and `lookup_token_list`
- Creating and second-signing swaps with `create_swap_tx` and `verify_sign_swap_tx`
On Windows, add `.exe` to each command name.
When a tool asks for a wallet path or transaction file path, press `<Tab>` to search and auto-complete files and folders.
## Important Token Model
Contractless token creation tools mint tokens directly to the wallet that creates the token transaction.
These are not smart-contract tokens. They are not released by programmable vesting rules, bonding curves, administrator contracts, or contract-controlled supply schedules. When a token creation transaction confirms, the full initial supply is credited to the creator wallet. When an issue-more transaction confirms, the additional supply is credited directly to the original creator wallet.
If a creator wants a timed release, public sale, treasury distribution, or external business rule, that must be handled outside the token creation transaction. The chain records the native token supply and balances; it does not run token-sale contract logic.
Token amounts are entered in display units but stored in atomic units. One display token equals `100000000` atomic units.
Example:
```text
1 token = 100000000 atomic units
1000 tokens = 100000000000 atomic units
```
The CLI tools usually ask for display units and then write atomic units into the saved transaction JSON.
## CLI Tools
| Tool | Purpose |
| --- | --- |
| `create_tokens_tx` | Creates and signs a new token creation transaction. |
| `create_issue_token_tx` | Creates and signs an additional-issuance transaction for an existing token. |
| `create_swap_tx` | Creates the first-signed side of a two-party swap. |
| `verify_sign_swap_tx` | Lets the counterparty verify terms, sign the second side, and save the completed swap. |
| `create_burn_tx` | Creates and signs a token or NFT burn transaction. |
| `lookup_token` | Looks up one token by ticker/name. |
| `lookup_token_list` | Lists known tokens from the connected node. |
Creation tools save signed transaction JSON under:
```text
./transactions/<hash>.json
```
Saving a transaction file does not broadcast it. Submit saved transactions with `broadcast_transaction` or with the GUI wallet import/broadcast flow.
## Token Creation
Use `create_tokens_tx` to create a new fungible token.
Usage:
```text
create_tokens_tx
```
The transaction type is `3`.
### Token Creation Fields
| Field | Size | Meaning |
| --- | ---: | --- |
| `txtype` | 1 byte | Transaction type. For token creation this is `3`. |
| `time` / `timestamp` | 4 bytes | UTC timestamp when the transaction is created. |
| `creator` | 22 bytes | Creator short address. The initial supply is credited here. |
| `ticker` | 15 bytes | Unique token ticker/name, padded to 15 bytes. |
| `number` | 8 bytes | Initial token supply in atomic units. |
| `hard_limit` | 1 byte | `1` means hard capped forever; `0` means the creator may issue more later. |
| `txfee` | 8 bytes | Base-currency transaction fee. Minimum is `500` CLC or CLTC. |
| `signature` | 666 bytes | Creator signature. |
### Prompts
`create_tokens_tx` asks for:
| Prompt | What To Enter |
| --- | --- |
| Ticker / token name | A unique 3 to 15 character alphanumeric token name. |
| Amount to create | Initial supply in display tokens, such as `1`, `1000`, or `100000`. |
| Hard cap | `1` for yes, `0` for no. |
| Fee | Minimum `500` CLC or CLTC. |
| Wallet path | Path to the creator wallet file. |
| Wallet key | Creator wallet decryption key. |
Token names are normalized to lowercase alphanumeric identifiers and stored in a fixed 15-byte field. The name must be globally unique. The first confirmed transaction for a ticker owns that ticker.
The base coin ticker is reserved and cannot be created as a custom token.
### Hard Cap
The hard cap choice is permanent.
If `hard_limit = 1`, no future `create_issue_token_tx` transaction can issue more supply for that token.
If `hard_limit = 0`, only the original creator wallet may issue more supply later.
### Output Shape
The saved JSON includes a `hash` field so the file can be named and later broadcast:
```json
{
"txtype": 3,
"timestamp": 1780000000,
"creator": "creator_short_address.cltc",
"ticker": "example ",
"number": 100000000000,
"hard_limit": 0,
"txfee": 50000000000,
"hash": "transaction_hash",
"signature": "creator_signature"
}
```
In this example, `number = 100000000000` means `1000` display tokens.
## Issuing More Tokens
Use `create_issue_token_tx` to issue more units of an existing token.
Usage:
```text
create_issue_token_tx
```
The transaction type is `11`.
Additional issuance is only valid when:
- The token already exists.
- The token was created with `hard_limit = 0`.
- The signing wallet is the original token creator.
- The creator has enough base currency for the issue-token fee.
When the transaction confirms, the newly issued tokens are credited directly to the original creator wallet.
### Issue More Fields
| Field | Size | Meaning |
| --- | ---: | --- |
| `txtype` | 1 byte | Transaction type. For issue-more this is `11`. |
| `time` / `timestamp` | 4 bytes | UTC timestamp when the transaction is created. |
| `creator` | 22 bytes | Original creator short address. New supply is credited here. |
| `ticker` | 15 bytes | Existing token ticker/name, padded to 15 bytes. |
| `number` | 8 bytes | Additional supply in atomic units. |
| `txfee` | 8 bytes | Base-currency transaction fee. Minimum is `100` CLC or CLTC. |
| `signature` | 666 bytes | Creator signature. |
### Prompts
`create_issue_token_tx` asks for:
| Prompt | What To Enter |
| --- | --- |
| Ticker / token name | Existing token name. |
| Additional tokens | Amount to add in display tokens. |
| Fee | Minimum `100` CLC or CLTC. |
| Wallet path | Path to the original creator wallet file. |
| Wallet key | Creator wallet decryption key. |
### Output Shape
```json
{
"txtype": 11,
"timestamp": 1780000000,
"creator": "creator_short_address.cltc",
"ticker": "example ",
"number": 50000000000,
"txfee": 10000000000,
"hash": "transaction_hash",
"signature": "creator_signature"
}
```
In this example, `number = 50000000000` means `500` additional display tokens.
## Burning Tokens
Use `create_burn_tx` to permanently destroy a token or NFT balance.
Usage:
```text
create_burn_tx
```
The transaction type is `10`.
The base coin cannot be burned with this transaction type.
### Burn Fields
| Field | Size | Meaning |
| --- | ---: | --- |
| `txtype` | 1 byte | Transaction type. For burn this is `10`. |
| `time` / `timestamp` | 4 bytes | UTC timestamp when the transaction is created. |
| `address` | 22 bytes | Wallet address burning the asset. |
| `coin` | 15 bytes | Token or NFT base name, padded to 15 bytes. |
| `nft_series` | 4 bytes | `0` for fungible tokens or one-of-one NFTs; series item number for numbered NFTs. |
| `value` | 8 bytes | Amount to burn in atomic units. NFT burns must use exactly `100000000`. |
| `txfee` | 8 bytes | Base-currency transaction fee. Minimum is `0.0001` CLC or CLTC. |
| `signature` | 666 bytes | Burner signature. |
### Prompts
`create_burn_tx` asks for:
| Prompt | What To Enter |
| --- | --- |
| Token or NFT name | Asset name to burn. Base coin is rejected. |
| NFT series number | `0` for standard tokens or one-of-one NFTs; item number for collection NFTs. |
| Burn amount | Token amount in display units. Use `1.0` for NFTs. |
| Fee | Minimum `0.0001` CLC or CLTC. |
| Wallet path | Path to the wallet that owns the asset. |
| Wallet key | Wallet decryption key. |
For fungible tokens, `value` may be any positive amount the wallet owns.
For NFTs, the burn must destroy exactly one full NFT unit. The CLI prompt says to use `1.0`; internally that becomes `100000000`.
## Token Lookup
Use `lookup_token` to inspect one token by name.
Usage:
```text
lookup_token <token_name>
```
Example:
```text
lookup_token example
```
The tool asks for a wallet path and wallet key for authenticated RPC handshake. The lookup does not spend funds.
Expected output:
```json
{
"token_name": "example",
"genesis": "creation_transaction_hash",
"creator": "creator_short_address.cltc",
"token_count": 1500.0,
"token_spread": "2 addresses",
"hard_limit": false,
"issued_hashes": [
"issue_transaction_hash"
],
"burned_hashes": [
"burn_transaction_hash"
]
}
```
Field meanings:
| Field | Meaning |
| --- | --- |
| `token_name` | Token ticker/name. |
| `genesis` | Token creation transaction hash. |
| `creator` | Original creator wallet. |
| `token_count` | Current supply after issuance and burns, displayed in whole-token units. |
| `token_spread` | Number of addresses holding the token. |
| `hard_limit` | Whether additional issuance is permanently blocked. |
| `issued_hashes` | Confirmed issue-more transaction hashes. |
| `burned_hashes` | Confirmed burn transaction hashes. |
## Token List
Use `lookup_token_list` to list known tokens from the connected node.
Usage:
```text
lookup_token_list
```
The tool asks for a wallet path and wallet key for authenticated RPC handshake. The lookup does not spend funds.
Expected output:
```json
{
"tokens": [
{
"token": "example",
"hash": "creation_transaction_hash"
}
]
}
```
If the connected node has no token entries, the output is:
```json
{
"tokens": []
}
```
## Swaps
Contractless swaps are native two-party transactions. A swap has one shared unsigned transaction body and two signatures:
- `signature1` from `sender1`
- `signature2` from `sender2`
There is no automatic match-making service in the CLI tools. Before creating a swap, the two parties must already agree on the trade terms off-chain. The first signer creates the offer, saves the transaction file, and sends that file to the counterparty. The counterparty verifies the terms, signs the second half, and then the completed transaction can be broadcast.
The CLI does not find counterparties for you.
The transaction type is `6`.
## Creating a Swap Offer
Use `create_swap_tx` to create the first-signed swap offer.
Usage:
```text
create_swap_tx
```
The active wallet becomes `sender1`.
### Swap Fields
| Field | Size | Meaning |
| --- | ---: | --- |
| `txtype` | 1 byte | Transaction type. For swaps this is `6`. |
| `timestamp` | 4 bytes | UTC time when the offer is created. |
| `offer_expiration` | 4 bytes | UTC expiration timestamp. Must not be earlier than `timestamp` or more than 30 days later. |
| `ticker1` | 15 bytes | Asset offered by `sender1`. |
| `nft_series1` | 4 bytes | `0` for fungible assets and one-of-one NFTs; numbered NFT item for series NFTs. |
| `value1` | 8 bytes | Amount offered by `sender1`, in atomic units. |
| `ticker2` | 15 bytes | Asset expected from `sender2`. |
| `nft_series2` | 4 bytes | `0` for fungible assets and one-of-one NFTs; numbered NFT item for series NFTs. |
| `value2` | 8 bytes | Amount expected from `sender2`, in atomic units. |
| `sender1` | 22 bytes | First signer short address. |
| `sender2` | 22 bytes | Counterparty short address. |
| `tip1` | 8 bytes | Miner tip paid from `sender1` asset side. |
| `tip2` | 8 bytes | Miner tip paid from `sender2` asset side. |
| `txfee1` | 8 bytes | Base-currency fee paid by `sender1`. Minimum is `1` CLC or CLTC. |
| `txfee2` | 8 bytes | Base-currency fee paid by `sender2`. Minimum is `1` CLC or CLTC. |
| `signature1` | 666 bytes | Sender1 signature. |
| `signature2` | 666 bytes | Sender2 signature. Present only after the counterparty signs. |
### Swap Prompts
`create_swap_tx` asks for:
| Prompt | What To Enter |
| --- | --- |
| Coin or token to send | Base coin or token/NFT name offered by `sender1`. |
| NFT series number to send | `0` for coins/tokens/one-of-one NFTs; item number for series NFTs. |
| Amount to send | Amount in display units. Use `1` for NFTs. |
| Coin or token to receive | Base coin or token/NFT name expected from `sender2`. |
| NFT series number to receive | `0` for coins/tokens/one-of-one NFTs; item number for series NFTs. |
| Amount to receive | Amount in display units. Use `1` for NFTs. |
| Wallet path | Path to sender1 wallet. |
| Wallet key | Sender1 wallet decryption key. |
| Counterparty address | Sender2 short address or vanity address. |
| First miner tip | Tip paid from sender1 asset side. |
| Second miner tip | Tip paid from sender2 asset side. |
| Fee you will pay | Sender1 base-currency fee. Minimum `1`. |
| Fee other party will pay | Sender2 base-currency fee. Minimum `1`. |
| Hours until offer expires | Expiration window in hours. Maximum effective window is 30 days. |
### Swap Fees and Tips
Each party pays:
- A base-currency swap fee of at least `1` CLC or CLTC.
- A miner tip for their offered asset side.
For fungible assets, the tip must be at least 1% of the offered amount. The tip is paid in the asset being offered by that side.
For NFT sides, the tip must be `0` because NFTs cannot be fractionally tipped.
Each party must have enough balance for:
- The offered amount.
- The offered-side tip.
- The base-currency transaction fee.
### Swap Expiration
Swap offers expire. The expiration timestamp must be:
- Not earlier than the creation timestamp.
- Not more than 30 days after the creation timestamp.
- Still in the future when the completed transaction is broadcast.
The transaction timestamp must also be within the normal 30-day transaction window.
### Output Shape
`create_swap_tx` creates a partially signed transaction:
```json
{
"txtype": 6,
"timestamp": 1780000000,
"offer_expiration": 1780259200,
"ticker1": "example ",
"nft_series1": 0,
"value1": 100000000000,
"ticker2": "CLTC ",
"nft_series2": 0,
"value2": 5000000000,
"sender1": "sender1_short_address.cltc",
"sender2": "sender2_short_address.cltc",
"tip1": 1000000000,
"tip2": 50000000,
"txfee1": 100000000,
"txfee2": 100000000,
"hash": "swap_hash",
"signature1": "sender1_signature"
}
```
This file is not ready to broadcast yet because it has no `signature2`.
## Counterparty Signing
The counterparty uses `verify_sign_swap_tx` to inspect and sign the offer.
Usage:
```text
verify_sign_swap_tx <path/to/file.json>
```
The tool:
1. Loads the swap JSON file.
2. Loads the counterparty wallet.
3. Confirms the active wallet matches `sender2`.
4. Shows the received amount, sent amount, fee, and tip.
5. Asks yes/no verification questions.
6. Recomputes the swap hash.
7. Signs only if the included hash matches the transaction body.
8. Saves a completed transaction JSON with both `signature1` and `signature2`.
The completed file is saved under:
```text
./transactions/<hash>.json
```
After both signatures exist, either party can broadcast the transaction.
## Swap Safety Checklist
Before signing the second half of a swap, the counterparty should verify:
- The wallet address is their wallet address.
- The asset they receive is correct.
- The amount they receive is correct.
- The asset they send is correct.
- The amount they send is correct.
- The fee they pay is acceptable.
- The tip they pay is acceptable.
- The offer has not expired.
- The file came from the expected counterparty.
Because there is no match-making service, the transaction file must be exchanged manually. Common methods include email, direct message, file sharing, or another off-chain communication channel.
## Broadcasting
All creation tools in this guide create transaction JSON files. They do not mine or confirm the transaction by themselves.
To finish a transaction:
1. Create and sign the JSON file.
2. For swaps, have the counterparty verify and second-sign the JSON file.
3. Broadcast the completed file with `broadcast_transaction` or the GUI wallet.
4. Wait for confirmation.
## Common Problems
| Problem | Likely Cause |
| --- | --- |
| Token creation rejected | Ticker already exists, ticker is reserved, creator wallet is not registered, or fee is below `500`. |
| Issue-more rejected | Token is hard capped, token does not exist, signer is not the original creator, amount is zero, or fee is below `100`. |
| Burn rejected | Asset does not exist, base coin was selected, wallet lacks balance, NFT value is not exactly `1.0`, or fee is below `0.0001`. |
| Swap rejected | Missing second signature, bad signature, insufficient balance, expired offer, bad asset name, missing token/NFT, fee below `1`, or tip below the required amount. |
| Counterparty cannot sign swap | The active wallet does not match `sender2`, or the transaction hash no longer matches the file contents. |

View File

@ -1,533 +0,0 @@
# 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.

423
docs/VALIDATION_TOOLS.md Normal file
View File

@ -0,0 +1,423 @@
# Validation Tools
Contractless includes several command-line tools for manually inspecting hashes, addresses, messages, block headers, torrents, and partially signed transactions.
On Windows, add `.exe` to each command name.
When a tool asks for a wallet path, block path, torrent path, transaction path, or other file path, press `<Tab>` to search and auto-complete files and folders.
## skein_hasher
Hashes text, a full file, or a selected byte range from a file.
This is a custom Contractless hashing tool. It is built for Contractless debugging and manual verification. Its output may not match unrelated Skein tools because Contractless uses its own helper functions, encodings, and hash-size conventions. Use this tool when you want to reproduce hashes the Contractless software expects.
Main uses:
- Manually verify text hashes.
- Manually verify file hashes.
- Manually verify a byte range from a block or torrent file.
- Create hashes that will be used by Contractless tools or debugging flows.
Usage:
```text
skein_hasher text large|small <value>
skein_hasher file large|small <file_path>
skein_hasher bytes large|small <start_byte> <stop_byte> <file_path>
```
Hash sizes:
| Size | Meaning |
| --- | --- |
| `small` | Contractless 128-bit Skein hash output |
| `large` | Contractless 256-bit Skein hash output |
Modes:
| Mode | What It Hashes |
| --- | --- |
| `text` | The exact text passed as `<value>` |
| `file` | The full contents of `<file_path>` |
| `bytes` | The byte range from `<start_byte>` up to, but not including, `<stop_byte>` |
Examples:
```text
skein_hasher text large hello
skein_hasher file small ./blocks/testnet/1000.block
skein_hasher bytes small 0 1024 ./blocks/testnet/1000.block
```
Expected output examples:
```text
Hash of text 'hello': <hash>
Hash of file <hash>
Hash of bytes 0 through 1024 of file './blocks/testnet/1000.block': <hash>
```
## unpack_block_header
Reads a local block file by block number, decodes the block header, prints the header hash value, and prints the decoded header JSON.
Use this when you want to manually inspect the header of a block stored on disk.
Usage:
```text
unpack_block_header <block_number>
```
The tool loads the block from the active network block path in `settings.ini`.
Expected output:
```text
difficulty: <numeric hash value>
hash_value: <block header hash>
{
"unmined_block": {
"timestamp": <block timestamp>,
"miner": "<miner short address>",
"previous_hash": "<parent block hash>",
"next_block_difficulty": <difficulty target>,
"nonce": <nonce>
},
"vrf": <vrf value>,
"proof": "<miner signature proof>"
}
```
Output fields:
| Field | Meaning |
| --- | --- |
| `difficulty` | Numeric value derived from the block header hash. This is useful when manually comparing the block hash against the difficulty target. |
| `hash_value` | Contractless hash of the block header. |
| `unmined_block.timestamp` | Timestamp stored in the block header. |
| `unmined_block.miner` | Short address of the miner that mined the block. |
| `unmined_block.previous_hash` | Parent block hash. |
| `unmined_block.next_block_difficulty` | Difficulty target used for this block. |
| `unmined_block.nonce` | One-byte nonce used by the miner. |
| `vrf` | Random value derived from the miner proof. |
| `proof` | Miner signature proof over the unmined block header hash. |
## unpack_torrent
Reads a local torrent file by block number and prints the decoded torrent metadata as JSON.
Use this when you want to manually inspect the torrent metadata used to fetch, verify, and reconstruct a block.
Usage:
```text
unpack_torrent <block_number>
```
The tool loads the torrent from the active network torrent path in `settings.ini`.
Expected output:
```json
{
"info": {
"length": 0,
"this_block_difficulty": 0,
"timestamp": 0,
"nonce": 0,
"vrf": 0,
"block_hash": "<block header hash>",
"previous_hash": "<parent block hash>",
"piece_length": 0,
"info_hash": "<block file hash>",
"pieces": [
{
"1": "<piece hash>"
}
]
},
"mined_by": "<miner short address>"
}
```
Output fields:
| Field | Meaning |
| --- | --- |
| `info.length` | Total byte length of the matching block file. |
| `info.this_block_difficulty` | Difficulty target recorded in the torrent metadata. |
| `info.timestamp` | Timestamp copied from the block header. |
| `info.nonce` | Nonce copied from the block header. |
| `info.vrf` | VRF value copied from the block header. |
| `info.block_hash` | Header hash of the matching block. |
| `info.previous_hash` | Parent block hash. |
| `info.piece_length` | Number of bytes per block piece, except possibly the final piece. |
| `info.info_hash` | Contractless hash of the full block file bytes. |
| `info.pieces` | Piece number to piece-hash map. Each piece hash lets a node verify one downloaded block chunk. |
| `mined_by` | Short address of the miner that mined the block. |
If the torrent file is missing, the tool prints:
```text
Torrent file not found: <path>
```
If the torrent bytes cannot be parsed, the tool prints:
```text
Failed to parse torrent: <error>
```
## validate_torrent_and_block_headers
Validates that a local block file, local block header, and local torrent file agree with each other.
This is an automated validation helper. It is useful when you want more than a manual unpack, but less than a full node sync/replay. The tool validates several pieces of the block and torrent relationship and prints a pass/fail line for each check.
Usage:
```text
validate_torrent_and_block_headers <block_number>
```
or:
```text
validate_torrent_and_block_headers <block_number> <miner_public_key_hex>
```
If you do not pass `miner_public_key_hex`, the tool looks up the miner public key through the live wallet registry. That lookup relies on node trust: the tool trusts the reachable node to return the correct registered public key for the miner address. The command prompts for a wallet path and decryption key so it can authenticate the lookup.
If you pass `miner_public_key_hex`, the tool uses that public key directly and does not need to ask the node for the miner public key.
Interactive prompts when the public key is not supplied:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Checks performed:
| Check | What It Proves |
| --- | --- |
| `timestamp match` | The block header timestamp and torrent timestamp are the same. |
| `nonce match` | The block header nonce and torrent nonce are the same. |
| `wallet address match` | The miner address in the block header matches the `mined_by` address in the torrent. |
| `block difficulty check` | The numeric block hash value is below the torrent difficulty target. |
| `VRF match` | The VRF value in the block header matches the VRF value in the torrent. |
| `file size check` | The local block file byte length matches `info.length` in the torrent. |
| `block header hash check` | The locally computed block header hash matches `info.block_hash` in the torrent. |
| `VRF validation check` | The VRF value verifies against the unmined block hash, miner public key, and miner proof. |
| `VRF Proof check` | The miner proof is a valid signature over the unmined block hash. |
| `piece <n> hash check` | Each block-file piece hashes to the value recorded in the torrent. |
| `block hash check` | The full local block file hashes to the torrent `info_hash`. |
Expected successful output shape:
```text
timestamp match: [PASSED]
nonce match: [PASSED]
wallet address match: [PASSED]
block difficulty check: [PASSED]
VRF match: [PASSED]
file size check: [PASSED]
block header hash check: [PASSED]
VRF validation check: [PASSED]
VRF Proof check: [PASSED]
piece 1 hash check: [PASSED]
block hash check: [PASSED]
Block <block_number> fully validated.
```
If any required check fails, the final line is:
```text
Block <block_number> FAILED validation.
```
## verify_address
Checks whether a directly pasted address is valid.
This tool is also documented in [Wallet Tools](src/branch/main/docs/WALLET_TOOLS.md), because address validation is useful both for wallet management and for general validation work.
Usage:
```text
verify_address <wallet_address>
```
Interactive prompt:
```text
Please enter the wallet address:
```
Expected output:
```text
true
```
or:
```text
false
```
This command expects the address itself, not a path to a file containing the address.
## verify_message
Verifies a message signature against a wallet address.
This tool is also documented in [Wallet Tools](src/branch/main/docs/WALLET_TOOLS.md), because message verification is commonly used to prove wallet ownership outside of transactions.
Usage:
```text
verify_message "<message to verify>" <wallet_address> <signature>
```
Interactive prompts:
```text
Please enter the message to verify:
Please enter the wallet address:
Please enter the signature:
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Expected output:
```text
valid signature
```
or:
```text
invalid signature
```
The message must match the originally signed text exactly. This command expects the wallet address and signature directly, not paths to files containing them.
## verify_sign_loan_tx
Reviews, verifies, and signs a lender-created loan transaction as the borrower.
This tool is also covered in the loan documentation. In this validation document, the important point is that the tool is not a blind signer. It reads the loan JSON, rebuilds the unsigned loan transaction, checks that the hash still matches, confirms that the active wallet is the borrower, asks the borrower to confirm the human-readable loan terms, then writes a fully signed transaction JSON.
Usage:
```text
verify_sign_loan_tx <path/to/file.json>
```
The current usage text printed by the tool is:
```text
Usage: ./validate_sign_loan_tx <path/to/file.json>
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
The tool asks the borrower to confirm:
- Expected loan amount and asset.
- Collateral amount and collateral asset.
- Payment period.
- Total number of payments.
- Payment amount.
- Grace period.
- Maximum overdue value.
- Loan start date.
- Lender wallet address.
Validation performed before signing:
- Confirms the JSON can be read and parsed.
- Resolves lender and borrower addresses, including vanity inputs.
- Confirms the loaded wallet matches the borrower address.
- Confirms the borrower wallet short address is valid.
- Rebuilds the unsigned loan transaction from the JSON fields.
- Recomputes the loan hash.
- Refuses to sign if the recomputed hash does not match the included hash.
- Signs with the borrower wallet only after all prompts and hash validation pass.
Output:
The tool writes the fully signed transaction JSON under:
```text
./transactions/<hash>.json
```
It also prints the signed JSON to the terminal.
If the user rejects any confirmation prompt, or if the hash does not match, the tool prints an error and does not sign.
## verify_sign_swap_tx
Reviews, verifies, and signs a sender1-created swap transaction as sender2.
This tool is also covered in the swap documentation. In this validation document, the important point is that the tool protects the second signer. It reads the swap JSON, confirms the active wallet is sender2, asks sender2 to confirm the human-readable swap terms, rebuilds the unsigned swap, verifies the included hash, then writes a fully signed transaction JSON.
Usage:
```text
verify_sign_swap_tx <path/to/file.json>
```
The current usage text printed by the tool is:
```text
Usage:./sign_swap <path/to/file.json>
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
The tool asks sender2 to confirm:
- Amount and asset sender2 expects to receive.
- Amount and asset sender2 expects to send.
- Sender2 fee in the base coin.
- Sender2 miner tip.
Validation performed before signing:
- Confirms the JSON can be read and parsed.
- Resolves sender1 and sender2 addresses, including vanity inputs.
- Confirms the loaded wallet matches sender2.
- Normalizes ticker names or accepts the base coin.
- Rebuilds the unsigned swap transaction from the JSON fields.
- Recomputes the swap hash.
- Refuses to sign if the recomputed hash does not match the included hash.
- Signs with the sender2 wallet only after all prompts and hash validation pass.
Output:
The tool writes the fully signed transaction JSON under:
```text
./transactions/<hash>.json
```
It also prints the signed JSON to the terminal.
If the user rejects any confirmation prompt, or if the hash does not match, the tool prints an error and does not sign.

426
docs/WALLET_TOOLS.md Normal file
View File

@ -0,0 +1,426 @@
# Wallet Tools
Contractless wallet tools create wallets, restore wallets, export private-key image backups, register wallet addresses with the network, and sign or verify messages.
This page also covers balance lookup helpers:
- `lookup_local_balance`
- `lookup_remote_balance`
On Windows, add `.exe` to each command name.
When a tool asks for a wallet path, image path, private-key path, or output directory, press `<Tab>` to search and auto-complete files and folders.
## Private Key Images and Encryption Keys
Contractless wallets use a private key image as a backup format. This is original to Contractless: the wallet private key can be represented as image data, and that image data can be saved as a PNG backup.
A private key image is not enough by itself. To restore a wallet from a private key image, you also need the encryption/decryption key used to protect that image. Always back up both pieces:
- Store the private key image digitally in a safe location.
- Store the encryption/decryption key offline and securely.
- Do not store the image and the encryption/decryption key together.
If you lose the private key image, you may not be able to restore the wallet. If you lose the encryption/decryption key for the image, you may not be able to decrypt the image and recover the wallet.
The raw private key itself does not require the encryption/decryption key after it has been recovered. However, the raw private key can only be retrieved from wallet/image data with `private_key_from_image`. Treat the raw private key as the most sensitive form of the wallet: anyone who has it can recreate the wallet and sign as that address.
The normal wallet file is encrypted and opened with the wallet encryption/decryption key. If the node starts and the configured wallet file does not exist, the node can create a wallet automatically. These tools are for manual wallet creation, backup, restoration, registration, and message signing.
## create_new_wallet
Creates a new encrypted wallet file at the requested path and filename.
Use this when you want to create a wallet manually instead of letting the node create one during startup.
Usage:
```text
create_new_wallet <wallet_path> <wallet_filename>
```
Interactive prompts:
```text
Please enter the path to your wallet file:
Please enter wallet filename:
Please enter your wallet encryption key, if you do not have a wallet yet, enter a new encryption key here:
```
The `wallet_path` is the folder where the wallet file should be created. The `wallet_filename` is the file name to write inside that folder.
The tool refuses to overwrite an existing wallet file. After creation, it prints the wallet display information, including the wallet address information.
## recreate_wallet
Recreates an encrypted wallet file from a raw private key stored in a file.
Use this after recovering the raw private key with `private_key_from_image`, or when you already have the raw private key saved separately. This tool does not require the old wallet file. It uses the private key to rebuild the wallet file and asks for an encryption key for the rebuilt wallet/private-key image data.
Usage:
```text
recreate_wallet <private_key_file> <output_wallet_file>
```
or:
```text
recreate_wallet <private_key_file> <output_wallet_dir> <output_wallet_filename>
```
Interactive prompts:
```text
Please enter the path to the file containing your wallet private key:
Please enter the directory path where the rebuilt wallet JSON should be written:
Please enter the filename for the rebuilt wallet JSON:
Please enter your encryption key:
```
The output is a rebuilt wallet JSON file. If a registered vanity address exists for the wallet, the tool attempts to include it in the rebuilt wallet file.
## recreate_wallet_from_image
Recreates an encrypted wallet file from private-key image data.
Use this when restoring from a private key image backup. The input can be a PNG image file or a file containing base64 image data. You must know the encryption/decryption key for that private key image.
Usage:
```text
recreate_wallet_from_image <base64_or_png_file_path> <output_wallet_file>
```
or:
```text
recreate_wallet_from_image <base64_or_png_file_path> <output_wallet_dir> <output_wallet_filename>
```
Interactive prompts:
```text
Please enter the path to the Base64 or PNG image file:
Please enter the directory path where the rebuilt wallet JSON should be written:
Please enter the filename for the rebuilt wallet JSON:
Please enter your encryption key:
```
The output is a rebuilt wallet JSON file. If a registered vanity address exists for the wallet, the tool attempts to include it in the rebuilt wallet file.
## save_private_key_image
Writes the wallet private key image data to a PNG file.
Use this to create a digital image backup from an existing wallet file or from a file that already contains private-key image data. This tool does not print the raw private key. It only writes the image backup.
Usage:
```text
save_private_key_image <wallet_or_image_file> <output_png_path>
```
or:
```text
save_private_key_image <wallet_or_image_file> <output_png_dir> <output_png_filename>
```
Interactive prompts:
```text
Please enter the path to the wallet/image file:
Please enter the directory path where the PNG should be written:
Please enter the filename for the PNG:
```
The source file can be a wallet JSON file or a file containing base64 private-key image data. The output is a PNG file.
Remember: this PNG backup still needs the encryption/decryption key to restore the wallet.
## private_key_from_image
Extracts the raw private key from wallet/image data.
Use this only when you truly need the raw private key, such as before using `recreate_wallet`. The tool reads wallet/image data, asks for the encryption/decryption key, decrypts the private-key image data, and prints the raw private key.
Usage:
```text
private_key_from_image <wallet_or_image_data_file>
```
Interactive prompts:
```text
Please enter the path to the file containing the wallet/image data:
Please enter your encryption key:
```
The input file can be a wallet JSON file or a file containing base64 private-key image data.
The printed private key does not require the encryption/decryption key after it has been recovered. Protect it carefully and do not leave it in terminal scrollback, screenshots, cloud notes, chat messages, or unsecured text files.
## register_wallet
Registers the wallet with the network wallet registry.
Important: a wallet must be registered before it can receive funds or be used in any transaction. If the wallet is not registered, other nodes cannot validate the address and public key relationship needed for normal wallet activity.
Wallet registration submits a signed short-address to public-key mapping to a reachable peer. This lets other tools and nodes look up the wallet public key when they need to verify signatures for that address.
Usage:
```text
register_wallet
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Expected success output:
```text
Wallet registered: <short address>
```
Run this after creating or restoring a wallet if the address is not already registered with the network.
## verify_address
Checks whether a directly pasted address is valid.
Use this to quickly test short addresses or vanity addresses before using them in a transaction or sharing them with someone else.
Usage:
```text
verify_address <wallet_address>
```
Interactive prompt:
```text
Please enter the wallet address:
```
Expected output:
```text
true
```
or:
```text
false
```
This command expects the address itself, not a path to a file containing the address.
## create_vanity_tx
Creates and signs a vanity address registration transaction for the selected wallet.
A vanity address is a human-readable alias that maps back to the wallet's real short address. The vanity name is not a separate wallet and does not create a new private key. It is a registered address alias owned by the wallet that signs the transaction.
Usage:
```text
create_vanity_tx <vanity_name> <txfee>
```
Interactive prompts:
```text
Enter the vanity name to register (letters only, up to 20 characters):
Please enter the vanity registration fee: (e.g. 1.0, minimum fee 5.00000000):
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Vanity name rules:
- 1 to 20 characters
- letters only
- no numbers
- no punctuation
- no spaces
The tool automatically lowercases the vanity address and appends the correct wallet/network suffix. For example, entering:
```text
BruceBates
```
may create a vanity address like:
```text
brucebates.cltc
```
The transaction fee is entered as a decimal amount. The minimum vanity registration fee is:
```text
5.00000000
```
The tool saves the signed transaction JSON into:
```text
./transactions/<transaction_hash>.json
```
Creating the file does not register the vanity address by itself. Broadcast the saved transaction with `broadcast_transaction`.
After the transaction confirms, tools that support address resolution can use the vanity address as an alias for the wallet's real short address.
## sign_message
Signs a plain-text message with the selected wallet.
Use this to prove control of a wallet address without sending a transaction. The message text is hashed and then signed with the wallet private key.
Usage:
```text
sign_message "<message to sign>"
```
Interactive prompts:
```text
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Expected output:
```text
message: <message>, signature: <signature>
```
The exact message matters. If someone later verifies the signature, they must verify the same text.
## verify_message
Verifies a message signature against a wallet address.
Use this when someone gives you a message, wallet address, and signature, and you want to confirm that the address signed that exact message.
Usage:
```text
verify_message "<message to verify>" <wallet_address> <signature>
```
Interactive prompts:
```text
Please enter the message to verify:
Please enter the wallet address:
Please enter the signature:
Please enter the path to your wallet file:
What is your wallet decryption key?
```
Expected output:
```text
valid signature
```
or:
```text
invalid signature
```
This command expects the wallet address and signature directly, not paths to files containing them. The tool uses the selected wallet to authenticate the network lookup needed to retrieve the public key for the address being checked.
## lookup_local_balance
Reads a local balance file from disk and prints the stored balance as a decimal value.
This is a low-level local inspection tool. It does not ask a node for current wallet state, does not include mempool changes, and does not prove that the value is the current network balance. It simply decodes the first 8 bytes of a local balance file as a little-endian `u64`.
Usage:
```text
lookup_local_balance <path/to/file.bal>
```
Interactive prompt:
```text
Please enter the path to the balance file:
```
When entering the path interactively, press `<Tab>` to search and auto-complete files and folders.
Expected output:
```json
{
"balance": "123.45678900"
}
```
Use this mainly for debugging or manually inspecting local balance-sheet files.
## lookup_remote_balance
Asks a configured node for all known asset balances for one wallet address.
Unlike `lookup_local_balance`, this is a network lookup. It contacts a peer, authenticates with the selected wallet, resolves short/long/vanity address input, and returns balances for the requested address.
Usage:
```text
lookup_remote_balance <address_file>
```
Interactive prompts:
```text
Please enter the path to the file containing the wallet address:
Please enter the path to your wallet file:
What is your wallet decryption key?
```
The address file may contain either:
- a plain wallet address
- a wallet JSON file containing `short_address` or `vanity_address`
When entering file paths interactively, press `<Tab>` to search and auto-complete files and folders.
Expected output:
```json
{
"balances": [
{
"asset": "CLTC",
"nft_series": 0,
"balance": "100.00000000"
},
{
"asset": "testcoin",
"nft_series": 0,
"balance": "25.00000000"
}
]
}
```
`asset` is the base coin, token, or NFT asset name.
`nft_series` is `0` for normal base-coin/token balances and 1-of-1 NFTs. For numbered NFT collections, it identifies the numbered item.
`balance` is displayed with 8 decimal places.

View File

@ -0,0 +1,284 @@
# Windows Installation
This guide explains how to build, install, configure, and run a Contractless testnet node as a Windows service.
Windows service commands should be run from an elevated PowerShell window. The service runs in the background and must be unlocked with `contractless-submit-key.exe` after it starts.
## Build Flags
The default build target is testnet:
```powershell
cargo build --release
```
The same testnet build can be requested explicitly:
```powershell
cargo build --release --features testnet
```
Mainnet has its own feature flag:
```powershell
cargo build --release --no-default-features --features mainnet
```
Mainnet is not active during the testnet phase. Use the testnet binary unless mainnet has been officially enabled.
After building, release binaries are written to:
```text
target\release\
```
The testnet node binary is:
```text
target\release\contractless-testnet.exe
```
The Windows service unlock helper is:
```text
target\release\contractless-submit-key.exe
```
## PostgreSQL Installation
**Do not expose PostgreSQL to the public Internet. The PostgreSQL port should not be publicly accessible.** Contractless needs a public RPC port for peers, but PostgreSQL should stay local or private, usually on `127.0.0.1`. Do not open PostgreSQL port `5432` in your router, cloud firewall, VPS firewall, or host firewall.
The Windows PostgreSQL installer requires an elevated terminal. If you prefer to create PostgreSQL manually, [create the PostgreSQL database manually](src/branch/main/docs/POSTGRES.md).
Contractless uses PostgreSQL for transaction lookup and mempool-style records. The node creates and updates its own tables during startup, but PostgreSQL itself must exist first with a database, user, password, and permissions.
Build the release binaries first:
```powershell
cargo build --release
```
Run the PostgreSQL installer from an elevated PowerShell window:
```powershell
.\target\release\postgres_installer.exe
```
The Windows installer:
- checks whether PostgreSQL already exists at the selected install path
- downloads the PostgreSQL installer when needed
- installs PostgreSQL unattended
- creates or updates the configured database user
- creates the configured database when it does not already exist
- verifies the blockchain database login
- prints the `[Postgres]` and `[Postgres-Testnet]` settings blocks to paste into `settings.ini`
For testnet builds, paste the printed values into `[Postgres-Testnet]` in the active `settings.ini`.
Example:
```ini
[Postgres-Testnet]
host = 127.0.0.1
port = 5432
user = contractless
password = your_postgres_password_here
dbname = contractless_db
```
## Run Path and Settings Location
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
For a stable Windows service install, use:
```text
C:\Program Files\Contractless\settings.ini
```
Create the install directory from an elevated PowerShell window:
```powershell
New-Item -ItemType Directory -Force "C:\Program Files\Contractless"
```
Move the repository settings file into the install directory:
```powershell
Move-Item .\settings.ini "C:\Program Files\Contractless\settings.ini"
```
The node also scopes runtime folders by network internally, so testnet data and mainnet data do not collide.
Important runtime paths:
| Setting | Purpose |
| --- | --- |
| `BLOCK_PATH` | Saved block files |
| `TORRENT_PATH` | Torrent metadata and staged torrents |
| `DB_PATH` | sled state |
| `BALANCE_SHEET` | Balance files |
| `LOG_PATH` | Runtime logs |
| `WALLET_PATH` | Wallet directory |
| `WALLET_NAME` | Wallet filename |
## Copy Binaries
Copy the testnet node binary and Windows unlock helper into the install directory:
```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 the PostgreSQL installer if you want it available from the install directory:
```powershell
Copy-Item .\target\release\postgres_installer.exe "C:\Program Files\Contractless\"
```
Copy wallet tools:
```powershell
Copy-Item .\target\release\create_new_wallet.exe "C:\Program Files\Contractless\"
Copy-Item .\target\release\recreate_wallet.exe "C:\Program Files\Contractless\"
Copy-Item .\target\release\recreate_wallet_from_image.exe "C:\Program Files\Contractless\"
Copy-Item .\target\release\register_wallet.exe "C:\Program Files\Contractless\"
Copy-Item .\target\release\verify_address.exe "C:\Program Files\Contractless\"
```
Copy transaction and lookup tools as needed:
```powershell
Copy-Item .\target\release\create_transfer_tx.exe "C:\Program Files\Contractless\"
Copy-Item .\target\release\broadcast_transaction.exe "C:\Program Files\Contractless\"
Copy-Item .\target\release\lookup_height.exe "C:\Program Files\Contractless\"
Copy-Item .\target\release\lookup_transaction.exe "C:\Program Files\Contractless\"
Copy-Item .\target\release\lookup_remote_balance.exe "C:\Program Files\Contractless\"
```
You can copy any additional tools from `target\release` using the same pattern.
## Install the Service
Install the Windows service from an elevated PowerShell window:
```powershell
& "C:\Program Files\Contractless\contractless-testnet.exe" --install-service
```
This command is only needed the first time the service is installed. Do not run `--install-service` every time you restart the node, rebuild from source, or copy in a newer binary.
The service is installed using the binary path where `contractless-testnet.exe` is located when `--install-service` is run. If you later move the binary to a different folder, uninstall and reinstall the service from the new path.
## Start and Unlock the Service
Start the service:
```powershell
& "C:\Program Files\Contractless\contractless-testnet.exe" --start-service
```
After the service starts, submit the wallet decryption key:
```powershell
& "C:\Program Files\Contractless\contractless-submit-key.exe"
```
The service starts in a locked state. It does not begin normal unlocked node operation until `contractless-submit-key.exe` accepts the wallet key.
Check the unlock pipe:
```powershell
& "C:\Program Files\Contractless\contractless-submit-key.exe" ping
```
Check the service unlock state:
```powershell
& "C:\Program Files\Contractless\contractless-submit-key.exe" status
```
## Stop the Service
Stop the service from an elevated PowerShell window:
```powershell
& "C:\Program Files\Contractless\contractless-testnet.exe" --stop-service
```
## Automatic Startup
The service can be configured to start automatically with Windows.
From an elevated PowerShell window:
```powershell
sc.exe config ContractlessTestnet start= auto
```
Or use Windows Services:
1. Open `services.msc`
2. Find `Contractless Testnet`
3. Open Properties
4. Set Startup type to `Automatic`
If the service is set to start automatically, you do not need to run `--start-service` after every reboot. You still need to run `contractless-submit-key.exe` after the computer restarts because the service cannot unlock the wallet without the wallet decryption key.
## Updating a Node
For normal source updates, do not reinstall the service.
Stop the service:
```powershell
& "C:\Program Files\Contractless\contractless-testnet.exe" --stop-service
```
Pull the latest source:
```powershell
git pull
```
Rebuild:
```powershell
cargo build --release
```
Copy the updated binaries:
```powershell
Copy-Item .\target\release\contractless-testnet.exe "C:\Program Files\Contractless\" -Force
Copy-Item .\target\release\contractless-submit-key.exe "C:\Program Files\Contractless\" -Force
```
Start the service:
```powershell
& "C:\Program Files\Contractless\contractless-testnet.exe" --start-service
```
Submit the wallet decryption key:
```powershell
& "C:\Program Files\Contractless\contractless-submit-key.exe"
```
## Uninstall the Service
Uninstall the service only when you want to remove the Windows service registration:
```powershell
& "C:\Program Files\Contractless\contractless-testnet.exe" --uninstall-service
```
Uninstalling is not part of normal updates.

View File

@ -1,8 +1,8 @@
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::env;
use blockchain::fs;
use blockchain::Duration;
use blockchain::Error;
use contractless::common::network_paths_and_settings::block_extension_and_paths;
use contractless::env;
use contractless::fs;
use contractless::Duration;
use contractless::Error;
fn calculate_average_time_between_timestamps(
start_block: u32,

View File

@ -1,12 +1,12 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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;
use blockchain::Path;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::from_slice;
use contractless::read;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::wallets::structures::SavedWallet;
use contractless::Path;
use serde_json::Value;
fn display_vanity_address(short_address: &str) -> Option<String> {

View File

@ -1,11 +1,11 @@
#[cfg(windows)]
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
use contractless::common::cli_prompts::prompt_hidden_nonempty;
#[cfg(windows)]
use blockchain::startup::unlock_pipe::pipe_name;
use contractless::startup::unlock_pipe::pipe_name;
#[cfg(windows)]
use blockchain::startup::unlock_structs::{UnlockPipeRequest, UnlockPipeResponse};
use contractless::startup::unlock_structs::{UnlockPipeRequest, UnlockPipeResponse};
#[cfg(windows)]
use blockchain::{env, from_slice, to_string};
use contractless::{env, from_slice, to_string};
#[cfg(windows)]
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(windows)]

View File

@ -1,14 +1,14 @@
use blockchain::blocks::burn::{BurnTransaction, UnsignedBurnTransaction};
use blockchain::common::asset_names::padded_normalized_asset_input;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use contractless::blocks::burn::{BurnTransaction, UnsignedBurnTransaction};
use contractless::common::asset_names::padded_normalized_asset_input;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::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};
use contractless::common::types::BURN_FEE;
use contractless::json;
use contractless::wallets::structures::Wallet;
use contractless::File;
use contractless::Utc;
use contractless::{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.

View File

@ -1,14 +1,14 @@
use blockchain::blocks::collateral::{
use contractless::blocks::collateral::{
CollateralClaimTransaction, UnsignedCollateralClaimTransaction,
};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::COLLATERAL_FEE;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::common::types::COLLATERAL_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
use contractless::json;
use contractless::wallets::structures::Wallet;
use contractless::File;
use contractless::Utc;
use contractless::{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.

View File

@ -1,13 +1,13 @@
use blockchain::blocks::issue_token::{IssueTokenTransaction, UnsignedIssueTokenTransaction};
use blockchain::common::asset_names::padded_normalized_asset_input;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::ISSUE_TOKEN_FEE;
use contractless::blocks::issue_token::{IssueTokenTransaction, UnsignedIssueTokenTransaction};
use contractless::common::asset_names::padded_normalized_asset_input;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::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};
use contractless::json;
use contractless::wallets::structures::Wallet;
use contractless::File;
use contractless::Utc;
use contractless::{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.

View File

@ -1,14 +1,14 @@
use blockchain::blocks::loan_payment::{
use contractless::blocks::loan_payment::{
ContractPaymentTransaction, UnsignedContractPaymentTransaction,
};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::BORROWER_FEE;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::common::types::BORROWER_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
use contractless::json;
use contractless::wallets::structures::Wallet;
use contractless::File;
use contractless::Utc;
use contractless::{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.

View File

@ -1,17 +1,17 @@
use blockchain::blocks::loans::UnsignedLoanContractTransaction;
use blockchain::common::asset_names::{
use contractless::blocks::loans::UnsignedLoanContractTransaction;
use contractless::common::asset_names::{
padded_loan_collateral_input_or_base, padded_normalized_asset_input,
};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::common::types::LENDER_FEE;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::common::network_paths_and_settings::block_extension_and_paths;
use contractless::common::types::LENDER_FEE;
use blockchain::json;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::{create_dir_all, AsyncWriteExt};
use blockchain::{NaiveDate, NaiveTime, TimeZone, Utc};
use contractless::json;
use contractless::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use contractless::wallets::structures::Wallet;
use contractless::File;
use contractless::{create_dir_all, AsyncWriteExt};
use contractless::{NaiveDate, NaiveTime, TimeZone, Utc};
fn normalize_asset_or_base(input: &str) -> Option<String> {
let base_coin = block_extension_and_paths().1;

View File

@ -1,12 +1,12 @@
use blockchain::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::MARKETING_FEE;
use contractless::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction};
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::common::types::MARKETING_FEE;
use blockchain::json;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
use contractless::json;
use contractless::wallets::structures::Wallet;
use contractless::File;
use contractless::Utc;
use contractless::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String {

View File

@ -1,7 +1,7 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use blockchain::env;
use blockchain::metadata;
use blockchain::wallets::structures::Wallet;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible};
use contractless::env;
use contractless::metadata;
use contractless::wallets::structures::Wallet;
use std::path::PathBuf;
#[tokio::main]

View File

@ -1,13 +1,13 @@
use blockchain::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction};
use blockchain::common::asset_names::padded_normalized_asset_input;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::CREATE_NFT_FEE;
use contractless::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction};
use contractless::common::asset_names::padded_normalized_asset_input;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::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};
use contractless::json;
use contractless::wallets::structures::Wallet;
use contractless::File;
use contractless::Utc;
use contractless::{create_dir_all, AsyncWriteExt};
// pad the coin to ensure 15 characters
fn pad_to_width(input: &str, width: usize) -> String {

View File

@ -1,16 +1,16 @@
use blockchain::blocks::swap::UnsignedSwapTransaction;
use blockchain::common::asset_names::padded_normalized_asset_input;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::common::types::SWAP_FEE;
use contractless::blocks::swap::UnsignedSwapTransaction;
use contractless::common::asset_names::padded_normalized_asset_input;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::common::network_paths_and_settings::block_extension_and_paths;
use contractless::common::types::SWAP_FEE;
use blockchain::json;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::wallets::structures::Wallet;
use blockchain::Duration;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
use contractless::json;
use contractless::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use contractless::wallets::structures::Wallet;
use contractless::Duration;
use contractless::File;
use contractless::Utc;
use contractless::{create_dir_all, AsyncWriteExt};
fn normalize_asset_or_base(input: &str) -> Option<String> {
let base_coin = block_extension_and_paths().1;

View File

@ -1,13 +1,13 @@
use blockchain::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction};
use blockchain::common::asset_names::padded_normalized_asset_input;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::CREATE_TOKEN_FEE;
use contractless::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction};
use contractless::common::asset_names::padded_normalized_asset_input;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::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};
use contractless::json;
use contractless::wallets::structures::Wallet;
use contractless::File;
use contractless::Utc;
use contractless::{create_dir_all, AsyncWriteExt};
fn display_fee(value: u64) -> f64 {
value as f64 / 100_000_000.0

View File

@ -1,16 +1,16 @@
use blockchain::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
use blockchain::common::asset_names::padded_normalized_asset_input;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use contractless::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
use contractless::common::asset_names::padded_normalized_asset_input;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::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::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::wallets::structures::Wallet;
use blockchain::File;
use blockchain::Utc;
use blockchain::{create_dir_all, AsyncWriteExt};
use contractless::common::types::{NON_BASE_TRANSFER_MIN_FEE, TRANSFER_FEE};
use contractless::env;
use contractless::json;
use contractless::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use contractless::wallets::structures::Wallet;
use contractless::File;
use contractless::Utc;
use contractless::{create_dir_all, AsyncWriteExt};
fn normalize_asset_or_base(input: &str) -> Option<String> {
let base_coin = block_extension_and_paths().1;

View File

@ -1,13 +1,13 @@
use blockchain::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction};
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::types::{VANITY_ADDRESS_FEE, VANITY_ADDRESS_TYPE};
use contractless::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction};
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::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};
use contractless::env;
use contractless::json;
use contractless::wallets::structures::Wallet;
use contractless::File;
use contractless::Utc;
use contractless::{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.

View File

@ -1,25 +1,28 @@
use blockchain::blocks::storage_key::{StorageKey, UnsignedStorageKey};
use blockchain::blocks::storage_values::{
use contractless::blocks::storage_key::{StorageKey, UnsignedStorageKey};
use contractless::blocks::storage_values::{
StorageBool, StorageString, StorageU128, StorageU16, StorageU32, StorageU64, StorageU8,
UnsignedStorageBool, UnsignedStorageString, UnsignedStorageU128, UnsignedStorageU16,
UnsignedStorageU32, UnsignedStorageU64, UnsignedStorageU8, STORAGE_KEY_HASH_BYTES,
STORAGE_STRING_VALUE_BYTES, STORAGE_VALUE_KEY_BYTES,
};
use blockchain::common::cli_prompts::{
use contractless::common::cli_prompts::{
prompt_hidden_nonempty, prompt_visible, prompt_visible_with_default, prompt_wallet_path,
};
use blockchain::common::types::{
use contractless::common::types::{
STORAGE_BOOL_TYPE, STORAGE_KEY_FEE, STORAGE_KEY_TYPE, STORAGE_STRING_TYPE, STORAGE_U128_TYPE,
STORAGE_U16_TYPE, STORAGE_U32_TYPE, STORAGE_U64_TYPE, STORAGE_U8_TYPE, STORAGE_VALUE_FEE,
};
use blockchain::wallets::structures::Wallet;
use blockchain::{create_dir_all, decode, env, json, AsyncWriteExt, File, Utc};
use contractless::wallets::structures::Wallet;
use contractless::{create_dir_all, decode, env, json, AsyncWriteExt, File, Utc};
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
fn usage() {
println!("Usage:");
println!(" ./data_storage_tx create_storage [txfee]");
println!(
" ./data_storage_tx create_storage [txfee] default fee: {:.8}",
display_fee(STORAGE_KEY_FEE)
);
println!(" ./data_storage_tx bool <storage_key_hash> <field_key> <true|false> [txfee]");
println!(" ./data_storage_tx u8|8bit <storage_key_hash> <field_key> <value> [txfee]");
println!(" ./data_storage_tx u16|16bit <storage_key_hash> <field_key> <value> [txfee]");
@ -27,7 +30,11 @@ fn usage() {
println!(" ./data_storage_tx u64|64bit <storage_key_hash> <field_key> <value> [txfee]");
println!(" ./data_storage_tx u128|128bit <storage_key_hash> <field_key> <value> [txfee]");
println!(
" ./data_storage_tx string <storage_key_hash> <field_key> <value> [previous_hash] [txfee]"
" ./data_storage_tx string <storage_key_hash> <field_key> <value> [previous_hash|0] [txfee]"
);
println!(
" Storage value transactions default to fee: {:.8}",
display_fee(STORAGE_VALUE_FEE)
);
}
@ -43,11 +50,14 @@ fn parse_fee(value: &str) -> Option<u64> {
Some((parsed * 100_000_000.0).round() as u64)
}
async fn prompt_fee(default_fee: u64) -> u64 {
async fn prompt_fee(label: &str, default_fee: u64) -> u64 {
let default_display = format!("{:.8}", display_fee(default_fee));
loop {
let value =
prompt_visible_with_default("Please enter the transaction fee", &default_display).await;
let value = prompt_visible_with_default(
&format!("Please enter the {label} transaction fee"),
&default_display,
)
.await;
if let Some(fee) = parse_fee(&value) {
return fee;
}
@ -74,8 +84,16 @@ fn valid_storage_key_hash(value: &str) -> bool {
matches!(decode(value.trim()), Ok(bytes) if bytes.len() == STORAGE_KEY_HASH_BYTES)
}
fn valid_previous_hash(value: &str) -> bool {
valid_storage_key_hash(value)
fn normalize_previous_hash(value: &str) -> Option<String> {
let trimmed = value.trim();
if trimmed == "0" {
return Some(ZERO_HASH.to_string());
}
if valid_storage_key_hash(trimmed) {
Some(trimmed.to_string())
} else {
None
}
}
fn normalize_storage_type(input: &str) -> Option<&'static str> {
@ -96,7 +114,7 @@ async fn prompt_storage_hash(args: &[String], index: usize) -> Option<String> {
let value = if args.len() > index {
args[index].clone()
} else {
prompt_visible("Please enter the storage key hash: ").await
prompt_visible("Please enter the storage key hash (32-byte / 64-character hex): ").await
};
if !valid_storage_key_hash(&value) {
println!("Storage key hash must be a 32-byte / 64-character hex string.");
@ -109,7 +127,7 @@ async fn prompt_field_key(args: &[String], index: usize) -> Option<String> {
let value = if args.len() > index {
args[index].clone()
} else {
prompt_visible("Please enter the storage field key: ").await
prompt_visible("Please enter the storage field key (1 to 50 bytes): ").await
};
let Some(key) = fixed_field_key(&value) else {
println!("Storage field key must be 1 to 50 bytes.");
@ -165,7 +183,7 @@ async fn create_storage_key(args: &[String], timestamp: u32, wallet: &Wallet) {
}
}
} else {
prompt_fee(STORAGE_KEY_FEE).await
prompt_fee("storage key creation", STORAGE_KEY_FEE).await
};
let address = wallet.saved.short_address.trim();
@ -233,7 +251,7 @@ macro_rules! create_numeric_storage {
}
}
} else {
prompt_fee(STORAGE_VALUE_FEE).await
prompt_fee(&format!("{} storage value", $label), STORAGE_VALUE_FEE).await
};
let address = $wallet.saved.short_address.trim();
if !Wallet::short_address_validation(address) {
@ -303,7 +321,7 @@ async fn create_bool_storage(
}
}
} else {
prompt_fee(STORAGE_VALUE_FEE).await
prompt_fee("bool storage value", STORAGE_VALUE_FEE).await
};
let address = wallet.saved.short_address.trim();
if !Wallet::short_address_validation(address) {
@ -354,7 +372,7 @@ async fn create_string_storage(
let value_input = if args.len() > 4 {
args[4].clone()
} else {
prompt_visible("Please enter the string value: ").await
prompt_visible("Please enter the string value (180 bytes or less): ").await
};
let Some(value) = fixed_string_value(&value_input) else {
println!("Storage string value must be 180 bytes or less.");
@ -364,12 +382,16 @@ async fn create_string_storage(
let previous = if args.len() > 5 {
args[5].clone()
} else {
prompt_visible_with_default("Please enter the previous string tx hash", ZERO_HASH).await
prompt_visible_with_default(
"Please enter the previous string tx hash (use 0 for none)",
"0",
)
.await
};
if !valid_previous_hash(&previous) {
println!("Previous hash must be a 32-byte / 64-character hex string.");
let Some(previous) = normalize_previous_hash(&previous) else {
println!("Previous hash must be 0 or a 32-byte / 64-character hex string.");
return;
}
};
let txfee_arg_index = if args.len() > 6 { Some(6) } else { None };
let txfee = if let Some(index) = txfee_arg_index {
@ -381,7 +403,7 @@ async fn create_string_storage(
}
}
} else {
prompt_fee(STORAGE_VALUE_FEE).await
prompt_fee("string storage value", STORAGE_VALUE_FEE).await
};
let address = wallet.saved.short_address.trim();
if !Wallet::short_address_validation(address) {
@ -396,7 +418,7 @@ async fn create_string_storage(
key,
value,
address: address.to_string(),
previous: previous.trim().to_string(),
previous,
txfee,
};
let transaction = match StorageString::new(unsigned, &wallet.saved.private_key).await {
@ -430,7 +452,10 @@ async fn main() {
let command = if args.len() > 1 {
args[1].clone()
} else {
prompt_visible("Please enter storage transaction type: ").await
prompt_visible(
"Please enter storage transaction type (create_storage, bool, u8, u16, u32, u64, u128, string): ",
)
.await
};
let Some(storage_type) = normalize_storage_type(&command) else {
usage();

View File

@ -1,11 +1,11 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::json;
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;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::records::unpack_block::load_by_binary_data::load_block_from_binary;
use contractless::standalone_tools::connections::handshake;
use contractless::to_string_pretty;
#[tokio::main]
async fn main() {

View File

@ -1,11 +1,11 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::json;
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;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::records::unpack_block::load_by_binary_data::load_block_from_binary;
use contractless::standalone_tools::connections::handshake;
use contractless::to_string_pretty;
#[tokio::main]
async fn main() {

View File

@ -1,99 +0,0 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::wallets::structures::Wallet;
#[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 wallet_path = prompt_wallet_path().await;
let encryption_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(encryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let wallet_address = match resolve_wallet_address_input(&wallet_address, &wallet).await {
Ok(address) => address,
Err(err) => {
eprintln!("wallet address is not valid: {err}");
return;
}
};
// 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::WalletParts {
public_key: wallet.saved.public_key.clone(),
private_key: wallet.saved.private_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!(
"{}",
serde_json::to_string_pretty(&json!({ "hex": encode(response) })).unwrap()
);
connected = true;
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -1,84 +0,0 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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;
#[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 wallet_path = prompt_wallet_path().await;
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: encryption_key.clone(),
wallet_path: wallet_path.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!(
"{}",
serde_json::to_string_pretty(&json!({ "hex": encode(response) })).unwrap()
);
connected = true;
}
}
Err(_) => {
connected = false;
}
}
}
if !connected {
eprintln!("failed to connect");
}
}

View File

@ -1,9 +1,9 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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 contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
#[tokio::main]
async fn main() {

View File

@ -1,9 +1,9 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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 contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
#[tokio::main]
async fn main() {

View File

@ -1,9 +1,9 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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 contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
fn format_balance(value: u64) -> String {
let whole = value / 100_000_000;

View File

@ -1,13 +1,13 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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::standalone_tools::loan_lookup_json::parse_loan_lookup_by_address;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::wallets::structures::Wallet;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::encode;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::standalone_tools::loan_lookup_json::parse_loan_lookup_by_address;
use contractless::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use contractless::wallets::structures::Wallet;
#[tokio::main]
async fn main() {

View File

@ -1,11 +1,11 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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::standalone_tools::loan_lookup_json::parse_loan_summary;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::encode;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::standalone_tools::loan_lookup_json::parse_loan_summary;
#[tokio::main]
async fn main() {

View File

@ -1,7 +1,7 @@
use blockchain::env;
use blockchain::fs;
use blockchain::json;
use blockchain::tilde;
use contractless::env;
use contractless::fs;
use contractless::json;
use contractless::tilde;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};

View File

@ -1,29 +1,29 @@
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 contractless::blocks::burn::BurnTransaction;
use contractless::blocks::collateral::CollateralClaimTransaction;
use contractless::blocks::issue_token::IssueTokenTransaction;
use contractless::blocks::loan_payment::ContractPaymentTransaction;
use contractless::blocks::loans::LoanContractTransaction;
use contractless::blocks::marketing::MarketingTransaction;
use contractless::blocks::nft::CreateNftTransaction;
use contractless::blocks::swap::SwapTransaction;
use contractless::blocks::token::CreateTokenTransaction;
use contractless::blocks::transfer::TransferTransaction;
use contractless::blocks::vanity::VanityAddressTransaction;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::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::json;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::rpc::command_maps;
use blockchain::standalone_tools::connections::handshake;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::to_string_pretty;
use blockchain::wallets::structures::Wallet;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::rpc::command_maps;
use contractless::standalone_tools::connections::handshake;
use contractless::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use contractless::to_string_pretty;
use contractless::wallets::structures::Wallet;
async fn decode_one_transaction(tx_bytes: &[u8]) -> Option<String> {
let txtype = *tx_bytes.first()?;

View File

@ -1,26 +1,26 @@
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 contractless::blocks::burn::BurnTransaction;
use contractless::blocks::collateral::CollateralClaimTransaction;
use contractless::blocks::issue_token::IssueTokenTransaction;
use contractless::blocks::loan_payment::ContractPaymentTransaction;
use contractless::blocks::loans::LoanContractTransaction;
use contractless::blocks::marketing::MarketingTransaction;
use contractless::blocks::nft::CreateNftTransaction;
use contractless::blocks::swap::SwapTransaction;
use contractless::blocks::token::CreateTokenTransaction;
use contractless::blocks::transfer::TransferTransaction;
use contractless::blocks::vanity::VanityAddressTransaction;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::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::json;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::to_string_pretty;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::to_string_pretty;
async fn decode_mempool_transaction(response: &[u8]) -> Option<String> {
// Mempool transaction replies are the original transaction bytes,

View File

@ -1,9 +1,9 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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 contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
#[tokio::main]
async fn main() {

View File

@ -1,10 +1,10 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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 contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::to_string_pretty;
const NETWORK_NAME_BYTES: usize = 7;
const NETWORK_INFO_FIXED_BYTES_WITHOUT_PREFIX: usize = 40;

View File

@ -1,14 +1,14 @@
use blockchain::common::binary_conversions::binary_to_string;
use contractless::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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};
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::encode;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::wallets::structures::Wallet;
use contractless::{Map, Value};
const NFT_NAME_BYTES: usize = 15;
const SERIES_BYTES: usize = 4;

View File

@ -1,13 +1,13 @@
use blockchain::common::binary_conversions::binary_to_string;
use contractless::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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};
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::to_string_pretty;
use contractless::{Map, Value};
fn decode_nft_list(response: &[u8]) -> Option<String> {
// An empty binary payload means the peer has no NFT index entries.

View File

@ -1,10 +1,10 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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::{TimeZone, Utc};
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::{TimeZone, Utc};
#[tokio::main]
async fn main() {

View File

@ -1,15 +1,15 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::env;
use blockchain::from_str;
use blockchain::json;
use blockchain::read_to_string;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::tilde;
use blockchain::wallets::structures::Wallet;
use blockchain::Value;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::from_str;
use contractless::json;
use contractless::read_to_string;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use contractless::tilde;
use contractless::wallets::structures::Wallet;
use contractless::Value;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};

285
src/bin/lookup_storage.rs Normal file
View File

@ -0,0 +1,285 @@
use contractless::blocks::storage_values::{STORAGE_KEY_HASH_BYTES, STORAGE_VALUE_KEY_BYTES};
use contractless::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
use contractless::common::cli_prompts::{
ask_yes_no_question, prompt_hidden_nonempty, prompt_wallet_path,
};
use contractless::common::network_paths_and_settings::block_extension_and_paths;
use contractless::common::network_startup::get_connections;
use contractless::common::types::minimum_transfer_fee;
use contractless::decode;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::wallets::structures::Wallet;
use contractless::{encode, Utc};
#[derive(Clone)]
struct StorageCostQuote {
payment_address: String,
lookup_cost: u64,
cost_per_byte: u64,
total_bytes: u32,
}
fn display_base_units(value: u64) -> String {
format!("{:.8}", value as f64 / 100_000_000.0)
}
fn decimal_base_units(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
#[tokio::main]
async fn main() {
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
println!("Usage: ./lookup_storage <storage_key_hash> <data_key_or_all> <wallet_address>");
println!(" storage_key_hash: 64 hex characters from the StorageKey transaction hash");
println!(" data_key_or_all: a storage field key up to 50 bytes, or all");
println!(
" wallet_address: short, long, or vanity address whose stored data will be returned"
);
return;
}
let storage_key = args[1].trim().to_string();
let data_key = args[2].trim().to_string();
let address = args[3].trim().to_string();
if let Err(err) = validate_lookup_args(&storage_key, &data_key, &address) {
eprintln!("{err}");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_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(encryption_key, Some(&wallet_path)).await {
Ok(wallet) => wallet,
Err(err) => {
eprintln!("Wallet decryption failed: {err}");
return;
}
};
let request = format!("{storage_key}|{data_key}|{address}");
let Some((peer, quote)) = quote_storage_lookup(&request, &wallet).await else {
eprintln!("failed to connect");
return;
};
let requester_is_node = wallet.saved.short_address == quote.payment_address;
let txfee = if requester_is_node || quote.lookup_cost == 0 {
0
} else {
minimum_transfer_fee(quote.lookup_cost, true)
};
let total_spend = quote.lookup_cost.saturating_add(txfee);
let quote_json = json!({
"payment_address": quote.payment_address,
"lookup_cost": decimal_base_units(quote.lookup_cost),
"txfee": decimal_base_units(txfee),
"total_cost": decimal_base_units(total_spend),
"cost_per_byte": decimal_base_units(quote.cost_per_byte),
"total_bytes": quote.total_bytes
});
println!(
"{}",
serde_json::to_string_pretty(&quote_json).unwrap_or_else(|_| quote_json.to_string())
);
if total_spend > 0 {
let (_, base_coin, _, _, _, _, _, _, _) = block_extension_and_paths();
println!(
"This lookup will spend {} {} total: {} lookup payment + {} transfer fee.",
display_base_units(total_spend),
base_coin.trim(),
display_base_units(quote.lookup_cost),
display_base_units(txfee)
);
if !ask_yes_no_question("Continue with paid storage lookup?").await {
println!("Storage lookup cancelled.");
return;
}
}
let transfer_hex = match build_payment_transfer(&wallet, &quote, quote.lookup_cost, txfee).await
{
Ok(hex) => hex,
Err(err) => {
eprintln!("{err}");
return;
}
};
let lookup_request = format!("{request}|{transfer_hex}");
match call_storage_lookup(peer, lookup_request, &wallet).await {
Ok(response) => print_storage_response(&response),
Err(err) => eprintln!("{err}"),
}
}
async fn quote_storage_lookup(
request: &str,
wallet: &Wallet,
) -> Option<(std::net::SocketAddr, StorageCostQuote)> {
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,
request.to_string(),
52,
handshake::HandshakeWallet::WalletParts {
public_key: wallet.saved.public_key.clone(),
private_key: wallet.saved.private_key.clone(),
},
generate_uid(),
)
.await;
let Ok(response) = result else {
continue;
};
if let Some(quote) = decode_storage_cost_response(&response) {
return Some((socket_address, quote));
}
let text = String::from_utf8_lossy(&response);
if !text.trim().is_empty() {
eprintln!("{}", text.trim());
return None;
}
}
None
}
async fn call_storage_lookup(
peer: std::net::SocketAddr,
request: String,
wallet: &Wallet,
) -> Result<Vec<u8>, String> {
handshake::connect_and_handshake(
peer,
request,
53,
handshake::HandshakeWallet::WalletParts {
public_key: wallet.saved.public_key.clone(),
private_key: wallet.saved.private_key.clone(),
},
generate_uid(),
)
.await
.map_err(|err| format!("Storage lookup failed: {err}"))
}
async fn build_payment_transfer(
wallet: &Wallet,
quote: &StorageCostQuote,
value: u64,
txfee: u64,
) -> Result<String, String> {
let (_, base_coin, _, _, _, _, _, _, _) = block_extension_and_paths();
let unsigned = UnsignedTransferTransaction::new(
2,
Utc::now().timestamp() as u32,
value,
&base_coin,
0,
&wallet.saved.short_address,
&quote.payment_address,
txfee,
)
.await;
let transfer = TransferTransaction::new(unsigned, &wallet.saved.private_key)
.await
.map_err(|err| format!("Failed to sign storage lookup payment: {err}"))?;
let bytes = transfer
.to_bytes()
.await
.map_err(|err| format!("Failed to serialize storage lookup payment: {err}"))?;
Ok(encode(&bytes))
}
fn decode_storage_cost_response(response: &[u8]) -> Option<StorageCostQuote> {
const TOTAL_BYTES_FIELD: usize = 4;
const COST_PER_BYTE_FIELD: usize = 8;
const TOTAL_COST_FIELD: usize = 8;
const RESPONSE_BYTES: usize = TOTAL_BYTES_FIELD
+ COST_PER_BYTE_FIELD
+ TOTAL_COST_FIELD
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH;
if response.len() != RESPONSE_BYTES {
return None;
}
let mut cursor = 0usize;
let total_bytes = u32::from_le_bytes(
response[cursor..cursor + TOTAL_BYTES_FIELD]
.try_into()
.ok()?,
);
cursor += TOTAL_BYTES_FIELD;
let cost_per_byte = u64::from_le_bytes(
response[cursor..cursor + COST_PER_BYTE_FIELD]
.try_into()
.ok()?,
);
cursor += COST_PER_BYTE_FIELD;
let lookup_cost = u64::from_le_bytes(
response[cursor..cursor + TOTAL_COST_FIELD]
.try_into()
.ok()?,
);
cursor += TOTAL_COST_FIELD;
let payment_address = Wallet::bytes_to_short_address(&response[cursor..])?;
Some(StorageCostQuote {
payment_address,
lookup_cost,
cost_per_byte,
total_bytes,
})
}
fn print_storage_response(response: &[u8]) {
let text = String::from_utf8_lossy(response);
let trimmed = text.trim();
if trimmed.is_empty() {
return;
}
match serde_json::from_str::<serde_json::Value>(trimmed) {
Ok(value) => match serde_json::to_string_pretty(&value) {
Ok(pretty) => println!("{pretty}"),
Err(_) => println!("{trimmed}"),
},
Err(_) => println!("{trimmed}"),
}
}
fn validate_lookup_args(storage_key: &str, data_key: &str, address: &str) -> Result<(), String> {
let storage_key_bytes =
decode(storage_key).map_err(|err| format!("Invalid storage key hash: {err}"))?;
if storage_key_bytes.len() != STORAGE_KEY_HASH_BYTES {
return Err("Storage key hash must decode to 32 bytes.".to_string());
}
if data_key.is_empty() {
return Err("Data key cannot be empty. Use all to lookup every key.".to_string());
}
if !data_key.eq_ignore_ascii_case("all") && data_key.as_bytes().len() > STORAGE_VALUE_KEY_BYTES
{
return Err("Data key must be 50 bytes or less.".to_string());
}
if Wallet::normalize_to_short_address(address).is_none() {
return Err("Wallet address must be a valid short, long, or vanity address.".to_string());
}
Ok(())
}

View File

@ -0,0 +1,161 @@
use contractless::blocks::storage_values::{STORAGE_KEY_HASH_BYTES, STORAGE_VALUE_KEY_BYTES};
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::decode;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::wallets::structures::Wallet;
fn decimal_base_units(value: u64) -> f64 {
value as f64 / 100_000_000.0
}
#[tokio::main]
async fn main() {
// Command 52 asks a peer to price a storage lookup before the paid lookup is submitted.
let hashmap_key = generate_uid();
let rpc_command = 52;
let args: Vec<String> = env::args().collect();
if args.len() != 4 {
println!(
"Usage: ./lookup_storage_cost <storage_key_hash> <data_key_or_all> <wallet_address>"
);
println!(" storage_key_hash: 64 hex characters from the StorageKey transaction hash");
println!(" data_key_or_all: a storage field key up to 50 bytes, or all");
println!(
" wallet_address: short, long, or vanity address whose stored data will be priced"
);
return;
}
let storage_key = args[1].trim().to_string();
let data_key = args[2].trim().to_string();
let address = args[3].trim().to_string();
if let Err(err) = validate_lookup_args(&storage_key, &data_key, &address) {
eprintln!("{err}");
return;
}
let wallet_path = prompt_wallet_path().await;
let encryption_key = prompt_hidden_nonempty(
"What is your wallet decryption key? ",
"Wallet key cannot be empty. Please try again.",
)
.await;
let request = format!("{storage_key}|{data_key}|{address}");
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,
request.clone(),
rpc_command,
handshake::HandshakeWallet::WalletKey {
encryption_key: encryption_key.clone(),
wallet_path: wallet_path.clone(),
},
hashmap_key,
)
.await;
match result {
Ok(response) => {
if let Some(output) = decode_storage_cost_response(&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");
}
}
fn decode_storage_cost_response(response: &[u8]) -> Option<String> {
const TOTAL_BYTES_FIELD: usize = 4;
const COST_PER_BYTE_FIELD: usize = 8;
const TOTAL_COST_FIELD: usize = 8;
const STORAGE_COST_RESPONSE_BYTES: usize = TOTAL_BYTES_FIELD
+ COST_PER_BYTE_FIELD
+ TOTAL_COST_FIELD
+ Wallet::SHORT_ADDRESS_BYTES_LENGTH;
if response.len() != STORAGE_COST_RESPONSE_BYTES {
return None;
}
let mut cursor = 0usize;
let total_bytes = u32::from_le_bytes(
response[cursor..cursor + TOTAL_BYTES_FIELD]
.try_into()
.ok()?,
);
cursor += TOTAL_BYTES_FIELD;
let cost_per_byte = u64::from_le_bytes(
response[cursor..cursor + COST_PER_BYTE_FIELD]
.try_into()
.ok()?,
);
cursor += COST_PER_BYTE_FIELD;
let total_cost = u64::from_le_bytes(
response[cursor..cursor + TOTAL_COST_FIELD]
.try_into()
.ok()?,
);
cursor += TOTAL_COST_FIELD;
let payment_address = Wallet::bytes_to_short_address(&response[cursor..])?;
let output = json!({
"payment_address": payment_address,
"total_cost": decimal_base_units(total_cost),
"cost_per_byte": decimal_base_units(cost_per_byte),
"total_bytes": total_bytes
});
serde_json::to_string_pretty(&output).ok()
}
fn validate_lookup_args(storage_key: &str, data_key: &str, address: &str) -> Result<(), String> {
let storage_key_bytes =
decode(storage_key).map_err(|err| format!("Invalid storage key hash: {err}"))?;
if storage_key_bytes.len() != STORAGE_KEY_HASH_BYTES {
return Err("Storage key hash must decode to 32 bytes.".to_string());
}
if data_key.is_empty() {
return Err("Data key cannot be empty. Use all to price every key.".to_string());
}
if !data_key.eq_ignore_ascii_case("all") && data_key.as_bytes().len() > STORAGE_VALUE_KEY_BYTES
{
return Err("Data key must be 50 bytes or less.".to_string());
}
if Wallet::normalize_to_short_address(address).is_none() {
return Err("Wallet address must be a valid short, long, or vanity address.".to_string());
}
Ok(())
}

View File

@ -1,12 +1,12 @@
use blockchain::common::binary_conversions::binary_to_string;
use contractless::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::wallets::structures::Wallet;
const TOKEN_NAME_BYTES: usize = 15;
const HASH_BYTES: usize = 32;

View File

@ -1,12 +1,12 @@
use blockchain::common::binary_conversions::binary_to_string;
use contractless::common::binary_conversions::binary_to_string;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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 contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::to_string_pretty;
fn decode_token_list(response: &[u8]) -> Option<String> {
// An empty binary payload means the peer has no token index entries.

View File

@ -1,11 +1,11 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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::torrent::structs::Torrent;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::to_string_pretty;
use contractless::torrent::structs::Torrent;
#[tokio::main]
async fn main() {

View File

@ -1,14 +1,14 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::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;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
#[tokio::main]
async fn main() {

View File

@ -1,26 +1,26 @@
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 contractless::blocks::collateral::CollateralClaimTransaction;
use contractless::blocks::genesis::GenesisTransaction;
use contractless::blocks::loan_payment::ContractPaymentTransaction;
use contractless::blocks::loans::LoanContractTransaction;
use contractless::blocks::marketing::MarketingTransaction;
use contractless::blocks::nft::CreateNftTransaction;
use contractless::blocks::rewards::RewardsTransaction;
use contractless::blocks::swap::SwapTransaction;
use contractless::blocks::token::CreateTokenTransaction;
use contractless::blocks::transfer::TransferTransaction;
use contractless::blocks::vanity::VanityAddressTransaction;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_startup::get_connections;
use blockchain::common::types::{
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::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::json;
use blockchain::records::memory::response_channels::generate_uid;
use blockchain::standalone_tools::connections::handshake;
use blockchain::to_string_pretty;
use contractless::env;
use contractless::json;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::to_string_pretty;
#[tokio::main]
async fn main() {

View File

@ -1,4 +1,4 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible_with_default};
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible_with_default};
#[cfg(windows)]
use std::env;
#[cfg(unix)]

View File

@ -1,11 +1,11 @@
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 contractless::common::cli_prompts::prompt_hidden_nonempty;
use contractless::decode_image_and_extract_text;
use contractless::decrypts;
use contractless::env;
use contractless::from_str;
use contractless::fs;
use contractless::tilde;
use contractless::Value;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};

View File

@ -1,14 +1,14 @@
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 contractless::common::cli_prompts::prompt_hidden_nonempty;
use contractless::common::network_startup::get_connections;
use contractless::create_img;
use contractless::encrypts;
use contractless::env;
use contractless::fs;
use contractless::read_to_string;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::tilde;
use contractless::wallets::structures::{SavedWallet, Wallet};
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
@ -208,7 +208,7 @@ async fn main() {
match Wallet::regenerate_public_key(&private_key) {
Ok(public_key_bytes) => {
let public_key = blockchain::encode(public_key_bytes.clone());
let public_key = contractless::encode(public_key_bytes.clone());
let short_address_bytes =
Wallet::public_key_bytes_to_short_address_bytes(&public_key_bytes)
.expect("Failed to derive short address bytes");

View File

@ -1,18 +1,18 @@
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 contractless::common::cli_prompts::prompt_hidden_nonempty;
use contractless::common::network_startup::get_connections;
use contractless::decode_image_and_extract_text;
use contractless::decrypts;
use contractless::env;
use contractless::fs;
use contractless::read_to_string;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::stdout;
use contractless::tilde;
use contractless::wallets::structures::{SavedWallet, Wallet};
use contractless::AsyncWriteExt;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};
@ -245,7 +245,7 @@ async fn main() {
match Wallet::regenerate_public_key(&private_key) {
Ok(public_key_bytes) => {
let public_key = blockchain::encode(public_key_bytes.clone());
let public_key = contractless::encode(public_key_bytes.clone());
let short_address_bytes =
Wallet::public_key_bytes_to_short_address_bytes(&public_key_bytes)
.expect("Failed to derive short address bytes");

View File

@ -1,11 +1,11 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::common::skein::skein_256_hash_bytes;
use contractless::env;
use contractless::records::memory::response_channels::generate_uid;
use contractless::rpc::command_maps::RPC_REGISTER_WALLET;
use contractless::standalone_tools::connections::handshake;
use contractless::wallets::structures::Wallet;
#[tokio::main]
async fn main() {
@ -64,7 +64,7 @@ async fn main() {
let signature = Wallet::sign_transaction(&payload_hash, &wallet.saved.private_key).await;
// sending_request encodes command 38 from this pipe-delimited payload.
let public_key_hex = blockchain::encode(&public_key_bytes);
let public_key_hex = contractless::encode(&public_key_bytes);
let json = format!("{short_address}|{public_key_hex}|{signature}");
let rpc_command = 38;
let connections = get_connections().await;

View File

@ -1,11 +1,11 @@
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 contractless::env;
use contractless::from_str;
use contractless::fs;
use contractless::read_to_string;
use contractless::tilde;
use contractless::Value;
use rustyline::completion::FilenameCompleter;
use rustyline::error::ReadlineError;
use rustyline::{history::DefaultHistory, CompletionType, Config, Editor};

View File

@ -1,9 +1,9 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::wallets::structures::Wallet;
#[tokio::main]
async fn main() {

View File

@ -1,9 +1,9 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
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;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_startup::get_connections;
use contractless::env;
use contractless::records::memory::response_channels::generate_uid;
use contractless::standalone_tools::connections::handshake;
use contractless::wallets::structures::Wallet;
#[tokio::main]
async fn main() {

View File

@ -1,7 +1,7 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::skein::skein_256_hash_data;
use blockchain::env;
use blockchain::wallets::structures::Wallet;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::skein::skein_256_hash_data;
use contractless::env;
use contractless::wallets::structures::Wallet;
#[tokio::main]
async fn main() {

View File

@ -1,10 +1,10 @@
use blockchain::common::skein::{
use contractless::common::skein::{
skein_128_hash_bytes, skein_128_hash_data, skein_256_hash_bytes, skein_256_hash_data,
};
use blockchain::env;
use blockchain::File;
use blockchain::SeekFrom;
use blockchain::{AsyncReadExt, AsyncSeekExt};
use contractless::env;
use contractless::File;
use contractless::SeekFrom;
use contractless::{AsyncReadExt, AsyncSeekExt};
#[tokio::main]
async fn main() {

View File

@ -1,8 +1,8 @@
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;
use contractless::common::binary_conversions::hex_to_u64;
use contractless::env;
use contractless::io;
use contractless::records::unpack_block::unpack_header::load_block_header;
use contractless::to_string_pretty;
#[tokio::main]
async fn main() -> io::Result<()> {

View File

@ -1,12 +1,12 @@
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;
use contractless::common::network_paths_and_settings::block_extension_and_paths;
use contractless::env;
use contractless::exit;
use contractless::io;
use contractless::to_string_pretty;
use contractless::torrent::structs::Torrent;
use contractless::AsyncReadExt;
use contractless::File;
use contractless::Path;
#[tokio::main]
async fn main() -> io::Result<()> {

View File

@ -1,14 +1,14 @@
use blockchain::common::binary_conversions::hex_to_u64;
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use blockchain::common::skein::skein_128_hash_bytes;
use blockchain::encode;
use blockchain::env;
use blockchain::records::unpack_block::unpack_header::load_block_header;
use blockchain::standalone_tools::wallet_registry_lookup::lookup_pubkey_from_live_registry;
use blockchain::torrent::structs::Torrent;
use blockchain::wallets::structures::Wallet;
use blockchain::{AsyncReadExt, File};
use contractless::common::binary_conversions::hex_to_u64;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path};
use contractless::common::network_paths_and_settings::block_extension_and_paths;
use contractless::common::skein::skein_128_hash_bytes;
use contractless::encode;
use contractless::env;
use contractless::records::unpack_block::unpack_header::load_block_header;
use contractless::standalone_tools::wallet_registry_lookup::lookup_pubkey_from_live_registry;
use contractless::torrent::structs::Torrent;
use contractless::wallets::structures::Wallet;
use contractless::{AsyncReadExt, File};
use colored::*;
use std::process;

View File

@ -1,7 +1,7 @@
use blockchain::common::cli_prompts::prompt_visible;
use blockchain::env;
use blockchain::standalone_tools::vanity_resolver::canonical_vanity_address_input;
use blockchain::wallets::structures::Wallet;
use contractless::common::cli_prompts::prompt_visible;
use contractless::env;
use contractless::standalone_tools::vanity_resolver::canonical_vanity_address_input;
use contractless::wallets::structures::Wallet;
#[tokio::main]
async fn main() {

View File

@ -1,8 +1,8 @@
use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use blockchain::common::skein::skein_256_hash_data;
use blockchain::env;
use blockchain::standalone_tools::wallet_registry_lookup::lookup_pubkey_from_live_registry;
use blockchain::wallets::structures::Wallet;
use contractless::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path};
use contractless::common::skein::skein_256_hash_data;
use contractless::env;
use contractless::standalone_tools::wallet_registry_lookup::lookup_pubkey_from_live_registry;
use contractless::wallets::structures::Wallet;
#[tokio::main]
async fn main() {

View File

@ -1,18 +1,18 @@
use blockchain::blocks::loans::UnsignedLoanContractTransaction;
use blockchain::common::cli_prompts::{
use contractless::blocks::loans::UnsignedLoanContractTransaction;
use contractless::common::cli_prompts::{
ask_yes_no_question, prompt_hidden_nonempty, prompt_wallet_path,
};
use blockchain::env;
use contractless::env;
use blockchain::from_str;
use blockchain::fs;
use blockchain::json;
use blockchain::read_to_string;
use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::to_string_pretty;
use blockchain::wallets::structures::Wallet;
use blockchain::Value;
use blockchain::{Local, TimeZone};
use contractless::from_str;
use contractless::fs;
use contractless::json;
use contractless::read_to_string;
use contractless::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use contractless::to_string_pretty;
use contractless::wallets::structures::Wallet;
use contractless::Value;
use contractless::{Local, TimeZone};
fn display_amount(value: u64) -> f64 {
// Transaction JSON stores atomic units; prompts show whole-coin decimals.

View File

@ -1,17 +1,17 @@
use blockchain::blocks::swap::UnsignedSwapTransaction;
use blockchain::common::asset_names::padded_normalized_asset_input;
use blockchain::common::cli_prompts::{
use contractless::blocks::swap::UnsignedSwapTransaction;
use contractless::common::asset_names::padded_normalized_asset_input;
use contractless::common::cli_prompts::{
ask_yes_no_question, prompt_hidden_nonempty, prompt_wallet_path,
};
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
use contractless::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::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use blockchain::wallets::structures::Wallet;
use blockchain::Value;
use contractless::env;
use contractless::fs;
use contractless::json;
use contractless::read_to_string;
use contractless::standalone_tools::vanity_resolver::resolve_wallet_address_input;
use contractless::wallets::structures::Wallet;
use contractless::Value;
fn normalize_asset_or_base(input: &str, base_coin: &str) -> Option<String> {
if input.trim().eq_ignore_ascii_case(base_coin.trim()) {

View File

@ -36,6 +36,14 @@ pub struct Settings {
}
impl Settings {
fn parse_decimal_coin_units(value: &str) -> Result<u64, Box<dyn std::error::Error>> {
let parsed = value.trim().parse::<f64>()?;
if parsed.is_sign_negative() {
return Err("decimal coin value cannot be negative".into());
}
Ok((parsed * 100_000_000.0).round() as u64)
}
fn expand(path: &str, base_dir: &Path) -> String {
let expanded = PathBuf::from(tilde(path).as_ref());
if expanded.is_relative() {
@ -200,8 +208,9 @@ impl Settings {
threads,
storage_lookup_fee_per_byte: conf
.get_from(Some("Settings"), "STORAGE_LOOKUP_FEE_PER_BYTE")
.unwrap_or("0")
.parse::<u64>()?,
.map(Self::parse_decimal_coin_units)
.transpose()?
.unwrap_or(0),
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

View File

@ -1,16 +1,16 @@
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;
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::Arc;
use blockchain::Runtime;
use contractless::exit;
use contractless::log::{error, logger};
use contractless::startup::daemonize::daemonize_after_wallet_prompt;
use contractless::startup::daemonize::handle_control_command;
use contractless::startup::initialize_startup::obtain_startup_wallet;
use contractless::startup::initialize_startup::prepare_pre_wallet_startup;
use contractless::startup::node_runtime::initialize_node_logging;
use contractless::startup::node_runtime::install_panic_cleanup;
use contractless::startup::node_runtime::run_unlocked_node;
use contractless::startup::windows_service::handle_windows_service_command;
use contractless::startup::windows_service::try_run_as_windows_service;
use contractless::Arc;
use contractless::Runtime;
use tokio::runtime::Builder;
fn main() {

View File

@ -230,8 +230,9 @@ pub use lookups::{
total_transactions, transaction_by_signature, transactions_by_address,
};
pub use processing::{
delete_by_signatures, mark_processed_by_signatures, mark_selected_transactions_processed,
restore_processed_by_signatures, restore_selected_transactions_processed,
delete_by_signatures, delete_unprocessed_by_address, mark_processed_by_signatures,
mark_selected_transactions_processed, restore_processed_by_signatures,
restore_selected_transactions_processed,
spawn_processed_cleanup,
};
pub use schema::{

View File

@ -304,3 +304,81 @@ pub async fn delete_by_signatures(signatures: &[String]) -> Result<()> {
Ok(())
}
pub async fn delete_unprocessed_by_address(db: &Db, address: &str) -> Result<u64> {
// A stale local double-spend can poison local block assembly after another
// node mines the valid spend first. When a selected mempool save fails on
// a subtraction balance effect, purge this local address from unprocessed
// mempool rows so mining can continue with the remaining transactions.
let addresses = canonical_mempool_addresses(db, address);
let params: [&(dyn ToSql + Sync); 1] = [&addresses];
let mut deleted = 0_u64;
deleted += pg_execute(
"DELETE FROM transfer WHERE processed = false AND (sender = ANY($1) OR receiver = ANY($1))",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM token WHERE processed = false AND creator = ANY($1)",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM issue_token WHERE processed = false AND creator = ANY($1)",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM burn WHERE processed = false AND address = ANY($1)",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM nft WHERE processed = false AND creator = ANY($1)",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM marketing WHERE processed = false AND advertiser = ANY($1)",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM vanity_address WHERE processed = false AND address = ANY($1)",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM swap WHERE processed = false AND (sender1 = ANY($1) OR sender2 = ANY($1))",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM loan_contract WHERE processed = false AND (lender = ANY($1) OR borrower = ANY($1))",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM loan_payment WHERE processed = false AND address = ANY($1)",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM collateral_claim WHERE processed = false AND address = ANY($1)",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM storage_key WHERE processed = false AND address = ANY($1)",
&params,
)
.await?;
deleted += pg_execute(
"DELETE FROM storage_value WHERE processed = false AND address = ANY($1)",
&params,
)
.await?;
Ok(deleted)
}

View File

@ -15,7 +15,7 @@ use crate::records::memory::chain_state::{
cached_tip_hash, cached_tip_header, update_chain_state_after_save,
};
use crate::records::memory::mempool::{
apply_selected_transaction_math, mark_processed_by_signatures,
apply_selected_transaction_math, delete_unprocessed_by_address, mark_processed_by_signatures,
mark_selected_transactions_processed, restore_processed_by_signatures,
restore_selected_transactions_processed, select_transactions_for_block,
spawn_processed_cleanup, stream_selected_transaction_originals,
@ -320,6 +320,17 @@ async fn save_binary_data_with_mempool_stream(
let mut applied_effects = match pending_effects.apply(db) {
Ok(applied_effects) => applied_effects,
Err(err) => {
if let Some(address) = failed_subtraction_balance_address(&err) {
match delete_unprocessed_by_address(db, &address).await {
Ok(deleted) if deleted > 0 => info!(
"[mempool] removed {deleted} unprocessed local transactions involving {address} after selected transaction balance failure"
),
Ok(_) => {}
Err(cleanup_err) => error!(
"[mempool] failed to remove unprocessed local transactions involving {address}: {cleanup_err}"
),
}
}
cleanup_block_file(&file_name);
return Err(err);
}
@ -408,6 +419,24 @@ async fn save_binary_data_with_mempool_stream(
Ok(())
}
fn failed_subtraction_balance_address(err: &str) -> Option<String> {
if !err.contains("Failed to apply balance effect") || !err.contains("operand=subtraction") {
return None;
}
let address_marker = " address=";
let address_start = err.find(address_marker)? + address_marker.len();
let address_tail = &err[address_start..];
let address_end = address_tail.find(" coin=")?;
let address = address_tail[..address_end].trim();
if address.is_empty() {
None
} else {
Some(address.to_string())
}
}
async fn save_binary_data(params: SaveBinaryDataParams<'_>) -> Result<(), String> {
let SaveBinaryDataParams {
data,

View File

@ -11,31 +11,16 @@ use crate::rpc::commands::tx_submit::save_and_submit;
use crate::rpc::responses::RpcResponse;
use crate::sled::Db;
use crate::wallets::structures::Wallet;
use crate::{encode, json, to_string, Arc, Mutex, Serialize};
use crate::{json, to_string, Arc, Mutex, Serialize};
use serde_json::{Map, Value};
#[derive(Serialize)]
struct StorageLookupEntry {
key: String,
value_hex: String,
value_text: Option<String>,
bytes: usize,
value: Vec<u8>,
}
#[derive(Serialize)]
struct StorageLookupString {
key: String,
value: String,
chunks: usize,
bytes: usize,
}
#[derive(Serialize)]
struct StorageLookupPayload {
storage_key: String,
data_key: String,
address: String,
records: Vec<StorageLookupEntry>,
strings: Vec<StorageLookupString>,
data: Value,
data_bytes: usize,
fee_per_byte: u64,
total_cost: u64,
@ -74,18 +59,6 @@ fn display_key(prefix_len: usize, key: &[u8]) -> String {
.to_string()
}
fn text_value(value: &[u8]) -> Option<String> {
let text = String::from_utf8(value.to_vec()).ok()?;
if text
.chars()
.any(|ch| ch.is_control() && ch != '\n' && ch != '\r' && ch != '\t')
{
None
} else {
Some(text)
}
}
fn string_chunk_text(value: &[u8]) -> String {
if value.len() <= STORAGE_STRING_STORED_HASH_BYTES * 2 {
return String::new();
@ -125,9 +98,7 @@ fn lookup_exact_record(
Ok(vec![StorageLookupEntry {
key: trimmed_key(data_key),
value_hex: encode(&value),
value_text: text_value(&value),
bytes: value.len(),
value: value.to_vec(),
}])
}
@ -136,17 +107,15 @@ fn lookup_string_records(
storage_key: &[u8],
address: &str,
base_key: &str,
) -> Result<(Vec<StorageLookupEntry>, Vec<StorageLookupString>), String> {
) -> Result<Vec<StorageLookupEntry>, String> {
if base_key.is_empty() {
return Ok((Vec::new(), Vec::new()));
return Ok(Vec::new());
}
let storage_tree = db
.open_tree(storage_key)
.map_err(|err| format!("Failed to open storage tree: {err}"))?;
let mut entries = Vec::new();
let mut assembled = String::new();
let mut total_bytes = 0usize;
for index in 0..STORAGE_STRING_CHUNK_LIMIT {
let chunk_key = format!("{base_key}_{index:02}");
@ -156,41 +125,25 @@ fn lookup_string_records(
.map_err(|err| format!("Failed to read storage string value: {err}"))?
else {
if index == 0 {
return Ok((Vec::new(), Vec::new()));
return Ok(Vec::new());
}
break;
};
let chunk_text = string_chunk_text(&value);
assembled.push_str(&chunk_text);
total_bytes = total_bytes.saturating_add(value.len());
entries.push(StorageLookupEntry {
key: chunk_key,
value_hex: encode(&value),
value_text: Some(chunk_text),
bytes: value.len(),
value: value.to_vec(),
});
}
let strings = if entries.is_empty() {
Vec::new()
} else {
vec![StorageLookupString {
key: base_key.to_string(),
value: assembled,
chunks: entries.len(),
bytes: total_bytes,
}]
};
Ok((entries, strings))
Ok(entries)
}
fn lookup_all_records(
db: &Db,
storage_key: &[u8],
address: &str,
) -> Result<(Vec<StorageLookupEntry>, Vec<StorageLookupString>), String> {
) -> Result<Vec<StorageLookupEntry>, String> {
let storage_tree = db
.open_tree(storage_key)
.map_err(|err| format!("Failed to open storage tree: {err}"))?;
@ -202,48 +155,85 @@ fn lookup_all_records(
let (key, value) = item.map_err(|err| format!("Failed to scan storage tree: {err}"))?;
entries.push(StorageLookupEntry {
key: display_key(prefix_len, key.as_ref()),
value_hex: encode(&value),
value_text: text_value(&value),
bytes: value.len(),
value: value.to_vec(),
});
}
let mut strings = Vec::new();
let mut grouped = std::collections::BTreeMap::<String, Vec<&StorageLookupEntry>>::new();
for entry in &entries {
Ok(entries)
}
fn scalar_json_value(value: &[u8]) -> Value {
match value.len() {
1 => json!(value[0]),
2 => {
let bytes = [value[0], value[1]];
json!(u16::from_le_bytes(bytes))
}
4 => {
let bytes = [value[0], value[1], value[2], value[3]];
json!(u32::from_le_bytes(bytes))
}
8 => {
let bytes = [
value[0], value[1], value[2], value[3], value[4], value[5], value[6], value[7],
];
json!(u64::from_le_bytes(bytes))
}
16 => {
let bytes = [
value[0], value[1], value[2], value[3], value[4], value[5], value[6], value[7],
value[8], value[9], value[10], value[11], value[12], value[13], value[14],
value[15],
];
json!(u128::from_le_bytes(bytes).to_string())
}
_ => json!(crate::encode(value)),
}
}
fn compact_storage_data(records: &[StorageLookupEntry]) -> Value {
let mut data = Map::new();
let mut grouped_strings = std::collections::BTreeMap::<String, Vec<&StorageLookupEntry>>::new();
for entry in records {
if let Some(base_key) = entry.key.strip_suffix("_00") {
grouped.entry(base_key.to_string()).or_default();
grouped_strings.entry(base_key.to_string()).or_default();
}
}
for entry in &entries {
for entry in records {
let Some((base_key, suffix)) = entry.key.rsplit_once('_') else {
continue;
};
if suffix.len() == 2 && suffix.chars().all(|ch| ch.is_ascii_digit()) {
if let Some(chunks) = grouped.get_mut(base_key) {
if let Some(chunks) = grouped_strings.get_mut(base_key) {
chunks.push(entry);
}
}
}
for (base_key, mut chunks) in grouped {
for (base_key, mut chunks) in grouped_strings {
chunks.sort_by(|left, right| left.key.cmp(&right.key));
let mut value = String::new();
let mut bytes = 0usize;
for chunk in &chunks {
bytes = bytes.saturating_add(chunk.bytes);
if let Ok(raw) = crate::decode(&chunk.value_hex) {
value.push_str(&string_chunk_text(&raw));
}
value.push_str(&string_chunk_text(&chunk.value));
}
strings.push(StorageLookupString {
key: base_key,
value,
chunks: chunks.len(),
bytes,
});
data.insert(base_key, json!(value));
}
Ok((entries, strings))
for entry in records {
let Some((base_key, suffix)) = entry.key.rsplit_once('_') else {
data.insert(entry.key.clone(), scalar_json_value(&entry.value));
continue;
};
let is_string_chunk = suffix.len() == 2
&& suffix.chars().all(|ch| ch.is_ascii_digit())
&& data.contains_key(base_key);
if !is_string_chunk {
data.insert(entry.key.clone(), scalar_json_value(&entry.value));
}
}
Value::Object(data)
}
fn lookup_payload(
@ -264,27 +254,26 @@ fn lookup_payload(
}
let data_key_text = trimmed_key(&data_key);
let (mut records, strings) = if data_key_text.eq_ignore_ascii_case("all") {
let mut records = if data_key_text.eq_ignore_ascii_case("all") {
lookup_all_records(db, &storage_key, &address)?
} else {
let mut records = lookup_exact_record(db, &storage_key, &address, &data_key)?;
let (mut string_records, strings) =
lookup_string_records(db, &storage_key, &address, &data_key_text)?;
let mut string_records = lookup_string_records(db, &storage_key, &address, &data_key_text)?;
records.append(&mut string_records);
(records, strings)
records
};
records.sort_by(|left, right| left.key.cmp(&right.key));
let data_bytes = records.iter().map(|entry| entry.bytes).sum::<usize>();
let data = compact_storage_data(&records);
let data_bytes = to_string(&data)
.map_err(|err| format!("Could not calculate compact storage payload size: {err}"))?
.as_bytes()
.len();
let fee_per_byte = SETTINGS.storage_lookup_fee_per_byte;
let total_cost = (data_bytes as u64).saturating_mul(fee_per_byte);
Ok(StorageLookupPayload {
storage_key: encode(storage_key),
data_key: data_key_text,
address,
records,
strings,
data,
data_bytes,
fee_per_byte,
total_cost,
@ -300,7 +289,24 @@ pub async fn lookup_cost(
wallet: Arc<Wallet>,
) -> RpcResponse {
match lookup_payload(db, storage_key, data_key, address, wallet) {
Ok(payload) => json_response(&payload),
Ok(payload) => {
let payment_address =
match Wallet::short_address_to_bytes(&payload.node_payment_address) {
Some(bytes) => bytes,
None => return error_response("Node payment address is invalid."),
};
let total_bytes = match u32::try_from(payload.data_bytes) {
Ok(bytes) => bytes,
Err(_) => return error_response("Storage lookup byte count is too large."),
};
let mut response = Vec::with_capacity(4 + 8 + 8 + Wallet::SHORT_ADDRESS_BYTES_LENGTH);
response.extend_from_slice(&total_bytes.to_le_bytes());
response.extend_from_slice(&payload.fee_per_byte.to_le_bytes());
response.extend_from_slice(&payload.total_cost.to_le_bytes());
response.extend_from_slice(&payment_address);
RpcResponse::Binary(response)
}
Err(err) => error_response(&err),
}
}
@ -321,7 +327,7 @@ pub async fn lookup_store(
};
if payload.total_cost == 0 {
return json_response(&json!({ "payment_required": false, "data": payload }));
return json_response(&payload.data);
}
if txtype != TRANSFER_TYPE {
@ -344,7 +350,7 @@ pub async fn lookup_store(
)
.await
{
return json_response(&json!({ "payment_required": false, "data": payload }));
return json_response(&payload.data);
}
return error_response("Node wallet storage lookup signature is invalid.");
}
@ -368,5 +374,5 @@ pub async fn lookup_store(
));
}
json_response(&json!({ "payment_required": true, "payment_accepted": true, "data": payload }))
json_response(&payload.data)
}

View File

@ -1076,6 +1076,9 @@ pub async fn start_loop(
.await;
continue;
}
// Read the full transaction body for the declared payment type.
// For paid storage lookups this should be a type 2 transfer
// body; the tx type byte was already consumed above.
let tx = read_bytes_from_stream::read_usize_from_stream(
&connections_key,
txsize - 1,

View File

@ -1,3 +1,4 @@
use crate::blocks::storage_values::{STORAGE_KEY_HASH_BYTES, STORAGE_VALUE_KEY_BYTES};
use crate::common::binary_conversions::ip_to_binary;
use crate::decode;
use crate::io;
@ -8,10 +9,11 @@ use crate::rpc::command_maps::{
RPC_CONTRACT_BY_ADDRESS, RPC_DIFFICULTY, RPC_LARGEST_TX_FEE, RPC_LATEST_ADDRESS_TRANSACTIONS,
RPC_LOAN_CONTRACT, RPC_MARKETING_CAMPAIGN_HISTORY, RPC_MEMPOOL_TX_BY_ADDRESS,
RPC_MEMPOOL_TX_BY_SIGNATURE, RPC_MEMPOOL_TX_COUNT, RPC_NETWORK_INFO, RPC_NFT_DETAILS,
RPC_NFT_LIST, RPC_REGISTER_WALLET, RPC_TIME, RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS,
RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID,
RPC_UNBLOCK_IP, RPC_VALIDATE_MESSAGE, RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP,
RPC_WALLET_REGISTRATION_STATUS, RPC_WALLET_REGISTRY_SYNC,
RPC_NFT_LIST, RPC_REGISTER_WALLET, RPC_STORAGE_LOOKUP, RPC_STORAGE_LOOKUP_COST, RPC_TIME,
RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT,
RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP, RPC_VALIDATE_MESSAGE,
RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP, RPC_WALLET_REGISTRATION_STATUS,
RPC_WALLET_REGISTRY_SYNC,
};
use crate::standalone_tools::transaction_creator::create_transaction_request;
use crate::standalone_tools::vanity_resolver::canonical_vanity_address_input;
@ -643,6 +645,132 @@ async fn build_request_bytes(
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&contract_bytes);
}
// Estimate paid storage lookup cost as "storage_key_hash|data_key_or_all|address".
52 => {
let command_number: u8 = RPC_STORAGE_LOOKUP_COST;
let parts = command_input.split('|').collect::<Vec<_>>();
if parts.len() != 3 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Storage lookup cost input must be storage_key_hash|data_key_or_all|address",
));
}
let storage_key = decode(parts[0]).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid storage key hash: {err}"),
)
})?;
if storage_key.len() != STORAGE_KEY_HASH_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Storage key hash must decode to 32 bytes",
));
}
let data_key = parts[1].trim();
if data_key.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Data key cannot be empty. Use all to price every key for this address.",
));
}
let mut data_key_bytes = if data_key.eq_ignore_ascii_case("all") {
b"all".to_vec()
} else {
data_key.as_bytes().to_vec()
};
if data_key_bytes.len() > STORAGE_VALUE_KEY_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Data key must be 50 bytes or less",
));
}
data_key_bytes.resize(STORAGE_VALUE_KEY_BYTES, b' ');
let address_bytes = Wallet::normalize_to_short_address(parts[2])
.and_then(|short| Wallet::short_address_to_bytes(&short))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid wallet address")
})?;
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&storage_key);
bin_msg.extend_from_slice(&data_key_bytes);
bin_msg.extend_from_slice(&address_bytes);
}
// Paid storage lookup as "storage_key_hash|data_key_or_all|address|transfer_tx_hex".
53 => {
let command_number: u8 = RPC_STORAGE_LOOKUP;
let parts = command_input.split('|').collect::<Vec<_>>();
if parts.len() != 4 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Storage lookup input must be storage_key_hash|data_key_or_all|address|transfer_tx_hex",
));
}
let storage_key = decode(parts[0]).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid storage key hash: {err}"),
)
})?;
if storage_key.len() != STORAGE_KEY_HASH_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Storage key hash must decode to 32 bytes",
));
}
let data_key = parts[1].trim();
if data_key.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Data key cannot be empty. Use all to lookup every key for this address.",
));
}
let mut data_key_bytes = if data_key.eq_ignore_ascii_case("all") {
b"all".to_vec()
} else {
data_key.as_bytes().to_vec()
};
if data_key_bytes.len() > STORAGE_VALUE_KEY_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Data key must be 50 bytes or less",
));
}
data_key_bytes.resize(STORAGE_VALUE_KEY_BYTES, b' ');
let address_bytes = Wallet::normalize_to_short_address(parts[2])
.and_then(|short| Wallet::short_address_to_bytes(&short))
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Invalid wallet address")
})?;
let transfer_tx = decode(parts[3]).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid storage lookup transfer transaction hex: {err}"),
)
})?;
if transfer_tx.is_empty() || transfer_tx[0] != 2 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Storage lookup payment must be a type 2 transfer transaction",
));
}
bin_msg.extend_from_slice(&command_number.to_le_bytes());
bin_msg.extend_from_slice(&hashmap_key);
bin_msg.extend_from_slice(&storage_key);
bin_msg.extend_from_slice(&data_key_bytes);
bin_msg.extend_from_slice(&address_bytes);
bin_msg.extend_from_slice(&transfer_tx);
}
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,