API Spec
Introduction
This API provides functionality for managing transferable accounts on a blockchain. It allows for creating, transferring, and managing accounts with various operations such as approving addresses and signing data. The API implements a locking mechanism for enhanced security.
Base URL
For local development, the base URL for all API endpoints is:
http://localhost:8080/v1
Authentication
Authentication is handled through timed signatures. Each request that modifies the state requires a valid timed signature.
Timed Signature Authentication Mechanism
The verifyTimedSignature
function implements a time-limited signature verification process:
-
Expiration Check: Ensures the signature hasn't expired by comparing the current block timestamp with the
validFor
parameter. -
Message Hash Verification: Recalculates the message hash using
validFor
andsigner
, then compares it with the providedmessageHash
to ensure message integrity. -
Signature Verification:
- Prepends a Mycel-specific prefix to the message hash, inspired by ERC-191 standards. This approach involves adding "\x19Mycel Signed Message:\n32" before the original message hash and then hashing the result:
bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Mycel Signed Message:\n32", messageHash));
- Splits the signature into its components (r, s, v).
- Uses
ecrecover
to recover the signer's address from the signature. - Compares the recovered address with the provided
signer
address.
- Prepends a Mycel-specific prefix to the message hash, inspired by ERC-191 standards. This approach involves adding "\x19Mycel Signed Message:\n32" before the original message hash and then hashing the result:
This process ensures that the signature is valid, hasn't expired, and was indeed created by the claimed signer. The Mycel-specific prefix helps prevent signed messages from being executed as transactions on Mycel.
Generating Timed Signatures for Authentication
To create a signature that can be verified by the verifyTimedSignature
function, the client-side process involves the following steps:
-
Create the Message Hash:
- Combine the
validFor
timestamp and the signer's address. - Hash this combined data using keccak256.
- Combine the
-
Apply Mycel-Specific Prefix:
- Prepend "\x19Mycel Signed Message:\n32" to the message hash.
- Hash the result again with keccak256 to create the final message to be signed.
-
Generate the Signature:
- Use the signer's private key to sign the prefixed and hashed message.
Here's a Go example to implement this process:
package main
import (
"crypto/ecdsa"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
func generateTimedSignature(validFor int64, privateKey *ecdsa.PrivateKey) (address common.Address, messageHash [32]byte, signature []byte, err error) {
address = crypto.PubkeyToAddress(privateKey.PublicKey)
// Step 1: Create the message hash
// Combine validFor timestamp and signer's address, then hash with Keccak256
messageHash = crypto.Keccak256Hash(
common.LeftPadBytes(big.NewInt(validFor).Bytes(), 8),
common.LeftPadBytes(address.Bytes(), 20),
)
// Step 2: Apply Mycel-specific prefix
// Prepend "\x19Mycel Signed Message:\n32" and hash again
prefixedMessage := fmt.Sprintf("\x19Mycel Signed Message:\n32%s", messageHash)
prefixedMessageHash := crypto.Keccak256Hash([]byte(prefixedMessage))
// Step 3: Generate the signature
// Sign the prefixed message hash with the private key
signature, err = crypto.Sign(prefixedMessageHash.Bytes(), privateKey)
if err != nil {
return common.Address{}, [32]byte{}, nil, err
}
// Adjust the v value of the signature (add 27)
// This ensures compatibility with Mycel's signature standard
signature[64] += 27
return address, messageHash, signature, nil
}
For a complete implementation of these scripts, including additional helper functions and error handling, please refer to the full version available in the Astraeus repository:
https://github.com/mycel-labs/astraeus/blob/main/scripts/e2e/main.go
Endpoints
Create Account
Creates a new transferable account. The account is created in a locked state by default.
- URL:
/accounts
- Method:
POST
- Request Body:
{
"proof": {
"valid_for": 1234567890,
"message_hash": "0x1234...",
"signature": "0xabcd...",
"signer": "0x5678..."
}
} - Response:
{
"tx_hash": "0xdef...",
"account_id": "0x9876..."
}
Example:
curl -X POST http://localhost:8080/v1/accounts \
-H "Content-Type: application/json" \
-d '{"proof":{"valid_for":1234567890,"message_hash":"0x1234...","signature":"0xabcd...","signer":"0x5678..."}}'
Transfer Account
Transfers an account to a new owner. This operation can only be performed when the account is locked.
- URL:
/accounts/{account_id}/transfer
- Method:
POST
- Request Body:
{
"base": {
"account_id": "0x9876...",
"proof": {
"valid_for": 1234567890,
"message_hash": "0x1234...",
"signature": "0xabcd...",
"signer": "0x5678..."
}
},
"to": "0xffff..."
} - Response:
{
"tx_hash": "0xdef..."
}
Example:
curl -X POST http://localhost:8080/v1/accounts/0x9876.../transfer \
-H "Content-Type: application/json" \
-d '{"base":{"account_id":"0x9876...","proof":{"valid_for":1234567890,"message_hash":"0x1234...","signature":"0xabcd...","signer":"0x5678..."}},"to":"0xffff..."}'
Delete Account
Deletes an existing account. This operation can be performed regardless of the account's lock status.
- URL:
/accounts/{account_id}/delete
- Method:
POST
- Request Body:
{
"base": {
"account_id": "0x9876...",
"proof": {
"valid_for": 1234567890,
"message_hash": "0x1234...",
"signature": "0xabcd...",
"signer": "0x5678..."
}
}
} - Response:
{
"tx_hash": "0xdef..."
}
Example:
curl -X POST http://localhost:8080/v1/accounts/0x9876... \
-H "Content-Type: application/json" \
-d '{"base":{"account_id":"0x9876...","proof":{"valid_for":1234567890,"message_hash":"0x1234...","signature":"0xabcd...","signer":"0x5678..."}}}'
Unlock Account
Unlocks a locked account. This operation can only be performed on a locked account.
- URL:
/accounts/{account_id}/unlock
- Method:
POST
- Request Body:
{
"base": {
"account_id": "0x9876...",
"proof": {
"valid_for": 1234567890,
"message_hash": "0x1234...",
"signature": "0xabcd...",
"signer": "0x5678..."
}
}
} - Response:
{
"tx_hash": "0xdef..."
}
Example:
curl -X POST http://localhost:8080/v1/accounts/0x9876.../unlock \
-H "Content-Type: application/json" \
-d '{"base":{"account_id":"0x9876...","proof":{"valid_for":1234567890,"message_hash":"0x1234...","signature":"0xabcd...","signer":"0x5678..."}}}'
Approve Address
Approves an address for the account. This operation can only be performed when the account is locked.
- URL:
/accounts/{account_id}/approve
- Method:
POST
- Request Body:
{
"base": {
"account_id": "0x9876...",
"proof": {
"valid_for": 1234567890,
"message_hash": "0x1234...",
"signature": "0xabcd...",
"signer": "0x5678..."
}
},
"address": "0xaaaa..."
} - Response:
{
"tx_hash": "0xdef..."
}
Example:
curl -X POST http://localhost:8080/v1/accounts/0x9876.../approve \
-H "Content-Type: application/json" \
-d '{"base":{"account_id":"0x9876...","proof":{"valid_for":1234567890,"message_hash":"0x1234...","signature":"0xabcd...","signer":"0x5678..."}},"address":"0xaaaa..."}'
Revoke Approval
Revokes approval for an address. This operation can only be performed when the account is locked.
- URL:
/accounts/{account_id}/revoke
- Method:
POST
- Request Body:
{
"base": {
"account_id": "0x9876...",
"proof": {
"valid_for": 1234567890,
"message_hash": "0x1234...",
"signature": "0xabcd...",
"signer": "0x5678..."
}
},
"address": "0xaaaa..."
} - Response:
{
"tx_hash": "0xdef..."
}
Example:
curl -X POST http://localhost:8080/v1/accounts/0x9876.../revoke \
-H "Content-Type: application/json" \
-d '{"base":{"account_id":"0x9876...","proof":{"valid_for":1234567890,"message_hash":"0x1234...","signature":"0xabcd...","signer":"0x5678..."}},"address":"0xaaaa..."}'
Sign
Signs data using the account's private key. This operation can only be performed when the account is unlocked.
- URL:
/accounts/{account_id}/sign
- Method:
POST
- Request Body:
{
"base": {
"account_id": "0x9876...",
"proof": {
"valid_for": 1234567890,
"message_hash": "0x1234...",
"signature": "0xabcd...",
"signer": "0x5678..."
}
},
"data": "0xffff..."
} - Response:
{
"tx_hash": "0xdef...",
"signature": "0xeeee..."
}
Example:
curl -X POST http://localhost:8080/v1/accounts/0x9876.../sign \
-H "Content-Type: application/json" \
-d '{"base":{"account_id":"0x9876...","proof":{"valid_for":1234567890,"message_hash":"0x1234...","signature":"0xabcd...","signer":"0x5678..."}},"data":"0xffff..."}'
Get Account
Retrieves account information. This operation can be performed regardless of the account's lock status.
- URL:
/accounts/{account_id}
- Method:
GET
- Response:
{
"account": {
"account_id": "0x9876...",
"owner": "0x5678...",
"public_key_x": "0x1111...",
"public_key_y": "0x2222...",
"curve": 1,
"is_locked": false
}
}
Example:
curl -X GET http://localhost:8080/v1/accounts/0x9876...
Get IsApproved
Checks if an address is approved for the account. This operation can be performed regardless of the account's lock status.
- URL:
/accounts/{account_id}/approved/{address}
- Method:
GET
- Response:
{
"result": true
}
Example:
curl -X GET http://localhost:8080/v1/accounts/0x9876.../approved/0xaaaa...
Get IsOwner
Checks if an address is the owner of the account. This operation can be performed regardless of the account's lock status.
- URL:
/accounts/{account_id}/owner/{address}
- Method:
GET
- Response:
{
"result": true
}
Example:
curl -X GET http://localhost:8080/v1/accounts/0x9876.../owner/0x5678...
Get IsAccountLocked
Checks if the account is locked.
- URL:
/accounts/{account_id}/locked
- Method:
GET
- Response:
{
"result": false
}
Example:
curl -X GET http://localhost:8080/v1/accounts/0x9876.../locked
Data Structures
TimedSignature
valid_for
: uint64 (Unix timestamp)message_hash
: string (hex encoded)signature
: string (hex encoded)signer
: string (address)
Account
account_id
: stringowner
: string (address)public_key_x
: string (hex encoded)public_key_y
: string (hex encoded)curve
: enum (1 = ECDSA, 2 = EDDSA)is_locked
: boolean
Account Locking Mechanism
The TransferableAccountStore implements a locking mechanism for enhanced security. Here's how it affects various operations:
- Account Creation: Accounts are created in a locked state by default.
- Locked State Operations:
- Transfer Account
- Approve Address
- Revoke Approval These operations can only be performed when the account is locked.
- Unlocked State Operations:
- Sign This operation can only be performed when the account is unlocked.
- State-Independent Operations:
- Delete Account
- Get Account
- Get IsApproved
- Get IsOwner
- Get IsAccountLocked These operations can be performed regardless of the account's lock status.
- Unlocking: Only the account owner can unlock the account.
This locking mechanism adds an extra layer of security, ensuring that sensitive operations like transfers and approvals can only be performed in a locked state, while signing operations require the account to be unlocked.
Error Handling
This API uses gRPC status codes to indicate the success or failure of a request. These status codes are mapped to HTTP status codes in the gRPC-Gateway. In case of an error, the response body will contain a JSON object with error details.
Example error response:
{
"code": 2,
"message": "error occurred during transaction execution: transaction failed: execution reverted",
"details": []
}
Common gRPC status codes and their HTTP equivalents:
- OK (0): 200 OK
- CANCELLED (1): 499 Client Closed Request
- UNKNOWN (2): 500 Internal Server Error
- INVALID_ARGUMENT (3): 400 Bad Request
- DEADLINE_EXCEEDED (4): 504 Gateway Timeout
- NOT_FOUND (5): 404 Not Found
- ALREADY_EXISTS (6): 409 Conflict
- PERMISSION_DENIED (7): 403 Forbidden
- UNAUTHENTICATED (16): 401 Unauthorized
- RESOURCE_EXHAUSTED (8): 429 Too Many Requests
- FAILED_PRECONDITION (9): 400 Bad Request
- ABORTED (10): 409 Conflict
- INTERNAL (13): 500 Internal Server Error
For detailed error messages, refer to the "message" field in the error response.
When handling errors in your client application, it's recommended to check the gRPC status code rather than the HTTP status code, as this provides more precise error information.