added wallet registration endpoint

This commit is contained in:
viraladmin 2026-07-29 12:05:54 -06:00
parent 69722b9bba
commit 7ca99356fb
4 changed files with 86 additions and 5 deletions

View File

@ -156,6 +156,7 @@ All responses use JSON. Successful responses contain `success: true` and a
| `GET` | `/api/v1/addresses/validate?address=...` | Validate a registered canonical address | | `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/vanity/resolve?address=...` | Resolve a vanity address to its owner |
| `GET` | `/api/v1/addresses/registration?address=...` | Check wallet registration | | `GET` | `/api/v1/addresses/registration?address=...` | Check wallet registration |
| `POST` | `/api/v1/addresses/register` | Submit a locally signed wallet registration |
| `GET` | `/api/v1/balances/base?coin=CLTC&address=...` | Return one base-coin balance | | `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/balances?address=...` | Return every balance owned by an address |
| `GET` | `/api/v1/transactions/lookup?txid=...` | Return a confirmed transaction | | `GET` | `/api/v1/transactions/lookup?txid=...` | Return a confirmed transaction |
@ -184,6 +185,27 @@ Transaction broadcasting accepts:
The API never creates or signs a transaction for the caller. It only forwards The API never creates or signs a transaction for the caller. It only forwards
the complete signed transaction through `Client::submitTransaction()`. the complete signed transaction through `Client::submitTransaction()`.
Wallet registration accepts:
```json
{
"address": "40-character-address.cltc",
"public_key": "1794-character-public-key-hex",
"signature": "1332-character-registration-signature-hex"
}
```
The authenticated address and public key must match the registration body.
The API forwards the registration through `Client::registerWallet()`; it never
creates the registration signature.
Browser extensions must also be included in `API_CORS_ORIGINS` using their
exact origin:
```text
API_CORS_ORIGINS=https://wallet.example.com,chrome-extension://extension-id
```
## Reliability ## Reliability
Each RPC request still opens one connection, makes one request, receives one Each RPC request still opens one connection, makes one request, receives one
@ -479,7 +501,7 @@ GET /api/v1/governance/proposals?proposal_key=...
This returns the node's proposal, vote, implementation, and activation state as This returns the node's proposal, vote, implementation, and activation state as
JSON. JSON.
The public API does not currently expose wallet-registration transaction The public API does not create wallet-registration signatures. A browser
creation. A browser wallet must create and sign registration data locally wallet must create and sign registration data locally before submitting it
before an appropriate broadcast path can submit it. The API never registers a through `/api/v1/addresses/register`. The API never registers a
wallet using credentials owned by the API operator. wallet using credentials owned by the API operator.

View File

@ -3,7 +3,7 @@
APP_ENV=production APP_ENV=production
# Give every endpoint a unique name. Separate endpoints with commas. # Give every endpoint a unique name. Separate endpoints with commas.
CONTRACTLESS_RPC_NODES=node-1=contractless.dev:50050 CONTRACTLESS_RPC_NODES=node-1=127.0.0.1:50050,node-2=192.0.2.10:50050
CONTRACTLESS_RPC_TIMEOUT=10 CONTRACTLESS_RPC_TIMEOUT=10
# An endpoint enters a temporary cooldown after this many consecutive # An endpoint enters a temporary cooldown after this many consecutive

View File

@ -132,6 +132,62 @@ try {
), ),
]); ]);
}); });
$router->post(
'/api/v1/addresses/register',
static function () use ($application, $requestCredentials): never {
$body = Request::json();
$address = Request::canonicalAddress(
Request::bodyString($body, 'address', 45),
);
$publicKeyHex = strtolower(
Request::bodyString($body, 'public_key', 1_794),
);
$signature = Request::signature(
Request::bodyString($body, 'signature', 1_332),
);
if (
strlen($publicKeyHex) !== 1_794
|| !ctype_xdigit($publicKeyHex)
) {
throw new HttpException(
422,
'The public key must contain 1,794 hexadecimal characters.',
);
}
if (!hash_equals($requestCredentials->address, $address)) {
throw new HttpException(
403,
'A wallet may only submit its own registration.',
);
}
$publicKey = hex2bin($publicKeyHex);
if (
$publicKey === false
|| !hash_equals(
$requestCredentials->handshakeProof->publicKey,
$publicKey,
)
) {
throw new HttpException(
403,
'The registration public key does not match the authenticated wallet.',
);
}
JsonResponse::success([
'address' => $address,
'registered' => RpcReplyDecoder::registrationStatus(
$application->client->registerWallet(
$address,
$publicKey,
$signature,
),
),
]);
},
);
$router->get('/api/v1/balances/base', static function () use ($application): never { $router->get('/api/v1/balances/base', static function () use ($application): never {
$address = Request::canonicalAddress(Request::queryString('address', 45)); $address = Request::canonicalAddress(Request::queryString('address', 45));
$coin = strtoupper(Request::queryString('coin', 15)); $coin = strtoupper(Request::queryString('coin', 15));

View File

@ -26,7 +26,10 @@ final class RateLimiter
'limit' => $this->config->messageVerificationQuota, 'limit' => $this->config->messageVerificationQuota,
]; ];
} }
if ($path === '/api/v1/transactions/broadcast') { if (
$path === '/api/v1/transactions/broadcast'
|| $path === '/api/v1/addresses/register'
) {
$limits[] = [ $limits[] = [
'scope' => 'broadcast', 'scope' => 'broadcast',
'limit' => $this->config->broadcastQuota, 'limit' => $this->config->broadcastQuota,