498 lines
16 KiB
Markdown
498 lines
16 KiB
Markdown
# Contractless PHP API
|
|
|
|
HTTP API foundation for applications that communicate with Contractless nodes
|
|
through `contractless/contractless-php-rpc`.
|
|
|
|
## Install
|
|
|
|
```bash
|
|
composer install
|
|
cp api.env.example api.env
|
|
```
|
|
|
|
Set one or more RPC nodes in `api.env`. The API has no wallet file,
|
|
decryption key, public key, or private key of its own. The environment file
|
|
must not be placed inside `public/`.
|
|
|
|
Point the web server document root to the `public` directory. During local
|
|
development, PHP's built-in server can be started from the project root:
|
|
|
|
```bash
|
|
php -S 127.0.0.1:8080 -t public public/index.php
|
|
```
|
|
|
|
The included `public/.htaccess` routes Apache and normal Hestia
|
|
Nginx-to-Apache installations through `public/index.php`. Apache must have
|
|
`mod_rewrite` enabled and permit `.htaccess` overrides.
|
|
|
|
For a pure Nginx or Nginx-to-PHP-FPM installation, add this inside the domain's
|
|
server configuration:
|
|
|
|
```nginx
|
|
location / {
|
|
try_files $uri $uri/ /index.php?$query_string;
|
|
}
|
|
```
|
|
|
|
After changing an Nginx configuration, validate and reload it:
|
|
|
|
```bash
|
|
sudo nginx -t
|
|
sudo systemctl reload nginx
|
|
```
|
|
|
|
The version root is available at `GET /api/v1`.
|
|
|
|
## Wallet Authentication
|
|
|
|
Except for `GET /api/v1/live` and CORS `OPTIONS` requests, callers provide the
|
|
wallet identity used for the node RPC handshake:
|
|
|
|
```text
|
|
X-Contractless-Address: 40-character-address.cltc
|
|
X-Contractless-Public-Key: Falcon-public-key-hex
|
|
X-Contractless-Signature: 1332-character-signature-hex
|
|
```
|
|
|
|
`X-Contractless-Public-Key` may contain either the raw 897-byte Falcon public
|
|
key or the 898-byte wallet representation with its leading network byte.
|
|
|
|
`X-Contractless-Signature` is the caller's Falcon signature of the 32-byte
|
|
Skein-256 digest of the exact ASCII text `aced`. The browser wallet or
|
|
application creates this proof locally. The API never receives the private key
|
|
and never signs on behalf of the user.
|
|
|
|
Before opening an RPC connection, the API verifies:
|
|
|
|
- The address, public key, and signature have the required sizes and formats
|
|
- The public key satisfies the Contractless public-key rule
|
|
- The wallet address is derived from the supplied public key
|
|
- The supplied signature validates against the Contractless handshake digest
|
|
|
|
The verified public key and signature are then passed to the PHP RPC library
|
|
as a `HandshakeProof`. Every HTTP request creates its RPC client from that
|
|
caller's proof. The API operator does not maintain a registered hot wallet.
|
|
|
|
This handshake signature identifies the caller to a Contractless node. It does
|
|
not authorize spending. Transactions must still be created and signed locally
|
|
by the user's wallet and submitted as complete signed transaction bytes.
|
|
|
|
Because the handshake proof can be reused by anyone who sees it, production
|
|
APIs must use HTTPS. Replaying it can identify a caller for public lookups, but
|
|
cannot produce valid wallet transactions without the user's private key.
|
|
|
|
For example:
|
|
|
|
```bash
|
|
curl https://api.example.com/api/v1/height \
|
|
-H "X-Contractless-Address: 0123456789abcdef0123456789abcdef01234567.cltc" \
|
|
-H "X-Contractless-Public-Key: $CONTRACTLESS_PUBLIC_KEY_HEX" \
|
|
-H "X-Contractless-Signature: $CONTRACTLESS_HANDSHAKE_SIGNATURE_HEX"
|
|
```
|
|
|
|
A browser wallet uses the same headers:
|
|
|
|
```javascript
|
|
const response = await fetch(`${apiEndpoint}/api/v1/height`, {
|
|
headers: {
|
|
"X-Contractless-Address": wallet.address,
|
|
"X-Contractless-Public-Key": wallet.publicKeyHex,
|
|
"X-Contractless-Signature": wallet.handshakeSignatureHex,
|
|
},
|
|
});
|
|
const result = await response.json();
|
|
```
|
|
|
|
The wallet can generate the handshake proof when it is unlocked and reuse it
|
|
for later API calls. It does not send its private key or wallet decryption key.
|
|
|
|
### Test Tool
|
|
|
|
The CLI-only test tool sends an existing wallet handshake proof through the
|
|
same headers a browser wallet will use. Set the credentials in the current
|
|
shell:
|
|
|
|
```bash
|
|
export CONTRACTLESS_TEST_ADDRESS="$(jq -r '.short_address' /path/to/wallet)"
|
|
export CONTRACTLESS_TEST_PUBLIC_KEY="$(jq -r '.public_key' /path/to/wallet)"
|
|
```
|
|
|
|
Generate the handshake signature with the Contractless `sign_message` tool:
|
|
|
|
```bash
|
|
./sign_message "aced"
|
|
```
|
|
|
|
Enter the same wallet path and decryption key. Copy only the hexadecimal value
|
|
printed after `signature:`:
|
|
|
|
```bash
|
|
export CONTRACTLESS_TEST_SIGNATURE='1332-character-signature-from-sign-message'
|
|
```
|
|
|
|
`sign_message` and the node handshake both sign the Skein-256 hash of the exact
|
|
text `aced`, so this produces the proof expected by the API.
|
|
|
|
Then test the default `/api/v1/height` route:
|
|
|
|
```bash
|
|
php tools/test_api.php https://api.contractless.dev
|
|
```
|
|
|
|
An alternative GET route may be provided:
|
|
|
|
```bash
|
|
php tools/test_api.php \
|
|
https://api.contractless.dev \
|
|
/api/v1/network
|
|
```
|
|
|
|
The tool never accepts or loads a private key. It tests the exact public proof
|
|
that applications will submit to the API and returns a nonzero exit status
|
|
when the HTTP request fails.
|
|
|
|
## Initial Endpoints
|
|
|
|
All responses use JSON. Successful responses contain `success: true` and a
|
|
`data` object. Errors contain `success: false` and a sanitized `error` message.
|
|
|
|
| Method | Route | Purpose |
|
|
| --- | --- | --- |
|
|
| `GET` | `/api/v1` | API version information |
|
|
| `GET` | `/api/v1/live` | Confirm the PHP API configuration and process are reachable |
|
|
| `GET` | `/api/v1/ready` | Confirm an RPC node is reachable and show endpoint health |
|
|
| `GET` | `/api/v1/health` | Confirm RPC availability and return chain height |
|
|
| `GET` | `/api/v1/network` | Return decoded network information |
|
|
| `GET` | `/api/v1/height` | Return the current block height |
|
|
| `GET` | `/api/v1/time` | Return the node's Unix timestamp |
|
|
| `GET` | `/api/v1/addresses/validate?address=...` | Validate a registered canonical address |
|
|
| `GET` | `/api/v1/addresses/vanity/resolve?address=...` | Resolve a vanity address to its owner |
|
|
| `GET` | `/api/v1/addresses/registration?address=...` | Check wallet registration |
|
|
| `GET` | `/api/v1/balances/base?coin=CLTC&address=...` | Return one base-coin balance |
|
|
| `GET` | `/api/v1/balances?address=...` | Return every balance owned by an address |
|
|
| `GET` | `/api/v1/transactions/lookup?txid=...` | Return a confirmed transaction |
|
|
| `GET` | `/api/v1/addresses/history?address=...&skip=0&limit=100` | Return paginated history |
|
|
| `POST` | `/api/v1/messages/verify` | Verify a signed message |
|
|
| `POST` | `/api/v1/transactions/broadcast` | Broadcast a complete signed transaction |
|
|
|
|
Message verification accepts:
|
|
|
|
```json
|
|
{
|
|
"message": "Exact message bytes",
|
|
"address": "40-character-address.cltc",
|
|
"signature": "1332-character-signature-hex"
|
|
}
|
|
```
|
|
|
|
Transaction broadcasting accepts:
|
|
|
|
```json
|
|
{
|
|
"transaction_hex": "complete-signed-transaction-hex"
|
|
}
|
|
```
|
|
|
|
The API never creates or signs a transaction for the caller. It only forwards
|
|
the complete signed transaction through `Client::submitTransaction()`.
|
|
|
|
## Reliability
|
|
|
|
Each RPC request still opens one connection, makes one request, receives one
|
|
reply, and closes the connection. The API does not create persistent node
|
|
connections.
|
|
|
|
The endpoint pool retries another configured node only when a connection,
|
|
timeout, read, or write failure occurs. A protocol error or rejected response
|
|
is returned as a failure instead of being hidden by another node.
|
|
|
|
Endpoint health survives separate PHP requests in the locked API state file:
|
|
|
|
```ini
|
|
CONTRACTLESS_RPC_TIMEOUT=10
|
|
RPC_FAILURE_THRESHOLD=2
|
|
RPC_COOLDOWN_SECONDS=30
|
|
```
|
|
|
|
After the configured number of consecutive transport failures, that endpoint
|
|
is skipped for the cooldown period. A successful request clears its failure
|
|
count. If every endpoint is cooling down or unreachable, the API returns
|
|
`503 Service Unavailable`.
|
|
|
|
Use `GET /api/v1/live` for a process liveness check. It does not require wallet
|
|
headers or contact a Contractless node. Use `GET /api/v1/ready` with wallet
|
|
headers when a deployment check must confirm that at least one RPC node can
|
|
answer.
|
|
|
|
Malformed upstream protocol replies return `502 Bad Gateway`. Internal API
|
|
failures return a sanitized `500` response. Every response includes an
|
|
`X-Request-ID` header. Server error logs contain that identifier and the error
|
|
class, but never request bodies, wallet authentication headers, private keys,
|
|
or raw node responses.
|
|
|
|
Paid storage lookups remain pinned to the endpoint that generated their quote.
|
|
They are never moved to another endpoint because payment addresses and lookup
|
|
prices can differ between node operators.
|
|
|
|
Rate limits, HMAC nonces, and endpoint health share the same file-backed state
|
|
store. Every read-modify-write operation holds an exclusive file lock, so
|
|
concurrent PHP workers cannot overwrite each other's updates.
|
|
|
|
## API Security
|
|
|
|
General HTTP security controls run before caller wallet authentication or RPC
|
|
client construction. This prevents rejected or rate-limited requests from
|
|
triggering Falcon verification.
|
|
|
|
### HTTPS
|
|
|
|
`API_REQUIRE_HTTPS=true` rejects non-HTTPS production requests. For direct TLS,
|
|
no proxy configuration is needed.
|
|
|
|
If HTTPS terminates at a reverse proxy, add only IP addresses or CIDR ranges
|
|
belonging to that proxy:
|
|
|
|
```ini
|
|
API_TRUSTED_PROXIES=127.0.0.1,10.20.0.0/16
|
|
```
|
|
|
|
`X-Forwarded-For` and `X-Forwarded-Proto` are ignored unless the immediate
|
|
connection came from a configured trusted proxy. Never add arbitrary public
|
|
networks merely to make forwarded headers work.
|
|
|
|
For local development without HTTPS:
|
|
|
|
```ini
|
|
APP_ENV=development
|
|
API_REQUIRE_HTTPS=false
|
|
```
|
|
|
|
### CORS
|
|
|
|
List the exact websites or browser extensions permitted to call the API:
|
|
|
|
```ini
|
|
API_CORS_ORIGINS=https://wallet.example.com,chrome-extension://extension-id
|
|
```
|
|
|
|
Use `API_CORS_ORIGINS=*` only when intentionally operating a public API for
|
|
every website. Requests without an `Origin` header, including normal
|
|
server-to-server calls, are unaffected.
|
|
|
|
### Request Limits
|
|
|
|
```ini
|
|
API_MAX_BODY_BYTES=2000000
|
|
API_RATE_WINDOW_SECONDS=60
|
|
API_PUBLIC_REQUESTS_PER_WINDOW=120
|
|
API_MESSAGE_VERIFY_REQUESTS_PER_WINDOW=30
|
|
API_BROADCAST_REQUESTS_PER_WINDOW=10
|
|
```
|
|
|
|
Public callers are limited by resolved client IP. Message verification and
|
|
transaction broadcasting have additional route-specific limits. Rate-limit
|
|
responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`,
|
|
`X-RateLimit-Reset`, and, when blocked, `Retry-After`.
|
|
|
|
The default security state is stored in `storage/security-state.json`. Its
|
|
directory must be writable by the PHP process and must remain outside the web
|
|
document root.
|
|
|
|
All `POST`, `PUT`, and `PATCH` requests require
|
|
`Content-Type: application/json`. Oversized bodies are rejected before parsing.
|
|
The API does not write request bodies to its application logs.
|
|
|
|
### Transaction Broadcasting
|
|
|
|
Before contacting a node, the broadcast route decodes the first transaction
|
|
byte and checks the exact serialized length expected by Contractless.
|
|
Unsupported types, malformed lengths, genesis transactions, and node-generated
|
|
reward transactions are rejected by the API.
|
|
|
|
### Optional API Keys
|
|
|
|
API keys provide identified clients with their own quota:
|
|
|
|
```ini
|
|
API_KEYS=example-service=replace-with-at-least-32-random-characters:300
|
|
```
|
|
|
|
The client sends the ID and secret together:
|
|
|
|
```text
|
|
X-API-Key: example-service.replace-with-at-least-32-random-characters
|
|
```
|
|
|
|
Multiple entries are comma-separated. API keys require HTTPS but do not provide
|
|
request signing or replay protection.
|
|
|
|
### Optional HMAC Authentication
|
|
|
|
HMAC authentication is intended for trusted server-to-server applications:
|
|
|
|
```ini
|
|
API_HMAC_KEYS=backend=replace-with-at-least-32-random-characters:600
|
|
API_HMAC_CLOCK_SKEW_SECONDS=300
|
|
```
|
|
|
|
The caller sends:
|
|
|
|
```text
|
|
X-API-Key: backend
|
|
X-Timestamp: current Unix timestamp
|
|
X-Nonce: unique random value containing 16 to 128 letters, numbers, _ or -
|
|
X-Signature: lowercase hexadecimal HMAC-SHA256 signature
|
|
```
|
|
|
|
The signed canonical string is:
|
|
|
|
```text
|
|
HTTP_METHOD
|
|
/request/path
|
|
raw=query&string
|
|
unix_timestamp
|
|
nonce
|
|
sha256_hex_of_exact_request_body
|
|
```
|
|
|
|
The signature is:
|
|
|
|
```php
|
|
$bodyHash = hash('sha256', $exactRequestBody);
|
|
$canonical = implode("\n", [
|
|
strtoupper($method),
|
|
$path,
|
|
$rawQueryString,
|
|
(string) $timestamp,
|
|
$nonce,
|
|
$bodyHash,
|
|
]);
|
|
$signature = hash_hmac('sha256', $canonical, $hmacSecret);
|
|
```
|
|
|
|
Timestamps outside the configured clock window are rejected. Each valid nonce
|
|
is recorded until its timestamp can no longer be accepted, preventing replay.
|
|
|
|
The browser wallet uses public rate-limited access. It must never contain an
|
|
API-key secret or HMAC secret.
|
|
|
|
## Remaining RPC Endpoints
|
|
|
|
### Chain, Blocks, Headers, And Torrents
|
|
|
|
| Method | Route | Purpose |
|
|
| --- | --- | --- |
|
|
| `GET` | `/api/v1/chain/difficulty` | Current height and next-block difficulty |
|
|
| `GET` | `/api/v1/chain/largest-transaction-fee` | Largest eligible mempool fee |
|
|
| `GET` | `/api/v1/chain/transaction-counts` | Confirmed totals by transaction type |
|
|
| `GET` | `/api/v1/blocks/latest` | Latest raw block |
|
|
| `GET` | `/api/v1/blocks/by-height?height=...` | Raw block at a height |
|
|
| `GET` | `/api/v1/blocks/by-hash?hash=...` | Raw block matching a hash |
|
|
| `GET` | `/api/v1/blocks/hash?height=...` | Block hash at a height |
|
|
| `GET` | `/api/v1/headers/by-height?height=...` | Raw header at a height |
|
|
| `GET` | `/api/v1/headers/by-hash?hash=...` | Raw header matching a hash |
|
|
| `GET` | `/api/v1/headers/all` | Complete raw header history |
|
|
| `GET` | `/api/v1/torrents/by-height?height=...` | Raw torrent metadata |
|
|
|
|
Raw blocks, headers, and torrents are returned as `hex` plus their original
|
|
byte count. The API does not alter the blockchain artifact.
|
|
|
|
Large routes have a separate quota:
|
|
|
|
```ini
|
|
API_EXPENSIVE_REQUESTS_PER_WINDOW=5
|
|
```
|
|
|
|
Operators can disable costly raw routes independently:
|
|
|
|
```ini
|
|
API_ENABLE_RAW_BLOCKS=true
|
|
API_ENABLE_TORRENTS=true
|
|
API_ENABLE_ALL_HEADERS=false
|
|
```
|
|
|
|
Complete header-history downloads are disabled by default.
|
|
|
|
### Mempool
|
|
|
|
| Method | Route | Purpose |
|
|
| --- | --- | --- |
|
|
| `GET` | `/api/v1/mempool/count` | Number of pending transactions |
|
|
| `GET` | `/api/v1/mempool/by-signature?signature=...` | Pending transaction by signature |
|
|
| `GET` | `/api/v1/mempool/by-address?address=...` | Pending transactions involving an address |
|
|
|
|
Pending transactions are returned as their exact signed transaction hex,
|
|
original byte count, and transaction type.
|
|
|
|
### Tokens, NFTs, And RWAs
|
|
|
|
| Method | Route | Purpose |
|
|
| --- | --- | --- |
|
|
| `GET` | `/api/v1/tokens` | Token list and origin transaction IDs |
|
|
| `GET` | `/api/v1/tokens/catalog` | Token catalog |
|
|
| `GET` | `/api/v1/tokens/details?name=...` | Details for one token |
|
|
| `GET` | `/api/v1/nfts` | NFT/RWA list, ownership type, and supply |
|
|
| `GET` | `/api/v1/nfts/details?name=...&series=...` | Details for one NFT/RWA item |
|
|
|
|
### Loans And Marketing
|
|
|
|
| Method | Route | Purpose |
|
|
| --- | --- | --- |
|
|
| `GET` | `/api/v1/loans/by-hash?hash=...` | Loan by contract hash |
|
|
| `GET` | `/api/v1/loans/by-address?address=...` | Loans involving an address |
|
|
| `GET` | `/api/v1/loans/collateral?hash=...` | Loan collateral status |
|
|
| `GET` | `/api/v1/marketing/history?advertiser=...&campaign=...&skip=0&limit=100` | Campaign records |
|
|
|
|
### Additional Address Routes
|
|
|
|
| Method | Route | Purpose |
|
|
| --- | --- | --- |
|
|
| `GET` | `/api/v1/addresses/latest?address=...&limit=25` | Newest confirmed and pending activity |
|
|
| `GET` | `/api/v1/addresses/vanity?address=...` | Vanity address registered to a canonical wallet |
|
|
|
|
### Storage
|
|
|
|
Get a storage lookup quote:
|
|
|
|
```text
|
|
GET /api/v1/storage/cost?storage_key=...&data_key=all&address=...
|
|
```
|
|
|
|
Along with the byte count, price, and payment address, the quote returns
|
|
`rpc_endpoint`. This opaque configured endpoint ID must be submitted with the
|
|
paid lookup:
|
|
|
|
```json
|
|
{
|
|
"storage_key": "64-character-storage-key-hash",
|
|
"data_key": "all",
|
|
"address": "40-character-address.cltc",
|
|
"rpc_endpoint": "node-1",
|
|
"payment_transaction_hex": "complete-signed-type-2-transfer"
|
|
}
|
|
```
|
|
|
|
Send that JSON to:
|
|
|
|
```text
|
|
POST /api/v1/storage/lookup
|
|
```
|
|
|
|
The API uses the same node that issued the quote. It intentionally does not
|
|
fail over this request because another node may use a different payment
|
|
address or lookup price.
|
|
|
|
### Governance
|
|
|
|
```text
|
|
GET /api/v1/governance/proposals?proposal_key=...
|
|
```
|
|
|
|
This returns the node's proposal, vote, implementation, and activation state as
|
|
JSON.
|
|
|
|
The public API does not currently expose wallet-registration transaction
|
|
creation. A browser wallet must create and sign registration data locally
|
|
before an appropriate broadcast path can submit it. The API never registers a
|
|
wallet using credentials owned by the API operator.
|