diff --git a/README.md b/README.md index 502678d..d932cc1 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,26 @@ # PHP Contractless RPC -`contractless-php-rpc` connects PHP applications directly to a Contractless -node. It implements the authenticated binary RPC protocol without invoking -Contractless CLI programs. +`contractless-php-rpc` allows PHP applications to connect directly to a +Contractless node through its authenticated binary RPC protocol. It does not +invoke Contractless CLI programs and does not depend on centralized blockchain +APIs. -The package is intended for faucets, block explorers, wallets, application -backends, and other public Contractless integrations. +The package can be used to build faucets, block explorers, wallets, application +backends, storage applications, and other public Contractless integrations. ## Scope -The client includes public blockchain lookups, wallet and address lookups, -mempool lookups, asset lookups, loan lookups, storage lookups, governance -lookups, wallet registration, signed transaction submission, and an RPC -interface restricted to application-safe commands. +The library exposes application-safe public RPC commands, including: + +- Network and blockchain information +- Blocks, headers, and torrents +- Confirmed transactions and mempool records +- Wallet balances, registration, addresses, and history +- Tokens, NFTs, RWAs, swaps, and loans +- Marketing records +- On-chain data storage +- Governance proposals +- Transaction creation, signing, and broadcasting Miner relay commands, network mapping changes, monitor-state commands, node setup synchronization, and server-owner IP controls are intentionally not @@ -20,50 +28,32 @@ exposed. ## Requirements -- PHP 8.1 or newer, running as a 64-bit build -- The PHP GD extension -- The PHP OpenSSL extension -- The `skein` module from `contractless-php-crypto` -- The `oqsphp` module from `contractless-php-crypto` +- PHP 8.1 or newer using a 64-bit build +- PHP GD +- PHP OpenSSL +- The `skein` module from Contractless PHP Modules +- The `oqsphp` module from Contractless PHP Modules Contractless uses the same Falcon key and signature sizes as `Falcon-padded-512`, but its FN-DSA signing mode requires a small message adaptation. `NativeCrypto` performs that adaptation automatically. +Install and test the Contractless PHP Modules before installing this package: + +https://contractless.dev/contractless/Contractless-PHP-Modules + ## Installation -Install and enable `contractless-php-crypto` first. Its repository contains the -native module build, installation, and compatibility-test instructions. - -When this package is available through Packagist: +Install version `0.2` through Packagist: ```bash -composer require contractless/contractless-php-rpc +composer require contractless/contractless-php-rpc:^0.2.0 -W ``` -Until the package is published, add it as a Composer path repository: +Composer installs the package under `vendor/` and generates the required +autoload files. -```json -{ - "repositories": [ - { - "type": "path", - "url": "../contractless-php-rpc" - } - ], - "require": { - "contractless/contractless-php-rpc": "@dev" - } -} -``` - -Then install it: - -```bash -composer update contractless/contractless-php-rpc -``` - -## Basic Use +## Create A Client ```php totalBalance( - 'ab13318c26250b048db92920a80a86127c933b0c.cltc', +Use port `50050` for the default testnet RPC and `50055` for the default +mainnet RPC unless the node operator configured another port. + +Every RPC call performs a signed client handshake. The wallet used for the +handshake does not have to own an address being looked up. + +## RPC Replies + +RPC methods return the node's original reply as a PHP string. Depending on the +command, that string may contain text, JSON, or raw binary data. + +Decode JSON replies when appropriate: + +```php +$data = json_decode( + $client->networkInfo(), + true, + flags: JSON_THROW_ON_ERROR, ); ``` -RPC replies are returned as their original bytes. Commands that return JSON can -be decoded with `json_decode($reply, true, flags: JSON_THROW_ON_ERROR)`. Binary -block, torrent, header, and transaction replies remain available without a -lossy conversion. +Do not apply text conversion to raw blocks, headers, torrents, transactions, or +binary integer replies. -## Transaction Submission +A coin balance is returned as an eight-byte little-endian unsigned integer: -`submitTransaction()` accepts the complete serialized signed transaction as a -hexadecimal string: +```php +$reply = $client->coinBalance('CLTC', $address); +$balanceAtomic = unpack('Pbalance', $reply)['balance']; +$balance = number_format($balanceAtomic / 100_000_000, 8, '.', ''); +``` + +All transaction amounts and fees supplied to builders use atomic units. +Contractless has `100,000,000` atomic units per `CLC` or `CLTC`. + +## Network And Chain RPC + +```php +$network = $client->networkInfo(); +$height = $client->blockHeight(); +$nodeTime = $client->nodeTime(); +$difficulty = $client->difficulty(); +$latestBlock = $client->latestBlock(); +$largestFee = $client->largestTransactionFee(); +$confirmedCount = $client->totalConfirmedTransactions(); +``` + +- `networkInfo()` returns the node's public network information. +- `blockHeight()` returns the current saved chain height. +- `nodeTime()` returns the node's current time. +- `difficulty()` returns the current mining difficulty. +- `latestBlock()` returns the latest saved block. +- `largestTransactionFee()` returns the largest eligible mempool fee. +- `totalConfirmedTransactions()` returns the confirmed transaction count. + +## Block, Header, And Torrent RPC + +Hashes passed to these methods are 64-character hexadecimal hashes: + +```php +$block = $client->blockByHeight(1000); +$block = $client->blockByHash($blockHash); +$torrent = $client->torrentByHeight(1000); +$header = $client->headerByHeight(1000); +$header = $client->headerByHash($headerHash); +$blockHash = $client->blockHashAtHeight(1000); +$headers = $client->allHeaders(); +``` + +- `blockByHeight()` returns the raw saved block at a height. +- `blockByHash()` returns the raw block matching a block hash. +- `torrentByHeight()` returns the raw torrent metadata for a block. +- `headerByHeight()` returns the raw block header at a height. +- `headerByHash()` returns the raw header matching a header hash. +- `blockHashAtHeight()` returns the hash recorded at a height. +- `allHeaders()` returns the saved block-header history. + +## Transaction And Mempool RPC + +```php +$mempoolCount = $client->mempoolCount(); +$mempoolTransaction = $client->mempoolTransactionBySignature( + $signatureHex, +); +$addressMempool = $client->mempoolTransactionsByAddress($address); +$transaction = $client->transactionById($transactionId); +``` + +- `mempoolCount()` returns the number of pending transactions. +- `mempoolTransactionBySignature()` accepts a 1,332-character hexadecimal + Falcon signature and returns the matching pending transaction. +- `mempoolTransactionsByAddress()` returns pending transactions involving an + address. +- `transactionById()` accepts a 64-character hexadecimal transaction ID and + returns the confirmed transaction and its recorded location. + +Submit a complete serialized signed transaction represented as hexadecimal: ```php $reply = $client->submitTransaction($signedTransactionHex); ``` -The first transaction builder covers the transfer used by faucets and ordinary -currency, token, NFT, and RWA transfers: +Submit a `SignedTransaction` produced by one of the included builders: + +```php +$reply = $client->submitSignedTransaction($signedTransaction); +``` + +## Wallet And Address RPC + +```php +$coinBalance = $client->coinBalance('CLTC', $address); +$allBalances = $client->totalBalance($address); +$addressValid = $client->validateAddress($address); +$registration = $client->walletRegistrationStatus($address); +$history = $client->addressHistory($address, skip: 0, limit: 100); +$latest = $client->latestAddressTransactions($address, limit: 25); +$canonical = $client->vanityLookup('my-vanity-address.cltc'); +$vanityOwner = $client->vanityOwner('my-vanity-address.cltc'); +``` + +- `coinBalance()` returns one confirmed asset balance as an eight-byte integer. +- `totalBalance()` returns all confirmed balances owned by an address. +- `validateAddress()` asks the node to validate an address. +- `walletRegistrationStatus()` returns whether the wallet is registered. +- `addressHistory()` returns a paginated confirmed transaction history. +- `latestAddressTransactions()` returns the newest confirmed transactions. +- `vanityLookup()` resolves a vanity address to its canonical wallet. +- `vanityOwner()` returns the canonical owner of a vanity address. + +Verify a signed message: + +```php +$result = $client->validateMessage( + message: 'Message to verify', + address: $address, + signature: $signatureHex, +); +``` + +Register the wallet loaded into `$credentials`: + +```php +$result = $client->registerOwnedWallet($address); +``` + +The lower-level registration method is available when an application has +already created the required signature: + +```php +$result = $client->registerWallet($address, $signatureHex); +``` + +## Token, NFT, RWA, Swap, And Loan RPC + +```php +$tokens = $client->tokenList(); +$tokenCatalog = $client->tokenCatalog(); +$token = $client->tokenDetails('TOKEN'); + +$nfts = $client->nftList(); +$nft = $client->nftDetails('ART', series: 1); + +$loan = $client->loanByHash($loanHash); +$addressContracts = $client->contractsByAddress($address); +$collateral = $client->collateralStatus($loanHash); +``` + +- `tokenList()` returns the node's token list. +- `tokenCatalog()` returns token catalog data. +- `tokenDetails()` returns details for one token ticker. +- `nftList()` returns the node's NFT and RWA list. +- `nftDetails()` returns one NFT/RWA name and series record. +- `loanByHash()` returns a loan using its contract hash. +- `contractsByAddress()` returns loan contracts involving an address. +- `collateralStatus()` returns the collateral state of a loan. + +## Marketing RPC + +```php +$records = $client->marketingCampaignHistory( + advertiser: $address, + campaign: 42, + skip: 0, + limit: 100, +); +``` + +`marketingCampaignHistory()` returns paginated marketing records for one +advertiser and campaign number. + +## Data Storage RPC + +Get the cost of returning one data key: + +```php +$quote = $client->storageLookupCost( + storageKey: $storageKeyHash, + dataKey: 'username', + address: $address, +); +``` + +Get the cost of returning every key stored under the storage key and address: + +```php +$quote = $client->storageLookupCost( + storageKey: $storageKeyHash, + dataKey: 'all', + address: $address, +); +``` + +Perform a paid lookup using a complete signed type-2 transfer represented as +hexadecimal: + +```php +$data = $client->storageLookup( + storageKey: $storageKeyHash, + dataKey: 'all', + address: $address, + paymentTransactionHex: $paymentTransactionHex, +); +``` + +The payment must match the quote returned by the selected node. A node operator +looking up data through their own node may receive a zero-cost quote. + +## Governance RPC + +```php +$proposal = $client->governanceProposal($proposalKeyHash); +``` + +`governanceProposal()` returns the recorded state of a Contractless proposal, +including its voting and activation information. + +## Create And Broadcast A Transfer ```php use Contractless\Rpc\Transaction\TransferBuilder; $transfer = (new TransferBuilder($crypto, $credentials))->create( - sender: 'sender-address.cltc', - receiver: 'receiver-address.cltc', + sender: $sender, + receiver: $receiver, coin: 'CLTC', value: 2_500_000_000, - fee: 2_500, + fee: 25_000_000, + nftSeries: 0, ); $reply = $client->submitSignedTransaction($transfer); ``` -Transaction builders are a separate layer over the protocol client. This keeps -transport and node access stable when new transaction types are introduced. +The same builder transfers base currency, tokens, NFTs, and RWAs. Set +`nftSeries` to the applicable series number for an NFT/RWA transfer. -## Transaction Builders - -- `TransferBuilder`: currency, token, NFT, and RWA transfers -- `AssetBuilder`: token creation, token issuance, NFT/RWA creation, burns, and vanity addresses -- `AgreementBuilder`: two-party swaps and loan contracts -- `MiscellaneousBuilder`: marketing records, loan payments, and collateral claims -- `StorageBuilder`: storage keys, bool, signed and unsigned integers, strings, and deletion -- `GovernanceBuilder`: proposal keys, proposal votes, and activation votes - -Swaps and loans return `DualSignedTransaction`. The first signer exports it, -the second signer imports it and calls `signNext()`, and only a complete result -can be converted with `toSignedTransaction()` and broadcast. - -The 128-bit storage methods accept decimal strings. This preserves the complete -Rust `u128` and `i128` ranges instead of silently losing precision through PHP -integers or floating-point numbers. - -## Wallet Material - -The library can load a normal Contractless wallet file directly: +## Token, NFT, RWA, Burn, And Vanity Builders ```php -use Contractless\Rpc\Wallet\Credentials; +use Contractless\Rpc\Transaction\AssetBuilder; +$assets = new AssetBuilder($crypto, $credentials); + +$createToken = $assets->createToken( + creator: $address, + ticker: 'TOKEN', + number: 1_000_000_000, + hardLimit: true, + fee: 100_000_000, +); + +$issueToken = $assets->issueToken( + creator: $address, + ticker: 'TOKEN', + number: 500_000_000, + fee: 100_000_000, +); + +$createNft = $assets->createNft( + creator: $address, + series: false, + fractionalOwnership: false, + name: 'ART', + ipfs: 'ipfs-content-identifier', + count: 1, + description: 'Description of the NFT or RWA', + fee: 100_000_000, +); + +$burn = $assets->burn( + address: $address, + coin: 'TOKEN', + nftSeries: 0, + value: 100_000_000, + fee: 100_000_000, +); + +$vanity = $assets->vanity( + address: $address, + vanityAddress: 'my-address.cltc', + fee: 100_000_000, +); + +$client->submitSignedTransaction($createToken); +``` + +## Swap And Loan Builders + +Swaps and loans require two wallet signatures. The first wallet creates and +signs the transaction: + +```php +use Contractless\Rpc\Transaction\AgreementBuilder; + +$agreements = new AgreementBuilder($crypto, $credentials); + +$swap = $agreements->swap( + expiration: time() + 3600, + ticker1: 'CLTC', + series1: 0, + value1: 100_000_000, + ticker2: 'TOKEN', + series2: 0, + value2: 500_000_000, + sender1: $firstAddress, + sender2: $secondAddress, + tip1: 1_000_000, + tip2: 1_000_000, + fee1: 2_500, + fee2: 2_500, +); + +$loan = $agreements->loan( + loanCoin: 'CLTC', + loanAmount: 1_000_000_000, + lender: $lender, + collateral: 'TOKEN', + collateralAmount: 2_000_000_000, + borrower: $borrower, + paymentPeriod: 'm', + paymentNumber: 12, + paymentAmount: 100_000_000, + gracePeriod: 2, + maxLateValue: 300_000_000, + fee: 100_000_000, +); +``` + +Export the partially signed transaction for the second party: + +```php +$exported = json_encode( + $swap->export(), + JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR, +); +``` + +The second party imports and signs it using their own `$crypto` and +`$credentials`: + +```php +use Contractless\Rpc\Transaction\DualSignedTransaction; + +$swap = DualSignedTransaction::import( + json_decode($exported, true, flags: JSON_THROW_ON_ERROR), +); + +$swap = $swap->signNext($crypto, $credentials); + +if ($swap->isComplete()) { + $client->submitSignedTransaction($swap->toSignedTransaction()); +} +``` + +## Loan Payment And Collateral Builders + +```php +use Contractless\Rpc\Transaction\MiscellaneousBuilder; + +$miscellaneous = new MiscellaneousBuilder($crypto, $credentials); + +$payment = $miscellaneous->loanPayment( + amount: 100_000_000, + contractHash: $loanHash, + address: $borrower, + tip: 1_000_000, + fee: 2_500, +); + +$claim = $miscellaneous->collateralClaim( + contractHash: $loanHash, + address: $lender, + fee: 100_000_000, +); + +$client->submitSignedTransaction($payment); +``` + +## Marketing Transaction Builder + +```php +$marketing = $miscellaneous->marketing( + campaign: 42, + adType: 'banner', + keyword: 'blockchain', + displayed: 'https://example.com/page', + impressions: 1, + clicks: 0, + impressionValue: 100, + clickValue: 500, + advertiser: $address, + fee: 100_000_000, +); + +$client->submitSignedTransaction($marketing); +``` + +## Data Storage Transaction Builders + +```php +use Contractless\Rpc\Transaction\StorageBuilder; + +$storage = new StorageBuilder($crypto, $credentials); + +$createKey = $storage->createKey( + address: $address, + fee: 50_000_000_000, +); + +$boolean = $storage->boolean( + $storageKey, 'active', true, $address, 100_000_000, +); + +$u8 = $storage->u8($storageKey, 'u8-key', 255, $address, 100_000_000); +$u16 = $storage->u16($storageKey, 'u16-key', 65535, $address, 100_000_000); +$u32 = $storage->u32($storageKey, 'u32-key', 100000, $address, 100_000_000); +$u64 = $storage->u64($storageKey, 'u64-key', 100000, $address, 100_000_000); +$u128 = $storage->u128( + $storageKey, + 'u128-key', + '340282366920938463463374607431768211455', + $address, + 100_000_000, +); + +$i8 = $storage->i8($storageKey, 'i8-key', -128, $address, 100_000_000); +$i16 = $storage->i16($storageKey, 'i16-key', -32768, $address, 100_000_000); +$i32 = $storage->i32($storageKey, 'i32-key', -100000, $address, 100_000_000); +$i64 = $storage->i64($storageKey, 'i64-key', -100000, $address, 100_000_000); +$i128 = $storage->i128( + $storageKey, + 'i128-key', + '-170141183460469231731687303715884105728', + $address, + 100_000_000, +); + +$string = $storage->string( + storageKey: $storageKey, + key: 'username', + value: 'contractless-user', + address: $address, + previousHash: str_repeat('0', 64), + fee: 100_000_000, +); + +$delete = $storage->delete( + storageKey: $storageKey, + key: 'username', + address: $address, + fee: 100_000_000, +); + +$client->submitSignedTransaction($string); +``` + +The `u128()` and `i128()` methods accept decimal strings so PHP does not lose +precision. String values may contain up to 180 bytes per linked transaction. +The first string uses 64 zeroes for `previousHash`; later chunks use the hash +of the preceding string transaction. + +## Governance Transaction Builders + +```php +use Contractless\Rpc\Transaction\GovernanceBuilder; + +$governance = new GovernanceBuilder($crypto, $credentials); + +$proposal = $governance->proposal( + proposalHash: $proposalDocumentHash, + address: $address, + fee: 100_000_000, +); + +$proposalVote = $governance->proposalVote( + proposalKey: $proposalKey, + address: $address, + approve: true, + fee: 100_000_000, +); + +$activationVote = $governance->activationVote( + proposalKey: $proposalKey, + developmentHash: $implementationDocumentHash, + developmentLocation: 'CLP-0001-IMPLEMENTATION.md', + address: $address, + approve: true, + fee: 100_000_000, +); + +$client->submitSignedTransaction($proposalVote); +``` + +The network still determines whether the submitting wallet is an eligible node +for proposal and activation voting. + +## Signed Transaction Data + +Every single-signature builder returns `SignedTransaction`: + +```php +$rawBytes = $transaction->bytes; +$hexadecimal = $transaction->toHex(); +$signatureBytes = $transaction->signature; +$unsignedSigningJson = $transaction->unsignedJson; +``` + +Use `submitSignedTransaction()` to broadcast the exact bytes without converting +them to hexadecimal first. + +## Wallet Files + +Load a normal Contractless wallet: + +```php $credentials = Credentials::fromWalletFile( '/private/path/contractless.wallet', 'wallet decryption key', ); ``` -The loader decodes all supported Contractless private-key image orientations, -verifies the encrypted payload HMAC, decrypts the Falcon private key, proves -that the public and private keys match, and verifies the wallet's canonical -short address. +The loader: + +- Decodes every supported Contractless private-key image orientation +- Verifies the encrypted payload HMAC +- Decrypts the Falcon private key +- Proves that the public and private keys match +- Verifies the wallet's canonical short address Wallet files and decryption keys must remain outside public web directories, logs, repositories, and client-visible configuration. -Verify a wallet file and decryption key before using them in an application: +Verify a wallet before using it: ```bash -php examples/load_wallet.php /private/path/contractless.wallet 'wallet decryption key' +php vendor/contractless/contractless-php-rpc/examples/load_wallet.php \ + /private/path/contractless.wallet \ + 'wallet decryption key' ``` -Successful output confirms that the image was decoded, its encrypted payload -was authenticated and decrypted, the Falcon keypair matches, and the wallet -address is valid. - -Raw Falcon key bytes remain supported for applications that already manage -their own protected key storage: +Applications that already manage protected raw keys may load hexadecimal +Falcon keys directly: ```php $credentials = Credentials::fromHex($publicKeyHex, $privateKeyHex); ``` +## Direct Public Command Calls + +The named methods above are wrappers around `call()`. Applications may invoke +an exposed public command directly when they need to construct its binary +payload themselves: + +```php +use Contractless\Rpc\Protocol\Command; + +$reply = $client->call(Command::BLOCK_HEIGHT); +``` + +With a payload: + +```php +use Contractless\Rpc\Protocol\Binary; + +$reply = $client->call( + Command::BLOCK_BY_HEIGHT, + Binary::u32(1000), +); +``` + +`call()` rejects internal, administrative, mining, monitor, and unsupported +command numbers. Prefer the named methods whenever one exists because they +validate and encode the command payload correctly. + +## Error Handling + +RPC transport, protocol, validation, and wallet failures throw exceptions: + +```php +try { + $height = $client->blockHeight(); +} catch (Throwable $error) { + error_log($error->getMessage()); +} +``` + +Do not expose wallet errors, filesystem paths, private material, or internal +node replies directly to public users. + ## Testing ```bash @@ -188,17 +700,8 @@ composer install composer test ``` -## Preparing The Composer Package - -Validate the package before publishing: +Validate package metadata before publishing a release: ```bash composer validate --strict -composer install -composer test ``` - -Commit `composer.json`, `README.md`, `src/`, and `tests/` to the -`contractless-php-rpc` repository. Create a release tag such as `v0.1.0`, then -submit the repository URL to Packagist. Future version tags become installable -Composer releases.