diff --git a/sdk/c/src/contract/synor_contract.h b/sdk/c/src/contract/synor_contract.h new file mode 100644 index 0000000..450124a --- /dev/null +++ b/sdk/c/src/contract/synor_contract.h @@ -0,0 +1,383 @@ +/** + * Synor Contract SDK for C + * Smart contract deployment, interaction, and event handling. + */ + +#ifndef SYNOR_CONTRACT_H +#define SYNOR_CONTRACT_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define SYNOR_CONTRACT_VERSION "0.1.0" + +/* ==================== Error Handling ==================== */ + +typedef enum { + SYNOR_CONTRACT_OK = 0, + SYNOR_CONTRACT_ERR_NULL_PARAM = -1, + SYNOR_CONTRACT_ERR_HTTP = -2, + SYNOR_CONTRACT_ERR_JSON = -3, + SYNOR_CONTRACT_ERR_API = -4, + SYNOR_CONTRACT_ERR_CLOSED = -5, + SYNOR_CONTRACT_ERR_MEMORY = -6, + SYNOR_CONTRACT_ERR_TIMEOUT = -7 +} SynorContractError; + +typedef struct { + int status_code; + char *code; + char *message; +} SynorContractErrorInfo; + +/* ==================== Configuration ==================== */ + +typedef struct { + const char *api_key; + const char *endpoint; + int timeout_ms; + int retries; +} SynorContractConfig; + +/* ==================== Client Handle ==================== */ + +typedef struct SynorContractClient SynorContractClient; + +/* ==================== ABI Types ==================== */ + +typedef struct SynorAbiParameter { + char *name; + char *type; + bool indexed; + struct SynorAbiParameter *components; + size_t component_count; +} SynorAbiParameter; + +typedef struct { + char *type; + char *name; + SynorAbiParameter *inputs; + size_t input_count; + SynorAbiParameter *outputs; + size_t output_count; + char *state_mutability; + bool anonymous; +} SynorAbiEntry; + +/* ==================== Deployment ==================== */ + +typedef struct { + const char *bytecode; + const SynorAbiEntry *abi; + size_t abi_count; + const char **constructor_args; + size_t constructor_arg_count; + const char *value; + int64_t gas_limit; + const char *gas_price; + int64_t nonce; +} SynorDeployContractOptions; + +typedef struct { + char *contract_address; + char *transaction_hash; + char *deployer; + int64_t gas_used; + int64_t block_number; + char *block_hash; +} SynorDeploymentResult; + +/* ==================== Contract Interaction ==================== */ + +typedef struct { + const char *contract; + const char *method; + const char **args; + size_t arg_count; + const SynorAbiEntry *abi; + size_t abi_count; +} SynorCallContractOptions; + +typedef struct { + const char *contract; + const char *method; + const char **args; + size_t arg_count; + const SynorAbiEntry *abi; + size_t abi_count; + const char *value; + int64_t gas_limit; + const char *gas_price; + int64_t nonce; +} SynorSendContractOptions; + +typedef struct SynorEventLog { + char *address; + char **topics; + size_t topic_count; + char *data; + int64_t block_number; + char *transaction_hash; + int log_index; + char *block_hash; + bool removed; +} SynorEventLog; + +typedef struct { + char *transaction_hash; + int64_t block_number; + char *block_hash; + int64_t gas_used; + char *effective_gas_price; + char *status; + SynorEventLog *logs; + size_t log_count; +} SynorTransactionResult; + +/* ==================== Events ==================== */ + +typedef struct { + char *name; + char *signature; + char *args_json; /* JSON string for decoded args */ + SynorEventLog log; +} SynorDecodedEvent; + +typedef struct { + const char *contract; + const char *event; + int64_t from_block; + int64_t to_block; + const char **topics; + size_t topic_count; + const SynorAbiEntry *abi; + size_t abi_count; +} SynorEventFilter; + +/* ==================== Gas Estimation ==================== */ + +typedef struct { + int64_t gas_limit; + char *gas_price; + char *max_fee_per_gas; + char *max_priority_fee_per_gas; + char *estimated_cost; +} SynorGasEstimation; + +/* ==================== Contract Information ==================== */ + +typedef struct { + char *bytecode; + char *deployed_bytecode; + int size; + bool is_contract; +} SynorBytecodeInfo; + +typedef struct { + const char *address; + const char *source_code; + const char *compiler_version; + const char *constructor_args; + bool optimization; + int optimization_runs; + const char *license; +} SynorVerifyContractOptions; + +typedef struct { + bool verified; + char *address; + char *compiler_version; + bool optimization; + int optimization_runs; + char *license; + SynorAbiEntry *abi; + size_t abi_count; + char *source_code; +} SynorVerificationResult; + +/* ==================== Multicall ==================== */ + +typedef struct { + const char *contract; + const char *method; + const char **args; + size_t arg_count; + const SynorAbiEntry *abi; + size_t abi_count; +} SynorMulticallRequest; + +typedef struct { + bool success; + char *result_json; + char *error; +} SynorMulticallResult; + +/* ==================== Client Lifecycle ==================== */ + +SynorContractError synor_contract_client_create( + const SynorContractConfig *config, + SynorContractClient **client +); + +void synor_contract_client_close(SynorContractClient *client); + +bool synor_contract_client_is_closed(const SynorContractClient *client); + +bool synor_contract_health_check(SynorContractClient *client); + +const SynorContractErrorInfo* synor_contract_get_last_error(const SynorContractClient *client); + +/* ==================== Contract Deployment ==================== */ + +SynorContractError synor_contract_deploy( + SynorContractClient *client, + const SynorDeployContractOptions *options, + SynorDeploymentResult *result +); + +SynorContractError synor_contract_deploy_create2( + SynorContractClient *client, + const SynorDeployContractOptions *options, + const char *salt, + SynorDeploymentResult *result +); + +SynorContractError synor_contract_predict_address( + SynorContractClient *client, + const char *bytecode, + const char *salt, + const char *deployer, + char **address +); + +/* ==================== Contract Interaction ==================== */ + +SynorContractError synor_contract_call( + SynorContractClient *client, + const SynorCallContractOptions *options, + char **result_json +); + +SynorContractError synor_contract_send( + SynorContractClient *client, + const SynorSendContractOptions *options, + SynorTransactionResult *result +); + +/* ==================== Events ==================== */ + +SynorContractError synor_contract_get_events( + SynorContractClient *client, + const SynorEventFilter *filter, + SynorDecodedEvent **events, + size_t *event_count +); + +SynorContractError synor_contract_get_logs( + SynorContractClient *client, + const char *contract, + int64_t from_block, + int64_t to_block, + SynorEventLog **logs, + size_t *log_count +); + +/* ==================== ABI Utilities ==================== */ + +SynorContractError synor_contract_encode_call( + SynorContractClient *client, + const char *method, + const char **args, + size_t arg_count, + const SynorAbiEntry *abi, + size_t abi_count, + char **data +); + +SynorContractError synor_contract_decode_result( + SynorContractClient *client, + const char *data, + const char *method, + const SynorAbiEntry *abi, + size_t abi_count, + char **result_json +); + +SynorContractError synor_contract_get_selector( + SynorContractClient *client, + const char *signature, + char **selector +); + +/* ==================== Gas Estimation ==================== */ + +SynorContractError synor_contract_estimate_gas( + SynorContractClient *client, + const SynorCallContractOptions *options, + const char *value, + SynorGasEstimation *result +); + +/* ==================== Contract Information ==================== */ + +SynorContractError synor_contract_get_bytecode( + SynorContractClient *client, + const char *address, + SynorBytecodeInfo *result +); + +SynorContractError synor_contract_verify( + SynorContractClient *client, + const SynorVerifyContractOptions *options, + SynorVerificationResult *result +); + +SynorContractError synor_contract_get_verification_status( + SynorContractClient *client, + const char *address, + SynorVerificationResult *result +); + +/* ==================== Multicall ==================== */ + +SynorContractError synor_contract_multicall( + SynorContractClient *client, + const SynorMulticallRequest *requests, + size_t request_count, + SynorMulticallResult **results, + size_t *result_count +); + +/* ==================== Storage ==================== */ + +SynorContractError synor_contract_read_storage( + SynorContractClient *client, + const char *contract, + const char *slot, + int64_t block_number, + char **value +); + +/* ==================== Memory Management ==================== */ + +void synor_contract_free_string(char *str); +void synor_contract_free_deployment_result(SynorDeploymentResult *result); +void synor_contract_free_transaction_result(SynorTransactionResult *result); +void synor_contract_free_event_logs(SynorEventLog *logs, size_t count); +void synor_contract_free_decoded_events(SynorDecodedEvent *events, size_t count); +void synor_contract_free_gas_estimation(SynorGasEstimation *estimation); +void synor_contract_free_bytecode_info(SynorBytecodeInfo *info); +void synor_contract_free_verification_result(SynorVerificationResult *result); +void synor_contract_free_multicall_results(SynorMulticallResult *results, size_t count); +void synor_contract_free_abi(SynorAbiEntry *abi, size_t count); + +#ifdef __cplusplus +} +#endif + +#endif /* SYNOR_CONTRACT_H */ diff --git a/sdk/c/src/privacy/synor_privacy.h b/sdk/c/src/privacy/synor_privacy.h new file mode 100644 index 0000000..323d272 --- /dev/null +++ b/sdk/c/src/privacy/synor_privacy.h @@ -0,0 +1,310 @@ +/** + * Synor Privacy SDK for C + * Confidential transactions, ring signatures, stealth addresses, and cryptographic commitments. + */ + +#ifndef SYNOR_PRIVACY_H +#define SYNOR_PRIVACY_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define SYNOR_PRIVACY_VERSION "0.1.0" + +/* ==================== Error Handling ==================== */ + +typedef enum { + SYNOR_PRIVACY_OK = 0, + SYNOR_PRIVACY_ERR_NULL_PARAM = -1, + SYNOR_PRIVACY_ERR_HTTP = -2, + SYNOR_PRIVACY_ERR_JSON = -3, + SYNOR_PRIVACY_ERR_API = -4, + SYNOR_PRIVACY_ERR_CLOSED = -5, + SYNOR_PRIVACY_ERR_MEMORY = -6, + SYNOR_PRIVACY_ERR_TIMEOUT = -7 +} SynorPrivacyError; + +typedef struct { + int status_code; + char *code; + char *message; +} SynorPrivacyErrorInfo; + +/* ==================== Configuration ==================== */ + +typedef struct { + const char *api_key; + const char *endpoint; + int timeout_ms; + int retries; +} SynorPrivacyConfig; + +/* ==================== Client Handle ==================== */ + +typedef struct SynorPrivacyClient SynorPrivacyClient; + +/* ==================== Confidential Transactions ==================== */ + +typedef struct { + char *commitment; + char *blinding_factor; + char *value; + char *key_image; +} SynorConfidentialTxInput; + +typedef struct { + char *commitment; + char *blinding_factor; + char *value; + char *recipient_public_key; + char *range_proof; +} SynorConfidentialTxOutput; + +typedef struct { + char *id; + SynorConfidentialTxInput *inputs; + size_t input_count; + SynorConfidentialTxOutput *outputs; + size_t output_count; + char *fee; + char *excess; + char *excess_sig; + char *kernel_offset; +} SynorConfidentialTransaction; + +/* ==================== Commitments ==================== */ + +typedef struct { + char *commitment; + char *blinding_factor; +} SynorCommitment; + +typedef struct { + char *proof; + char *commitment; + int64_t min_value; + int64_t max_value; +} SynorRangeProof; + +/* ==================== Ring Signatures ==================== */ + +typedef struct { + char *c0; + char **s; + size_t s_count; + char *key_image; + char **ring; + size_t ring_count; +} SynorRingSignature; + +/* ==================== Stealth Addresses ==================== */ + +typedef struct { + char *spend_public_key; + char *spend_private_key; + char *view_public_key; + char *view_private_key; +} SynorStealthKeyPair; + +typedef struct { + char *address; + char *ephemeral_public_key; + char *tx_public_key; +} SynorStealthAddress; + +typedef struct { + char *tx_hash; + int output_index; + char *stealth_address; + char *amount; + int64_t block_height; +} SynorStealthOutput; + +/* ==================== Client Lifecycle ==================== */ + +/** + * Create a new Privacy client. + * @param config Configuration options + * @param client Output pointer to client handle + * @return Error code + */ +SynorPrivacyError synor_privacy_client_create( + const SynorPrivacyConfig *config, + SynorPrivacyClient **client +); + +/** + * Close and free the client. + */ +void synor_privacy_client_close(SynorPrivacyClient *client); + +/** + * Check if the client is closed. + */ +bool synor_privacy_client_is_closed(const SynorPrivacyClient *client); + +/** + * Check service health. + */ +bool synor_privacy_health_check(SynorPrivacyClient *client); + +/** + * Get the last error info. + */ +const SynorPrivacyErrorInfo* synor_privacy_get_last_error(const SynorPrivacyClient *client); + +/* ==================== Confidential Transactions ==================== */ + +SynorPrivacyError synor_privacy_create_confidential_tx( + SynorPrivacyClient *client, + const SynorConfidentialTxInput *inputs, + size_t input_count, + const SynorConfidentialTxOutput *outputs, + size_t output_count, + SynorConfidentialTransaction *result +); + +SynorPrivacyError synor_privacy_verify_confidential_tx( + SynorPrivacyClient *client, + const SynorConfidentialTransaction *tx, + bool *valid +); + +SynorPrivacyError synor_privacy_create_commitment( + SynorPrivacyClient *client, + const char *value, + const char *blinding_factor, + SynorCommitment *result +); + +SynorPrivacyError synor_privacy_verify_commitment( + SynorPrivacyClient *client, + const char *commitment, + const char *value, + const char *blinding_factor, + bool *valid +); + +SynorPrivacyError synor_privacy_create_range_proof( + SynorPrivacyClient *client, + const char *value, + const char *blinding_factor, + int64_t min_value, + int64_t max_value, + SynorRangeProof *result +); + +SynorPrivacyError synor_privacy_verify_range_proof( + SynorPrivacyClient *client, + const SynorRangeProof *proof, + bool *valid +); + +/* ==================== Ring Signatures ==================== */ + +SynorPrivacyError synor_privacy_create_ring_signature( + SynorPrivacyClient *client, + const char *message, + const char **ring, + size_t ring_count, + int signer_index, + const char *private_key, + SynorRingSignature *result +); + +SynorPrivacyError synor_privacy_verify_ring_signature( + SynorPrivacyClient *client, + const SynorRingSignature *signature, + const char *message, + bool *valid +); + +SynorPrivacyError synor_privacy_generate_decoys( + SynorPrivacyClient *client, + int count, + const char *exclude_key, + char ***decoys, + size_t *decoy_count +); + +SynorPrivacyError synor_privacy_check_key_image( + SynorPrivacyClient *client, + const char *key_image, + bool *spent +); + +/* ==================== Stealth Addresses ==================== */ + +SynorPrivacyError synor_privacy_generate_stealth_keypair( + SynorPrivacyClient *client, + SynorStealthKeyPair *result +); + +SynorPrivacyError synor_privacy_derive_stealth_address( + SynorPrivacyClient *client, + const char *spend_public_key, + const char *view_public_key, + SynorStealthAddress *result +); + +SynorPrivacyError synor_privacy_recover_stealth_private_key( + SynorPrivacyClient *client, + const char *stealth_address, + const char *view_private_key, + const char *spend_private_key, + char **private_key +); + +SynorPrivacyError synor_privacy_scan_outputs( + SynorPrivacyClient *client, + const char *view_private_key, + const char *spend_public_key, + int64_t from_block, + int64_t to_block, + SynorStealthOutput **outputs, + size_t *output_count +); + +/* ==================== Blinding ==================== */ + +SynorPrivacyError synor_privacy_generate_blinding_factor( + SynorPrivacyClient *client, + char **blinding_factor +); + +SynorPrivacyError synor_privacy_blind_value( + SynorPrivacyClient *client, + const char *value, + const char *blinding_factor, + char **blinded_value +); + +SynorPrivacyError synor_privacy_unblind_value( + SynorPrivacyClient *client, + const char *blinded_value, + const char *blinding_factor, + char **value +); + +/* ==================== Memory Management ==================== */ + +void synor_privacy_free_string(char *str); +void synor_privacy_free_string_array(char **arr, size_t count); +void synor_privacy_free_confidential_tx(SynorConfidentialTransaction *tx); +void synor_privacy_free_ring_signature(SynorRingSignature *sig); +void synor_privacy_free_stealth_keypair(SynorStealthKeyPair *keypair); +void synor_privacy_free_stealth_address(SynorStealthAddress *addr); +void synor_privacy_free_stealth_outputs(SynorStealthOutput *outputs, size_t count); +void synor_privacy_free_commitment(SynorCommitment *commitment); +void synor_privacy_free_range_proof(SynorRangeProof *proof); + +#ifdef __cplusplus +} +#endif + +#endif /* SYNOR_PRIVACY_H */ diff --git a/sdk/cpp/include/synor/contract.hpp b/sdk/cpp/include/synor/contract.hpp new file mode 100644 index 0000000..a591ed9 --- /dev/null +++ b/sdk/cpp/include/synor/contract.hpp @@ -0,0 +1,297 @@ +/** + * Synor Contract SDK for C++ + * Smart contract deployment, interaction, and event handling. + */ + +#ifndef SYNOR_CONTRACT_HPP +#define SYNOR_CONTRACT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace synor::contract { + +inline constexpr const char* VERSION = "0.1.0"; + +// ==================== Exceptions ==================== + +class ContractException : public std::runtime_error { +public: + ContractException(const std::string& message, + std::optional status_code = std::nullopt, + std::optional code = std::nullopt) + : std::runtime_error(message), status_code_(status_code), code_(code) {} + + std::optional status_code() const { return status_code_; } + std::optional code() const { return code_; } + +private: + std::optional status_code_; + std::optional code_; +}; + +// ==================== Configuration ==================== + +struct ContractConfig { + std::string api_key; + std::string endpoint = "https://contract.synor.io"; + int timeout_ms = 30000; + int retries = 3; +}; + +// ==================== ABI Types ==================== + +struct AbiParameter { + std::optional name; + std::string type; + std::optional indexed; + std::optional> components; +}; + +struct AbiEntry { + std::string type; + std::optional name; + std::optional> inputs; + std::optional> outputs; + std::optional state_mutability; + std::optional anonymous; +}; + +using Abi = std::vector; + +// ==================== Deployment ==================== + +struct DeployContractOptions { + std::string bytecode; + std::optional abi; + std::optional> constructor_args; + std::optional value; + std::optional gas_limit; + std::optional gas_price; + std::optional nonce; +}; + +struct DeploymentResult { + std::string contract_address; + std::string transaction_hash; + std::optional deployer; + std::optional gas_used; + std::optional block_number; + std::optional block_hash; +}; + +// ==================== Contract Interaction ==================== + +struct CallContractOptions { + std::string contract; + std::string method; + std::vector args; + Abi abi; +}; + +struct SendContractOptions { + std::string contract; + std::string method; + std::vector args; + Abi abi; + std::optional value; + std::optional gas_limit; + std::optional gas_price; + std::optional nonce; +}; + +struct EventLog { + std::string address; + std::vector topics; + std::string data; + std::optional block_number; + std::optional transaction_hash; + std::optional log_index; + std::optional block_hash; + std::optional removed; +}; + +struct TransactionResult { + std::string transaction_hash; + std::optional block_number; + std::optional block_hash; + std::optional gas_used; + std::optional effective_gas_price; + std::optional status; + std::optional> logs; +}; + +// ==================== Events ==================== + +struct DecodedEvent { + std::string name; + std::optional signature; + std::optional> args; + std::optional log; +}; + +struct EventFilter { + std::string contract; + std::optional event; + std::optional from_block; + std::optional to_block; + std::optional>> topics; + std::optional abi; +}; + +// ==================== ABI Utilities ==================== + +struct EncodeCallOptions { + std::string method; + std::vector args; + Abi abi; +}; + +struct DecodeResultOptions { + std::string data; + std::string method; + Abi abi; +}; + +// ==================== Gas Estimation ==================== + +struct EstimateGasOptions { + std::string contract; + std::string method; + std::vector args; + Abi abi; + std::optional value; +}; + +struct GasEstimation { + int64_t gas_limit; + std::optional gas_price; + std::optional max_fee_per_gas; + std::optional max_priority_fee_per_gas; + std::optional estimated_cost; +}; + +// ==================== Contract Information ==================== + +struct BytecodeInfo { + std::string bytecode; + std::optional deployed_bytecode; + std::optional size; + std::optional is_contract; +}; + +struct VerifyContractOptions { + std::string address; + std::string source_code; + std::string compiler_version; + std::optional constructor_args; + std::optional optimization; + std::optional optimization_runs; + std::optional license; +}; + +struct VerificationResult { + bool verified; + std::optional address; + std::optional compiler_version; + std::optional optimization; + std::optional optimization_runs; + std::optional license; + std::optional abi; + std::optional source_code; +}; + +// ==================== Multicall ==================== + +struct MulticallRequest { + std::string contract; + std::string method; + std::vector args; + Abi abi; +}; + +struct MulticallResult { + bool success; + std::optional result; + std::optional error; +}; + +// ==================== Storage ==================== + +struct ReadStorageOptions { + std::string contract; + std::string slot; + std::optional block_number; +}; + +// ==================== Client ==================== + +class ContractClient { +public: + explicit ContractClient(const ContractConfig& config); + ~ContractClient(); + + // Non-copyable + ContractClient(const ContractClient&) = delete; + ContractClient& operator=(const ContractClient&) = delete; + + // Movable + ContractClient(ContractClient&&) noexcept; + ContractClient& operator=(ContractClient&&) noexcept; + + // Deployment + std::future deploy(const DeployContractOptions& options); + std::future deploy_create2(const DeployContractOptions& options, const std::string& salt); + std::future predict_address(const std::string& bytecode, const std::string& salt, + const std::optional& deployer = std::nullopt); + + // Contract Interaction + std::future call(const CallContractOptions& options); + std::future send(const SendContractOptions& options); + + // Events + std::future> get_events(const EventFilter& filter); + std::future> get_logs(const std::string& contract, + std::optional from_block = std::nullopt, + std::optional to_block = std::nullopt); + std::future> decode_logs(const std::vector& logs, const Abi& abi); + + // ABI Utilities + std::future encode_call(const EncodeCallOptions& options); + std::future decode_result(const DecodeResultOptions& options); + std::future get_selector(const std::string& signature); + + // Gas Estimation + std::future estimate_gas(const EstimateGasOptions& options); + + // Contract Information + std::future get_bytecode(const std::string& address); + std::future verify(const VerifyContractOptions& options); + std::future get_verification_status(const std::string& address); + + // Multicall + std::future> multicall(const std::vector& requests); + + // Storage + std::future read_storage(const ReadStorageOptions& options); + + // Lifecycle + std::future health_check(); + void close(); + bool is_closed() const; + +private: + class Impl; + std::unique_ptr impl_; +}; + +} // namespace synor::contract + +#endif // SYNOR_CONTRACT_HPP diff --git a/sdk/cpp/include/synor/privacy.hpp b/sdk/cpp/include/synor/privacy.hpp new file mode 100644 index 0000000..b2d7c98 --- /dev/null +++ b/sdk/cpp/include/synor/privacy.hpp @@ -0,0 +1,229 @@ +/** + * Synor Privacy SDK for C++ + * Confidential transactions, ring signatures, stealth addresses, and cryptographic commitments. + */ + +#ifndef SYNOR_PRIVACY_HPP +#define SYNOR_PRIVACY_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace synor::privacy { + +inline constexpr const char* VERSION = "0.1.0"; + +// ==================== Exceptions ==================== + +class PrivacyException : public std::runtime_error { +public: + PrivacyException(const std::string& message, + std::optional status_code = std::nullopt, + std::optional code = std::nullopt) + : std::runtime_error(message), status_code_(status_code), code_(code) {} + + std::optional status_code() const { return status_code_; } + std::optional code() const { return code_; } + +private: + std::optional status_code_; + std::optional code_; +}; + +// ==================== Configuration ==================== + +struct PrivacyConfig { + std::string api_key; + std::string endpoint = "https://privacy.synor.io"; + int timeout_ms = 30000; + int retries = 3; +}; + +// ==================== Confidential Transactions ==================== + +struct ConfidentialTxInput { + std::string commitment; + std::string blinding_factor; + std::string value; + std::optional key_image; +}; + +struct ConfidentialTxOutput { + std::string commitment; + std::string blinding_factor; + std::string value; + std::string recipient_public_key; + std::optional range_proof; +}; + +struct ConfidentialTransaction { + std::string id; + std::vector inputs; + std::vector outputs; + std::string fee; + std::string excess; + std::string excess_sig; + std::optional kernel_offset; +}; + +// ==================== Commitments ==================== + +struct Commitment { + std::string commitment; + std::string blinding_factor; +}; + +struct RangeProof { + std::string proof; + std::string commitment; + int64_t min_value; + int64_t max_value; +}; + +// ==================== Ring Signatures ==================== + +struct RingSignature { + std::string c0; + std::vector s; + std::string key_image; + std::vector ring; +}; + +// ==================== Stealth Addresses ==================== + +struct StealthKeyPair { + std::string spend_public_key; + std::string spend_private_key; + std::string view_public_key; + std::string view_private_key; +}; + +struct StealthAddress { + std::string address; + std::string ephemeral_public_key; + std::optional tx_public_key; +}; + +struct StealthOutput { + std::string tx_hash; + int output_index; + std::string stealth_address; + std::string amount; + int64_t block_height; +}; + +// ==================== Client ==================== + +class PrivacyClient { +public: + explicit PrivacyClient(const PrivacyConfig& config); + ~PrivacyClient(); + + // Non-copyable + PrivacyClient(const PrivacyClient&) = delete; + PrivacyClient& operator=(const PrivacyClient&) = delete; + + // Movable + PrivacyClient(PrivacyClient&&) noexcept; + PrivacyClient& operator=(PrivacyClient&&) noexcept; + + // Confidential Transactions + std::future create_confidential_tx( + const std::vector& inputs, + const std::vector& outputs + ); + + std::future verify_confidential_tx(const ConfidentialTransaction& tx); + + std::future create_commitment( + const std::string& value, + const std::string& blinding_factor + ); + + std::future verify_commitment( + const std::string& commitment, + const std::string& value, + const std::string& blinding_factor + ); + + std::future create_range_proof( + const std::string& value, + const std::string& blinding_factor, + int64_t min_value, + int64_t max_value + ); + + std::future verify_range_proof(const RangeProof& proof); + + // Ring Signatures + std::future create_ring_signature( + const std::string& message, + const std::vector& ring, + int signer_index, + const std::string& private_key + ); + + std::future verify_ring_signature( + const RingSignature& signature, + const std::string& message + ); + + std::future> generate_decoys( + int count, + const std::optional& exclude_key = std::nullopt + ); + + std::future check_key_image(const std::string& key_image); + + // Stealth Addresses + std::future generate_stealth_keypair(); + + std::future derive_stealth_address( + const std::string& spend_public_key, + const std::string& view_public_key + ); + + std::future recover_stealth_private_key( + const std::string& stealth_address, + const std::string& view_private_key, + const std::string& spend_private_key + ); + + std::future> scan_outputs( + const std::string& view_private_key, + const std::string& spend_public_key, + int64_t from_block, + std::optional to_block = std::nullopt + ); + + // Blinding + std::future generate_blinding_factor(); + + std::future blind_value( + const std::string& value, + const std::string& blinding_factor + ); + + std::future unblind_value( + const std::string& blinded_value, + const std::string& blinding_factor + ); + + // Lifecycle + std::future health_check(); + void close(); + bool is_closed() const; + +private: + class Impl; + std::unique_ptr impl_; +}; + +} // namespace synor::privacy + +#endif // SYNOR_PRIVACY_HPP diff --git a/sdk/csharp/src/Synor.Contract/ContractClient.cs b/sdk/csharp/src/Synor.Contract/ContractClient.cs new file mode 100644 index 0000000..610bc4f --- /dev/null +++ b/sdk/csharp/src/Synor.Contract/ContractClient.cs @@ -0,0 +1,307 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +namespace Synor.Contract +{ + /// + /// Synor Contract SDK client for C#/.NET. + /// Smart contract deployment, interaction, and event handling. + /// + public sealed class ContractClient : IDisposable + { + public const string Version = "0.1.0"; + + private readonly ContractConfig _config; + private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonOptions; + private bool _closed; + + public ContractClient(ContractConfig config) + { + _config = config ?? throw new ArgumentNullException(nameof(config)); + _httpClient = new HttpClient + { + BaseAddress = new Uri(config.Endpoint), + Timeout = TimeSpan.FromMilliseconds(config.TimeoutMs) + }; + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}"); + _httpClient.DefaultRequestHeaders.Add("X-SDK-Version", $"csharp/{Version}"); + + _jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + } + + #region Contract Deployment + + public async Task DeployAsync(DeployContractOptions options, CancellationToken ct = default) + { + return await PostAsync("/contract/deploy", options, ct); + } + + public async Task DeployCreate2Async(DeployContractOptions options, string salt, CancellationToken ct = default) + { + var body = new Dictionary + { + ["bytecode"] = options.Bytecode, + ["salt"] = salt, + ["abi"] = options.Abi, + ["constructor_args"] = options.ConstructorArgs, + ["value"] = options.Value, + ["gas_limit"] = options.GasLimit, + ["gas_price"] = options.GasPrice + }; + return await PostAsync("/contract/deploy/create2", body, ct); + } + + public async Task PredictAddressAsync(string bytecode, string salt, string? deployer = null, CancellationToken ct = default) + { + var body = new { bytecode, salt, deployer }; + var response = await PostAsync>("/contract/predict-address", body, ct); + return response["address"]?.ToString() ?? throw new ContractException("Missing address"); + } + + #endregion + + #region Contract Interaction + + public async Task CallAsync(CallContractOptions options, CancellationToken ct = default) + { + return await PostAsync("/contract/call", options, ct); + } + + public async Task SendAsync(SendContractOptions options, CancellationToken ct = default) + { + return await PostAsync("/contract/send", options, ct); + } + + #endregion + + #region Events + + public async Task GetEventsAsync(EventFilter filter, CancellationToken ct = default) + { + return await PostAsync("/contract/events", filter, ct); + } + + public async Task GetLogsAsync(string contract, long? fromBlock = null, long? toBlock = null, CancellationToken ct = default) + { + var path = $"/contract/logs?contract={HttpUtility.UrlEncode(contract)}"; + if (fromBlock.HasValue) path += $"&from_block={fromBlock}"; + if (toBlock.HasValue) path += $"&to_block={toBlock}"; + return await GetAsync(path, ct); + } + + public async Task DecodeLogsAsync(IEnumerable logs, IEnumerable abi, CancellationToken ct = default) + { + var body = new { logs, abi }; + return await PostAsync("/contract/decode-logs", body, ct); + } + + #endregion + + #region ABI Utilities + + public async Task EncodeCallAsync(EncodeCallOptions options, CancellationToken ct = default) + { + var response = await PostAsync>("/contract/encode", options, ct); + return response["data"]?.ToString() ?? throw new ContractException("Missing data"); + } + + public async Task DecodeResultAsync(DecodeResultOptions options, CancellationToken ct = default) + { + var response = await PostAsync>("/contract/decode", options, ct); + return response["result"]; + } + + public async Task GetSelectorAsync(string signature, CancellationToken ct = default) + { + var response = await GetAsync>($"/contract/selector?signature={HttpUtility.UrlEncode(signature)}", ct); + return response["selector"]?.ToString() ?? throw new ContractException("Missing selector"); + } + + #endregion + + #region Gas Estimation + + public async Task EstimateGasAsync(EstimateGasOptions options, CancellationToken ct = default) + { + return await PostAsync("/contract/estimate-gas", options, ct); + } + + #endregion + + #region Contract Information + + public async Task GetBytecodeAsync(string address, CancellationToken ct = default) + { + return await GetAsync($"/contract/{HttpUtility.UrlEncode(address)}/bytecode", ct); + } + + public async Task VerifyAsync(VerifyContractOptions options, CancellationToken ct = default) + { + return await PostAsync("/contract/verify", options, ct); + } + + public async Task GetVerificationStatusAsync(string address, CancellationToken ct = default) + { + return await GetAsync($"/contract/{HttpUtility.UrlEncode(address)}/verification", ct); + } + + #endregion + + #region Multicall + + public async Task MulticallAsync(IEnumerable requests, CancellationToken ct = default) + { + var body = new { calls = requests }; + return await PostAsync("/contract/multicall", body, ct); + } + + #endregion + + #region Storage + + public async Task ReadStorageAsync(ReadStorageOptions options, CancellationToken ct = default) + { + var path = $"/contract/storage?contract={HttpUtility.UrlEncode(options.Contract)}&slot={HttpUtility.UrlEncode(options.Slot)}"; + if (options.BlockNumber.HasValue) path += $"&block={options.BlockNumber}"; + var response = await GetAsync>(path, ct); + return response["value"]?.ToString() ?? throw new ContractException("Missing value"); + } + + #endregion + + #region Lifecycle + + public async Task HealthCheckAsync(CancellationToken ct = default) + { + if (_closed) return false; + try + { + var response = await GetAsync>("/health", ct); + return response.TryGetValue("status", out var status) && + status is JsonElement elem && elem.GetString() == "healthy"; + } + catch + { + return false; + } + } + + public void Dispose() + { + _closed = true; + _httpClient.Dispose(); + } + + public bool IsClosed => _closed; + + #endregion + + #region HTTP Helpers + + private async Task GetAsync(string path, CancellationToken ct) + { + ThrowIfClosed(); + return await ExecuteWithRetryAsync(async () => + { + var response = await _httpClient.GetAsync(path, ct); + return await HandleResponseAsync(response, ct); + }); + } + + private async Task PostAsync(string path, object body, CancellationToken ct) + { + ThrowIfClosed(); + return await ExecuteWithRetryAsync(async () => + { + var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct); + return await HandleResponseAsync(response, ct); + }); + } + + private async Task HandleResponseAsync(HttpResponseMessage response, CancellationToken ct) + { + var content = await response.Content.ReadAsStringAsync(ct); + + if (response.IsSuccessStatusCode) + { + return JsonSerializer.Deserialize(content, _jsonOptions) + ?? throw new ContractException("Failed to deserialize response"); + } + + try + { + var error = JsonSerializer.Deserialize>(content, _jsonOptions); + var message = error?.GetValueOrDefault("message")?.ToString() + ?? error?.GetValueOrDefault("error")?.ToString() + ?? "Unknown error"; + var code = error?.GetValueOrDefault("code")?.ToString(); + throw new ContractException(message, (int)response.StatusCode, code); + } + catch (JsonException) + { + throw new ContractException(content, (int)response.StatusCode); + } + } + + private async Task ExecuteWithRetryAsync(Func> action) + { + Exception? lastException = null; + for (var attempt = 0; attempt < _config.Retries; attempt++) + { + try + { + return await action(); + } + catch (Exception ex) + { + lastException = ex; + if (attempt < _config.Retries - 1) + { + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); + } + } + } + throw lastException ?? new ContractException("Request failed"); + } + + private void ThrowIfClosed() + { + if (_closed) throw new ContractException("Client has been closed"); + } + + #endregion + } + + public class ContractConfig + { + public required string ApiKey { get; init; } + public string Endpoint { get; init; } = "https://contract.synor.io"; + public int TimeoutMs { get; init; } = 30000; + public int Retries { get; init; } = 3; + } + + public class ContractException : Exception + { + public int? StatusCode { get; } + public string? Code { get; } + + public ContractException(string message, int? statusCode = null, string? code = null) + : base(message) + { + StatusCode = statusCode; + Code = code; + } + } +} diff --git a/sdk/csharp/src/Synor.Contract/ContractTypes.cs b/sdk/csharp/src/Synor.Contract/ContractTypes.cs new file mode 100644 index 0000000..333def8 --- /dev/null +++ b/sdk/csharp/src/Synor.Contract/ContractTypes.cs @@ -0,0 +1,394 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Synor.Contract +{ + // ==================== ABI Types ==================== + + public record AbiParameter + { + [JsonPropertyName("name")] + public string? Name { get; init; } + + [JsonPropertyName("type")] + public required string Type { get; init; } + + [JsonPropertyName("indexed")] + public bool? Indexed { get; init; } + + [JsonPropertyName("components")] + public AbiParameter[]? Components { get; init; } + } + + public record AbiEntry + { + [JsonPropertyName("type")] + public required string Type { get; init; } + + [JsonPropertyName("name")] + public string? Name { get; init; } + + [JsonPropertyName("inputs")] + public AbiParameter[]? Inputs { get; init; } + + [JsonPropertyName("outputs")] + public AbiParameter[]? Outputs { get; init; } + + [JsonPropertyName("stateMutability")] + public string? StateMutability { get; init; } + + [JsonPropertyName("anonymous")] + public bool? Anonymous { get; init; } + } + + // ==================== Deployment ==================== + + public record DeployContractOptions + { + [JsonPropertyName("bytecode")] + public required string Bytecode { get; init; } + + [JsonPropertyName("abi")] + public AbiEntry[]? Abi { get; init; } + + [JsonPropertyName("constructor_args")] + public object[]? ConstructorArgs { get; init; } + + [JsonPropertyName("value")] + public string? Value { get; init; } + + [JsonPropertyName("gas_limit")] + public long? GasLimit { get; init; } + + [JsonPropertyName("gas_price")] + public string? GasPrice { get; init; } + + [JsonPropertyName("nonce")] + public long? Nonce { get; init; } + } + + public record DeploymentResult + { + [JsonPropertyName("contract_address")] + public required string ContractAddress { get; init; } + + [JsonPropertyName("transaction_hash")] + public required string TransactionHash { get; init; } + + [JsonPropertyName("deployer")] + public string? Deployer { get; init; } + + [JsonPropertyName("gas_used")] + public long? GasUsed { get; init; } + + [JsonPropertyName("block_number")] + public long? BlockNumber { get; init; } + + [JsonPropertyName("block_hash")] + public string? BlockHash { get; init; } + } + + // ==================== Contract Interaction ==================== + + public record CallContractOptions + { + [JsonPropertyName("contract")] + public required string Contract { get; init; } + + [JsonPropertyName("method")] + public required string Method { get; init; } + + [JsonPropertyName("args")] + public object[] Args { get; init; } = []; + + [JsonPropertyName("abi")] + public required AbiEntry[] Abi { get; init; } + } + + public record SendContractOptions + { + [JsonPropertyName("contract")] + public required string Contract { get; init; } + + [JsonPropertyName("method")] + public required string Method { get; init; } + + [JsonPropertyName("args")] + public object[] Args { get; init; } = []; + + [JsonPropertyName("abi")] + public required AbiEntry[] Abi { get; init; } + + [JsonPropertyName("value")] + public string? Value { get; init; } + + [JsonPropertyName("gas_limit")] + public long? GasLimit { get; init; } + + [JsonPropertyName("gas_price")] + public string? GasPrice { get; init; } + + [JsonPropertyName("nonce")] + public long? Nonce { get; init; } + } + + public record EventLog + { + [JsonPropertyName("address")] + public required string Address { get; init; } + + [JsonPropertyName("topics")] + public required string[] Topics { get; init; } + + [JsonPropertyName("data")] + public required string Data { get; init; } + + [JsonPropertyName("block_number")] + public long? BlockNumber { get; init; } + + [JsonPropertyName("transaction_hash")] + public string? TransactionHash { get; init; } + + [JsonPropertyName("log_index")] + public int? LogIndex { get; init; } + + [JsonPropertyName("block_hash")] + public string? BlockHash { get; init; } + + [JsonPropertyName("removed")] + public bool? Removed { get; init; } + } + + public record TransactionResult + { + [JsonPropertyName("transaction_hash")] + public required string TransactionHash { get; init; } + + [JsonPropertyName("block_number")] + public long? BlockNumber { get; init; } + + [JsonPropertyName("block_hash")] + public string? BlockHash { get; init; } + + [JsonPropertyName("gas_used")] + public long? GasUsed { get; init; } + + [JsonPropertyName("effective_gas_price")] + public string? EffectiveGasPrice { get; init; } + + [JsonPropertyName("status")] + public string? Status { get; init; } + + [JsonPropertyName("logs")] + public EventLog[]? Logs { get; init; } + } + + // ==================== Events ==================== + + public record DecodedEvent + { + [JsonPropertyName("name")] + public required string Name { get; init; } + + [JsonPropertyName("signature")] + public string? Signature { get; init; } + + [JsonPropertyName("args")] + public Dictionary? Args { get; init; } + + [JsonPropertyName("log")] + public EventLog? Log { get; init; } + } + + public record EventFilter + { + [JsonPropertyName("contract")] + public required string Contract { get; init; } + + [JsonPropertyName("event")] + public string? Event { get; init; } + + [JsonPropertyName("from_block")] + public long? FromBlock { get; init; } + + [JsonPropertyName("to_block")] + public long? ToBlock { get; init; } + + [JsonPropertyName("topics")] + public string?[]? Topics { get; init; } + + [JsonPropertyName("abi")] + public AbiEntry[]? Abi { get; init; } + } + + // ==================== ABI Utilities ==================== + + public record EncodeCallOptions + { + [JsonPropertyName("method")] + public required string Method { get; init; } + + [JsonPropertyName("args")] + public object[] Args { get; init; } = []; + + [JsonPropertyName("abi")] + public required AbiEntry[] Abi { get; init; } + } + + public record DecodeResultOptions + { + [JsonPropertyName("data")] + public required string Data { get; init; } + + [JsonPropertyName("method")] + public required string Method { get; init; } + + [JsonPropertyName("abi")] + public required AbiEntry[] Abi { get; init; } + } + + // ==================== Gas Estimation ==================== + + public record EstimateGasOptions + { + [JsonPropertyName("contract")] + public required string Contract { get; init; } + + [JsonPropertyName("method")] + public required string Method { get; init; } + + [JsonPropertyName("args")] + public object[] Args { get; init; } = []; + + [JsonPropertyName("abi")] + public required AbiEntry[] Abi { get; init; } + + [JsonPropertyName("value")] + public string? Value { get; init; } + } + + public record GasEstimation + { + [JsonPropertyName("gas_limit")] + public long GasLimit { get; init; } + + [JsonPropertyName("gas_price")] + public string? GasPrice { get; init; } + + [JsonPropertyName("max_fee_per_gas")] + public string? MaxFeePerGas { get; init; } + + [JsonPropertyName("max_priority_fee_per_gas")] + public string? MaxPriorityFeePerGas { get; init; } + + [JsonPropertyName("estimated_cost")] + public string? EstimatedCost { get; init; } + } + + // ==================== Contract Information ==================== + + public record BytecodeInfo + { + [JsonPropertyName("bytecode")] + public required string Bytecode { get; init; } + + [JsonPropertyName("deployed_bytecode")] + public string? DeployedBytecode { get; init; } + + [JsonPropertyName("size")] + public int? Size { get; init; } + + [JsonPropertyName("is_contract")] + public bool? IsContract { get; init; } + } + + public record VerifyContractOptions + { + [JsonPropertyName("address")] + public required string Address { get; init; } + + [JsonPropertyName("source_code")] + public required string SourceCode { get; init; } + + [JsonPropertyName("compiler_version")] + public required string CompilerVersion { get; init; } + + [JsonPropertyName("constructor_args")] + public string? ConstructorArgs { get; init; } + + [JsonPropertyName("optimization")] + public bool? Optimization { get; init; } + + [JsonPropertyName("optimization_runs")] + public int? OptimizationRuns { get; init; } + + [JsonPropertyName("license")] + public string? License { get; init; } + } + + public record VerificationResult + { + [JsonPropertyName("verified")] + public bool Verified { get; init; } + + [JsonPropertyName("address")] + public string? Address { get; init; } + + [JsonPropertyName("compiler_version")] + public string? CompilerVersion { get; init; } + + [JsonPropertyName("optimization")] + public bool? Optimization { get; init; } + + [JsonPropertyName("optimization_runs")] + public int? OptimizationRuns { get; init; } + + [JsonPropertyName("license")] + public string? License { get; init; } + + [JsonPropertyName("abi")] + public AbiEntry[]? Abi { get; init; } + + [JsonPropertyName("source_code")] + public string? SourceCode { get; init; } + } + + // ==================== Multicall ==================== + + public record MulticallRequest + { + [JsonPropertyName("contract")] + public required string Contract { get; init; } + + [JsonPropertyName("method")] + public required string Method { get; init; } + + [JsonPropertyName("args")] + public object[] Args { get; init; } = []; + + [JsonPropertyName("abi")] + public required AbiEntry[] Abi { get; init; } + } + + public record MulticallResult + { + [JsonPropertyName("success")] + public bool Success { get; init; } + + [JsonPropertyName("result")] + public JsonElement? Result { get; init; } + + [JsonPropertyName("error")] + public string? Error { get; init; } + } + + // ==================== Storage ==================== + + public record ReadStorageOptions + { + public required string Contract { get; init; } + public required string Slot { get; init; } + public long? BlockNumber { get; init; } + } +} diff --git a/sdk/csharp/src/Synor.Privacy/PrivacyClient.cs b/sdk/csharp/src/Synor.Privacy/PrivacyClient.cs new file mode 100644 index 0000000..18a9c35 --- /dev/null +++ b/sdk/csharp/src/Synor.Privacy/PrivacyClient.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + +namespace Synor.Privacy +{ + /// + /// Synor Privacy SDK client for C#/.NET. + /// Confidential transactions, ring signatures, stealth addresses, and cryptographic commitments. + /// + public sealed class PrivacyClient : IDisposable + { + public const string Version = "0.1.0"; + + private readonly PrivacyConfig _config; + private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonOptions; + private bool _closed; + + public PrivacyClient(PrivacyConfig config) + { + _config = config ?? throw new ArgumentNullException(nameof(config)); + _httpClient = new HttpClient + { + BaseAddress = new Uri(config.Endpoint), + Timeout = TimeSpan.FromMilliseconds(config.TimeoutMs) + }; + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}"); + _httpClient.DefaultRequestHeaders.Add("X-SDK-Version", $"csharp/{Version}"); + + _jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + } + + #region Confidential Transactions + + public async Task CreateConfidentialTxAsync( + IEnumerable inputs, + IEnumerable outputs, + CancellationToken ct = default) + { + var body = new { inputs, outputs }; + return await PostAsync("/privacy/confidential/create", body, ct); + } + + public async Task VerifyConfidentialTxAsync(ConfidentialTransaction tx, CancellationToken ct = default) + { + var body = new { transaction = tx }; + var response = await PostAsync>("/privacy/confidential/verify", body, ct); + return response.TryGetValue("valid", out var valid) && valid is JsonElement elem && elem.GetBoolean(); + } + + public async Task CreateCommitmentAsync( + string value, + string blindingFactor, + CancellationToken ct = default) + { + var body = new { value, blinding_factor = blindingFactor }; + return await PostAsync("/privacy/commitment/create", body, ct); + } + + public async Task VerifyCommitmentAsync( + string commitment, + string value, + string blindingFactor, + CancellationToken ct = default) + { + var body = new { commitment, value, blinding_factor = blindingFactor }; + var response = await PostAsync>("/privacy/commitment/verify", body, ct); + return response.TryGetValue("valid", out var valid) && valid is JsonElement elem && elem.GetBoolean(); + } + + public async Task CreateRangeProofAsync( + string value, + string blindingFactor, + long minValue, + long maxValue, + CancellationToken ct = default) + { + var body = new { value, blinding_factor = blindingFactor, min_value = minValue, max_value = maxValue }; + return await PostAsync("/privacy/range-proof/create", body, ct); + } + + public async Task VerifyRangeProofAsync(RangeProof proof, CancellationToken ct = default) + { + var body = new { proof }; + var response = await PostAsync>("/privacy/range-proof/verify", body, ct); + return response.TryGetValue("valid", out var valid) && valid is JsonElement elem && elem.GetBoolean(); + } + + #endregion + + #region Ring Signatures + + public async Task CreateRingSignatureAsync( + string message, + IEnumerable ring, + int signerIndex, + string privateKey, + CancellationToken ct = default) + { + var body = new { message, ring, signer_index = signerIndex, private_key = privateKey }; + return await PostAsync("/privacy/ring/sign", body, ct); + } + + public async Task VerifyRingSignatureAsync(RingSignature signature, string message, CancellationToken ct = default) + { + var body = new { signature, message }; + var response = await PostAsync>("/privacy/ring/verify", body, ct); + return response.TryGetValue("valid", out var valid) && valid is JsonElement elem && elem.GetBoolean(); + } + + public async Task GenerateDecoysAsync(int count, string? excludeKey = null, CancellationToken ct = default) + { + var path = $"/privacy/ring/decoys?count={count}"; + if (!string.IsNullOrEmpty(excludeKey)) + path += $"&exclude={Uri.EscapeDataString(excludeKey)}"; + return await GetAsync(path, ct); + } + + public async Task CheckKeyImageAsync(string keyImage, CancellationToken ct = default) + { + var response = await GetAsync>($"/privacy/ring/key-image/{keyImage}", ct); + return response.TryGetValue("spent", out var spent) && spent is JsonElement elem && elem.GetBoolean(); + } + + #endregion + + #region Stealth Addresses + + public async Task GenerateStealthKeyPairAsync(CancellationToken ct = default) + { + return await PostAsync("/privacy/stealth/generate", new { }, ct); + } + + public async Task DeriveStealthAddressAsync( + string spendPublicKey, + string viewPublicKey, + CancellationToken ct = default) + { + var body = new { spend_public_key = spendPublicKey, view_public_key = viewPublicKey }; + return await PostAsync("/privacy/stealth/derive", body, ct); + } + + public async Task RecoverStealthPrivateKeyAsync( + string stealthAddress, + string viewPrivateKey, + string spendPrivateKey, + CancellationToken ct = default) + { + var body = new + { + stealth_address = stealthAddress, + view_private_key = viewPrivateKey, + spend_private_key = spendPrivateKey + }; + var response = await PostAsync>("/privacy/stealth/recover", body, ct); + return response["private_key"]?.ToString() ?? throw new PrivacyException("Missing private_key"); + } + + public async Task ScanOutputsAsync( + string viewPrivateKey, + string spendPublicKey, + long fromBlock, + long? toBlock = null, + CancellationToken ct = default) + { + var body = new Dictionary + { + ["view_private_key"] = viewPrivateKey, + ["spend_public_key"] = spendPublicKey, + ["from_block"] = fromBlock + }; + if (toBlock.HasValue) body["to_block"] = toBlock.Value; + return await PostAsync("/privacy/stealth/scan", body, ct); + } + + #endregion + + #region Blinding + + public async Task GenerateBlindingFactorAsync(CancellationToken ct = default) + { + var response = await PostAsync>("/privacy/blinding/generate", new { }, ct); + return response["blinding_factor"]?.ToString() ?? throw new PrivacyException("Missing blinding_factor"); + } + + public async Task BlindValueAsync(string value, string blindingFactor, CancellationToken ct = default) + { + var body = new { value, blinding_factor = blindingFactor }; + var response = await PostAsync>("/privacy/blinding/blind", body, ct); + return response["blinded_value"]?.ToString() ?? throw new PrivacyException("Missing blinded_value"); + } + + public async Task UnblindValueAsync(string blindedValue, string blindingFactor, CancellationToken ct = default) + { + var body = new { blinded_value = blindedValue, blinding_factor = blindingFactor }; + var response = await PostAsync>("/privacy/blinding/unblind", body, ct); + return response["value"]?.ToString() ?? throw new PrivacyException("Missing value"); + } + + #endregion + + #region Lifecycle + + public async Task HealthCheckAsync(CancellationToken ct = default) + { + if (_closed) return false; + try + { + var response = await GetAsync>("/health", ct); + return response.TryGetValue("status", out var status) && + status is JsonElement elem && elem.GetString() == "healthy"; + } + catch + { + return false; + } + } + + public void Dispose() + { + _closed = true; + _httpClient.Dispose(); + } + + public bool IsClosed => _closed; + + #endregion + + #region HTTP Helpers + + private async Task GetAsync(string path, CancellationToken ct) + { + ThrowIfClosed(); + return await ExecuteWithRetryAsync(async () => + { + var response = await _httpClient.GetAsync(path, ct); + return await HandleResponseAsync(response, ct); + }); + } + + private async Task PostAsync(string path, object body, CancellationToken ct) + { + ThrowIfClosed(); + return await ExecuteWithRetryAsync(async () => + { + var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct); + return await HandleResponseAsync(response, ct); + }); + } + + private async Task HandleResponseAsync(HttpResponseMessage response, CancellationToken ct) + { + var content = await response.Content.ReadAsStringAsync(ct); + + if (response.IsSuccessStatusCode) + { + return JsonSerializer.Deserialize(content, _jsonOptions) + ?? throw new PrivacyException("Failed to deserialize response"); + } + + try + { + var error = JsonSerializer.Deserialize>(content, _jsonOptions); + var message = error?.GetValueOrDefault("message")?.ToString() + ?? error?.GetValueOrDefault("error")?.ToString() + ?? "Unknown error"; + var code = error?.GetValueOrDefault("code")?.ToString(); + throw new PrivacyException(message, (int)response.StatusCode, code); + } + catch (JsonException) + { + throw new PrivacyException(content, (int)response.StatusCode); + } + } + + private async Task ExecuteWithRetryAsync(Func> action) + { + Exception? lastException = null; + for (var attempt = 0; attempt < _config.Retries; attempt++) + { + try + { + return await action(); + } + catch (Exception ex) + { + lastException = ex; + if (attempt < _config.Retries - 1) + { + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); + } + } + } + throw lastException ?? new PrivacyException("Request failed"); + } + + private void ThrowIfClosed() + { + if (_closed) throw new PrivacyException("Client has been closed"); + } + + #endregion + } + + public class PrivacyConfig + { + public required string ApiKey { get; init; } + public string Endpoint { get; init; } = "https://privacy.synor.io"; + public int TimeoutMs { get; init; } = 30000; + public int Retries { get; init; } = 3; + } + + public class PrivacyException : Exception + { + public int? StatusCode { get; } + public string? Code { get; } + + public PrivacyException(string message, int? statusCode = null, string? code = null) + : base(message) + { + StatusCode = statusCode; + Code = code; + } + } +} diff --git a/sdk/csharp/src/Synor.Privacy/PrivacyTypes.cs b/sdk/csharp/src/Synor.Privacy/PrivacyTypes.cs new file mode 100644 index 0000000..b8171b3 --- /dev/null +++ b/sdk/csharp/src/Synor.Privacy/PrivacyTypes.cs @@ -0,0 +1,153 @@ +using System.Text.Json.Serialization; + +namespace Synor.Privacy +{ + // ==================== Confidential Transactions ==================== + + public record ConfidentialTxInput + { + [JsonPropertyName("commitment")] + public required string Commitment { get; init; } + + [JsonPropertyName("blinding_factor")] + public required string BlindingFactor { get; init; } + + [JsonPropertyName("value")] + public required string Value { get; init; } + + [JsonPropertyName("key_image")] + public string? KeyImage { get; init; } + } + + public record ConfidentialTxOutput + { + [JsonPropertyName("commitment")] + public required string Commitment { get; init; } + + [JsonPropertyName("blinding_factor")] + public required string BlindingFactor { get; init; } + + [JsonPropertyName("value")] + public required string Value { get; init; } + + [JsonPropertyName("recipient_public_key")] + public required string RecipientPublicKey { get; init; } + + [JsonPropertyName("range_proof")] + public string? RangeProof { get; init; } + } + + public record ConfidentialTransaction + { + [JsonPropertyName("id")] + public required string Id { get; init; } + + [JsonPropertyName("inputs")] + public required ConfidentialTxInput[] Inputs { get; init; } + + [JsonPropertyName("outputs")] + public required ConfidentialTxOutput[] Outputs { get; init; } + + [JsonPropertyName("fee")] + public required string Fee { get; init; } + + [JsonPropertyName("excess")] + public required string Excess { get; init; } + + [JsonPropertyName("excess_sig")] + public required string ExcessSig { get; init; } + + [JsonPropertyName("kernel_offset")] + public string? KernelOffset { get; init; } + } + + // ==================== Commitments ==================== + + public record Commitment + { + [JsonPropertyName("commitment")] + public required string CommitmentValue { get; init; } + + [JsonPropertyName("blinding_factor")] + public required string BlindingFactor { get; init; } + } + + public record RangeProof + { + [JsonPropertyName("proof")] + public required string Proof { get; init; } + + [JsonPropertyName("commitment")] + public required string Commitment { get; init; } + + [JsonPropertyName("min_value")] + public long MinValue { get; init; } + + [JsonPropertyName("max_value")] + public long MaxValue { get; init; } + } + + // ==================== Ring Signatures ==================== + + public record RingSignature + { + [JsonPropertyName("c0")] + public required string C0 { get; init; } + + [JsonPropertyName("s")] + public required string[] S { get; init; } + + [JsonPropertyName("key_image")] + public required string KeyImage { get; init; } + + [JsonPropertyName("ring")] + public required string[] Ring { get; init; } + } + + // ==================== Stealth Addresses ==================== + + public record StealthKeyPair + { + [JsonPropertyName("spend_public_key")] + public required string SpendPublicKey { get; init; } + + [JsonPropertyName("spend_private_key")] + public required string SpendPrivateKey { get; init; } + + [JsonPropertyName("view_public_key")] + public required string ViewPublicKey { get; init; } + + [JsonPropertyName("view_private_key")] + public required string ViewPrivateKey { get; init; } + } + + public record StealthAddress + { + [JsonPropertyName("address")] + public required string Address { get; init; } + + [JsonPropertyName("ephemeral_public_key")] + public required string EphemeralPublicKey { get; init; } + + [JsonPropertyName("tx_public_key")] + public string? TxPublicKey { get; init; } + } + + public record StealthOutput + { + [JsonPropertyName("tx_hash")] + public required string TxHash { get; init; } + + [JsonPropertyName("output_index")] + public int OutputIndex { get; init; } + + [JsonPropertyName("stealth_address")] + public required string StealthAddress { get; init; } + + [JsonPropertyName("amount")] + public required string Amount { get; init; } + + [JsonPropertyName("block_height")] + public long BlockHeight { get; init; } + } +} diff --git a/sdk/flutter/lib/src/contract/contract_client.dart b/sdk/flutter/lib/src/contract/contract_client.dart new file mode 100644 index 0000000..4fdeeec --- /dev/null +++ b/sdk/flutter/lib/src/contract/contract_client.dart @@ -0,0 +1,320 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'contract_types.dart'; + +/// Synor Contract SDK client for Flutter/Dart. +/// Smart contract deployment, interaction, and event handling. +class ContractClient { + static const String version = '0.1.0'; + + final ContractConfig config; + final http.Client _client; + bool _closed = false; + + ContractClient(this.config) : _client = http.Client(); + + // ==================== Contract Deployment ==================== + + Future deploy(DeployContractOptions options) async { + final body = { + 'bytecode': options.bytecode, + if (options.abi != null) 'abi': options.abi!.map((e) => e.toJson()).toList(), + if (options.constructorArgs != null) 'constructor_args': options.constructorArgs, + if (options.value != null) 'value': options.value, + if (options.gasLimit != null) 'gas_limit': options.gasLimit, + if (options.gasPrice != null) 'gas_price': options.gasPrice, + if (options.nonce != null) 'nonce': options.nonce, + }; + final response = await _post('/contract/deploy', body); + return DeploymentResult.fromJson(response); + } + + Future deployCreate2(DeployContractOptions options, String salt) async { + final body = { + 'bytecode': options.bytecode, + 'salt': salt, + if (options.abi != null) 'abi': options.abi!.map((e) => e.toJson()).toList(), + if (options.constructorArgs != null) 'constructor_args': options.constructorArgs, + if (options.value != null) 'value': options.value, + if (options.gasLimit != null) 'gas_limit': options.gasLimit, + if (options.gasPrice != null) 'gas_price': options.gasPrice, + }; + final response = await _post('/contract/deploy/create2', body); + return DeploymentResult.fromJson(response); + } + + Future predictAddress(String bytecode, String salt, {String? deployer}) async { + final body = { + 'bytecode': bytecode, + 'salt': salt, + if (deployer != null) 'deployer': deployer, + }; + final response = await _post('/contract/predict-address', body); + return response['address'] as String; + } + + // ==================== Contract Interaction ==================== + + Future call(CallContractOptions options) async { + final body = { + 'contract': options.contract, + 'method': options.method, + 'args': options.args, + 'abi': options.abi.map((e) => e.toJson()).toList(), + }; + return await _post('/contract/call', body); + } + + Future send(SendContractOptions options) async { + final body = { + 'contract': options.contract, + 'method': options.method, + 'args': options.args, + 'abi': options.abi.map((e) => e.toJson()).toList(), + if (options.value != null) 'value': options.value, + if (options.gasLimit != null) 'gas_limit': options.gasLimit, + if (options.gasPrice != null) 'gas_price': options.gasPrice, + if (options.nonce != null) 'nonce': options.nonce, + }; + final response = await _post('/contract/send', body); + return TransactionResult.fromJson(response); + } + + // ==================== Events ==================== + + Future> getEvents(EventFilter filter) async { + final body = { + 'contract': filter.contract, + if (filter.event != null) 'event': filter.event, + if (filter.fromBlock != null) 'from_block': filter.fromBlock, + if (filter.toBlock != null) 'to_block': filter.toBlock, + if (filter.topics != null) 'topics': filter.topics, + if (filter.abi != null) 'abi': filter.abi!.map((e) => e.toJson()).toList(), + }; + final response = await _post('/contract/events', body); + return (response as List).map((e) => DecodedEvent.fromJson(e)).toList(); + } + + Future> getLogs(String contract, {int? fromBlock, int? toBlock}) async { + var path = '/contract/logs?contract=${Uri.encodeComponent(contract)}'; + if (fromBlock != null) path += '&from_block=$fromBlock'; + if (toBlock != null) path += '&to_block=$toBlock'; + final response = await _get(path); + return (response as List).map((e) => EventLog.fromJson(e)).toList(); + } + + Future> decodeLogs(List logs, List abi) async { + final body = { + 'logs': logs.map((e) => e.toJson()).toList(), + 'abi': abi.map((e) => e.toJson()).toList(), + }; + final response = await _post('/contract/decode-logs', body); + return (response as List).map((e) => DecodedEvent.fromJson(e)).toList(); + } + + // ==================== ABI Utilities ==================== + + Future encodeCall(EncodeCallOptions options) async { + final body = { + 'method': options.method, + 'args': options.args, + 'abi': options.abi.map((e) => e.toJson()).toList(), + }; + final response = await _post('/contract/encode', body); + return response['data'] as String; + } + + Future decodeResult(DecodeResultOptions options) async { + final body = { + 'data': options.data, + 'method': options.method, + 'abi': options.abi.map((e) => e.toJson()).toList(), + }; + final response = await _post('/contract/decode', body); + return response['result']; + } + + Future getSelector(String signature) async { + final response = await _get('/contract/selector?signature=${Uri.encodeComponent(signature)}'); + return response['selector'] as String; + } + + // ==================== Gas Estimation ==================== + + Future estimateGas(EstimateGasOptions options) async { + final body = { + 'contract': options.contract, + 'method': options.method, + 'args': options.args, + 'abi': options.abi.map((e) => e.toJson()).toList(), + if (options.value != null) 'value': options.value, + }; + final response = await _post('/contract/estimate-gas', body); + return GasEstimation.fromJson(response); + } + + // ==================== Contract Information ==================== + + Future getBytecode(String address) async { + final response = await _get('/contract/${Uri.encodeComponent(address)}/bytecode'); + return BytecodeInfo.fromJson(response); + } + + Future verify(VerifyContractOptions options) async { + final body = { + 'address': options.address, + 'source_code': options.sourceCode, + 'compiler_version': options.compilerVersion, + if (options.constructorArgs != null) 'constructor_args': options.constructorArgs, + if (options.optimization != null) 'optimization': options.optimization, + if (options.optimizationRuns != null) 'optimization_runs': options.optimizationRuns, + if (options.license != null) 'license': options.license, + }; + final response = await _post('/contract/verify', body); + return VerificationResult.fromJson(response); + } + + Future getVerificationStatus(String address) async { + final response = await _get('/contract/${Uri.encodeComponent(address)}/verification'); + return VerificationResult.fromJson(response); + } + + // ==================== Multicall ==================== + + Future> multicall(List requests) async { + final body = { + 'calls': requests.map((e) => e.toJson()).toList(), + }; + final response = await _post('/contract/multicall', body); + return (response as List).map((e) => MulticallResult.fromJson(e)).toList(); + } + + // ==================== Storage ==================== + + Future readStorage(ReadStorageOptions options) async { + var path = '/contract/storage?contract=${Uri.encodeComponent(options.contract)}' + '&slot=${Uri.encodeComponent(options.slot)}'; + if (options.blockNumber != null) path += '&block=${options.blockNumber}'; + final response = await _get(path); + return response['value'] as String; + } + + // ==================== Lifecycle ==================== + + Future healthCheck() async { + if (_closed) return false; + try { + final response = await _get('/health'); + return response['status'] == 'healthy'; + } catch (_) { + return false; + } + } + + void close() { + _closed = true; + _client.close(); + } + + bool get isClosed => _closed; + + // ==================== HTTP Helpers ==================== + + Future _get(String path) async { + _checkClosed(); + return _executeWithRetry(() async { + final response = await _client + .get( + Uri.parse('${config.endpoint}$path'), + headers: _headers, + ) + .timeout(Duration(milliseconds: config.timeoutMs)); + return _handleResponse(response); + }); + } + + Future> _post(String path, Map body) async { + _checkClosed(); + return _executeWithRetry(() async { + final response = await _client + .post( + Uri.parse('${config.endpoint}$path'), + headers: _headers, + body: jsonEncode(body), + ) + .timeout(Duration(milliseconds: config.timeoutMs)); + return _handleResponse(response); + }); + } + + Map get _headers => { + 'Authorization': 'Bearer ${config.apiKey}', + 'Content-Type': 'application/json', + 'X-SDK-Version': 'dart/$version', + }; + + dynamic _handleResponse(http.Response response) { + final body = jsonDecode(response.body); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return body; + } + + final message = body['message'] ?? body['error'] ?? 'Unknown error'; + final code = body['code']?.toString(); + throw ContractException( + message: message, + statusCode: response.statusCode, + code: code, + ); + } + + Future _executeWithRetry(Future Function() action) async { + Object? lastError; + for (var attempt = 0; attempt < config.retries; attempt++) { + try { + return await action(); + } catch (e) { + lastError = e; + if (attempt < config.retries - 1) { + await Future.delayed(Duration(seconds: 1 << attempt)); + } + } + } + throw lastError ?? ContractException(message: 'Request failed'); + } + + void _checkClosed() { + if (_closed) { + throw ContractException(message: 'Client has been closed'); + } + } +} + +/// Contract SDK configuration. +class ContractConfig { + final String apiKey; + final String endpoint; + final int timeoutMs; + final int retries; + + ContractConfig({ + required this.apiKey, + this.endpoint = 'https://contract.synor.io', + this.timeoutMs = 30000, + this.retries = 3, + }); +} + +/// Contract SDK exception. +class ContractException implements Exception { + final String message; + final int? statusCode; + final String? code; + + ContractException({required this.message, this.statusCode, this.code}); + + @override + String toString() => 'ContractException: $message (code: $code, status: $statusCode)'; +} diff --git a/sdk/flutter/lib/src/contract/contract_types.dart b/sdk/flutter/lib/src/contract/contract_types.dart new file mode 100644 index 0000000..15666a2 --- /dev/null +++ b/sdk/flutter/lib/src/contract/contract_types.dart @@ -0,0 +1,496 @@ +// ==================== ABI Types ==================== + +class AbiParameter { + final String? name; + final String type; + final bool? indexed; + final List? components; + + AbiParameter({ + this.name, + required this.type, + this.indexed, + this.components, + }); + + factory AbiParameter.fromJson(Map json) { + return AbiParameter( + name: json['name'] as String?, + type: json['type'] as String, + indexed: json['indexed'] as bool?, + components: json['components'] != null + ? (json['components'] as List).map((e) => AbiParameter.fromJson(e)).toList() + : null, + ); + } + + Map toJson() => { + if (name != null) 'name': name, + 'type': type, + if (indexed != null) 'indexed': indexed, + if (components != null) 'components': components!.map((e) => e.toJson()).toList(), + }; +} + +class AbiEntry { + final String type; + final String? name; + final List? inputs; + final List? outputs; + final String? stateMutability; + final bool? anonymous; + + AbiEntry({ + required this.type, + this.name, + this.inputs, + this.outputs, + this.stateMutability, + this.anonymous, + }); + + factory AbiEntry.fromJson(Map json) { + return AbiEntry( + type: json['type'] as String, + name: json['name'] as String?, + inputs: json['inputs'] != null + ? (json['inputs'] as List).map((e) => AbiParameter.fromJson(e)).toList() + : null, + outputs: json['outputs'] != null + ? (json['outputs'] as List).map((e) => AbiParameter.fromJson(e)).toList() + : null, + stateMutability: json['stateMutability'] as String?, + anonymous: json['anonymous'] as bool?, + ); + } + + Map toJson() => { + 'type': type, + if (name != null) 'name': name, + if (inputs != null) 'inputs': inputs!.map((e) => e.toJson()).toList(), + if (outputs != null) 'outputs': outputs!.map((e) => e.toJson()).toList(), + if (stateMutability != null) 'stateMutability': stateMutability, + if (anonymous != null) 'anonymous': anonymous, + }; +} + +// ==================== Deployment ==================== + +class DeployContractOptions { + final String bytecode; + final List? abi; + final List? constructorArgs; + final String? value; + final int? gasLimit; + final String? gasPrice; + final int? nonce; + + DeployContractOptions({ + required this.bytecode, + this.abi, + this.constructorArgs, + this.value, + this.gasLimit, + this.gasPrice, + this.nonce, + }); +} + +class DeploymentResult { + final String contractAddress; + final String transactionHash; + final String? deployer; + final int? gasUsed; + final int? blockNumber; + final String? blockHash; + + DeploymentResult({ + required this.contractAddress, + required this.transactionHash, + this.deployer, + this.gasUsed, + this.blockNumber, + this.blockHash, + }); + + factory DeploymentResult.fromJson(Map json) { + return DeploymentResult( + contractAddress: json['contract_address'] as String, + transactionHash: json['transaction_hash'] as String, + deployer: json['deployer'] as String?, + gasUsed: json['gas_used'] as int?, + blockNumber: json['block_number'] as int?, + blockHash: json['block_hash'] as String?, + ); + } +} + +// ==================== Contract Interaction ==================== + +class CallContractOptions { + final String contract; + final String method; + final List args; + final List abi; + + CallContractOptions({ + required this.contract, + required this.method, + this.args = const [], + required this.abi, + }); +} + +class SendContractOptions { + final String contract; + final String method; + final List args; + final List abi; + final String? value; + final int? gasLimit; + final String? gasPrice; + final int? nonce; + + SendContractOptions({ + required this.contract, + required this.method, + this.args = const [], + required this.abi, + this.value, + this.gasLimit, + this.gasPrice, + this.nonce, + }); +} + +class TransactionResult { + final String transactionHash; + final int? blockNumber; + final String? blockHash; + final int? gasUsed; + final String? effectiveGasPrice; + final String? status; + final List? logs; + + TransactionResult({ + required this.transactionHash, + this.blockNumber, + this.blockHash, + this.gasUsed, + this.effectiveGasPrice, + this.status, + this.logs, + }); + + factory TransactionResult.fromJson(Map json) { + return TransactionResult( + transactionHash: json['transaction_hash'] as String, + blockNumber: json['block_number'] as int?, + blockHash: json['block_hash'] as String?, + gasUsed: json['gas_used'] as int?, + effectiveGasPrice: json['effective_gas_price'] as String?, + status: json['status'] as String?, + logs: json['logs'] != null + ? (json['logs'] as List).map((e) => EventLog.fromJson(e)).toList() + : null, + ); + } +} + +// ==================== Events ==================== + +class EventLog { + final String address; + final List topics; + final String data; + final int? blockNumber; + final String? transactionHash; + final int? logIndex; + final String? blockHash; + final bool? removed; + + EventLog({ + required this.address, + required this.topics, + required this.data, + this.blockNumber, + this.transactionHash, + this.logIndex, + this.blockHash, + this.removed, + }); + + factory EventLog.fromJson(Map json) { + return EventLog( + address: json['address'] as String, + topics: (json['topics'] as List).map((e) => e as String).toList(), + data: json['data'] as String, + blockNumber: json['block_number'] as int?, + transactionHash: json['transaction_hash'] as String?, + logIndex: json['log_index'] as int?, + blockHash: json['block_hash'] as String?, + removed: json['removed'] as bool?, + ); + } + + Map toJson() => { + 'address': address, + 'topics': topics, + 'data': data, + if (blockNumber != null) 'block_number': blockNumber, + if (transactionHash != null) 'transaction_hash': transactionHash, + if (logIndex != null) 'log_index': logIndex, + if (blockHash != null) 'block_hash': blockHash, + if (removed != null) 'removed': removed, + }; +} + +class DecodedEvent { + final String name; + final String? signature; + final Map? args; + final EventLog? log; + + DecodedEvent({ + required this.name, + this.signature, + this.args, + this.log, + }); + + factory DecodedEvent.fromJson(Map json) { + return DecodedEvent( + name: json['name'] as String, + signature: json['signature'] as String?, + args: json['args'] as Map?, + log: json['log'] != null ? EventLog.fromJson(json['log']) : null, + ); + } +} + +class EventFilter { + final String contract; + final String? event; + final int? fromBlock; + final int? toBlock; + final List? topics; + final List? abi; + + EventFilter({ + required this.contract, + this.event, + this.fromBlock, + this.toBlock, + this.topics, + this.abi, + }); +} + +// ==================== ABI Utilities ==================== + +class EncodeCallOptions { + final String method; + final List args; + final List abi; + + EncodeCallOptions({ + required this.method, + this.args = const [], + required this.abi, + }); +} + +class DecodeResultOptions { + final String data; + final String method; + final List abi; + + DecodeResultOptions({ + required this.data, + required this.method, + required this.abi, + }); +} + +// ==================== Gas Estimation ==================== + +class EstimateGasOptions { + final String contract; + final String method; + final List args; + final List abi; + final String? value; + + EstimateGasOptions({ + required this.contract, + required this.method, + this.args = const [], + required this.abi, + this.value, + }); +} + +class GasEstimation { + final int gasLimit; + final String? gasPrice; + final String? maxFeePerGas; + final String? maxPriorityFeePerGas; + final String? estimatedCost; + + GasEstimation({ + required this.gasLimit, + this.gasPrice, + this.maxFeePerGas, + this.maxPriorityFeePerGas, + this.estimatedCost, + }); + + factory GasEstimation.fromJson(Map json) { + return GasEstimation( + gasLimit: json['gas_limit'] as int, + gasPrice: json['gas_price'] as String?, + maxFeePerGas: json['max_fee_per_gas'] as String?, + maxPriorityFeePerGas: json['max_priority_fee_per_gas'] as String?, + estimatedCost: json['estimated_cost'] as String?, + ); + } +} + +// ==================== Contract Information ==================== + +class BytecodeInfo { + final String bytecode; + final String? deployedBytecode; + final int? size; + final bool? isContract; + + BytecodeInfo({ + required this.bytecode, + this.deployedBytecode, + this.size, + this.isContract, + }); + + factory BytecodeInfo.fromJson(Map json) { + return BytecodeInfo( + bytecode: json['bytecode'] as String, + deployedBytecode: json['deployed_bytecode'] as String?, + size: json['size'] as int?, + isContract: json['is_contract'] as bool?, + ); + } +} + +class VerifyContractOptions { + final String address; + final String sourceCode; + final String compilerVersion; + final String? constructorArgs; + final bool? optimization; + final int? optimizationRuns; + final String? license; + + VerifyContractOptions({ + required this.address, + required this.sourceCode, + required this.compilerVersion, + this.constructorArgs, + this.optimization, + this.optimizationRuns, + this.license, + }); +} + +class VerificationResult { + final bool verified; + final String? address; + final String? compilerVersion; + final bool? optimization; + final int? optimizationRuns; + final String? license; + final List? abi; + final String? sourceCode; + + VerificationResult({ + required this.verified, + this.address, + this.compilerVersion, + this.optimization, + this.optimizationRuns, + this.license, + this.abi, + this.sourceCode, + }); + + factory VerificationResult.fromJson(Map json) { + return VerificationResult( + verified: json['verified'] as bool, + address: json['address'] as String?, + compilerVersion: json['compiler_version'] as String?, + optimization: json['optimization'] as bool?, + optimizationRuns: json['optimization_runs'] as int?, + license: json['license'] as String?, + abi: json['abi'] != null + ? (json['abi'] as List).map((e) => AbiEntry.fromJson(e)).toList() + : null, + sourceCode: json['source_code'] as String?, + ); + } +} + +// ==================== Multicall ==================== + +class MulticallRequest { + final String contract; + final String method; + final List args; + final List abi; + + MulticallRequest({ + required this.contract, + required this.method, + this.args = const [], + required this.abi, + }); + + Map toJson() => { + 'contract': contract, + 'method': method, + 'args': args, + 'abi': abi.map((e) => e.toJson()).toList(), + }; +} + +class MulticallResult { + final bool success; + final dynamic result; + final String? error; + + MulticallResult({ + required this.success, + this.result, + this.error, + }); + + factory MulticallResult.fromJson(Map json) { + return MulticallResult( + success: json['success'] as bool, + result: json['result'], + error: json['error'] as String?, + ); + } +} + +// ==================== Storage ==================== + +class ReadStorageOptions { + final String contract; + final String slot; + final int? blockNumber; + + ReadStorageOptions({ + required this.contract, + required this.slot, + this.blockNumber, + }); +} diff --git a/sdk/flutter/lib/src/privacy/privacy_client.dart b/sdk/flutter/lib/src/privacy/privacy_client.dart new file mode 100644 index 0000000..051c0a3 --- /dev/null +++ b/sdk/flutter/lib/src/privacy/privacy_client.dart @@ -0,0 +1,317 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'privacy_types.dart'; + +/// Synor Privacy SDK client for Flutter/Dart. +/// Confidential transactions, ring signatures, stealth addresses, and cryptographic commitments. +class PrivacyClient { + static const String version = '0.1.0'; + + final PrivacyConfig config; + final http.Client _client; + bool _closed = false; + + PrivacyClient(this.config) : _client = http.Client(); + + // ==================== Confidential Transactions ==================== + + Future createConfidentialTx({ + required List inputs, + required List outputs, + }) async { + final body = { + 'inputs': inputs.map((i) => i.toJson()).toList(), + 'outputs': outputs.map((o) => o.toJson()).toList(), + }; + final response = await _post('/privacy/confidential/create', body); + return ConfidentialTransaction.fromJson(response); + } + + Future verifyConfidentialTx(ConfidentialTransaction tx) async { + final body = {'transaction': tx.toJson()}; + final response = await _post('/privacy/confidential/verify', body); + return response['valid'] as bool? ?? false; + } + + Future createCommitment({ + required String value, + required String blindingFactor, + }) async { + final body = { + 'value': value, + 'blinding_factor': blindingFactor, + }; + final response = await _post('/privacy/commitment/create', body); + return Commitment.fromJson(response); + } + + Future verifyCommitment({ + required String commitment, + required String value, + required String blindingFactor, + }) async { + final body = { + 'commitment': commitment, + 'value': value, + 'blinding_factor': blindingFactor, + }; + final response = await _post('/privacy/commitment/verify', body); + return response['valid'] as bool? ?? false; + } + + Future createRangeProof({ + required String value, + required String blindingFactor, + required int minValue, + required int maxValue, + }) async { + final body = { + 'value': value, + 'blinding_factor': blindingFactor, + 'min_value': minValue, + 'max_value': maxValue, + }; + final response = await _post('/privacy/range-proof/create', body); + return RangeProof.fromJson(response); + } + + Future verifyRangeProof(RangeProof proof) async { + final body = {'proof': proof.toJson()}; + final response = await _post('/privacy/range-proof/verify', body); + return response['valid'] as bool? ?? false; + } + + // ==================== Ring Signatures ==================== + + Future createRingSignature({ + required String message, + required List ring, + required int signerIndex, + required String privateKey, + }) async { + final body = { + 'message': message, + 'ring': ring, + 'signer_index': signerIndex, + 'private_key': privateKey, + }; + final response = await _post('/privacy/ring/sign', body); + return RingSignature.fromJson(response); + } + + Future verifyRingSignature(RingSignature signature, String message) async { + final body = { + 'signature': signature.toJson(), + 'message': message, + }; + final response = await _post('/privacy/ring/verify', body); + return response['valid'] as bool? ?? false; + } + + Future> generateDecoys(int count, {String? excludeKey}) async { + var path = '/privacy/ring/decoys?count=$count'; + if (excludeKey != null) { + path += '&exclude=${Uri.encodeComponent(excludeKey)}'; + } + final response = await _get(path); + return (response as List).map((e) => e as String).toList(); + } + + Future checkKeyImage(String keyImage) async { + final response = await _get('/privacy/ring/key-image/$keyImage'); + return response['spent'] as bool? ?? false; + } + + // ==================== Stealth Addresses ==================== + + Future generateStealthKeyPair() async { + final response = await _post('/privacy/stealth/generate', {}); + return StealthKeyPair.fromJson(response); + } + + Future deriveStealthAddress({ + required String spendPublicKey, + required String viewPublicKey, + }) async { + final body = { + 'spend_public_key': spendPublicKey, + 'view_public_key': viewPublicKey, + }; + final response = await _post('/privacy/stealth/derive', body); + return StealthAddress.fromJson(response); + } + + Future recoverStealthPrivateKey({ + required String stealthAddress, + required String viewPrivateKey, + required String spendPrivateKey, + }) async { + final body = { + 'stealth_address': stealthAddress, + 'view_private_key': viewPrivateKey, + 'spend_private_key': spendPrivateKey, + }; + final response = await _post('/privacy/stealth/recover', body); + return response['private_key'] as String; + } + + Future> scanOutputs({ + required String viewPrivateKey, + required String spendPublicKey, + required int fromBlock, + int? toBlock, + }) async { + final body = { + 'view_private_key': viewPrivateKey, + 'spend_public_key': spendPublicKey, + 'from_block': fromBlock, + if (toBlock != null) 'to_block': toBlock, + }; + final response = await _post('/privacy/stealth/scan', body); + return (response as List).map((e) => StealthOutput.fromJson(e)).toList(); + } + + // ==================== Blinding ==================== + + Future generateBlindingFactor() async { + final response = await _post('/privacy/blinding/generate', {}); + return response['blinding_factor'] as String; + } + + Future blindValue(String value, String blindingFactor) async { + final body = { + 'value': value, + 'blinding_factor': blindingFactor, + }; + final response = await _post('/privacy/blinding/blind', body); + return response['blinded_value'] as String; + } + + Future unblindValue(String blindedValue, String blindingFactor) async { + final body = { + 'blinded_value': blindedValue, + 'blinding_factor': blindingFactor, + }; + final response = await _post('/privacy/blinding/unblind', body); + return response['value'] as String; + } + + // ==================== Lifecycle ==================== + + Future healthCheck() async { + if (_closed) return false; + try { + final response = await _get('/health'); + return response['status'] == 'healthy'; + } catch (_) { + return false; + } + } + + void close() { + _closed = true; + _client.close(); + } + + bool get isClosed => _closed; + + // ==================== HTTP Helpers ==================== + + Future _get(String path) async { + _checkClosed(); + return _executeWithRetry(() async { + final response = await _client + .get( + Uri.parse('${config.endpoint}$path'), + headers: _headers, + ) + .timeout(Duration(milliseconds: config.timeoutMs)); + return _handleResponse(response); + }); + } + + Future> _post(String path, Map body) async { + _checkClosed(); + return _executeWithRetry(() async { + final response = await _client + .post( + Uri.parse('${config.endpoint}$path'), + headers: _headers, + body: jsonEncode(body), + ) + .timeout(Duration(milliseconds: config.timeoutMs)); + return _handleResponse(response); + }); + } + + Map get _headers => { + 'Authorization': 'Bearer ${config.apiKey}', + 'Content-Type': 'application/json', + 'X-SDK-Version': 'dart/$version', + }; + + dynamic _handleResponse(http.Response response) { + final body = jsonDecode(response.body); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return body; + } + + final message = body['message'] ?? body['error'] ?? 'Unknown error'; + final code = body['code']?.toString(); + throw PrivacyException( + message: message, + statusCode: response.statusCode, + code: code, + ); + } + + Future _executeWithRetry(Future Function() action) async { + Object? lastError; + for (var attempt = 0; attempt < config.retries; attempt++) { + try { + return await action(); + } catch (e) { + lastError = e; + if (attempt < config.retries - 1) { + await Future.delayed(Duration(seconds: 1 << attempt)); + } + } + } + throw lastError ?? PrivacyException(message: 'Request failed'); + } + + void _checkClosed() { + if (_closed) { + throw PrivacyException(message: 'Client has been closed'); + } + } +} + +/// Privacy SDK configuration. +class PrivacyConfig { + final String apiKey; + final String endpoint; + final int timeoutMs; + final int retries; + + PrivacyConfig({ + required this.apiKey, + this.endpoint = 'https://privacy.synor.io', + this.timeoutMs = 30000, + this.retries = 3, + }); +} + +/// Privacy SDK exception. +class PrivacyException implements Exception { + final String message; + final int? statusCode; + final String? code; + + PrivacyException({required this.message, this.statusCode, this.code}); + + @override + String toString() => 'PrivacyException: $message (code: $code, status: $statusCode)'; +} diff --git a/sdk/flutter/lib/src/privacy/privacy_types.dart b/sdk/flutter/lib/src/privacy/privacy_types.dart new file mode 100644 index 0000000..364496f --- /dev/null +++ b/sdk/flutter/lib/src/privacy/privacy_types.dart @@ -0,0 +1,265 @@ +// ==================== Confidential Transactions ==================== + +class ConfidentialTxInput { + final String commitment; + final String blindingFactor; + final String value; + final String? keyImage; + + ConfidentialTxInput({ + required this.commitment, + required this.blindingFactor, + required this.value, + this.keyImage, + }); + + factory ConfidentialTxInput.fromJson(Map json) { + return ConfidentialTxInput( + commitment: json['commitment'] as String, + blindingFactor: json['blinding_factor'] as String, + value: json['value'] as String, + keyImage: json['key_image'] as String?, + ); + } + + Map toJson() => { + 'commitment': commitment, + 'blinding_factor': blindingFactor, + 'value': value, + if (keyImage != null) 'key_image': keyImage, + }; +} + +class ConfidentialTxOutput { + final String commitment; + final String blindingFactor; + final String value; + final String recipientPublicKey; + final String? rangeProof; + + ConfidentialTxOutput({ + required this.commitment, + required this.blindingFactor, + required this.value, + required this.recipientPublicKey, + this.rangeProof, + }); + + factory ConfidentialTxOutput.fromJson(Map json) { + return ConfidentialTxOutput( + commitment: json['commitment'] as String, + blindingFactor: json['blinding_factor'] as String, + value: json['value'] as String, + recipientPublicKey: json['recipient_public_key'] as String, + rangeProof: json['range_proof'] as String?, + ); + } + + Map toJson() => { + 'commitment': commitment, + 'blinding_factor': blindingFactor, + 'value': value, + 'recipient_public_key': recipientPublicKey, + if (rangeProof != null) 'range_proof': rangeProof, + }; +} + +class ConfidentialTransaction { + final String id; + final List inputs; + final List outputs; + final String fee; + final String excess; + final String excessSig; + final String? kernelOffset; + + ConfidentialTransaction({ + required this.id, + required this.inputs, + required this.outputs, + required this.fee, + required this.excess, + required this.excessSig, + this.kernelOffset, + }); + + factory ConfidentialTransaction.fromJson(Map json) { + return ConfidentialTransaction( + id: json['id'] as String, + inputs: (json['inputs'] as List) + .map((e) => ConfidentialTxInput.fromJson(e)) + .toList(), + outputs: (json['outputs'] as List) + .map((e) => ConfidentialTxOutput.fromJson(e)) + .toList(), + fee: json['fee'] as String, + excess: json['excess'] as String, + excessSig: json['excess_sig'] as String, + kernelOffset: json['kernel_offset'] as String?, + ); + } + + Map toJson() => { + 'id': id, + 'inputs': inputs.map((i) => i.toJson()).toList(), + 'outputs': outputs.map((o) => o.toJson()).toList(), + 'fee': fee, + 'excess': excess, + 'excess_sig': excessSig, + if (kernelOffset != null) 'kernel_offset': kernelOffset, + }; +} + +// ==================== Commitments ==================== + +class Commitment { + final String commitment; + final String blindingFactor; + + Commitment({required this.commitment, required this.blindingFactor}); + + factory Commitment.fromJson(Map json) { + return Commitment( + commitment: json['commitment'] as String, + blindingFactor: json['blinding_factor'] as String, + ); + } + + Map toJson() => { + 'commitment': commitment, + 'blinding_factor': blindingFactor, + }; +} + +class RangeProof { + final String proof; + final String commitment; + final int minValue; + final int maxValue; + + RangeProof({ + required this.proof, + required this.commitment, + required this.minValue, + required this.maxValue, + }); + + factory RangeProof.fromJson(Map json) { + return RangeProof( + proof: json['proof'] as String, + commitment: json['commitment'] as String, + minValue: json['min_value'] as int, + maxValue: json['max_value'] as int, + ); + } + + Map toJson() => { + 'proof': proof, + 'commitment': commitment, + 'min_value': minValue, + 'max_value': maxValue, + }; +} + +// ==================== Ring Signatures ==================== + +class RingSignature { + final String c0; + final List s; + final String keyImage; + final List ring; + + RingSignature({ + required this.c0, + required this.s, + required this.keyImage, + required this.ring, + }); + + factory RingSignature.fromJson(Map json) { + return RingSignature( + c0: json['c0'] as String, + s: (json['s'] as List).map((e) => e as String).toList(), + keyImage: json['key_image'] as String, + ring: (json['ring'] as List).map((e) => e as String).toList(), + ); + } + + Map toJson() => { + 'c0': c0, + 's': s, + 'key_image': keyImage, + 'ring': ring, + }; +} + +// ==================== Stealth Addresses ==================== + +class StealthKeyPair { + final String spendPublicKey; + final String spendPrivateKey; + final String viewPublicKey; + final String viewPrivateKey; + + StealthKeyPair({ + required this.spendPublicKey, + required this.spendPrivateKey, + required this.viewPublicKey, + required this.viewPrivateKey, + }); + + factory StealthKeyPair.fromJson(Map json) { + return StealthKeyPair( + spendPublicKey: json['spend_public_key'] as String, + spendPrivateKey: json['spend_private_key'] as String, + viewPublicKey: json['view_public_key'] as String, + viewPrivateKey: json['view_private_key'] as String, + ); + } +} + +class StealthAddress { + final String address; + final String ephemeralPublicKey; + final String? txPublicKey; + + StealthAddress({ + required this.address, + required this.ephemeralPublicKey, + this.txPublicKey, + }); + + factory StealthAddress.fromJson(Map json) { + return StealthAddress( + address: json['address'] as String, + ephemeralPublicKey: json['ephemeral_public_key'] as String, + txPublicKey: json['tx_public_key'] as String?, + ); + } +} + +class StealthOutput { + final String txHash; + final int outputIndex; + final String stealthAddress; + final String amount; + final int blockHeight; + + StealthOutput({ + required this.txHash, + required this.outputIndex, + required this.stealthAddress, + required this.amount, + required this.blockHeight, + }); + + factory StealthOutput.fromJson(Map json) { + return StealthOutput( + txHash: json['tx_hash'] as String, + outputIndex: json['output_index'] as int, + stealthAddress: json['stealth_address'] as String, + amount: json['amount'] as String, + blockHeight: json['block_height'] as int, + ); + } +} diff --git a/sdk/go/contract/contract.go b/sdk/go/contract/contract.go new file mode 100644 index 0000000..f5e4870 --- /dev/null +++ b/sdk/go/contract/contract.go @@ -0,0 +1,810 @@ +// Package contract provides smart contract deployment and interaction. +package contract + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "sync/atomic" + "time" +) + +const DefaultEndpoint = "https://contract.synor.cc/api/v1" + +// Config is the contract client configuration. +type Config struct { + APIKey string + Endpoint string + Timeout time.Duration + Retries int + Debug bool + DefaultGasLimit string + DefaultGasPrice string +} + +// AbiEntryType is the type of ABI entry. +type AbiEntryType string + +const ( + AbiFunction AbiEntryType = "function" + AbiConstructor AbiEntryType = "constructor" + AbiEvent AbiEntryType = "event" + AbiError AbiEntryType = "error" + AbiFallback AbiEntryType = "fallback" + AbiReceive AbiEntryType = "receive" +) + +// StateMutability represents function state mutability. +type StateMutability string + +const ( + StatePure StateMutability = "pure" + StateView StateMutability = "view" + StateNonpayable StateMutability = "nonpayable" + StatePayable StateMutability = "payable" +) + +// TransactionStatus represents transaction status. +type TransactionStatus string + +const ( + StatusSuccess TransactionStatus = "success" + StatusReverted TransactionStatus = "reverted" +) + +// VerificationStatus represents verification status. +type VerificationStatus string + +const ( + VerificationVerified VerificationStatus = "verified" + VerificationPending VerificationStatus = "pending" + VerificationFailed VerificationStatus = "failed" +) + +// AbiParameter represents an ABI parameter. +type AbiParameter struct { + Name string `json:"name"` + Type string `json:"type"` + Indexed bool `json:"indexed,omitempty"` + Components []AbiParameter `json:"components,omitempty"` + InternalType string `json:"internalType,omitempty"` +} + +// AbiEntry represents an ABI entry. +type AbiEntry struct { + Type AbiEntryType `json:"type"` + Name string `json:"name,omitempty"` + Inputs []AbiParameter `json:"inputs,omitempty"` + Outputs []AbiParameter `json:"outputs,omitempty"` + StateMutability StateMutability `json:"stateMutability,omitempty"` + Anonymous bool `json:"anonymous,omitempty"` +} + +// Abi is a contract ABI. +type Abi []AbiEntry + +// DeploymentResult is the result of a contract deployment. +type DeploymentResult struct { + Address string `json:"address"` + TransactionHash string `json:"transactionHash"` + BlockNumber int64 `json:"blockNumber"` + GasUsed string `json:"gasUsed"` + EffectiveGasPrice string `json:"effectiveGasPrice"` +} + +// EventLog is a raw event log. +type EventLog struct { + LogIndex int `json:"logIndex"` + Address string `json:"address"` + Topics []string `json:"topics"` + Data string `json:"data"` + BlockNumber int64 `json:"blockNumber"` + TransactionHash string `json:"transactionHash"` +} + +// DecodedEvent is a decoded event. +type DecodedEvent struct { + Name string `json:"name"` + Signature string `json:"signature"` + Args map[string]interface{} `json:"args"` + Log EventLog `json:"log"` +} + +// TransactionResult is the result of a transaction. +type TransactionResult struct { + TransactionHash string `json:"transactionHash"` + BlockNumber int64 `json:"blockNumber"` + BlockHash string `json:"blockHash"` + GasUsed string `json:"gasUsed"` + EffectiveGasPrice string `json:"effectiveGasPrice"` + Status TransactionStatus `json:"status"` + Logs []EventLog `json:"logs"` + ReturnValue interface{} `json:"returnValue,omitempty"` + RevertReason string `json:"revertReason,omitempty"` +} + +// ContractInterface is a parsed contract interface. +type ContractInterface struct { + Abi Abi + Functions map[string]AbiEntry + Events map[string]AbiEntry + Errors map[string]AbiEntry +} + +// GasEstimation is a gas estimation result. +type GasEstimation struct { + GasLimit string `json:"gasLimit"` + GasPrice string `json:"gasPrice"` + EstimatedCost string `json:"estimatedCost"` +} + +// BytecodeMetadata contains bytecode metadata. +type BytecodeMetadata struct { + Compiler string `json:"compiler"` + Language string `json:"language"` + Sources []string `json:"sources"` +} + +// BytecodeInfo contains contract bytecode information. +type BytecodeInfo struct { + Bytecode string `json:"bytecode"` + DeployedBytecode string `json:"deployedBytecode,omitempty"` + Abi Abi `json:"abi,omitempty"` + Metadata *BytecodeMetadata `json:"metadata,omitempty"` +} + +// VerificationResult is the result of contract verification. +type VerificationResult struct { + Status VerificationStatus `json:"status"` + Message string `json:"message"` + Abi Abi `json:"abi,omitempty"` +} + +// MulticallRequest is a multicall request. +type MulticallRequest struct { + Address string `json:"address"` + CallData string `json:"callData"` + AllowFailure bool `json:"allowFailure,omitempty"` +} + +// MulticallResult is a multicall result. +type MulticallResult struct { + Success bool `json:"success"` + ReturnData string `json:"returnData"` + Decoded interface{} `json:"decoded,omitempty"` +} + +// Error is a contract error. +type Error struct { + Message string + Code string + StatusCode int +} + +func (e *Error) Error() string { + return e.Message +} + +// Client is a Synor Contract client. +type Client struct { + config Config + httpClient *http.Client + closed atomic.Bool +} + +// NewClient creates a new contract client. +func NewClient(config Config) *Client { + if config.Endpoint == "" { + config.Endpoint = DefaultEndpoint + } + if config.Timeout == 0 { + config.Timeout = 30 * time.Second + } + if config.Retries == 0 { + config.Retries = 3 + } + + return &Client{ + config: config, + httpClient: &http.Client{ + Timeout: config.Timeout, + }, + } +} + +// ==================== Deployment ==================== + +// DeployOptions are options for deploying a contract. +type DeployOptions struct { + Bytecode string + Abi Abi + Args []interface{} + GasLimit string + GasPrice string + Value string + Salt string +} + +// Deploy deploys a smart contract. +func (c *Client) Deploy(ctx context.Context, opts DeployOptions) (*DeploymentResult, error) { + body := map[string]interface{}{ + "bytecode": opts.Bytecode, + } + if opts.Abi != nil { + body["abi"] = opts.Abi + } + if opts.Args != nil { + body["args"] = opts.Args + } + gasLimit := opts.GasLimit + if gasLimit == "" { + gasLimit = c.config.DefaultGasLimit + } + if gasLimit != "" { + body["gasLimit"] = gasLimit + } + gasPrice := opts.GasPrice + if gasPrice == "" { + gasPrice = c.config.DefaultGasPrice + } + if gasPrice != "" { + body["gasPrice"] = gasPrice + } + if opts.Value != "" { + body["value"] = opts.Value + } + if opts.Salt != "" { + body["salt"] = opts.Salt + } + + var resp struct { + Deployment DeploymentResult `json:"deployment"` + } + if err := c.request(ctx, "POST", "/contracts/deploy", body, &resp); err != nil { + return nil, err + } + return &resp.Deployment, nil +} + +// PredictAddress predicts the CREATE2 deployment address. +func (c *Client) PredictAddress(ctx context.Context, bytecode, salt string, constructorArgs []interface{}) (string, error) { + body := map[string]interface{}{ + "bytecode": bytecode, + "salt": salt, + } + if constructorArgs != nil { + body["constructorArgs"] = constructorArgs + } + var resp struct { + Address string `json:"address"` + } + if err := c.request(ctx, "POST", "/contracts/predict-address", body, &resp); err != nil { + return "", err + } + return resp.Address, nil +} + +// ==================== Contract Interaction ==================== + +// CallOptions are options for calling a contract. +type CallOptions struct { + Address string + Method string + Args []interface{} + Abi Abi + BlockNumber interface{} +} + +// Call calls a view/pure function. +func (c *Client) Call(ctx context.Context, opts CallOptions) (interface{}, error) { + body := map[string]interface{}{ + "address": opts.Address, + "method": opts.Method, + "abi": opts.Abi, + } + if opts.Args != nil { + body["args"] = opts.Args + } + if opts.BlockNumber != nil { + body["blockNumber"] = opts.BlockNumber + } else { + body["blockNumber"] = "latest" + } + + var resp struct { + Result interface{} `json:"result"` + } + if err := c.request(ctx, "POST", "/contracts/call", body, &resp); err != nil { + return nil, err + } + return resp.Result, nil +} + +// SendOptions are options for sending a transaction. +type SendOptions struct { + Address string + Method string + Args []interface{} + Abi Abi + GasLimit string + GasPrice string + Value string +} + +// Send sends a state-changing transaction. +func (c *Client) Send(ctx context.Context, opts SendOptions) (*TransactionResult, error) { + body := map[string]interface{}{ + "address": opts.Address, + "method": opts.Method, + "abi": opts.Abi, + } + if opts.Args != nil { + body["args"] = opts.Args + } + gasLimit := opts.GasLimit + if gasLimit == "" { + gasLimit = c.config.DefaultGasLimit + } + if gasLimit != "" { + body["gasLimit"] = gasLimit + } + gasPrice := opts.GasPrice + if gasPrice == "" { + gasPrice = c.config.DefaultGasPrice + } + if gasPrice != "" { + body["gasPrice"] = gasPrice + } + if opts.Value != "" { + body["value"] = opts.Value + } + + var resp struct { + Transaction TransactionResult `json:"transaction"` + } + if err := c.request(ctx, "POST", "/contracts/send", body, &resp); err != nil { + return nil, err + } + return &resp.Transaction, nil +} + +// Multicall executes multiple calls in a single request. +func (c *Client) Multicall(ctx context.Context, requests []MulticallRequest, abis map[string]Abi) ([]MulticallResult, error) { + body := map[string]interface{}{ + "calls": requests, + } + if abis != nil { + body["abis"] = abis + } + var resp struct { + Results []MulticallResult `json:"results"` + } + if err := c.request(ctx, "POST", "/contracts/multicall", body, &resp); err != nil { + return nil, err + } + return resp.Results, nil +} + +// ==================== Events ==================== + +// EventFilter is a filter for events. +type EventFilter struct { + Address string + EventName string + Abi Abi + FromBlock interface{} + ToBlock interface{} + Filter map[string]interface{} +} + +// GetEvents gets historical events. +func (c *Client) GetEvents(ctx context.Context, filter EventFilter) ([]DecodedEvent, error) { + body := map[string]interface{}{ + "address": filter.Address, + } + if filter.Abi != nil { + body["abi"] = filter.Abi + } + if filter.EventName != "" { + body["eventName"] = filter.EventName + } + if filter.FromBlock != nil { + body["fromBlock"] = filter.FromBlock + } else { + body["fromBlock"] = "earliest" + } + if filter.ToBlock != nil { + body["toBlock"] = filter.ToBlock + } else { + body["toBlock"] = "latest" + } + if filter.Filter != nil { + body["filter"] = filter.Filter + } + + var resp struct { + Events []DecodedEvent `json:"events"` + } + if err := c.request(ctx, "POST", "/contracts/events", body, &resp); err != nil { + return nil, err + } + return resp.Events, nil +} + +// GetLogs gets raw event logs. +func (c *Client) GetLogs(ctx context.Context, filter EventFilter) ([]EventLog, error) { + body := map[string]interface{}{ + "address": filter.Address, + } + if filter.Abi != nil { + body["abi"] = filter.Abi + } + if filter.EventName != "" { + body["eventName"] = filter.EventName + } + if filter.FromBlock != nil { + body["fromBlock"] = filter.FromBlock + } else { + body["fromBlock"] = "earliest" + } + if filter.ToBlock != nil { + body["toBlock"] = filter.ToBlock + } else { + body["toBlock"] = "latest" + } + if filter.Filter != nil { + body["filter"] = filter.Filter + } + + var resp struct { + Logs []EventLog `json:"logs"` + } + if err := c.request(ctx, "POST", "/contracts/logs", body, &resp); err != nil { + return nil, err + } + return resp.Logs, nil +} + +// DecodeLog decodes a raw event log. +func (c *Client) DecodeLog(ctx context.Context, log EventLog, abi Abi) (*DecodedEvent, error) { + body := map[string]interface{}{ + "log": log, + "abi": abi, + } + var resp struct { + Event DecodedEvent `json:"event"` + } + if err := c.request(ctx, "POST", "/contracts/decode-log", body, &resp); err != nil { + return nil, err + } + return &resp.Event, nil +} + +// ==================== ABI Utilities ==================== + +// LoadAbi loads and parses a contract ABI. +func (c *Client) LoadAbi(abi Abi) *ContractInterface { + ci := &ContractInterface{ + Abi: abi, + Functions: make(map[string]AbiEntry), + Events: make(map[string]AbiEntry), + Errors: make(map[string]AbiEntry), + } + for _, entry := range abi { + switch entry.Type { + case AbiFunction: + if entry.Name != "" { + ci.Functions[entry.Name] = entry + } + case AbiEvent: + if entry.Name != "" { + ci.Events[entry.Name] = entry + } + case AbiError: + if entry.Name != "" { + ci.Errors[entry.Name] = entry + } + } + } + return ci +} + +// EncodeCall encodes a function call. +func (c *Client) EncodeCall(ctx context.Context, method string, args []interface{}, abi Abi) (string, error) { + body := map[string]interface{}{ + "method": method, + "args": args, + "abi": abi, + } + var resp struct { + Data string `json:"data"` + } + if err := c.request(ctx, "POST", "/contracts/encode", body, &resp); err != nil { + return "", err + } + return resp.Data, nil +} + +// DecodeResult decodes function return data. +func (c *Client) DecodeResult(ctx context.Context, method, data string, abi Abi) (interface{}, error) { + body := map[string]interface{}{ + "method": method, + "data": data, + "abi": abi, + } + var resp struct { + Result interface{} `json:"result"` + } + if err := c.request(ctx, "POST", "/contracts/decode", body, &resp); err != nil { + return nil, err + } + return resp.Result, nil +} + +// GetFunctionSelector gets the function selector. +func (c *Client) GetFunctionSelector(ctx context.Context, signature string) (string, error) { + body := map[string]interface{}{ + "signature": signature, + } + var resp struct { + Selector string `json:"selector"` + } + if err := c.request(ctx, "POST", "/contracts/selector", body, &resp); err != nil { + return "", err + } + return resp.Selector, nil +} + +// ==================== Gas Estimation ==================== + +// EstimateGasOptions are options for gas estimation. +type EstimateGasOptions struct { + Address string + Method string + Args []interface{} + Abi Abi + Bytecode string + Value string + From string +} + +// EstimateGas estimates gas for a contract call or deployment. +func (c *Client) EstimateGas(ctx context.Context, opts EstimateGasOptions) (*GasEstimation, error) { + body := map[string]interface{}{} + if opts.Address != "" { + body["address"] = opts.Address + } + if opts.Method != "" { + body["method"] = opts.Method + } + if opts.Args != nil { + body["args"] = opts.Args + } + if opts.Abi != nil { + body["abi"] = opts.Abi + } + if opts.Bytecode != "" { + body["bytecode"] = opts.Bytecode + } + if opts.Value != "" { + body["value"] = opts.Value + } + if opts.From != "" { + body["from"] = opts.From + } + + var resp struct { + Estimation GasEstimation `json:"estimation"` + } + if err := c.request(ctx, "POST", "/contracts/estimate-gas", body, &resp); err != nil { + return nil, err + } + return &resp.Estimation, nil +} + +// ==================== Contract Info ==================== + +// GetBytecode gets contract bytecode. +func (c *Client) GetBytecode(ctx context.Context, address string) (*BytecodeInfo, error) { + var info BytecodeInfo + if err := c.request(ctx, "GET", fmt.Sprintf("/contracts/%s/bytecode", address), nil, &info); err != nil { + return nil, err + } + return &info, nil +} + +// IsContract checks if an address is a contract. +func (c *Client) IsContract(ctx context.Context, address string) (bool, error) { + var resp struct { + IsContract bool `json:"isContract"` + } + if err := c.request(ctx, "GET", fmt.Sprintf("/contracts/%s/is-contract", address), nil, &resp); err != nil { + return false, err + } + return resp.IsContract, nil +} + +// ReadStorage reads a contract storage slot. +func (c *Client) ReadStorage(ctx context.Context, address, slot string, blockNumber interface{}) (string, error) { + body := map[string]interface{}{ + "address": address, + "slot": slot, + } + if blockNumber != nil { + body["blockNumber"] = blockNumber + } else { + body["blockNumber"] = "latest" + } + var resp struct { + Value string `json:"value"` + } + if err := c.request(ctx, "POST", "/contracts/storage", body, &resp); err != nil { + return "", err + } + return resp.Value, nil +} + +// ==================== Verification ==================== + +// VerifyContractOptions are options for contract verification. +type VerifyContractOptions struct { + Address string + SourceCode string + CompilerVersion string + ConstructorArguments string + Optimization bool + OptimizationRuns int + ContractName string +} + +// VerifyContract submits a contract for verification. +func (c *Client) VerifyContract(ctx context.Context, opts VerifyContractOptions) (*VerificationResult, error) { + body := map[string]interface{}{ + "address": opts.Address, + "sourceCode": opts.SourceCode, + "compilerVersion": opts.CompilerVersion, + "optimization": opts.Optimization, + "optimizationRuns": opts.OptimizationRuns, + } + if opts.ConstructorArguments != "" { + body["constructorArguments"] = opts.ConstructorArguments + } + if opts.ContractName != "" { + body["contractName"] = opts.ContractName + } + + var result VerificationResult + if err := c.request(ctx, "POST", "/contracts/verify", body, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetVerificationStatus gets verification status. +func (c *Client) GetVerificationStatus(ctx context.Context, address string) (*VerificationResult, error) { + var result VerificationResult + if err := c.request(ctx, "GET", fmt.Sprintf("/contracts/%s/verification", address), nil, &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetVerifiedAbi gets the verified contract ABI. +func (c *Client) GetVerifiedAbi(ctx context.Context, address string) (Abi, error) { + var resp struct { + Abi Abi `json:"abi"` + } + if err := c.request(ctx, "GET", fmt.Sprintf("/contracts/%s/abi", address), nil, &resp); err != nil { + return nil, err + } + return resp.Abi, nil +} + +// ==================== Lifecycle ==================== + +// HealthCheck checks if the service is healthy. +func (c *Client) HealthCheck(ctx context.Context) bool { + var resp struct { + Status string `json:"status"` + } + if err := c.request(ctx, "GET", "/health", nil, &resp); err != nil { + return false + } + return resp.Status == "healthy" +} + +// Close closes the client. +func (c *Client) Close() { + c.closed.Store(true) +} + +// IsClosed returns whether the client is closed. +func (c *Client) IsClosed() bool { + return c.closed.Load() +} + +// ==================== Private ==================== + +func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error { + if c.closed.Load() { + return &Error{Message: "client has been closed"} + } + + var lastErr error + for attempt := 0; attempt < c.config.Retries; attempt++ { + err := c.doRequest(ctx, method, path, body, result) + if err == nil { + return nil + } + lastErr = err + if attempt < c.config.Retries-1 { + time.Sleep(time.Duration(1<= 400 { + var errResp struct { + Message string `json:"message"` + Error string `json:"error"` + Code string `json:"code"` + } + json.Unmarshal(respBody, &errResp) + msg := errResp.Message + if msg == "" { + msg = errResp.Error + } + if msg == "" { + msg = fmt.Sprintf("HTTP %d", resp.StatusCode) + } + return &Error{ + Message: msg, + Code: errResp.Code, + StatusCode: resp.StatusCode, + } + } + + if result != nil { + if err := json.Unmarshal(respBody, result); err != nil { + return &Error{Message: fmt.Sprintf("failed to parse response: %v", err)} + } + } + + return nil +} diff --git a/sdk/go/privacy/privacy.go b/sdk/go/privacy/privacy.go new file mode 100644 index 0000000..005cebb --- /dev/null +++ b/sdk/go/privacy/privacy.go @@ -0,0 +1,682 @@ +// Package privacy provides privacy-enhancing cryptographic features. +package privacy + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "sync/atomic" + "time" +) + +const DefaultEndpoint = "https://privacy.synor.cc/api/v1" + +// Config is the privacy client configuration. +type Config struct { + APIKey string + Endpoint string + Timeout time.Duration + Retries int + Debug bool +} + +// KeyType represents the type of cryptographic key. +type KeyType string + +const ( + KeyTypeEd25519 KeyType = "ed25519" + KeyTypeSecp256k1 KeyType = "secp256k1" +) + +// GeneratorType represents the generator used for commitments. +type GeneratorType string + +const ( + GeneratorDefault GeneratorType = "default" + GeneratorAlternate GeneratorType = "alternate" +) + +// PublicKey represents a public key. +type PublicKey struct { + Key string `json:"key"` + Type KeyType `json:"type"` +} + +// PrivateKey represents a private key. +type PrivateKey struct { + Key string `json:"key"` + Type KeyType `json:"type"` +} + +// ConfidentialUTXO is a UTXO for confidential transactions. +type ConfidentialUTXO struct { + TxID string `json:"txid"` + Vout int `json:"vout"` + Commitment string `json:"commitment"` + RangeProof string `json:"rangeProof"` + Blinding string `json:"blinding,omitempty"` + Amount string `json:"amount,omitempty"` +} + +// ConfidentialOutput is an output for confidential transactions. +type ConfidentialOutput struct { + Recipient string `json:"recipient"` + Amount string `json:"amount"` + Blinding string `json:"blinding,omitempty"` +} + +// OutputFeatures contains output feature flags. +type OutputFeatures struct { + Flags int `json:"flags"` + LockHeight int `json:"lockHeight,omitempty"` +} + +// ConfidentialTxInput is a confidential transaction input. +type ConfidentialTxInput struct { + OutputRef string `json:"outputRef"` + Commitment string `json:"commitment"` +} + +// ConfidentialTxOutput is a confidential transaction output. +type ConfidentialTxOutput struct { + Commitment string `json:"commitment"` + RangeProof string `json:"rangeProof"` + Features OutputFeatures `json:"features"` +} + +// TransactionKernel is the kernel for confidential transactions. +type TransactionKernel struct { + Features int `json:"features"` + Fee string `json:"fee"` + LockHeight int `json:"lockHeight"` + Excess string `json:"excess"` + ExcessSignature string `json:"excessSignature"` +} + +// ConfidentialTransaction is a confidential transaction. +type ConfidentialTransaction struct { + TxID string `json:"txid"` + Version int `json:"version"` + Inputs []ConfidentialTxInput `json:"inputs"` + Outputs []ConfidentialTxOutput `json:"outputs"` + Kernel TransactionKernel `json:"kernel"` + Offset string `json:"offset"` + Raw string `json:"raw"` +} + +// VerifyConfidentialTxDetails contains verification details. +type VerifyConfidentialTxDetails struct { + CommitmentsBalance bool `json:"commitmentsBalance"` + RangeProofsValid bool `json:"rangeProofsValid"` + SignatureValid bool `json:"signatureValid"` + NoDuplicateInputs bool `json:"noDuplicateInputs"` +} + +// VerifyConfidentialTxResult is the result of verifying a confidential transaction. +type VerifyConfidentialTxResult struct { + Valid bool `json:"valid"` + Details VerifyConfidentialTxDetails `json:"details"` + Error string `json:"error,omitempty"` +} + +// RingSignatureComponents contains the signature components. +type RingSignatureComponents struct { + C []string `json:"c"` + R []string `json:"r"` +} + +// RingSignature is a ring signature. +type RingSignature struct { + ID string `json:"id"` + MessageHash string `json:"messageHash"` + Ring []string `json:"ring"` + KeyImage string `json:"keyImage"` + Signature RingSignatureComponents `json:"signature"` + RingSize int `json:"ringSize"` +} + +// VerifyRingSignatureResult is the result of verifying a ring signature. +type VerifyRingSignatureResult struct { + Valid bool `json:"valid"` + KeyImage string `json:"keyImage"` +} + +// StealthAddress is a stealth address. +type StealthAddress struct { + Address string `json:"address"` + ScanPublicKey string `json:"scanPublicKey"` + SpendPublicKey string `json:"spendPublicKey"` +} + +// StealthKeypair is a stealth address keypair. +type StealthKeypair struct { + Address StealthAddress `json:"address"` + ScanPrivateKey string `json:"scanPrivateKey"` + SpendPrivateKey string `json:"spendPrivateKey"` +} + +// OneTimeAddress is a one-time address derived from a stealth address. +type OneTimeAddress struct { + Address string `json:"address"` + EphemeralPublicKey string `json:"ephemeralPublicKey"` + SharedSecret string `json:"sharedSecret,omitempty"` +} + +// SharedSecret is the result of deriving a shared secret. +type SharedSecret struct { + Secret string `json:"secret"` + OneTimePrivateKey string `json:"oneTimePrivateKey"` + OneTimeAddress string `json:"oneTimeAddress"` +} + +// Commitment is a Pedersen commitment. +type Commitment struct { + Commitment string `json:"commitment"` + Generator GeneratorType `json:"generator"` +} + +// CommitmentWithBlinding is a commitment with its blinding factor. +type CommitmentWithBlinding struct { + Commitment Commitment `json:"commitment"` + Blinding string `json:"blinding"` +} + +// RangeProof is a Bulletproof range proof. +type RangeProof struct { + Proof string `json:"proof"` + Commitment string `json:"commitment"` + BitLength int `json:"bitLength"` +} + +// VerifyRangeProofResult is the result of verifying a range proof. +type VerifyRangeProofResult struct { + Valid bool `json:"valid"` + MinValue string `json:"minValue"` + MaxValue string `json:"maxValue"` +} + +// Error is a privacy error. +type Error struct { + Message string + Code string + StatusCode int +} + +func (e *Error) Error() string { + return e.Message +} + +// Client is a Synor Privacy client. +type Client struct { + config Config + httpClient *http.Client + closed atomic.Bool +} + +// NewClient creates a new privacy client. +func NewClient(config Config) *Client { + if config.Endpoint == "" { + config.Endpoint = DefaultEndpoint + } + if config.Timeout == 0 { + config.Timeout = 30 * time.Second + } + if config.Retries == 0 { + config.Retries = 3 + } + + return &Client{ + config: config, + httpClient: &http.Client{ + Timeout: config.Timeout, + }, + } +} + +// ==================== Confidential Transactions ==================== + +// CreateConfidentialTxOptions are options for creating a confidential transaction. +type CreateConfidentialTxOptions struct { + Inputs []ConfidentialUTXO + Outputs []ConfidentialOutput + Fee string + LockHeight int +} + +// CreateConfidentialTransaction creates a confidential transaction. +func (c *Client) CreateConfidentialTransaction(ctx context.Context, opts CreateConfidentialTxOptions) (*ConfidentialTransaction, error) { + body := map[string]interface{}{ + "inputs": opts.Inputs, + "outputs": opts.Outputs, + } + if opts.Fee != "" { + body["fee"] = opts.Fee + } + if opts.LockHeight > 0 { + body["lockHeight"] = opts.LockHeight + } + + var resp struct { + Transaction ConfidentialTransaction `json:"transaction"` + } + if err := c.request(ctx, "POST", "/transactions/confidential", body, &resp); err != nil { + return nil, err + } + return &resp.Transaction, nil +} + +// VerifyConfidentialTransaction verifies a confidential transaction. +func (c *Client) VerifyConfidentialTransaction(ctx context.Context, tx *ConfidentialTransaction) (*VerifyConfidentialTxResult, error) { + body := map[string]interface{}{ + "transaction": tx.Raw, + } + var result VerifyConfidentialTxResult + if err := c.request(ctx, "POST", "/transactions/confidential/verify", body, &result); err != nil { + return nil, err + } + return &result, nil +} + +// DecodeConfidentialOutput decodes a confidential output. +func (c *Client) DecodeConfidentialOutput(ctx context.Context, commitment, blinding string) (string, error) { + body := map[string]interface{}{ + "commitment": commitment, + "blinding": blinding, + } + var resp struct { + Amount string `json:"amount"` + } + if err := c.request(ctx, "POST", "/transactions/confidential/decode", body, &resp); err != nil { + return "", err + } + return resp.Amount, nil +} + +// ==================== Ring Signatures ==================== + +// CreateRingSignatureOptions are options for creating a ring signature. +type CreateRingSignatureOptions struct { + Message string + Ring []string + PrivateKey string + SignerIndex *int +} + +// CreateRingSignature creates a ring signature. +func (c *Client) CreateRingSignature(ctx context.Context, opts CreateRingSignatureOptions) (*RingSignature, error) { + body := map[string]interface{}{ + "message": opts.Message, + "ring": opts.Ring, + "privateKey": opts.PrivateKey, + } + if opts.SignerIndex != nil { + body["signerIndex"] = *opts.SignerIndex + } + + var resp struct { + Signature RingSignature `json:"signature"` + } + if err := c.request(ctx, "POST", "/ring-signatures/create", body, &resp); err != nil { + return nil, err + } + return &resp.Signature, nil +} + +// VerifyRingSignature verifies a ring signature. +func (c *Client) VerifyRingSignature(ctx context.Context, signature *RingSignature, message string) (*VerifyRingSignatureResult, error) { + body := map[string]interface{}{ + "signature": signature, + "message": message, + } + var result VerifyRingSignatureResult + if err := c.request(ctx, "POST", "/ring-signatures/verify", body, &result); err != nil { + return nil, err + } + return &result, nil +} + +// IsKeyImageUsed checks if a key image has been used. +func (c *Client) IsKeyImageUsed(ctx context.Context, keyImage string) (bool, error) { + var resp struct { + Used bool `json:"used"` + } + if err := c.request(ctx, "GET", fmt.Sprintf("/ring-signatures/key-images/%s", keyImage), nil, &resp); err != nil { + return false, err + } + return resp.Used, nil +} + +// GenerateRandomRing generates a random ring of public keys. +func (c *Client) GenerateRandomRing(ctx context.Context, size int, exclude []string) ([]string, error) { + body := map[string]interface{}{ + "size": size, + } + if len(exclude) > 0 { + body["exclude"] = exclude + } + var resp struct { + Ring []string `json:"ring"` + } + if err := c.request(ctx, "POST", "/ring-signatures/random-ring", body, &resp); err != nil { + return nil, err + } + return resp.Ring, nil +} + +// ==================== Stealth Addresses ==================== + +// GenerateStealthKeypair generates a new stealth address keypair. +func (c *Client) GenerateStealthKeypair(ctx context.Context) (*StealthKeypair, error) { + var resp struct { + Keypair StealthKeypair `json:"keypair"` + } + if err := c.request(ctx, "POST", "/stealth/generate", nil, &resp); err != nil { + return nil, err + } + return &resp.Keypair, nil +} + +// CreateOneTimeAddress creates a one-time address for a stealth address. +func (c *Client) CreateOneTimeAddress(ctx context.Context, stealthAddress *StealthAddress) (*OneTimeAddress, error) { + body := map[string]interface{}{ + "scanPublicKey": stealthAddress.ScanPublicKey, + "spendPublicKey": stealthAddress.SpendPublicKey, + } + var resp struct { + OneTimeAddress OneTimeAddress `json:"oneTimeAddress"` + } + if err := c.request(ctx, "POST", "/stealth/one-time-address", body, &resp); err != nil { + return nil, err + } + return &resp.OneTimeAddress, nil +} + +// DeriveSharedSecret derives the shared secret for a stealth payment. +func (c *Client) DeriveSharedSecret(ctx context.Context, stealthAddress *StealthAddress, privateKey, ephemeralPublicKey string) (*SharedSecret, error) { + body := map[string]interface{}{ + "scanPublicKey": stealthAddress.ScanPublicKey, + "spendPublicKey": stealthAddress.SpendPublicKey, + "privateKey": privateKey, + "ephemeralPublicKey": ephemeralPublicKey, + } + var secret SharedSecret + if err := c.request(ctx, "POST", "/stealth/derive-secret", body, &secret); err != nil { + return nil, err + } + return &secret, nil +} + +// ScanForPayments scans transactions for stealth payments. +func (c *Client) ScanForPayments(ctx context.Context, scanPrivateKey, spendPublicKey string, transactions []string) ([]OneTimeAddress, error) { + body := map[string]interface{}{ + "scanPrivateKey": scanPrivateKey, + "spendPublicKey": spendPublicKey, + "transactions": transactions, + } + var resp struct { + Payments []OneTimeAddress `json:"payments"` + } + if err := c.request(ctx, "POST", "/stealth/scan", body, &resp); err != nil { + return nil, err + } + return resp.Payments, nil +} + +// ==================== Commitments ==================== + +// CreateCommitment creates a Pedersen commitment. +func (c *Client) CreateCommitment(ctx context.Context, value string, blinding string) (*CommitmentWithBlinding, error) { + body := map[string]interface{}{ + "value": value, + } + if blinding != "" { + body["blinding"] = blinding + } + var result CommitmentWithBlinding + if err := c.request(ctx, "POST", "/commitments/create", body, &result); err != nil { + return nil, err + } + return &result, nil +} + +// OpenCommitment verifies a Pedersen commitment. +func (c *Client) OpenCommitment(ctx context.Context, commitment, value, blinding string) (bool, error) { + body := map[string]interface{}{ + "commitment": commitment, + "value": value, + "blinding": blinding, + } + var resp struct { + Valid bool `json:"valid"` + } + if err := c.request(ctx, "POST", "/commitments/open", body, &resp); err != nil { + return false, err + } + return resp.Valid, nil +} + +// AddCommitments adds two commitments. +func (c *Client) AddCommitments(ctx context.Context, commitment1, commitment2 string) (string, error) { + body := map[string]interface{}{ + "commitment1": commitment1, + "commitment2": commitment2, + } + var resp struct { + Commitment string `json:"commitment"` + } + if err := c.request(ctx, "POST", "/commitments/add", body, &resp); err != nil { + return "", err + } + return resp.Commitment, nil +} + +// SubtractCommitments subtracts two commitments. +func (c *Client) SubtractCommitments(ctx context.Context, commitment1, commitment2 string) (string, error) { + body := map[string]interface{}{ + "commitment1": commitment1, + "commitment2": commitment2, + } + var resp struct { + Commitment string `json:"commitment"` + } + if err := c.request(ctx, "POST", "/commitments/subtract", body, &resp); err != nil { + return "", err + } + return resp.Commitment, nil +} + +// ComputeBlindingSum computes the sum of blinding factors. +func (c *Client) ComputeBlindingSum(ctx context.Context, positive, negative []string) (string, error) { + body := map[string]interface{}{ + "positive": positive, + "negative": negative, + } + var resp struct { + Sum string `json:"sum"` + } + if err := c.request(ctx, "POST", "/commitments/blinding-sum", body, &resp); err != nil { + return "", err + } + return resp.Sum, nil +} + +// GenerateBlinding generates a random blinding factor. +func (c *Client) GenerateBlinding(ctx context.Context) (string, error) { + var resp struct { + Blinding string `json:"blinding"` + } + if err := c.request(ctx, "POST", "/commitments/random-blinding", nil, &resp); err != nil { + return "", err + } + return resp.Blinding, nil +} + +// ==================== Range Proofs ==================== + +// CreateRangeProofOptions are options for creating a range proof. +type CreateRangeProofOptions struct { + Value string + Blinding string + Message string + BitLength int +} + +// CreateRangeProof creates a Bulletproof range proof. +func (c *Client) CreateRangeProof(ctx context.Context, opts CreateRangeProofOptions) (*RangeProof, error) { + body := map[string]interface{}{ + "value": opts.Value, + "blinding": opts.Blinding, + } + if opts.Message != "" { + body["message"] = opts.Message + } + if opts.BitLength > 0 { + body["bitLength"] = opts.BitLength + } else { + body["bitLength"] = 64 + } + + var resp struct { + Proof RangeProof `json:"proof"` + } + if err := c.request(ctx, "POST", "/range-proofs/create", body, &resp); err != nil { + return nil, err + } + return &resp.Proof, nil +} + +// VerifyRangeProof verifies a Bulletproof range proof. +func (c *Client) VerifyRangeProof(ctx context.Context, commitment, proof string) (*VerifyRangeProofResult, error) { + body := map[string]interface{}{ + "commitment": commitment, + "proof": proof, + } + var result VerifyRangeProofResult + if err := c.request(ctx, "POST", "/range-proofs/verify", body, &result); err != nil { + return nil, err + } + return &result, nil +} + +// CreateAggregatedRangeProof creates an aggregated range proof. +func (c *Client) CreateAggregatedRangeProof(ctx context.Context, outputs []map[string]string) (string, error) { + body := map[string]interface{}{ + "outputs": outputs, + } + var resp struct { + Proof string `json:"proof"` + } + if err := c.request(ctx, "POST", "/range-proofs/aggregate", body, &resp); err != nil { + return "", err + } + return resp.Proof, nil +} + +// ==================== Lifecycle ==================== + +// HealthCheck checks if the service is healthy. +func (c *Client) HealthCheck(ctx context.Context) bool { + var resp struct { + Status string `json:"status"` + } + if err := c.request(ctx, "GET", "/health", nil, &resp); err != nil { + return false + } + return resp.Status == "healthy" +} + +// Close closes the client. +func (c *Client) Close() { + c.closed.Store(true) +} + +// IsClosed returns whether the client is closed. +func (c *Client) IsClosed() bool { + return c.closed.Load() +} + +// ==================== Private ==================== + +func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error { + if c.closed.Load() { + return &Error{Message: "client has been closed"} + } + + var lastErr error + for attempt := 0; attempt < c.config.Retries; attempt++ { + err := c.doRequest(ctx, method, path, body, result) + if err == nil { + return nil + } + lastErr = err + if attempt < c.config.Retries-1 { + time.Sleep(time.Duration(1<= 400 { + var errResp struct { + Message string `json:"message"` + Error string `json:"error"` + Code string `json:"code"` + } + json.Unmarshal(respBody, &errResp) + msg := errResp.Message + if msg == "" { + msg = errResp.Error + } + if msg == "" { + msg = fmt.Sprintf("HTTP %d", resp.StatusCode) + } + return &Error{ + Message: msg, + Code: errResp.Code, + StatusCode: resp.StatusCode, + } + } + + if result != nil { + if err := json.Unmarshal(respBody, result); err != nil { + return &Error{Message: fmt.Sprintf("failed to parse response: %v", err)} + } + } + + return nil +} diff --git a/sdk/java/src/main/java/io/synor/contract/ContractClient.java b/sdk/java/src/main/java/io/synor/contract/ContractClient.java new file mode 100644 index 0000000..ca66569 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/contract/ContractClient.java @@ -0,0 +1,324 @@ +package io.synor.contract; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import okhttp3.*; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Synor Contract SDK client for Java. + * Smart contract deployment, interaction, and event handling. + */ +public class ContractClient implements AutoCloseable { + private static final String SDK_VERSION = "0.1.0"; + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + private final ContractConfig config; + private final OkHttpClient httpClient; + private final Gson gson; + private final AtomicBoolean closed = new AtomicBoolean(false); + + public ContractClient(ContractConfig config) { + this.config = config; + this.gson = new GsonBuilder().create(); + this.httpClient = new OkHttpClient.Builder() + .connectTimeout(config.getTimeoutMs(), TimeUnit.MILLISECONDS) + .readTimeout(config.getTimeoutMs(), TimeUnit.MILLISECONDS) + .writeTimeout(config.getTimeoutMs(), TimeUnit.MILLISECONDS) + .build(); + } + + // ==================== Contract Deployment ==================== + + public CompletableFuture deploy(DeployContractOptions options) { + JsonObject body = new JsonObject(); + body.addProperty("bytecode", options.bytecode); + if (options.abi != null) body.add("abi", gson.toJsonTree(options.abi)); + if (options.constructorArgs != null) body.add("constructor_args", gson.toJsonTree(options.constructorArgs)); + if (options.value != null) body.addProperty("value", options.value); + if (options.gasLimit != null) body.addProperty("gas_limit", options.gasLimit); + if (options.gasPrice != null) body.addProperty("gas_price", options.gasPrice); + if (options.nonce != null) body.addProperty("nonce", options.nonce); + + return post("/contract/deploy", body, DeploymentResult.class); + } + + public CompletableFuture deployCreate2(DeployContractOptions options, String salt) { + JsonObject body = new JsonObject(); + body.addProperty("bytecode", options.bytecode); + body.addProperty("salt", salt); + if (options.abi != null) body.add("abi", gson.toJsonTree(options.abi)); + if (options.constructorArgs != null) body.add("constructor_args", gson.toJsonTree(options.constructorArgs)); + if (options.value != null) body.addProperty("value", options.value); + if (options.gasLimit != null) body.addProperty("gas_limit", options.gasLimit); + if (options.gasPrice != null) body.addProperty("gas_price", options.gasPrice); + + return post("/contract/deploy/create2", body, DeploymentResult.class); + } + + public CompletableFuture predictAddress(String bytecode, String salt, String deployer) { + JsonObject body = new JsonObject(); + body.addProperty("bytecode", bytecode); + body.addProperty("salt", salt); + if (deployer != null) body.addProperty("deployer", deployer); + + return post("/contract/predict-address", body, JsonObject.class) + .thenApply(response -> response.get("address").getAsString()); + } + + // ==================== Contract Interaction ==================== + + public CompletableFuture call(CallContractOptions options) { + JsonObject body = new JsonObject(); + body.addProperty("contract", options.contract); + body.addProperty("method", options.method); + body.add("args", gson.toJsonTree(options.args)); + body.add("abi", gson.toJsonTree(options.abi)); + + return post("/contract/call", body, Object.class); + } + + public CompletableFuture send(SendContractOptions options) { + JsonObject body = new JsonObject(); + body.addProperty("contract", options.contract); + body.addProperty("method", options.method); + body.add("args", gson.toJsonTree(options.args)); + body.add("abi", gson.toJsonTree(options.abi)); + if (options.value != null) body.addProperty("value", options.value); + if (options.gasLimit != null) body.addProperty("gas_limit", options.gasLimit); + if (options.gasPrice != null) body.addProperty("gas_price", options.gasPrice); + if (options.nonce != null) body.addProperty("nonce", options.nonce); + + return post("/contract/send", body, TransactionResult.class); + } + + // ==================== Events ==================== + + public CompletableFuture> getEvents(EventFilter filter) { + JsonObject body = new JsonObject(); + body.addProperty("contract", filter.contract); + if (filter.event != null) body.addProperty("event", filter.event); + if (filter.fromBlock != null) body.addProperty("from_block", filter.fromBlock); + if (filter.toBlock != null) body.addProperty("to_block", filter.toBlock); + if (filter.topics != null) body.add("topics", gson.toJsonTree(filter.topics)); + if (filter.abi != null) body.add("abi", gson.toJsonTree(filter.abi)); + + Type listType = new TypeToken>(){}.getType(); + return post("/contract/events", body, listType); + } + + public CompletableFuture> getLogs(String contract, Long fromBlock, Long toBlock) { + StringBuilder path = new StringBuilder("/contract/logs?contract=").append(encode(contract)); + if (fromBlock != null) path.append("&from_block=").append(fromBlock); + if (toBlock != null) path.append("&to_block=").append(toBlock); + + Type listType = new TypeToken>(){}.getType(); + return get(path.toString(), listType); + } + + public CompletableFuture> decodeLogs(List logs, List abi) { + JsonObject body = new JsonObject(); + body.add("logs", gson.toJsonTree(logs)); + body.add("abi", gson.toJsonTree(abi)); + + Type listType = new TypeToken>(){}.getType(); + return post("/contract/decode-logs", body, listType); + } + + // ==================== ABI Utilities ==================== + + public CompletableFuture encodeCall(EncodeCallOptions options) { + JsonObject body = new JsonObject(); + body.addProperty("method", options.method); + body.add("args", gson.toJsonTree(options.args)); + body.add("abi", gson.toJsonTree(options.abi)); + + return post("/contract/encode", body, JsonObject.class) + .thenApply(response -> response.get("data").getAsString()); + } + + public CompletableFuture decodeResult(DecodeResultOptions options) { + JsonObject body = new JsonObject(); + body.addProperty("data", options.data); + body.addProperty("method", options.method); + body.add("abi", gson.toJsonTree(options.abi)); + + return post("/contract/decode", body, JsonObject.class) + .thenApply(response -> response.get("result")); + } + + public CompletableFuture getSelector(String signature) { + return get("/contract/selector?signature=" + encode(signature), JsonObject.class) + .thenApply(response -> response.get("selector").getAsString()); + } + + // ==================== Gas Estimation ==================== + + public CompletableFuture estimateGas(EstimateGasOptions options) { + JsonObject body = new JsonObject(); + body.addProperty("contract", options.contract); + body.addProperty("method", options.method); + body.add("args", gson.toJsonTree(options.args)); + body.add("abi", gson.toJsonTree(options.abi)); + if (options.value != null) body.addProperty("value", options.value); + + return post("/contract/estimate-gas", body, GasEstimation.class); + } + + // ==================== Contract Information ==================== + + public CompletableFuture getBytecode(String address) { + return get("/contract/" + encode(address) + "/bytecode", BytecodeInfo.class); + } + + public CompletableFuture verify(VerifyContractOptions options) { + JsonObject body = new JsonObject(); + body.addProperty("address", options.address); + body.addProperty("source_code", options.sourceCode); + body.addProperty("compiler_version", options.compilerVersion); + if (options.constructorArgs != null) body.addProperty("constructor_args", options.constructorArgs); + if (options.optimization != null) body.addProperty("optimization", options.optimization); + if (options.optimizationRuns != null) body.addProperty("optimization_runs", options.optimizationRuns); + if (options.license != null) body.addProperty("license", options.license); + + return post("/contract/verify", body, VerificationResult.class); + } + + public CompletableFuture getVerificationStatus(String address) { + return get("/contract/" + encode(address) + "/verification", VerificationResult.class); + } + + // ==================== Multicall ==================== + + public CompletableFuture> multicall(List requests) { + JsonObject body = new JsonObject(); + body.add("calls", gson.toJsonTree(requests)); + + Type listType = new TypeToken>(){}.getType(); + return post("/contract/multicall", body, listType); + } + + // ==================== Storage ==================== + + public CompletableFuture readStorage(ReadStorageOptions options) { + StringBuilder path = new StringBuilder("/contract/storage?contract=") + .append(encode(options.contract)) + .append("&slot=").append(encode(options.slot)); + if (options.blockNumber != null) path.append("&block=").append(options.blockNumber); + + return get(path.toString(), JsonObject.class) + .thenApply(response -> response.get("value").getAsString()); + } + + // ==================== Lifecycle ==================== + + public CompletableFuture healthCheck() { + if (closed.get()) { + return CompletableFuture.completedFuture(false); + } + return get("/health", JsonObject.class) + .thenApply(response -> "healthy".equals(response.get("status").getAsString())) + .exceptionally(e -> false); + } + + @Override + public void close() { + closed.set(true); + httpClient.dispatcher().executorService().shutdown(); + httpClient.connectionPool().evictAll(); + } + + public boolean isClosed() { + return closed.get(); + } + + // ==================== HTTP Helpers ==================== + + private String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + private CompletableFuture get(String path, Type type) { + return executeRequest(path, null, type); + } + + private CompletableFuture post(String path, JsonObject body, Type type) { + return executeRequest(path, body, type); + } + + private CompletableFuture executeRequest(String path, JsonObject body, Type type) { + if (closed.get()) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new ContractException("Client has been closed")); + return future; + } + + CompletableFuture future = new CompletableFuture<>(); + + Request.Builder requestBuilder = new Request.Builder() + .url(config.getEndpoint() + path) + .header("Authorization", "Bearer " + config.getApiKey()) + .header("Content-Type", "application/json") + .header("X-SDK-Version", "java/" + SDK_VERSION); + + if (body != null) { + requestBuilder.post(RequestBody.create(gson.toJson(body), JSON)); + } else { + requestBuilder.get(); + } + + executeWithRetry(requestBuilder.build(), config.getRetries(), 0, future, type); + return future; + } + + private void executeWithRetry(Request request, int remainingRetries, int attempt, CompletableFuture future, Type type) { + httpClient.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + if (remainingRetries > 1) { + scheduleRetry(request, remainingRetries - 1, attempt + 1, future, type); + } else { + future.completeExceptionally(new ContractException("Request failed: " + e.getMessage(), e)); + } + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String bodyString = responseBody != null ? responseBody.string() : ""; + + if (response.isSuccessful()) { + T result = gson.fromJson(bodyString, type); + future.complete(result); + } else { + JsonObject errorObj = gson.fromJson(bodyString, JsonObject.class); + String message = errorObj.has("message") ? errorObj.get("message").getAsString() : "Unknown error"; + String code = errorObj.has("code") ? errorObj.get("code").getAsString() : null; + + if (remainingRetries > 1 && response.code() >= 500) { + scheduleRetry(request, remainingRetries - 1, attempt + 1, future, type); + } else { + future.completeExceptionally(new ContractException(message, response.code(), code)); + } + } + } + } + }); + } + + private void scheduleRetry(Request request, int remainingRetries, int attempt, CompletableFuture future, Type type) { + long delay = (long) Math.pow(2, attempt) * 1000; + CompletableFuture.delayedExecutor(delay, TimeUnit.MILLISECONDS) + .execute(() -> executeWithRetry(request, remainingRetries, attempt, future, type)); + } +} diff --git a/sdk/java/src/main/java/io/synor/contract/ContractConfig.java b/sdk/java/src/main/java/io/synor/contract/ContractConfig.java new file mode 100644 index 0000000..bdca108 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/contract/ContractConfig.java @@ -0,0 +1,49 @@ +package io.synor.contract; + +/** + * Configuration for the Contract SDK client. + */ +public class ContractConfig { + private final String apiKey; + private String endpoint = "https://contract.synor.io"; + private long timeoutMs = 30000; + private int retries = 3; + + public ContractConfig(String apiKey) { + if (apiKey == null || apiKey.isEmpty()) { + throw new IllegalArgumentException("API key is required"); + } + this.apiKey = apiKey; + } + + public ContractConfig endpoint(String endpoint) { + this.endpoint = endpoint; + return this; + } + + public ContractConfig timeoutMs(long timeoutMs) { + this.timeoutMs = timeoutMs; + return this; + } + + public ContractConfig retries(int retries) { + this.retries = retries; + return this; + } + + public String getApiKey() { + return apiKey; + } + + public String getEndpoint() { + return endpoint; + } + + public long getTimeoutMs() { + return timeoutMs; + } + + public int getRetries() { + return retries; + } +} diff --git a/sdk/java/src/main/java/io/synor/contract/ContractException.java b/sdk/java/src/main/java/io/synor/contract/ContractException.java new file mode 100644 index 0000000..d870412 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/contract/ContractException.java @@ -0,0 +1,35 @@ +package io.synor.contract; + +/** + * Exception thrown by the Contract SDK. + */ +public class ContractException extends RuntimeException { + private final Integer statusCode; + private final String errorCode; + + public ContractException(String message) { + super(message); + this.statusCode = null; + this.errorCode = null; + } + + public ContractException(String message, Throwable cause) { + super(message, cause); + this.statusCode = null; + this.errorCode = null; + } + + public ContractException(String message, int statusCode, String errorCode) { + super(message); + this.statusCode = statusCode; + this.errorCode = errorCode; + } + + public Integer getStatusCode() { + return statusCode; + } + + public String getErrorCode() { + return errorCode; + } +} diff --git a/sdk/java/src/main/java/io/synor/contract/ContractTypes.java b/sdk/java/src/main/java/io/synor/contract/ContractTypes.java new file mode 100644 index 0000000..23c5592 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/contract/ContractTypes.java @@ -0,0 +1,294 @@ +package io.synor.contract; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +import java.util.Map; + +/** + * Contract SDK type definitions. + */ +public class ContractTypes { + + // ==================== ABI Types ==================== + + public static class AbiParameter { + @SerializedName("name") + public String name; + + @SerializedName("type") + public String type; + + @SerializedName("indexed") + public Boolean indexed; + + @SerializedName("components") + public List components; + } + + public static class AbiEntry { + @SerializedName("type") + public String type; + + @SerializedName("name") + public String name; + + @SerializedName("inputs") + public List inputs; + + @SerializedName("outputs") + public List outputs; + + @SerializedName("stateMutability") + public String stateMutability; + + @SerializedName("anonymous") + public Boolean anonymous; + } + + // ==================== Deployment ==================== + + public static class DeployContractOptions { + public String bytecode; + public List abi; + public Object[] constructorArgs; + public String value; + public Long gasLimit; + public String gasPrice; + public Long nonce; + } + + public static class DeploymentResult { + @SerializedName("contract_address") + public String contractAddress; + + @SerializedName("transaction_hash") + public String transactionHash; + + @SerializedName("deployer") + public String deployer; + + @SerializedName("gas_used") + public Long gasUsed; + + @SerializedName("block_number") + public Long blockNumber; + + @SerializedName("block_hash") + public String blockHash; + } + + // ==================== Contract Interaction ==================== + + public static class CallContractOptions { + public String contract; + public String method; + public Object[] args; + public List abi; + } + + public static class SendContractOptions { + public String contract; + public String method; + public Object[] args; + public List abi; + public String value; + public Long gasLimit; + public String gasPrice; + public Long nonce; + } + + public static class TransactionResult { + @SerializedName("transaction_hash") + public String transactionHash; + + @SerializedName("block_number") + public Long blockNumber; + + @SerializedName("block_hash") + public String blockHash; + + @SerializedName("gas_used") + public Long gasUsed; + + @SerializedName("effective_gas_price") + public String effectiveGasPrice; + + @SerializedName("status") + public String status; + + @SerializedName("logs") + public List logs; + } + + // ==================== Events ==================== + + public static class EventLog { + @SerializedName("address") + public String address; + + @SerializedName("topics") + public List topics; + + @SerializedName("data") + public String data; + + @SerializedName("block_number") + public Long blockNumber; + + @SerializedName("transaction_hash") + public String transactionHash; + + @SerializedName("log_index") + public Integer logIndex; + + @SerializedName("block_hash") + public String blockHash; + + @SerializedName("removed") + public Boolean removed; + } + + public static class DecodedEvent { + @SerializedName("name") + public String name; + + @SerializedName("signature") + public String signature; + + @SerializedName("args") + public Map args; + + @SerializedName("log") + public EventLog log; + } + + public static class EventFilter { + public String contract; + public String event; + public Long fromBlock; + public Long toBlock; + public List topics; + public List abi; + } + + // ==================== ABI Utilities ==================== + + public static class EncodeCallOptions { + public String method; + public Object[] args; + public List abi; + } + + public static class DecodeResultOptions { + public String data; + public String method; + public List abi; + } + + // ==================== Gas Estimation ==================== + + public static class EstimateGasOptions { + public String contract; + public String method; + public Object[] args; + public List abi; + public String value; + } + + public static class GasEstimation { + @SerializedName("gas_limit") + public Long gasLimit; + + @SerializedName("gas_price") + public String gasPrice; + + @SerializedName("max_fee_per_gas") + public String maxFeePerGas; + + @SerializedName("max_priority_fee_per_gas") + public String maxPriorityFeePerGas; + + @SerializedName("estimated_cost") + public String estimatedCost; + } + + // ==================== Contract Information ==================== + + public static class BytecodeInfo { + @SerializedName("bytecode") + public String bytecode; + + @SerializedName("deployed_bytecode") + public String deployedBytecode; + + @SerializedName("size") + public Integer size; + + @SerializedName("is_contract") + public Boolean isContract; + } + + public static class VerifyContractOptions { + public String address; + public String sourceCode; + public String compilerVersion; + public String constructorArgs; + public Boolean optimization; + public Integer optimizationRuns; + public String license; + } + + public static class VerificationResult { + @SerializedName("verified") + public Boolean verified; + + @SerializedName("address") + public String address; + + @SerializedName("compiler_version") + public String compilerVersion; + + @SerializedName("optimization") + public Boolean optimization; + + @SerializedName("optimization_runs") + public Integer optimizationRuns; + + @SerializedName("license") + public String license; + + @SerializedName("abi") + public List abi; + + @SerializedName("source_code") + public String sourceCode; + } + + // ==================== Multicall ==================== + + public static class MulticallRequest { + public String contract; + public String method; + public Object[] args; + public List abi; + } + + public static class MulticallResult { + @SerializedName("success") + public Boolean success; + + @SerializedName("result") + public Object result; + + @SerializedName("error") + public String error; + } + + // ==================== Storage ==================== + + public static class ReadStorageOptions { + public String contract; + public String slot; + public Long blockNumber; + } +} diff --git a/sdk/java/src/main/java/io/synor/privacy/PrivacyClient.java b/sdk/java/src/main/java/io/synor/privacy/PrivacyClient.java new file mode 100644 index 0000000..6278d56 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/privacy/PrivacyClient.java @@ -0,0 +1,273 @@ +package io.synor.privacy; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import okhttp3.*; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Synor Privacy SDK client for Java. + * Confidential transactions, ring signatures, stealth addresses, and cryptographic commitments. + */ +public class PrivacyClient implements AutoCloseable { + private static final String SDK_VERSION = "0.1.0"; + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + private final PrivacyConfig config; + private final OkHttpClient httpClient; + private final Gson gson; + private final AtomicBoolean closed = new AtomicBoolean(false); + + public PrivacyClient(PrivacyConfig config) { + this.config = config; + this.gson = new GsonBuilder().create(); + this.httpClient = new OkHttpClient.Builder() + .connectTimeout(config.getTimeoutMs(), TimeUnit.MILLISECONDS) + .readTimeout(config.getTimeoutMs(), TimeUnit.MILLISECONDS) + .writeTimeout(config.getTimeoutMs(), TimeUnit.MILLISECONDS) + .build(); + } + + // ==================== Confidential Transactions ==================== + + public CompletableFuture createConfidentialTx(ConfidentialTxInput[] inputs, ConfidentialTxOutput[] outputs) { + JsonObject body = new JsonObject(); + body.add("inputs", gson.toJsonTree(inputs)); + body.add("outputs", gson.toJsonTree(outputs)); + return post("/privacy/confidential/create", body, ConfidentialTransaction.class); + } + + public CompletableFuture verifyConfidentialTx(ConfidentialTransaction tx) { + JsonObject body = new JsonObject(); + body.add("transaction", gson.toJsonTree(tx)); + return post("/privacy/confidential/verify", body, JsonObject.class) + .thenApply(response -> response.get("valid").getAsBoolean()); + } + + public CompletableFuture createCommitment(String value, String blindingFactor) { + JsonObject body = new JsonObject(); + body.addProperty("value", value); + body.addProperty("blinding_factor", blindingFactor); + return post("/privacy/commitment/create", body, Commitment.class); + } + + public CompletableFuture verifyCommitment(String commitment, String value, String blindingFactor) { + JsonObject body = new JsonObject(); + body.addProperty("commitment", commitment); + body.addProperty("value", value); + body.addProperty("blinding_factor", blindingFactor); + return post("/privacy/commitment/verify", body, JsonObject.class) + .thenApply(response -> response.get("valid").getAsBoolean()); + } + + public CompletableFuture createRangeProof(String value, String blindingFactor, long minValue, long maxValue) { + JsonObject body = new JsonObject(); + body.addProperty("value", value); + body.addProperty("blinding_factor", blindingFactor); + body.addProperty("min_value", minValue); + body.addProperty("max_value", maxValue); + return post("/privacy/range-proof/create", body, RangeProof.class); + } + + public CompletableFuture verifyRangeProof(RangeProof proof) { + JsonObject body = new JsonObject(); + body.add("proof", gson.toJsonTree(proof)); + return post("/privacy/range-proof/verify", body, JsonObject.class) + .thenApply(response -> response.get("valid").getAsBoolean()); + } + + // ==================== Ring Signatures ==================== + + public CompletableFuture createRingSignature(String message, String[] ring, int signerIndex, String privateKey) { + JsonObject body = new JsonObject(); + body.addProperty("message", message); + body.add("ring", gson.toJsonTree(ring)); + body.addProperty("signer_index", signerIndex); + body.addProperty("private_key", privateKey); + return post("/privacy/ring/sign", body, RingSignature.class); + } + + public CompletableFuture verifyRingSignature(RingSignature signature, String message) { + JsonObject body = new JsonObject(); + body.add("signature", gson.toJsonTree(signature)); + body.addProperty("message", message); + return post("/privacy/ring/verify", body, JsonObject.class) + .thenApply(response -> response.get("valid").getAsBoolean()); + } + + public CompletableFuture generateDecoys(int count, String excludeKey) { + String path = "/privacy/ring/decoys?count=" + count; + if (excludeKey != null) { + path += "&exclude=" + excludeKey; + } + return get(path, new TypeToken(){}.getType()); + } + + public CompletableFuture checkKeyImage(String keyImage) { + return get("/privacy/ring/key-image/" + keyImage, JsonObject.class) + .thenApply(response -> response.get("spent").getAsBoolean()); + } + + // ==================== Stealth Addresses ==================== + + public CompletableFuture generateStealthKeyPair() { + return post("/privacy/stealth/generate", new JsonObject(), StealthKeyPair.class); + } + + public CompletableFuture deriveStealthAddress(String spendPublicKey, String viewPublicKey) { + JsonObject body = new JsonObject(); + body.addProperty("spend_public_key", spendPublicKey); + body.addProperty("view_public_key", viewPublicKey); + return post("/privacy/stealth/derive", body, StealthAddress.class); + } + + public CompletableFuture recoverStealthPrivateKey(String stealthAddress, String viewPrivateKey, String spendPrivateKey) { + JsonObject body = new JsonObject(); + body.addProperty("stealth_address", stealthAddress); + body.addProperty("view_private_key", viewPrivateKey); + body.addProperty("spend_private_key", spendPrivateKey); + return post("/privacy/stealth/recover", body, JsonObject.class) + .thenApply(response -> response.get("private_key").getAsString()); + } + + public CompletableFuture scanOutputs(String viewPrivateKey, String spendPublicKey, long fromBlock, Long toBlock) { + JsonObject body = new JsonObject(); + body.addProperty("view_private_key", viewPrivateKey); + body.addProperty("spend_public_key", spendPublicKey); + body.addProperty("from_block", fromBlock); + if (toBlock != null) { + body.addProperty("to_block", toBlock); + } + return post("/privacy/stealth/scan", body, StealthOutput[].class); + } + + // ==================== Blinding ==================== + + public CompletableFuture generateBlindingFactor() { + return post("/privacy/blinding/generate", new JsonObject(), JsonObject.class) + .thenApply(response -> response.get("blinding_factor").getAsString()); + } + + public CompletableFuture blindValue(String value, String blindingFactor) { + JsonObject body = new JsonObject(); + body.addProperty("value", value); + body.addProperty("blinding_factor", blindingFactor); + return post("/privacy/blinding/blind", body, JsonObject.class) + .thenApply(response -> response.get("blinded_value").getAsString()); + } + + public CompletableFuture unblindValue(String blindedValue, String blindingFactor) { + JsonObject body = new JsonObject(); + body.addProperty("blinded_value", blindedValue); + body.addProperty("blinding_factor", blindingFactor); + return post("/privacy/blinding/unblind", body, JsonObject.class) + .thenApply(response -> response.get("value").getAsString()); + } + + // ==================== Lifecycle ==================== + + public CompletableFuture healthCheck() { + if (closed.get()) { + return CompletableFuture.completedFuture(false); + } + return get("/health", JsonObject.class) + .thenApply(response -> "healthy".equals(response.get("status").getAsString())) + .exceptionally(e -> false); + } + + @Override + public void close() { + closed.set(true); + httpClient.dispatcher().executorService().shutdown(); + httpClient.connectionPool().evictAll(); + } + + public boolean isClosed() { + return closed.get(); + } + + // ==================== HTTP Helpers ==================== + + private CompletableFuture get(String path, Type type) { + return executeRequest(path, null, type); + } + + private CompletableFuture post(String path, JsonObject body, Type type) { + return executeRequest(path, body, type); + } + + private CompletableFuture executeRequest(String path, JsonObject body, Type type) { + if (closed.get()) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new PrivacyException("Client has been closed")); + return future; + } + + CompletableFuture future = new CompletableFuture<>(); + + Request.Builder requestBuilder = new Request.Builder() + .url(config.getEndpoint() + path) + .header("Authorization", "Bearer " + config.getApiKey()) + .header("Content-Type", "application/json") + .header("X-SDK-Version", "java/" + SDK_VERSION); + + if (body != null) { + requestBuilder.post(RequestBody.create(gson.toJson(body), JSON)); + } else { + requestBuilder.get(); + } + + executeWithRetry(requestBuilder.build(), config.getRetries(), 0, future, type); + return future; + } + + private void executeWithRetry(Request request, int remainingRetries, int attempt, CompletableFuture future, Type type) { + httpClient.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + if (remainingRetries > 1) { + scheduleRetry(request, remainingRetries - 1, attempt + 1, future, type); + } else { + future.completeExceptionally(new PrivacyException("Request failed: " + e.getMessage(), e)); + } + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String bodyString = responseBody != null ? responseBody.string() : ""; + + if (response.isSuccessful()) { + T result = gson.fromJson(bodyString, type); + future.complete(result); + } else { + JsonObject errorObj = gson.fromJson(bodyString, JsonObject.class); + String message = errorObj.has("message") ? errorObj.get("message").getAsString() : "Unknown error"; + String code = errorObj.has("code") ? errorObj.get("code").getAsString() : null; + + if (remainingRetries > 1 && response.code() >= 500) { + scheduleRetry(request, remainingRetries - 1, attempt + 1, future, type); + } else { + future.completeExceptionally(new PrivacyException(message, response.code(), code)); + } + } + } + } + }); + } + + private void scheduleRetry(Request request, int remainingRetries, int attempt, CompletableFuture future, Type type) { + long delay = (long) Math.pow(2, attempt) * 1000; + CompletableFuture.delayedExecutor(delay, TimeUnit.MILLISECONDS) + .execute(() -> executeWithRetry(request, remainingRetries, attempt, future, type)); + } +} diff --git a/sdk/java/src/main/java/io/synor/privacy/PrivacyConfig.java b/sdk/java/src/main/java/io/synor/privacy/PrivacyConfig.java new file mode 100644 index 0000000..eb34054 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/privacy/PrivacyConfig.java @@ -0,0 +1,49 @@ +package io.synor.privacy; + +/** + * Configuration for the Privacy SDK client. + */ +public class PrivacyConfig { + private final String apiKey; + private String endpoint = "https://privacy.synor.io"; + private long timeoutMs = 30000; + private int retries = 3; + + public PrivacyConfig(String apiKey) { + if (apiKey == null || apiKey.isEmpty()) { + throw new IllegalArgumentException("API key is required"); + } + this.apiKey = apiKey; + } + + public PrivacyConfig endpoint(String endpoint) { + this.endpoint = endpoint; + return this; + } + + public PrivacyConfig timeoutMs(long timeoutMs) { + this.timeoutMs = timeoutMs; + return this; + } + + public PrivacyConfig retries(int retries) { + this.retries = retries; + return this; + } + + public String getApiKey() { + return apiKey; + } + + public String getEndpoint() { + return endpoint; + } + + public long getTimeoutMs() { + return timeoutMs; + } + + public int getRetries() { + return retries; + } +} diff --git a/sdk/java/src/main/java/io/synor/privacy/PrivacyException.java b/sdk/java/src/main/java/io/synor/privacy/PrivacyException.java new file mode 100644 index 0000000..46273c5 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/privacy/PrivacyException.java @@ -0,0 +1,35 @@ +package io.synor.privacy; + +/** + * Exception thrown by the Privacy SDK. + */ +public class PrivacyException extends RuntimeException { + private final Integer statusCode; + private final String errorCode; + + public PrivacyException(String message) { + super(message); + this.statusCode = null; + this.errorCode = null; + } + + public PrivacyException(String message, Throwable cause) { + super(message, cause); + this.statusCode = null; + this.errorCode = null; + } + + public PrivacyException(String message, int statusCode, String errorCode) { + super(message); + this.statusCode = statusCode; + this.errorCode = errorCode; + } + + public Integer getStatusCode() { + return statusCode; + } + + public String getErrorCode() { + return errorCode; + } +} diff --git a/sdk/java/src/main/java/io/synor/privacy/PrivacyTypes.java b/sdk/java/src/main/java/io/synor/privacy/PrivacyTypes.java new file mode 100644 index 0000000..017ddb8 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/privacy/PrivacyTypes.java @@ -0,0 +1,149 @@ +package io.synor.privacy; + +import com.google.gson.annotations.SerializedName; + +/** + * Privacy SDK type definitions. + */ +public class PrivacyTypes { + + // ==================== Confidential Transactions ==================== + + public static class ConfidentialTxInput { + @SerializedName("commitment") + public String commitment; + + @SerializedName("blinding_factor") + public String blindingFactor; + + @SerializedName("value") + public String value; + + @SerializedName("key_image") + public String keyImage; + } + + public static class ConfidentialTxOutput { + @SerializedName("commitment") + public String commitment; + + @SerializedName("blinding_factor") + public String blindingFactor; + + @SerializedName("value") + public String value; + + @SerializedName("recipient_public_key") + public String recipientPublicKey; + + @SerializedName("range_proof") + public String rangeProof; + } + + public static class ConfidentialTransaction { + @SerializedName("id") + public String id; + + @SerializedName("inputs") + public ConfidentialTxInput[] inputs; + + @SerializedName("outputs") + public ConfidentialTxOutput[] outputs; + + @SerializedName("fee") + public String fee; + + @SerializedName("excess") + public String excess; + + @SerializedName("excess_sig") + public String excessSig; + + @SerializedName("kernel_offset") + public String kernelOffset; + } + + // ==================== Commitments ==================== + + public static class Commitment { + @SerializedName("commitment") + public String commitment; + + @SerializedName("blinding_factor") + public String blindingFactor; + } + + public static class RangeProof { + @SerializedName("proof") + public String proof; + + @SerializedName("commitment") + public String commitment; + + @SerializedName("min_value") + public long minValue; + + @SerializedName("max_value") + public long maxValue; + } + + // ==================== Ring Signatures ==================== + + public static class RingSignature { + @SerializedName("c0") + public String c0; + + @SerializedName("s") + public String[] s; + + @SerializedName("key_image") + public String keyImage; + + @SerializedName("ring") + public String[] ring; + } + + // ==================== Stealth Addresses ==================== + + public static class StealthKeyPair { + @SerializedName("spend_public_key") + public String spendPublicKey; + + @SerializedName("spend_private_key") + public String spendPrivateKey; + + @SerializedName("view_public_key") + public String viewPublicKey; + + @SerializedName("view_private_key") + public String viewPrivateKey; + } + + public static class StealthAddress { + @SerializedName("address") + public String address; + + @SerializedName("ephemeral_public_key") + public String ephemeralPublicKey; + + @SerializedName("tx_public_key") + public String txPublicKey; + } + + public static class StealthOutput { + @SerializedName("tx_hash") + public String txHash; + + @SerializedName("output_index") + public int outputIndex; + + @SerializedName("stealth_address") + public String stealthAddress; + + @SerializedName("amount") + public String amount; + + @SerializedName("block_height") + public long blockHeight; + } +} diff --git a/sdk/js/src/contract/client.ts b/sdk/js/src/contract/client.ts new file mode 100644 index 0000000..dc35eab --- /dev/null +++ b/sdk/js/src/contract/client.ts @@ -0,0 +1,583 @@ +/** + * Synor Contract Client + */ + +import type { + ContractConfig, + Abi, + AbiEntry, + DeployContractOptions, + DeploymentResult, + CallContractOptions, + SendContractOptions, + TransactionResult, + EventLog, + DecodedEvent, + EventFilter, + EventCallback, + EventSubscription, + ContractInterface, + EncodeCallOptions, + DecodeResultOptions, + EstimateGasOptions, + GasEstimation, + BytecodeInfo, + VerifyContractOptions, + VerificationResult, + MulticallRequest, + MulticallResult, + ReadStorageOptions, +} from './types'; + +const DEFAULT_ENDPOINT = 'https://contract.synor.cc/api/v1'; + +/** + * Synor Contract SDK error. + */ +export class ContractError extends Error { + constructor( + message: string, + public statusCode?: number, + public code?: string + ) { + super(message); + this.name = 'ContractError'; + } +} + +/** + * Main Synor Contract client. + * + * Provides smart contract deployment, interaction, and event handling. + * + * @example + * ```ts + * const contract = new SynorContract({ apiKey: 'sk_...' }); + * + * // Deploy a contract + * const result = await contract.deploy({ + * bytecode: '0x608060...', + * abi: [...], + * args: [100, 'Token'], + * }); + * + * // Call a view function + * const balance = await contract.call({ + * address: result.address, + * method: 'balanceOf', + * args: ['0x...'], + * abi: [...], + * }); + * + * // Send a transaction + * const tx = await contract.send({ + * address: result.address, + * method: 'transfer', + * args: ['0x...', '1000000'], + * abi: [...], + * }); + * ``` + */ +export class SynorContract { + private config: Required> & { + defaultGasLimit?: string; + defaultGasPrice?: string; + }; + private closed = false; + private subscriptions = new Map(); + + constructor(config: ContractConfig) { + this.config = { + apiKey: config.apiKey, + endpoint: config.endpoint ?? DEFAULT_ENDPOINT, + timeout: config.timeout ?? 30000, + retries: config.retries ?? 3, + debug: config.debug ?? false, + defaultGasLimit: config.defaultGasLimit, + defaultGasPrice: config.defaultGasPrice, + }; + } + + // ==================== Deployment ==================== + + /** + * Deploy a smart contract. + * + * @param options - Deployment options + * @returns Deployment result with contract address + */ + async deploy(options: DeployContractOptions): Promise { + const response = await this.request('POST', '/contracts/deploy', { + bytecode: options.bytecode, + abi: options.abi, + args: options.args, + gasLimit: options.gasLimit ?? this.config.defaultGasLimit, + gasPrice: options.gasPrice ?? this.config.defaultGasPrice, + value: options.value, + salt: options.salt, + }); + + return response.deployment; + } + + /** + * Deploy using CREATE2 for deterministic addresses. + * + * @param options - Deployment options (salt required) + * @returns Deployment result + */ + async deployDeterministic(options: DeployContractOptions & { salt: string }): Promise { + return this.deploy(options); + } + + /** + * Predict CREATE2 deployment address. + * + * @param bytecode - Contract bytecode + * @param salt - Deployment salt + * @param constructorArgs - Constructor arguments + * @returns Predicted address + */ + async predictAddress(bytecode: string, salt: string, constructorArgs?: unknown[]): Promise { + const response = await this.request('POST', '/contracts/predict-address', { + bytecode, + salt, + constructorArgs, + }); + + return response.address; + } + + // ==================== Contract Interaction ==================== + + /** + * Call a view/pure function (no transaction). + * + * @param options - Call options + * @returns Function return value + */ + async call(options: CallContractOptions): Promise { + const response = await this.request('POST', '/contracts/call', { + address: options.address, + method: options.method, + args: options.args, + abi: options.abi, + blockNumber: options.blockNumber ?? 'latest', + }); + + return response.result; + } + + /** + * Send a state-changing transaction. + * + * @param options - Transaction options + * @returns Transaction result + */ + async send(options: SendContractOptions): Promise { + const response = await this.request('POST', '/contracts/send', { + address: options.address, + method: options.method, + args: options.args, + abi: options.abi, + gasLimit: options.gasLimit ?? this.config.defaultGasLimit, + gasPrice: options.gasPrice ?? this.config.defaultGasPrice, + value: options.value, + }); + + return response.transaction; + } + + /** + * Execute multiple calls in a single request. + * + * @param requests - Array of multicall requests + * @param abis - Optional ABIs for decoding (keyed by address) + * @returns Array of results + */ + async multicall( + requests: MulticallRequest[], + abis?: Record + ): Promise { + const response = await this.request('POST', '/contracts/multicall', { + calls: requests, + abis, + }); + + return response.results; + } + + // ==================== Events ==================== + + /** + * Get historical events. + * + * @param filter - Event filter + * @returns Array of decoded events + */ + async getEvents(filter: EventFilter): Promise { + const response = await this.request('POST', '/contracts/events', { + address: filter.address, + eventName: filter.eventName, + abi: filter.abi, + fromBlock: filter.fromBlock ?? 'earliest', + toBlock: filter.toBlock ?? 'latest', + filter: filter.filter, + }); + + return response.events; + } + + /** + * Get raw event logs. + * + * @param filter - Event filter + * @returns Array of raw logs + */ + async getLogs(filter: EventFilter): Promise { + const response = await this.request('POST', '/contracts/logs', { + address: filter.address, + eventName: filter.eventName, + abi: filter.abi, + fromBlock: filter.fromBlock ?? 'earliest', + toBlock: filter.toBlock ?? 'latest', + filter: filter.filter, + }); + + return response.logs; + } + + /** + * Subscribe to contract events. + * + * @param filter - Event filter + * @param callback - Callback for new events + * @returns Subscription handle + */ + async subscribeEvents(filter: EventFilter, callback: EventCallback): Promise { + const wsEndpoint = this.config.endpoint.replace('https://', 'wss://').replace('http://', 'ws://'); + const ws = new WebSocket(`${wsEndpoint}/contracts/events/subscribe`); + + const id = crypto.randomUUID(); + this.subscriptions.set(id, ws); + + return new Promise((resolve, reject) => { + ws.onopen = () => { + ws.send( + JSON.stringify({ + type: 'subscribe', + auth: this.config.apiKey, + filter: { + address: filter.address, + eventName: filter.eventName, + abi: filter.abi, + filter: filter.filter, + }, + }) + ); + + resolve({ + id, + unsubscribe: async () => { + ws.close(); + this.subscriptions.delete(id); + }, + }); + }; + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + if (data.type === 'event') { + callback(data.event); + } + } catch (e) { + if (this.config.debug) { + console.error('[SynorContract] Failed to parse event:', e); + } + } + }; + + ws.onerror = (error) => { + reject(new ContractError('WebSocket error')); + }; + }); + } + + /** + * Decode a raw event log. + * + * @param log - Raw log + * @param abi - Contract ABI + * @returns Decoded event + */ + async decodeLog(log: EventLog, abi: Abi): Promise { + const response = await this.request('POST', '/contracts/decode-log', { + log, + abi, + }); + + return response.event; + } + + // ==================== ABI Utilities ==================== + + /** + * Load and parse a contract ABI. + * + * @param abi - Contract ABI + * @returns Parsed contract interface + */ + loadAbi(abi: Abi): ContractInterface { + const functions: Record = {}; + const events: Record = {}; + const errors: Record = {}; + + for (const entry of abi) { + if (entry.type === 'function' && entry.name) { + functions[entry.name] = entry; + } else if (entry.type === 'event' && entry.name) { + events[entry.name] = entry; + } else if (entry.type === 'error' && entry.name) { + errors[entry.name] = entry; + } + } + + return { abi, functions, events, errors }; + } + + /** + * Encode a function call. + * + * @param options - Encoding options + * @returns Encoded calldata (hex) + */ + async encodeCall(options: EncodeCallOptions): Promise { + const response = await this.request('POST', '/contracts/encode', { + method: options.method, + args: options.args, + abi: options.abi, + }); + + return response.data; + } + + /** + * Decode function return data. + * + * @param options - Decoding options + * @returns Decoded result + */ + async decodeResult(options: DecodeResultOptions): Promise { + const response = await this.request('POST', '/contracts/decode', { + method: options.method, + data: options.data, + abi: options.abi, + }); + + return response.result; + } + + /** + * Get function selector (first 4 bytes of keccak256 hash). + * + * @param signature - Function signature (e.g., "transfer(address,uint256)") + * @returns Function selector (hex) + */ + async getFunctionSelector(signature: string): Promise { + const response = await this.request('POST', '/contracts/selector', { + signature, + }); + + return response.selector; + } + + // ==================== Gas Estimation ==================== + + /** + * Estimate gas for a contract call or deployment. + * + * @param options - Estimation options + * @returns Gas estimation + */ + async estimateGas(options: EstimateGasOptions): Promise { + const response = await this.request('POST', '/contracts/estimate-gas', { + address: options.address, + method: options.method, + args: options.args, + abi: options.abi, + bytecode: options.bytecode, + value: options.value, + from: options.from, + }); + + return response.estimation; + } + + // ==================== Contract Info ==================== + + /** + * Get contract bytecode. + * + * @param address - Contract address + * @returns Bytecode info + */ + async getBytecode(address: string): Promise { + const response = await this.request('GET', `/contracts/${address}/bytecode`); + return response; + } + + /** + * Check if an address is a contract. + * + * @param address - Address to check + * @returns True if address has code + */ + async isContract(address: string): Promise { + const response = await this.request('GET', `/contracts/${address}/is-contract`); + return response.isContract; + } + + /** + * Read contract storage slot. + * + * @param options - Storage read options + * @returns Storage value (hex) + */ + async readStorage(options: ReadStorageOptions): Promise { + const response = await this.request('POST', '/contracts/storage', { + address: options.address, + slot: options.slot, + blockNumber: options.blockNumber ?? 'latest', + }); + + return response.value; + } + + // ==================== Verification ==================== + + /** + * Submit contract for verification. + * + * @param options - Verification options + * @returns Verification result + */ + async verifyContract(options: VerifyContractOptions): Promise { + const response = await this.request('POST', '/contracts/verify', { + address: options.address, + sourceCode: options.sourceCode, + compilerVersion: options.compilerVersion, + constructorArguments: options.constructorArguments, + optimization: options.optimization, + optimizationRuns: options.optimizationRuns, + contractName: options.contractName, + }); + + return response; + } + + /** + * Check verification status. + * + * @param address - Contract address + * @returns Verification result + */ + async getVerificationStatus(address: string): Promise { + const response = await this.request('GET', `/contracts/${address}/verification`); + return response; + } + + /** + * Get verified contract ABI. + * + * @param address - Contract address + * @returns Contract ABI (if verified) + */ + async getVerifiedAbi(address: string): Promise { + try { + const response = await this.request('GET', `/contracts/${address}/abi`); + return response.abi; + } catch { + return null; + } + } + + // ==================== Lifecycle ==================== + + /** + * Check if the contract service is healthy. + * + * @returns True if the service is operational + */ + async healthCheck(): Promise { + try { + const response = await this.request('GET', '/health'); + return response.status === 'healthy'; + } catch { + return false; + } + } + + /** + * Close the client and release resources. + */ + close(): void { + this.closed = true; + for (const ws of this.subscriptions.values()) { + ws.close(); + } + this.subscriptions.clear(); + } + + /** + * Check if the client has been closed. + */ + isClosed(): boolean { + return this.closed; + } + + // ==================== Private ==================== + + private async request(method: string, path: string, body?: unknown): Promise { + if (this.closed) { + throw new ContractError('Client has been closed'); + } + + const url = `${this.config.endpoint}${path}`; + + if (this.config.debug) { + console.log(`[SynorContract] ${method} ${url}`); + } + + let lastError: Error | null = null; + + for (let attempt = 0; attempt < this.config.retries; attempt++) { + try { + const response = await fetch(url, { + method, + headers: { + Authorization: `Bearer ${this.config.apiKey}`, + 'Content-Type': 'application/json', + 'X-SDK-Version': 'js/1.0.0', + }, + body: body ? JSON.stringify(body) : undefined, + signal: AbortSignal.timeout(this.config.timeout), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: response.statusText })); + throw new ContractError(error.message || 'Request failed', response.status, error.code); + } + + return response.json(); + } catch (error) { + lastError = error as Error; + if (attempt < this.config.retries - 1) { + await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1000)); + } + } + } + + throw lastError; + } +} diff --git a/sdk/js/src/contract/index.ts b/sdk/js/src/contract/index.ts new file mode 100644 index 0000000..975cdec --- /dev/null +++ b/sdk/js/src/contract/index.ts @@ -0,0 +1,42 @@ +/** + * Synor Contract SDK + * + * Smart contract deployment, interaction, and event handling: + * - Deploy contracts (standard and CREATE2) + * - Call view/pure functions + * - Send state-changing transactions + * - Subscribe to events + * - ABI encoding/decoding utilities + * + * @packageDocumentation + */ + +export { SynorContract, ContractError } from './client'; +export type { + ContractConfig, + AbiEntryType, + AbiParameter, + AbiEntry, + Abi, + DeployContractOptions, + DeploymentResult, + CallContractOptions, + SendContractOptions, + TransactionResult, + EventLog, + DecodedEvent, + EventFilter, + EventCallback, + EventSubscription, + ContractInterface, + EncodeCallOptions, + DecodeResultOptions, + EstimateGasOptions, + GasEstimation, + BytecodeInfo, + VerifyContractOptions, + VerificationResult, + MulticallRequest, + MulticallResult, + ReadStorageOptions, +} from './types'; diff --git a/sdk/js/src/contract/types.ts b/sdk/js/src/contract/types.ts new file mode 100644 index 0000000..6b139c5 --- /dev/null +++ b/sdk/js/src/contract/types.ts @@ -0,0 +1,332 @@ +/** + * Synor Contract SDK Types + */ + +/** Contract SDK configuration */ +export interface ContractConfig { + /** API key for authentication */ + apiKey: string; + /** API endpoint (defaults to production) */ + endpoint?: string; + /** Request timeout in milliseconds */ + timeout?: number; + /** Number of retries for failed requests */ + retries?: number; + /** Enable debug logging */ + debug?: boolean; + /** Default gas limit */ + defaultGasLimit?: string; + /** Default gas price */ + defaultGasPrice?: string; +} + +/** ABI entry types */ +export type AbiEntryType = 'function' | 'constructor' | 'event' | 'error' | 'fallback' | 'receive'; + +/** ABI input/output parameter */ +export interface AbiParameter { + /** Parameter name */ + name: string; + /** Parameter type */ + type: string; + /** Indexed (for events) */ + indexed?: boolean; + /** Component types (for tuples) */ + components?: AbiParameter[]; + /** Internal type */ + internalType?: string; +} + +/** ABI entry */ +export interface AbiEntry { + /** Entry type */ + type: AbiEntryType; + /** Function/event name */ + name?: string; + /** Input parameters */ + inputs?: AbiParameter[]; + /** Output parameters */ + outputs?: AbiParameter[]; + /** State mutability */ + stateMutability?: 'pure' | 'view' | 'nonpayable' | 'payable'; + /** Anonymous (for events) */ + anonymous?: boolean; +} + +/** Contract ABI */ +export type Abi = AbiEntry[]; + +/** Deploy contract options */ +export interface DeployContractOptions { + /** Compiled bytecode (hex) */ + bytecode: string; + /** Contract ABI */ + abi?: Abi; + /** Constructor arguments */ + args?: unknown[]; + /** Gas limit */ + gasLimit?: string; + /** Gas price */ + gasPrice?: string; + /** Value to send with deployment */ + value?: string; + /** Salt for deterministic deployment (CREATE2) */ + salt?: string; +} + +/** Deployment result */ +export interface DeploymentResult { + /** Deployed contract address */ + address: string; + /** Deployment transaction hash */ + transactionHash: string; + /** Block number of deployment */ + blockNumber: number; + /** Gas used */ + gasUsed: string; + /** Effective gas price */ + effectiveGasPrice: string; +} + +/** Call contract options */ +export interface CallContractOptions { + /** Contract address */ + address: string; + /** Method name */ + method: string; + /** Method arguments */ + args?: unknown[]; + /** Contract ABI (required for encoding) */ + abi: Abi; + /** Block number or 'latest' */ + blockNumber?: number | 'latest'; +} + +/** Send transaction options */ +export interface SendContractOptions { + /** Contract address */ + address: string; + /** Method name */ + method: string; + /** Method arguments */ + args?: unknown[]; + /** Contract ABI (required for encoding) */ + abi: Abi; + /** Gas limit */ + gasLimit?: string; + /** Gas price */ + gasPrice?: string; + /** Value to send */ + value?: string; +} + +/** Transaction result */ +export interface TransactionResult { + /** Transaction hash */ + transactionHash: string; + /** Block number */ + blockNumber: number; + /** Block hash */ + blockHash: string; + /** Gas used */ + gasUsed: string; + /** Effective gas price */ + effectiveGasPrice: string; + /** Transaction status */ + status: 'success' | 'reverted'; + /** Logs emitted */ + logs: EventLog[]; + /** Return value (if any) */ + returnValue?: unknown; + /** Revert reason (if reverted) */ + revertReason?: string; +} + +/** Event log */ +export interface EventLog { + /** Log index */ + logIndex: number; + /** Contract address */ + address: string; + /** Raw topics */ + topics: string[]; + /** Raw data */ + data: string; + /** Block number */ + blockNumber: number; + /** Transaction hash */ + transactionHash: string; +} + +/** Decoded event */ +export interface DecodedEvent { + /** Event name */ + name: string; + /** Event signature */ + signature: string; + /** Decoded arguments */ + args: Record; + /** Raw log */ + log: EventLog; +} + +/** Event filter */ +export interface EventFilter { + /** Contract address */ + address: string; + /** Event name (optional) */ + eventName?: string; + /** Contract ABI (required for decoding) */ + abi?: Abi; + /** From block */ + fromBlock?: number | 'earliest'; + /** To block */ + toBlock?: number | 'latest'; + /** Indexed filter values */ + filter?: Record; +} + +/** Event subscription callback */ +export type EventCallback = (event: DecodedEvent) => void; + +/** Event subscription */ +export interface EventSubscription { + /** Subscription ID */ + id: string; + /** Unsubscribe */ + unsubscribe: () => Promise; +} + +/** Contract interface (from ABI) */ +export interface ContractInterface { + /** Contract ABI */ + abi: Abi; + /** Function signatures */ + functions: Record; + /** Event signatures */ + events: Record; + /** Error signatures */ + errors: Record; +} + +/** Encode call options */ +export interface EncodeCallOptions { + /** Method name */ + method: string; + /** Arguments */ + args: unknown[]; + /** Contract ABI */ + abi: Abi; +} + +/** Decode result options */ +export interface DecodeResultOptions { + /** Method name */ + method: string; + /** Raw data to decode */ + data: string; + /** Contract ABI */ + abi: Abi; +} + +/** Gas estimation options */ +export interface EstimateGasOptions { + /** Contract address (for calls) */ + address?: string; + /** Method name */ + method?: string; + /** Arguments */ + args?: unknown[]; + /** Contract ABI */ + abi?: Abi; + /** Bytecode (for deployment) */ + bytecode?: string; + /** Value to send */ + value?: string; + /** Sender address */ + from?: string; +} + +/** Gas estimation result */ +export interface GasEstimation { + /** Estimated gas units */ + gasLimit: string; + /** Current gas price */ + gasPrice: string; + /** Estimated cost */ + estimatedCost: string; +} + +/** Contract bytecode info */ +export interface BytecodeInfo { + /** Raw bytecode */ + bytecode: string; + /** Deployed bytecode (without constructor) */ + deployedBytecode?: string; + /** ABI */ + abi?: Abi; + /** Source metadata */ + metadata?: { + compiler: string; + language: string; + sources: string[]; + }; +} + +/** Contract verification options */ +export interface VerifyContractOptions { + /** Contract address */ + address: string; + /** Source code */ + sourceCode: string; + /** Compiler version */ + compilerVersion: string; + /** Constructor arguments (hex) */ + constructorArguments?: string; + /** Optimization enabled */ + optimization?: boolean; + /** Optimization runs */ + optimizationRuns?: number; + /** Contract name */ + contractName?: string; +} + +/** Contract verification result */ +export interface VerificationResult { + /** Verification status */ + status: 'verified' | 'pending' | 'failed'; + /** Verification message */ + message: string; + /** ABI (if verified) */ + abi?: Abi; +} + +/** Multicall request */ +export interface MulticallRequest { + /** Contract address */ + address: string; + /** Encoded call data */ + callData: string; + /** Allow failure */ + allowFailure?: boolean; +} + +/** Multicall result */ +export interface MulticallResult { + /** Success status */ + success: boolean; + /** Return data */ + returnData: string; + /** Decoded result (if ABI provided) */ + decoded?: unknown; +} + +/** Storage read options */ +export interface ReadStorageOptions { + /** Contract address */ + address: string; + /** Storage slot */ + slot: string; + /** Block number */ + blockNumber?: number | 'latest'; +} diff --git a/sdk/js/src/privacy/client.ts b/sdk/js/src/privacy/client.ts new file mode 100644 index 0000000..20d38b6 --- /dev/null +++ b/sdk/js/src/privacy/client.ts @@ -0,0 +1,530 @@ +/** + * Synor Privacy Client + */ + +import type { + PrivacyConfig, + ConfidentialUTXO, + ConfidentialOutput, + ConfidentialTransaction, + CreateConfidentialTxOptions, + VerifyConfidentialTxResult, + RingSignature, + CreateRingSignatureOptions, + VerifyRingSignatureOptions, + VerifyRingSignatureResult, + StealthAddress, + StealthKeypair, + OneTimeAddress, + DeriveSharedSecretOptions, + SharedSecret, + Commitment, + CreateCommitmentOptions, + CommitmentWithBlinding, + OpenCommitmentOptions, + RangeProof, + CreateRangeProofOptions, + VerifyRangeProofResult, + BlindingSumOptions, +} from './types'; + +const DEFAULT_ENDPOINT = 'https://privacy.synor.cc/api/v1'; + +/** + * Synor Privacy SDK error. + */ +export class PrivacyError extends Error { + constructor( + message: string, + public statusCode?: number, + public code?: string + ) { + super(message); + this.name = 'PrivacyError'; + } +} + +/** + * Main Synor Privacy client. + * + * Provides privacy-enhancing features including confidential transactions, + * ring signatures, stealth addresses, and Pedersen commitments. + * + * @example + * ```ts + * const privacy = new SynorPrivacy({ apiKey: 'sk_...' }); + * + * // Generate a stealth address for receiving private payments + * const keypair = await privacy.generateStealthKeypair(); + * console.log('Stealth address:', keypair.address.address); + * + * // Create a ring signature for anonymous signing + * const signature = await privacy.createRingSignature({ + * message: 'Hello, World!', + * ring: [pubKey1, pubKey2, myPubKey, pubKey3], + * privateKey: myPrivateKey, + * }); + * ``` + */ +export class SynorPrivacy { + private config: Required; + private closed = false; + + constructor(config: PrivacyConfig) { + this.config = { + apiKey: config.apiKey, + endpoint: config.endpoint ?? DEFAULT_ENDPOINT, + timeout: config.timeout ?? 30000, + retries: config.retries ?? 3, + debug: config.debug ?? false, + }; + } + + // ==================== Confidential Transactions ==================== + + /** + * Create a confidential transaction with hidden amounts. + * + * Uses Pedersen commitments and Bulletproof range proofs to hide + * transaction amounts while proving they are valid (non-negative). + * + * @param options - Transaction options + * @returns Confidential transaction ready for broadcast + */ + async createConfidentialTransaction( + options: CreateConfidentialTxOptions + ): Promise { + const response = await this.request('POST', '/transactions/confidential', { + inputs: options.inputs.map((input) => ({ + txid: input.txid, + vout: input.vout, + commitment: input.commitment, + rangeProof: input.rangeProof, + blinding: input.blinding, + amount: input.amount, + })), + outputs: options.outputs.map((output) => ({ + recipient: output.recipient, + amount: output.amount, + blinding: output.blinding, + })), + fee: options.fee, + lockHeight: options.lockHeight, + }); + + return response.transaction; + } + + /** + * Verify a confidential transaction. + * + * Checks that: + * - Commitments balance (inputs = outputs + fee) + * - All range proofs are valid (no negative amounts) + * - Kernel signature is valid + * - No duplicate inputs + * + * @param transaction - Transaction to verify + * @returns Verification result with details + */ + async verifyConfidentialTransaction( + transaction: ConfidentialTransaction + ): Promise { + const response = await this.request('POST', '/transactions/confidential/verify', { + transaction, + }); + + return response; + } + + /** + * Decode a confidential transaction output. + * + * If you have the blinding factor, you can decode the committed amount. + * + * @param commitment - Output commitment + * @param blinding - Blinding factor + * @returns Decoded amount + */ + async decodeConfidentialOutput(commitment: string, blinding: string): Promise { + const response = await this.request('POST', '/transactions/confidential/decode', { + commitment, + blinding, + }); + + return response.amount; + } + + // ==================== Ring Signatures ==================== + + /** + * Create a ring signature. + * + * Ring signatures allow signing a message as "one of N" without + * revealing which member of the ring actually signed. + * + * @param options - Signing options + * @returns Ring signature + */ + async createRingSignature(options: CreateRingSignatureOptions): Promise { + const response = await this.request('POST', '/ring-signatures/create', { + message: options.message, + ring: options.ring, + privateKey: options.privateKey, + signerIndex: options.signerIndex, + }); + + return response.signature; + } + + /** + * Verify a ring signature. + * + * Verifies that the signature was created by one of the ring members + * without revealing which one. + * + * @param options - Verification options + * @returns Verification result + */ + async verifyRingSignature(options: VerifyRingSignatureOptions): Promise { + const response = await this.request('POST', '/ring-signatures/verify', { + signature: options.signature, + message: options.message, + }); + + return response; + } + + /** + * Check if a key image has been used (double-spend detection). + * + * @param keyImage - Key image from ring signature + * @returns True if the key image has been seen before + */ + async isKeyImageUsed(keyImage: string): Promise { + const response = await this.request('GET', `/ring-signatures/key-images/${keyImage}`); + return response.used; + } + + /** + * Generate a random ring from available public keys. + * + * Useful for creating decoy sets for ring signatures. + * + * @param size - Desired ring size + * @param exclude - Public keys to exclude (e.g., your own) + * @returns Array of public keys for the ring + */ + async generateRandomRing(size: number, exclude?: string[]): Promise { + const response = await this.request('POST', '/ring-signatures/random-ring', { + size, + exclude, + }); + + return response.ring; + } + + // ==================== Stealth Addresses ==================== + + /** + * Generate a new stealth address keypair. + * + * Stealth addresses allow receiving payments at unique one-time + * addresses that cannot be linked to the recipient's public address. + * + * @returns Stealth keypair (keep private keys secure!) + */ + async generateStealthKeypair(): Promise { + const response = await this.request('POST', '/stealth/generate'); + return response.keypair; + } + + /** + * Create a one-time address for sending to a stealth address. + * + * The sender creates this address using the recipient's stealth + * address. Only the recipient can detect and spend from it. + * + * @param stealthAddress - Recipient's stealth address + * @returns One-time address and ephemeral key + */ + async createOneTimeAddress(stealthAddress: StealthAddress): Promise { + const response = await this.request('POST', '/stealth/one-time-address', { + scanPublicKey: stealthAddress.scanPublicKey, + spendPublicKey: stealthAddress.spendPublicKey, + }); + + return response.oneTimeAddress; + } + + /** + * Derive the shared secret for a stealth payment. + * + * The recipient uses this to derive the one-time private key + * needed to spend from the one-time address. + * + * @param options - Derivation options + * @returns Shared secret and derived keys + */ + async deriveSharedSecret(options: DeriveSharedSecretOptions): Promise { + const response = await this.request('POST', '/stealth/derive-secret', { + scanPublicKey: options.stealthAddress.scanPublicKey, + spendPublicKey: options.stealthAddress.spendPublicKey, + privateKey: options.privateKey, + ephemeralPublicKey: options.ephemeralPublicKey, + }); + + return response; + } + + /** + * Scan transactions to find stealth payments to you. + * + * @param scanPrivateKey - Your scan private key + * @param spendPublicKey - Your spend public key + * @param transactions - Transaction IDs to scan + * @returns Found payments with one-time addresses + */ + async scanForPayments( + scanPrivateKey: string, + spendPublicKey: string, + transactions: string[] + ): Promise { + const response = await this.request('POST', '/stealth/scan', { + scanPrivateKey, + spendPublicKey, + transactions, + }); + + return response.payments; + } + + // ==================== Pedersen Commitments ==================== + + /** + * Create a Pedersen commitment. + * + * C = vG + bH where v is the value and b is the blinding factor. + * + * @param options - Commitment options + * @returns Commitment and blinding factor + */ + async createCommitment(options: CreateCommitmentOptions): Promise { + const response = await this.request('POST', '/commitments/create', { + value: options.value, + blinding: options.blinding, + }); + + return response; + } + + /** + * Open (verify) a Pedersen commitment. + * + * Verifies that the commitment commits to the claimed value. + * + * @param options - Opening options + * @returns True if the commitment opens correctly + */ + async openCommitment(options: OpenCommitmentOptions): Promise { + const response = await this.request('POST', '/commitments/open', { + commitment: options.commitment, + value: options.value, + blinding: options.blinding, + }); + + return response.valid; + } + + /** + * Add two commitments. + * + * C1 + C2 = C3 where the values and blindings also add. + * + * @param commitment1 - First commitment + * @param commitment2 - Second commitment + * @returns Sum commitment + */ + async addCommitments(commitment1: string, commitment2: string): Promise { + const response = await this.request('POST', '/commitments/add', { + commitment1, + commitment2, + }); + + return response.commitment; + } + + /** + * Subtract two commitments. + * + * @param commitment1 - First commitment + * @param commitment2 - Second commitment + * @returns Difference commitment + */ + async subtractCommitments(commitment1: string, commitment2: string): Promise { + const response = await this.request('POST', '/commitments/subtract', { + commitment1, + commitment2, + }); + + return response.commitment; + } + + /** + * Compute blinding factor sum for transaction balancing. + * + * For a balanced transaction: sum(input blindings) - sum(output blindings) = kernel blinding + * + * @param options - Blinding factors + * @returns Net blinding factor + */ + async computeBlindingSum(options: BlindingSumOptions): Promise { + const response = await this.request('POST', '/commitments/blinding-sum', { + positive: options.positive, + negative: options.negative, + }); + + return response.sum; + } + + /** + * Generate a random blinding factor. + * + * @returns Random 32-byte blinding factor (hex) + */ + async generateBlinding(): Promise { + const response = await this.request('POST', '/commitments/random-blinding'); + return response.blinding; + } + + // ==================== Range Proofs ==================== + + /** + * Create a Bulletproof range proof. + * + * Proves that a committed value is in range [0, 2^n) without + * revealing the actual value. + * + * @param options - Range proof options + * @returns Range proof + */ + async createRangeProof(options: CreateRangeProofOptions): Promise { + const response = await this.request('POST', '/range-proofs/create', { + value: options.value, + blinding: options.blinding, + message: options.message, + bitLength: options.bitLength ?? 64, + }); + + return response.proof; + } + + /** + * Verify a Bulletproof range proof. + * + * @param commitment - Commitment the proof is for + * @param proof - Range proof data + * @returns Verification result + */ + async verifyRangeProof(commitment: string, proof: string): Promise { + const response = await this.request('POST', '/range-proofs/verify', { + commitment, + proof, + }); + + return response; + } + + /** + * Create an aggregated range proof for multiple outputs. + * + * More efficient than individual proofs for multiple outputs. + * + * @param outputs - Array of {value, blinding} pairs + * @returns Aggregated proof + */ + async createAggregatedRangeProof( + outputs: Array<{ value: string; blinding: string }> + ): Promise { + const response = await this.request('POST', '/range-proofs/aggregate', { + outputs, + }); + + return response.proof; + } + + // ==================== Lifecycle ==================== + + /** + * Check if the privacy service is healthy. + * + * @returns True if the service is operational + */ + async healthCheck(): Promise { + try { + const response = await this.request('GET', '/health'); + return response.status === 'healthy'; + } catch { + return false; + } + } + + /** + * Close the client and release resources. + */ + close(): void { + this.closed = true; + } + + /** + * Check if the client has been closed. + */ + isClosed(): boolean { + return this.closed; + } + + // ==================== Private ==================== + + private async request(method: string, path: string, body?: unknown): Promise { + if (this.closed) { + throw new PrivacyError('Client has been closed'); + } + + const url = `${this.config.endpoint}${path}`; + + if (this.config.debug) { + console.log(`[SynorPrivacy] ${method} ${url}`); + } + + let lastError: Error | null = null; + + for (let attempt = 0; attempt < this.config.retries; attempt++) { + try { + const response = await fetch(url, { + method, + headers: { + Authorization: `Bearer ${this.config.apiKey}`, + 'Content-Type': 'application/json', + 'X-SDK-Version': 'js/1.0.0', + }, + body: body ? JSON.stringify(body) : undefined, + signal: AbortSignal.timeout(this.config.timeout), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: response.statusText })); + throw new PrivacyError(error.message || 'Request failed', response.status, error.code); + } + + return response.json(); + } catch (error) { + lastError = error as Error; + if (attempt < this.config.retries - 1) { + await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1000)); + } + } + } + + throw lastError; + } +} diff --git a/sdk/js/src/privacy/index.ts b/sdk/js/src/privacy/index.ts new file mode 100644 index 0000000..fe498f1 --- /dev/null +++ b/sdk/js/src/privacy/index.ts @@ -0,0 +1,44 @@ +/** + * Synor Privacy SDK + * + * Privacy-enhancing features for the Synor blockchain: + * - Confidential transactions with hidden amounts + * - Ring signatures for anonymous signing + * - Stealth addresses for unlinkable payments + * - Pedersen commitments and Bulletproof range proofs + * + * @packageDocumentation + */ + +export { SynorPrivacy, PrivacyError } from './client'; +export type { + PrivacyConfig, + PublicKey, + PrivateKey, + ConfidentialUTXO, + ConfidentialOutput, + ConfidentialTransaction, + ConfidentialTxInput, + ConfidentialTxOutput, + OutputFeatures, + TransactionKernel, + CreateConfidentialTxOptions, + VerifyConfidentialTxResult, + RingSignature, + CreateRingSignatureOptions, + VerifyRingSignatureOptions, + VerifyRingSignatureResult, + StealthAddress, + StealthKeypair, + OneTimeAddress, + DeriveSharedSecretOptions, + SharedSecret, + Commitment, + CreateCommitmentOptions, + CommitmentWithBlinding, + OpenCommitmentOptions, + RangeProof, + CreateRangeProofOptions, + VerifyRangeProofResult, + BlindingSumOptions, +} from './types'; diff --git a/sdk/js/src/privacy/types.ts b/sdk/js/src/privacy/types.ts new file mode 100644 index 0000000..7915279 --- /dev/null +++ b/sdk/js/src/privacy/types.ts @@ -0,0 +1,321 @@ +/** + * Synor Privacy SDK Types + */ + +/** Privacy SDK configuration */ +export interface PrivacyConfig { + /** API key for authentication */ + apiKey: string; + /** API endpoint (defaults to production) */ + endpoint?: string; + /** Request timeout in milliseconds */ + timeout?: number; + /** Number of retries for failed requests */ + retries?: number; + /** Enable debug logging */ + debug?: boolean; +} + +/** Public key representation */ +export interface PublicKey { + /** Hex-encoded public key */ + key: string; + /** Key type */ + type: 'ed25519' | 'secp256k1'; +} + +/** Private key representation (for signing operations) */ +export interface PrivateKey { + /** Hex-encoded private key */ + key: string; + /** Key type */ + type: 'ed25519' | 'secp256k1'; +} + +/** UTXO for confidential transaction input */ +export interface ConfidentialUTXO { + /** Transaction hash */ + txid: string; + /** Output index */ + vout: number; + /** Commitment to the amount */ + commitment: string; + /** Range proof */ + rangeProof: string; + /** Blinding factor (private) */ + blinding?: string; + /** Clear amount (private, known only to owner) */ + amount?: string; +} + +/** Output for confidential transaction */ +export interface ConfidentialOutput { + /** Recipient stealth address or public key */ + recipient: string; + /** Amount to send */ + amount: string; + /** Optional blinding factor (generated if not provided) */ + blinding?: string; +} + +/** Confidential transaction */ +export interface ConfidentialTransaction { + /** Transaction ID */ + txid: string; + /** Version */ + version: number; + /** Inputs with commitments */ + inputs: ConfidentialTxInput[]; + /** Outputs with commitments */ + outputs: ConfidentialTxOutput[]; + /** Kernel with signature */ + kernel: TransactionKernel; + /** Transaction offset for signature aggregation */ + offset: string; + /** Raw transaction hex */ + raw: string; +} + +/** Confidential transaction input */ +export interface ConfidentialTxInput { + /** Reference to previous output */ + outputRef: string; + /** Commitment */ + commitment: string; +} + +/** Confidential transaction output */ +export interface ConfidentialTxOutput { + /** Pedersen commitment to the amount */ + commitment: string; + /** Bulletproof range proof */ + rangeProof: string; + /** Output features */ + features: OutputFeatures; +} + +/** Output features for confidential transactions */ +export interface OutputFeatures { + /** Feature flags */ + flags: number; + /** Lock height (if time-locked) */ + lockHeight?: number; +} + +/** Transaction kernel for confidential transactions */ +export interface TransactionKernel { + /** Kernel features */ + features: number; + /** Transaction fee */ + fee: string; + /** Lock height */ + lockHeight: number; + /** Excess commitment */ + excess: string; + /** Excess signature */ + excessSignature: string; +} + +/** Create confidential transaction options */ +export interface CreateConfidentialTxOptions { + /** Input UTXOs */ + inputs: ConfidentialUTXO[]; + /** Outputs to create */ + outputs: ConfidentialOutput[]; + /** Transaction fee */ + fee?: string; + /** Lock height */ + lockHeight?: number; +} + +/** Confidential transaction verification result */ +export interface VerifyConfidentialTxResult { + /** Whether the transaction is valid */ + valid: boolean; + /** Verification details */ + details: { + /** Commitments balance (inputs = outputs + fee) */ + commitmentsBalance: boolean; + /** All range proofs are valid */ + rangeProofsValid: boolean; + /** Kernel signature is valid */ + signatureValid: boolean; + /** No duplicate inputs */ + noDuplicateInputs: boolean; + }; + /** Error message if invalid */ + error?: string; +} + +/** Ring signature */ +export interface RingSignature { + /** Signature ID */ + id: string; + /** Message hash that was signed */ + messageHash: string; + /** Ring members (public keys) */ + ring: string[]; + /** Key image (for double-spend prevention) */ + keyImage: string; + /** Signature components */ + signature: { + /** Challenge values */ + c: string[]; + /** Response values */ + r: string[]; + }; + /** Ring size */ + ringSize: number; +} + +/** Create ring signature options */ +export interface CreateRingSignatureOptions { + /** Message to sign (hex or string) */ + message: string; + /** Ring of public keys (must include signer's key) */ + ring: string[]; + /** Signer's private key */ + privateKey: string; + /** Index of signer's key in ring (for optimization) */ + signerIndex?: number; +} + +/** Verify ring signature options */ +export interface VerifyRingSignatureOptions { + /** Ring signature to verify */ + signature: RingSignature; + /** Original message */ + message: string; +} + +/** Ring signature verification result */ +export interface VerifyRingSignatureResult { + /** Whether the signature is valid */ + valid: boolean; + /** Key image (can be used to detect double-signing) */ + keyImage: string; +} + +/** Stealth address */ +export interface StealthAddress { + /** One-time public address */ + address: string; + /** Scan public key (for detecting incoming payments) */ + scanPublicKey: string; + /** Spend public key */ + spendPublicKey: string; +} + +/** Stealth address keypair */ +export interface StealthKeypair { + /** Stealth address */ + address: StealthAddress; + /** Scan private key */ + scanPrivateKey: string; + /** Spend private key */ + spendPrivateKey: string; +} + +/** One-time address derived from stealth address */ +export interface OneTimeAddress { + /** The one-time address */ + address: string; + /** Ephemeral public key (included in transaction) */ + ephemeralPublicKey: string; + /** Shared secret (for sender) */ + sharedSecret?: string; +} + +/** Derive shared secret options */ +export interface DeriveSharedSecretOptions { + /** Stealth address */ + stealthAddress: StealthAddress; + /** Private key for derivation */ + privateKey: string; + /** Ephemeral public key from transaction */ + ephemeralPublicKey: string; +} + +/** Shared secret result */ +export interface SharedSecret { + /** The derived shared secret (hex) */ + secret: string; + /** One-time private key for spending */ + oneTimePrivateKey: string; + /** One-time address */ + oneTimeAddress: string; +} + +/** Pedersen commitment */ +export interface Commitment { + /** Commitment value (hex) */ + commitment: string; + /** Generator point used */ + generator: 'default' | 'alternate'; +} + +/** Create commitment options */ +export interface CreateCommitmentOptions { + /** Value to commit to */ + value: string; + /** Blinding factor (hex) - generated if not provided */ + blinding?: string; +} + +/** Commitment with blinding factor */ +export interface CommitmentWithBlinding { + /** The commitment */ + commitment: Commitment; + /** Blinding factor used (hex) */ + blinding: string; +} + +/** Open commitment options */ +export interface OpenCommitmentOptions { + /** Commitment to open/verify */ + commitment: string; + /** Claimed value */ + value: string; + /** Blinding factor */ + blinding: string; +} + +/** Bulletproof range proof */ +export interface RangeProof { + /** Proof data (hex) */ + proof: string; + /** Commitment the proof is for */ + commitment: string; + /** Bit length of the range */ + bitLength: number; +} + +/** Create range proof options */ +export interface CreateRangeProofOptions { + /** Value to prove is in range */ + value: string; + /** Blinding factor */ + blinding: string; + /** Optional message to embed in proof */ + message?: string; + /** Bit length (default 64) */ + bitLength?: number; +} + +/** Verify range proof result */ +export interface VerifyRangeProofResult { + /** Whether the proof is valid */ + valid: boolean; + /** Minimum value in range (usually 0) */ + minValue: string; + /** Maximum value in range */ + maxValue: string; +} + +/** Blinding factor sum options */ +export interface BlindingSumOptions { + /** Positive blinding factors (inputs) */ + positive: string[]; + /** Negative blinding factors (outputs) */ + negative: string[]; +} diff --git a/sdk/kotlin/src/main/kotlin/io/synor/contract/ContractClient.kt b/sdk/kotlin/src/main/kotlin/io/synor/contract/ContractClient.kt new file mode 100644 index 0000000..158b987 --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/io/synor/contract/ContractClient.kt @@ -0,0 +1,292 @@ +package io.synor.contract + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.delay +import kotlinx.serialization.json.* +import java.io.Closeable +import java.net.URLEncoder +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Synor Contract SDK client for Kotlin. + * Smart contract deployment, interaction, and event handling. + */ +class ContractClient(private val config: ContractConfig) : Closeable { + private val closed = AtomicBoolean(false) + private val json = Json { ignoreUnknownKeys = true } + + private val client = HttpClient(CIO) { + install(ContentNegotiation) { + json(json) + } + install(HttpTimeout) { + requestTimeoutMillis = config.timeoutMs + connectTimeoutMillis = config.timeoutMs + } + defaultRequest { + header("Authorization", "Bearer ${config.apiKey}") + header("X-SDK-Version", "kotlin/$VERSION") + contentType(ContentType.Application.Json) + } + } + + // ==================== Contract Deployment ==================== + + suspend fun deploy(options: DeployContractOptions): DeploymentResult { + val body = buildJsonObject { + put("bytecode", options.bytecode) + options.abi?.let { put("abi", json.encodeToJsonElement(it)) } + options.constructorArgs?.let { put("constructor_args", json.encodeToJsonElement(it)) } + options.value?.let { put("value", it) } + options.gasLimit?.let { put("gas_limit", it) } + options.gasPrice?.let { put("gas_price", it) } + options.nonce?.let { put("nonce", it) } + } + return post("/contract/deploy", body) + } + + suspend fun deployCreate2(options: DeployContractOptions, salt: String): DeploymentResult { + val body = buildJsonObject { + put("bytecode", options.bytecode) + put("salt", salt) + options.abi?.let { put("abi", json.encodeToJsonElement(it)) } + options.constructorArgs?.let { put("constructor_args", json.encodeToJsonElement(it)) } + options.value?.let { put("value", it) } + options.gasLimit?.let { put("gas_limit", it) } + options.gasPrice?.let { put("gas_price", it) } + } + return post("/contract/deploy/create2", body) + } + + suspend fun predictAddress(bytecode: String, salt: String, deployer: String? = null): String { + val body = buildJsonObject { + put("bytecode", bytecode) + put("salt", salt) + deployer?.let { put("deployer", it) } + } + val response: JsonObject = post("/contract/predict-address", body) + return response["address"]?.jsonPrimitive?.content ?: throw ContractException("Missing address") + } + + // ==================== Contract Interaction ==================== + + suspend fun call(options: CallContractOptions): JsonElement { + val body = buildJsonObject { + put("contract", options.contract) + put("method", options.method) + put("args", json.encodeToJsonElement(options.args)) + put("abi", json.encodeToJsonElement(options.abi)) + } + return post("/contract/call", body) + } + + suspend fun send(options: SendContractOptions): TransactionResult { + val body = buildJsonObject { + put("contract", options.contract) + put("method", options.method) + put("args", json.encodeToJsonElement(options.args)) + put("abi", json.encodeToJsonElement(options.abi)) + options.value?.let { put("value", it) } + options.gasLimit?.let { put("gas_limit", it) } + options.gasPrice?.let { put("gas_price", it) } + options.nonce?.let { put("nonce", it) } + } + return post("/contract/send", body) + } + + // ==================== Events ==================== + + suspend fun getEvents(filter: EventFilter): List { + val body = buildJsonObject { + put("contract", filter.contract) + filter.event?.let { put("event", it) } + filter.fromBlock?.let { put("from_block", it) } + filter.toBlock?.let { put("to_block", it) } + filter.topics?.let { put("topics", json.encodeToJsonElement(it)) } + filter.abi?.let { put("abi", json.encodeToJsonElement(it)) } + } + return post("/contract/events", body) + } + + suspend fun getLogs(contract: String, fromBlock: Long? = null, toBlock: Long? = null): List { + var path = "/contract/logs?contract=${encode(contract)}" + fromBlock?.let { path += "&from_block=$it" } + toBlock?.let { path += "&to_block=$it" } + return get(path) + } + + suspend fun decodeLogs(logs: List, abi: List): List { + val body = buildJsonObject { + put("logs", json.encodeToJsonElement(logs)) + put("abi", json.encodeToJsonElement(abi)) + } + return post("/contract/decode-logs", body) + } + + // ==================== ABI Utilities ==================== + + suspend fun encodeCall(options: EncodeCallOptions): String { + val body = buildJsonObject { + put("method", options.method) + put("args", json.encodeToJsonElement(options.args)) + put("abi", json.encodeToJsonElement(options.abi)) + } + val response: JsonObject = post("/contract/encode", body) + return response["data"]?.jsonPrimitive?.content ?: throw ContractException("Missing data") + } + + suspend fun decodeResult(options: DecodeResultOptions): JsonElement { + val body = buildJsonObject { + put("data", options.data) + put("method", options.method) + put("abi", json.encodeToJsonElement(options.abi)) + } + val response: JsonObject = post("/contract/decode", body) + return response["result"] ?: throw ContractException("Missing result") + } + + suspend fun getSelector(signature: String): String { + val response: JsonObject = get("/contract/selector?signature=${encode(signature)}") + return response["selector"]?.jsonPrimitive?.content ?: throw ContractException("Missing selector") + } + + // ==================== Gas Estimation ==================== + + suspend fun estimateGas(options: EstimateGasOptions): GasEstimation { + val body = buildJsonObject { + put("contract", options.contract) + put("method", options.method) + put("args", json.encodeToJsonElement(options.args)) + put("abi", json.encodeToJsonElement(options.abi)) + options.value?.let { put("value", it) } + } + return post("/contract/estimate-gas", body) + } + + // ==================== Contract Information ==================== + + suspend fun getBytecode(address: String): BytecodeInfo { + return get("/contract/${encode(address)}/bytecode") + } + + suspend fun verify(options: VerifyContractOptions): VerificationResult { + val body = buildJsonObject { + put("address", options.address) + put("source_code", options.sourceCode) + put("compiler_version", options.compilerVersion) + options.constructorArgs?.let { put("constructor_args", it) } + options.optimization?.let { put("optimization", it) } + options.optimizationRuns?.let { put("optimization_runs", it) } + options.license?.let { put("license", it) } + } + return post("/contract/verify", body) + } + + suspend fun getVerificationStatus(address: String): VerificationResult { + return get("/contract/${encode(address)}/verification") + } + + // ==================== Multicall ==================== + + suspend fun multicall(requests: List): List { + val body = buildJsonObject { + put("calls", json.encodeToJsonElement(requests)) + } + return post("/contract/multicall", body) + } + + // ==================== Storage ==================== + + suspend fun readStorage(options: ReadStorageOptions): String { + var path = "/contract/storage?contract=${encode(options.contract)}&slot=${encode(options.slot)}" + options.blockNumber?.let { path += "&block=$it" } + val response: JsonObject = get(path) + return response["value"]?.jsonPrimitive?.content ?: throw ContractException("Missing value") + } + + // ==================== Lifecycle ==================== + + suspend fun healthCheck(): Boolean { + if (closed.get()) return false + return try { + val response: JsonObject = get("/health") + response["status"]?.jsonPrimitive?.content == "healthy" + } catch (e: Exception) { + false + } + } + + override fun close() { + closed.set(true) + client.close() + } + + fun isClosed(): Boolean = closed.get() + + // ==================== HTTP Helpers ==================== + + private fun encode(value: String): String = URLEncoder.encode(value, "UTF-8") + + private suspend inline fun get(path: String): T { + checkClosed() + return executeWithRetry { client.get("${config.endpoint}$path").body() } + } + + private suspend inline fun post(path: String, body: JsonObject): T { + checkClosed() + return executeWithRetry { + client.post("${config.endpoint}$path") { + setBody(body) + }.body() + } + } + + private suspend inline fun executeWithRetry(crossinline block: suspend () -> T): T { + var lastException: Exception? = null + repeat(config.retries) { attempt -> + try { + return block() + } catch (e: Exception) { + lastException = e + if (attempt < config.retries - 1) { + delay((1L shl attempt) * 1000) + } + } + } + throw lastException ?: ContractException("Request failed") + } + + private fun checkClosed() { + if (closed.get()) throw ContractException("Client has been closed") + } + + companion object { + const val VERSION = "0.1.0" + } +} + +/** + * Contract SDK configuration. + */ +data class ContractConfig( + val apiKey: String, + val endpoint: String = "https://contract.synor.io", + val timeoutMs: Long = 30000, + val retries: Int = 3 +) + +/** + * Contract SDK exception. + */ +class ContractException( + message: String, + val statusCode: Int? = null, + val errorCode: String? = null +) : RuntimeException(message) diff --git a/sdk/kotlin/src/main/kotlin/io/synor/contract/ContractTypes.kt b/sdk/kotlin/src/main/kotlin/io/synor/contract/ContractTypes.kt new file mode 100644 index 0000000..1d0d56c --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/io/synor/contract/ContractTypes.kt @@ -0,0 +1,199 @@ +package io.synor.contract + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +// ==================== ABI Types ==================== + +@Serializable +data class AbiParameter( + val name: String? = null, + val type: String, + val indexed: Boolean? = null, + val components: List? = null +) + +@Serializable +data class AbiEntry( + val type: String, + val name: String? = null, + val inputs: List? = null, + val outputs: List? = null, + val stateMutability: String? = null, + val anonymous: Boolean? = null +) + +// ==================== Deployment ==================== + +data class DeployContractOptions( + val bytecode: String, + val abi: List? = null, + val constructorArgs: List? = null, + val value: String? = null, + val gasLimit: Long? = null, + val gasPrice: String? = null, + val nonce: Long? = null +) + +@Serializable +data class DeploymentResult( + @SerialName("contract_address") val contractAddress: String, + @SerialName("transaction_hash") val transactionHash: String, + val deployer: String? = null, + @SerialName("gas_used") val gasUsed: Long? = null, + @SerialName("block_number") val blockNumber: Long? = null, + @SerialName("block_hash") val blockHash: String? = null +) + +// ==================== Contract Interaction ==================== + +data class CallContractOptions( + val contract: String, + val method: String, + val args: List = emptyList(), + val abi: List +) + +data class SendContractOptions( + val contract: String, + val method: String, + val args: List = emptyList(), + val abi: List, + val value: String? = null, + val gasLimit: Long? = null, + val gasPrice: String? = null, + val nonce: Long? = null +) + +@Serializable +data class TransactionResult( + @SerialName("transaction_hash") val transactionHash: String, + @SerialName("block_number") val blockNumber: Long? = null, + @SerialName("block_hash") val blockHash: String? = null, + @SerialName("gas_used") val gasUsed: Long? = null, + @SerialName("effective_gas_price") val effectiveGasPrice: String? = null, + val status: String? = null, + val logs: List? = null +) + +// ==================== Events ==================== + +@Serializable +data class EventLog( + val address: String, + val topics: List, + val data: String, + @SerialName("block_number") val blockNumber: Long? = null, + @SerialName("transaction_hash") val transactionHash: String? = null, + @SerialName("log_index") val logIndex: Int? = null, + @SerialName("block_hash") val blockHash: String? = null, + val removed: Boolean? = null +) + +@Serializable +data class DecodedEvent( + val name: String, + val signature: String? = null, + val args: Map? = null, + val log: EventLog? = null +) + +data class EventFilter( + val contract: String, + val event: String? = null, + val fromBlock: Long? = null, + val toBlock: Long? = null, + val topics: List? = null, + val abi: List? = null +) + +// ==================== ABI Utilities ==================== + +data class EncodeCallOptions( + val method: String, + val args: List = emptyList(), + val abi: List +) + +data class DecodeResultOptions( + val data: String, + val method: String, + val abi: List +) + +// ==================== Gas Estimation ==================== + +data class EstimateGasOptions( + val contract: String, + val method: String, + val args: List = emptyList(), + val abi: List, + val value: String? = null +) + +@Serializable +data class GasEstimation( + @SerialName("gas_limit") val gasLimit: Long, + @SerialName("gas_price") val gasPrice: String? = null, + @SerialName("max_fee_per_gas") val maxFeePerGas: String? = null, + @SerialName("max_priority_fee_per_gas") val maxPriorityFeePerGas: String? = null, + @SerialName("estimated_cost") val estimatedCost: String? = null +) + +// ==================== Contract Information ==================== + +@Serializable +data class BytecodeInfo( + val bytecode: String, + @SerialName("deployed_bytecode") val deployedBytecode: String? = null, + val size: Int? = null, + @SerialName("is_contract") val isContract: Boolean? = null +) + +data class VerifyContractOptions( + val address: String, + val sourceCode: String, + val compilerVersion: String, + val constructorArgs: String? = null, + val optimization: Boolean? = null, + val optimizationRuns: Int? = null, + val license: String? = null +) + +@Serializable +data class VerificationResult( + val verified: Boolean, + val address: String? = null, + @SerialName("compiler_version") val compilerVersion: String? = null, + val optimization: Boolean? = null, + @SerialName("optimization_runs") val optimizationRuns: Int? = null, + val license: String? = null, + val abi: List? = null, + @SerialName("source_code") val sourceCode: String? = null +) + +// ==================== Multicall ==================== + +@Serializable +data class MulticallRequest( + val contract: String, + val method: String, + val args: List = emptyList(), + val abi: List +) + +@Serializable +data class MulticallResult( + val success: Boolean, + val result: JsonElement? = null, + val error: String? = null +) + +// ==================== Storage ==================== + +data class ReadStorageOptions( + val contract: String, + val slot: String, + val blockNumber: Long? = null +) diff --git a/sdk/kotlin/src/main/kotlin/io/synor/privacy/PrivacyClient.kt b/sdk/kotlin/src/main/kotlin/io/synor/privacy/PrivacyClient.kt new file mode 100644 index 0000000..9b8e5f2 --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/io/synor/privacy/PrivacyClient.kt @@ -0,0 +1,286 @@ +package io.synor.privacy + +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.delay +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.* +import java.io.Closeable +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Synor Privacy SDK client for Kotlin. + * Confidential transactions, ring signatures, stealth addresses, and cryptographic commitments. + */ +class PrivacyClient(private val config: PrivacyConfig) : Closeable { + private val closed = AtomicBoolean(false) + private val json = Json { ignoreUnknownKeys = true } + + private val client = HttpClient(CIO) { + install(ContentNegotiation) { + json(json) + } + install(HttpTimeout) { + requestTimeoutMillis = config.timeoutMs + connectTimeoutMillis = config.timeoutMs + } + defaultRequest { + header("Authorization", "Bearer ${config.apiKey}") + header("X-SDK-Version", "kotlin/$VERSION") + contentType(ContentType.Application.Json) + } + } + + // ==================== Confidential Transactions ==================== + + suspend fun createConfidentialTx( + inputs: List, + outputs: List + ): ConfidentialTransaction { + val body = buildJsonObject { + put("inputs", json.encodeToJsonElement(inputs)) + put("outputs", json.encodeToJsonElement(outputs)) + } + return post("/privacy/confidential/create", body) + } + + suspend fun verifyConfidentialTx(tx: ConfidentialTransaction): Boolean { + val body = buildJsonObject { + put("transaction", json.encodeToJsonElement(tx)) + } + val response: JsonObject = post("/privacy/confidential/verify", body) + return response["valid"]?.jsonPrimitive?.boolean ?: false + } + + suspend fun createCommitment(value: String, blindingFactor: String): Commitment { + val body = buildJsonObject { + put("value", value) + put("blinding_factor", blindingFactor) + } + return post("/privacy/commitment/create", body) + } + + suspend fun verifyCommitment(commitment: String, value: String, blindingFactor: String): Boolean { + val body = buildJsonObject { + put("commitment", commitment) + put("value", value) + put("blinding_factor", blindingFactor) + } + val response: JsonObject = post("/privacy/commitment/verify", body) + return response["valid"]?.jsonPrimitive?.boolean ?: false + } + + suspend fun createRangeProof( + value: String, + blindingFactor: String, + minValue: Long, + maxValue: Long + ): RangeProof { + val body = buildJsonObject { + put("value", value) + put("blinding_factor", blindingFactor) + put("min_value", minValue) + put("max_value", maxValue) + } + return post("/privacy/range-proof/create", body) + } + + suspend fun verifyRangeProof(proof: RangeProof): Boolean { + val body = buildJsonObject { + put("proof", json.encodeToJsonElement(proof)) + } + val response: JsonObject = post("/privacy/range-proof/verify", body) + return response["valid"]?.jsonPrimitive?.boolean ?: false + } + + // ==================== Ring Signatures ==================== + + suspend fun createRingSignature( + message: String, + ring: List, + signerIndex: Int, + privateKey: String + ): RingSignature { + val body = buildJsonObject { + put("message", message) + put("ring", json.encodeToJsonElement(ring)) + put("signer_index", signerIndex) + put("private_key", privateKey) + } + return post("/privacy/ring/sign", body) + } + + suspend fun verifyRingSignature(signature: RingSignature, message: String): Boolean { + val body = buildJsonObject { + put("signature", json.encodeToJsonElement(signature)) + put("message", message) + } + val response: JsonObject = post("/privacy/ring/verify", body) + return response["valid"]?.jsonPrimitive?.boolean ?: false + } + + suspend fun generateDecoys(count: Int, excludeKey: String? = null): List { + var path = "/privacy/ring/decoys?count=$count" + if (excludeKey != null) path += "&exclude=$excludeKey" + return get(path) + } + + suspend fun checkKeyImage(keyImage: String): Boolean { + val response: JsonObject = get("/privacy/ring/key-image/$keyImage") + return response["spent"]?.jsonPrimitive?.boolean ?: false + } + + // ==================== Stealth Addresses ==================== + + suspend fun generateStealthKeyPair(): StealthKeyPair { + return post("/privacy/stealth/generate", buildJsonObject {}) + } + + suspend fun deriveStealthAddress( + spendPublicKey: String, + viewPublicKey: String + ): StealthAddress { + val body = buildJsonObject { + put("spend_public_key", spendPublicKey) + put("view_public_key", viewPublicKey) + } + return post("/privacy/stealth/derive", body) + } + + suspend fun recoverStealthPrivateKey( + stealthAddress: String, + viewPrivateKey: String, + spendPrivateKey: String + ): String { + val body = buildJsonObject { + put("stealth_address", stealthAddress) + put("view_private_key", viewPrivateKey) + put("spend_private_key", spendPrivateKey) + } + val response: JsonObject = post("/privacy/stealth/recover", body) + return response["private_key"]?.jsonPrimitive?.content ?: throw PrivacyException("Missing private_key") + } + + suspend fun scanOutputs( + viewPrivateKey: String, + spendPublicKey: String, + fromBlock: Long, + toBlock: Long? = null + ): List { + val body = buildJsonObject { + put("view_private_key", viewPrivateKey) + put("spend_public_key", spendPublicKey) + put("from_block", fromBlock) + toBlock?.let { put("to_block", it) } + } + return post("/privacy/stealth/scan", body) + } + + // ==================== Blinding ==================== + + suspend fun generateBlindingFactor(): String { + val response: JsonObject = post("/privacy/blinding/generate", buildJsonObject {}) + return response["blinding_factor"]?.jsonPrimitive?.content ?: throw PrivacyException("Missing blinding_factor") + } + + suspend fun blindValue(value: String, blindingFactor: String): String { + val body = buildJsonObject { + put("value", value) + put("blinding_factor", blindingFactor) + } + val response: JsonObject = post("/privacy/blinding/blind", body) + return response["blinded_value"]?.jsonPrimitive?.content ?: throw PrivacyException("Missing blinded_value") + } + + suspend fun unblindValue(blindedValue: String, blindingFactor: String): String { + val body = buildJsonObject { + put("blinded_value", blindedValue) + put("blinding_factor", blindingFactor) + } + val response: JsonObject = post("/privacy/blinding/unblind", body) + return response["value"]?.jsonPrimitive?.content ?: throw PrivacyException("Missing value") + } + + // ==================== Lifecycle ==================== + + suspend fun healthCheck(): Boolean { + if (closed.get()) return false + return try { + val response: JsonObject = get("/health") + response["status"]?.jsonPrimitive?.content == "healthy" + } catch (e: Exception) { + false + } + } + + override fun close() { + closed.set(true) + client.close() + } + + fun isClosed(): Boolean = closed.get() + + // ==================== HTTP Helpers ==================== + + private suspend inline fun get(path: String): T { + checkClosed() + return executeWithRetry { client.get("${config.endpoint}$path").body() } + } + + private suspend inline fun post(path: String, body: JsonObject): T { + checkClosed() + return executeWithRetry { + client.post("${config.endpoint}$path") { + setBody(body) + }.body() + } + } + + private suspend inline fun executeWithRetry(crossinline block: suspend () -> T): T { + var lastException: Exception? = null + repeat(config.retries) { attempt -> + try { + return block() + } catch (e: Exception) { + lastException = e + if (attempt < config.retries - 1) { + delay((1L shl attempt) * 1000) + } + } + } + throw lastException ?: PrivacyException("Request failed") + } + + private fun checkClosed() { + if (closed.get()) throw PrivacyException("Client has been closed") + } + + companion object { + const val VERSION = "0.1.0" + } +} + +/** + * Privacy SDK configuration. + */ +data class PrivacyConfig( + val apiKey: String, + val endpoint: String = "https://privacy.synor.io", + val timeoutMs: Long = 30000, + val retries: Int = 3 +) + +/** + * Privacy SDK exception. + */ +class PrivacyException( + message: String, + val statusCode: Int? = null, + val errorCode: String? = null +) : RuntimeException(message) diff --git a/sdk/kotlin/src/main/kotlin/io/synor/privacy/PrivacyTypes.kt b/sdk/kotlin/src/main/kotlin/io/synor/privacy/PrivacyTypes.kt new file mode 100644 index 0000000..ce98c4d --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/io/synor/privacy/PrivacyTypes.kt @@ -0,0 +1,86 @@ +package io.synor.privacy + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +// ==================== Confidential Transactions ==================== + +@Serializable +data class ConfidentialTxInput( + val commitment: String, + @SerialName("blinding_factor") val blindingFactor: String, + val value: String, + @SerialName("key_image") val keyImage: String? = null +) + +@Serializable +data class ConfidentialTxOutput( + val commitment: String, + @SerialName("blinding_factor") val blindingFactor: String, + val value: String, + @SerialName("recipient_public_key") val recipientPublicKey: String, + @SerialName("range_proof") val rangeProof: String? = null +) + +@Serializable +data class ConfidentialTransaction( + val id: String, + val inputs: List, + val outputs: List, + val fee: String, + val excess: String, + @SerialName("excess_sig") val excessSig: String, + @SerialName("kernel_offset") val kernelOffset: String? = null +) + +// ==================== Commitments ==================== + +@Serializable +data class Commitment( + val commitment: String, + @SerialName("blinding_factor") val blindingFactor: String +) + +@Serializable +data class RangeProof( + val proof: String, + val commitment: String, + @SerialName("min_value") val minValue: Long, + @SerialName("max_value") val maxValue: Long +) + +// ==================== Ring Signatures ==================== + +@Serializable +data class RingSignature( + val c0: String, + val s: List, + @SerialName("key_image") val keyImage: String, + val ring: List +) + +// ==================== Stealth Addresses ==================== + +@Serializable +data class StealthKeyPair( + @SerialName("spend_public_key") val spendPublicKey: String, + @SerialName("spend_private_key") val spendPrivateKey: String, + @SerialName("view_public_key") val viewPublicKey: String, + @SerialName("view_private_key") val viewPrivateKey: String +) + +@Serializable +data class StealthAddress( + val address: String, + @SerialName("ephemeral_public_key") val ephemeralPublicKey: String, + @SerialName("tx_public_key") val txPublicKey: String? = null +) + +@Serializable +data class StealthOutput( + @SerialName("tx_hash") val txHash: String, + @SerialName("output_index") val outputIndex: Int, + @SerialName("stealth_address") val stealthAddress: String, + val amount: String, + @SerialName("block_height") val blockHeight: Long +) diff --git a/sdk/python/src/synor_contract/__init__.py b/sdk/python/src/synor_contract/__init__.py new file mode 100644 index 0000000..9a85ff6 --- /dev/null +++ b/sdk/python/src/synor_contract/__init__.py @@ -0,0 +1,50 @@ +""" +Synor Contract SDK + +Smart contract deployment, interaction, and event handling: +- Deploy contracts (standard and CREATE2) +- Call view/pure functions +- Send state-changing transactions +- Subscribe to events +- ABI encoding/decoding utilities +""" + +from .types import ( + ContractConfig, + ContractError, + AbiEntryType, + AbiParameter, + AbiEntry, + DeploymentResult, + TransactionResult, + EventLog, + DecodedEvent, + ContractInterface, + GasEstimation, + BytecodeInfo, + VerificationResult, + MulticallRequest, + MulticallResult, + DEFAULT_CONTRACT_ENDPOINT, +) + +from .client import SynorContract + +__all__ = [ + "SynorContract", + "ContractConfig", + "ContractError", + "AbiEntryType", + "AbiParameter", + "AbiEntry", + "DeploymentResult", + "TransactionResult", + "EventLog", + "DecodedEvent", + "ContractInterface", + "GasEstimation", + "BytecodeInfo", + "VerificationResult", + "MulticallRequest", + "MulticallResult", +] diff --git a/sdk/python/src/synor_contract/client.py b/sdk/python/src/synor_contract/client.py new file mode 100644 index 0000000..8da9b85 --- /dev/null +++ b/sdk/python/src/synor_contract/client.py @@ -0,0 +1,711 @@ +""" +Synor Contract SDK Client + +Smart contract deployment, interaction, and event handling. +""" + +import asyncio +from typing import Optional, List, Dict, Any, Union +import httpx + +from .types import ( + ContractConfig, + ContractError, + AbiEntry, + DeploymentResult, + TransactionResult, + EventLog, + DecodedEvent, + ContractInterface, + GasEstimation, + BytecodeInfo, + VerificationResult, + MulticallRequest, + MulticallResult, + DEFAULT_CONTRACT_ENDPOINT, +) + + +# Type alias for ABI +Abi = List[AbiEntry] + + +class SynorContract: + """ + Synor Contract Client. + + Provides smart contract deployment, interaction, and event handling. + + Example: + async with SynorContract(api_key="sk_...") as contract: + # Deploy a contract + result = await contract.deploy( + bytecode="0x608060...", + abi=[...], + args=[100, "Token"], + ) + + # Call a view function + balance = await contract.call( + address=result.address, + method="balanceOf", + args=["0x..."], + abi=[...], + ) + + # Send a transaction + tx = await contract.send( + address=result.address, + method="transfer", + args=["0x...", "1000000"], + abi=[...], + ) + """ + + def __init__( + self, + api_key: str, + endpoint: str = DEFAULT_CONTRACT_ENDPOINT, + timeout: float = 30.0, + retries: int = 3, + debug: bool = False, + default_gas_limit: Optional[str] = None, + default_gas_price: Optional[str] = None, + ): + self.config = ContractConfig( + api_key=api_key, + endpoint=endpoint, + timeout=timeout, + retries=retries, + debug=debug, + default_gas_limit=default_gas_limit, + default_gas_price=default_gas_price, + ) + self._client = httpx.AsyncClient( + base_url=endpoint, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "X-SDK-Version": "python/0.1.0", + }, + timeout=timeout, + ) + self._closed = False + + # ==================== Deployment ==================== + + async def deploy( + self, + bytecode: str, + abi: Optional[Abi] = None, + args: Optional[List[Any]] = None, + gas_limit: Optional[str] = None, + gas_price: Optional[str] = None, + value: Optional[str] = None, + salt: Optional[str] = None, + ) -> DeploymentResult: + """ + Deploy a smart contract. + + Args: + bytecode: Compiled bytecode (hex) + abi: Contract ABI + args: Constructor arguments + gas_limit: Gas limit + gas_price: Gas price + value: Value to send with deployment + salt: Salt for deterministic deployment (CREATE2) + + Returns: + Deployment result with contract address + """ + body: Dict[str, Any] = {"bytecode": bytecode} + if abi: + body["abi"] = [e.to_dict() for e in abi] + if args: + body["args"] = args + if gas_limit or self.config.default_gas_limit: + body["gasLimit"] = gas_limit or self.config.default_gas_limit + if gas_price or self.config.default_gas_price: + body["gasPrice"] = gas_price or self.config.default_gas_price + if value: + body["value"] = value + if salt: + body["salt"] = salt + + response = await self._request("POST", "/contracts/deploy", body) + return DeploymentResult.from_dict(response["deployment"]) + + async def deploy_deterministic( + self, + bytecode: str, + salt: str, + abi: Optional[Abi] = None, + args: Optional[List[Any]] = None, + gas_limit: Optional[str] = None, + gas_price: Optional[str] = None, + value: Optional[str] = None, + ) -> DeploymentResult: + """Deploy using CREATE2 for deterministic addresses.""" + return await self.deploy( + bytecode=bytecode, + abi=abi, + args=args, + gas_limit=gas_limit, + gas_price=gas_price, + value=value, + salt=salt, + ) + + async def predict_address( + self, + bytecode: str, + salt: str, + constructor_args: Optional[List[Any]] = None, + ) -> str: + """ + Predict CREATE2 deployment address. + + Args: + bytecode: Contract bytecode + salt: Deployment salt + constructor_args: Constructor arguments + + Returns: + Predicted address + """ + body: Dict[str, Any] = {"bytecode": bytecode, "salt": salt} + if constructor_args: + body["constructorArgs"] = constructor_args + + response = await self._request("POST", "/contracts/predict-address", body) + return response["address"] + + # ==================== Contract Interaction ==================== + + async def call( + self, + address: str, + method: str, + abi: Abi, + args: Optional[List[Any]] = None, + block_number: Optional[Union[int, str]] = None, + ) -> Any: + """ + Call a view/pure function (no transaction). + + Args: + address: Contract address + method: Method name + abi: Contract ABI + args: Method arguments + block_number: Block number or 'latest' + + Returns: + Function return value + """ + body: Dict[str, Any] = { + "address": address, + "method": method, + "abi": [e.to_dict() for e in abi], + "blockNumber": block_number or "latest", + } + if args: + body["args"] = args + + response = await self._request("POST", "/contracts/call", body) + return response["result"] + + async def send( + self, + address: str, + method: str, + abi: Abi, + args: Optional[List[Any]] = None, + gas_limit: Optional[str] = None, + gas_price: Optional[str] = None, + value: Optional[str] = None, + ) -> TransactionResult: + """ + Send a state-changing transaction. + + Args: + address: Contract address + method: Method name + abi: Contract ABI + args: Method arguments + gas_limit: Gas limit + gas_price: Gas price + value: Value to send + + Returns: + Transaction result + """ + body: Dict[str, Any] = { + "address": address, + "method": method, + "abi": [e.to_dict() for e in abi], + } + if args: + body["args"] = args + if gas_limit or self.config.default_gas_limit: + body["gasLimit"] = gas_limit or self.config.default_gas_limit + if gas_price or self.config.default_gas_price: + body["gasPrice"] = gas_price or self.config.default_gas_price + if value: + body["value"] = value + + response = await self._request("POST", "/contracts/send", body) + return TransactionResult.from_dict(response["transaction"]) + + async def multicall( + self, + requests: List[MulticallRequest], + abis: Optional[Dict[str, Abi]] = None, + ) -> List[MulticallResult]: + """ + Execute multiple calls in a single request. + + Args: + requests: Array of multicall requests + abis: Optional ABIs for decoding (keyed by address) + + Returns: + Array of results + """ + body: Dict[str, Any] = {"calls": [r.to_dict() for r in requests]} + if abis: + body["abis"] = { + addr: [e.to_dict() for e in abi] for addr, abi in abis.items() + } + + response = await self._request("POST", "/contracts/multicall", body) + return [MulticallResult.from_dict(r) for r in response["results"]] + + # ==================== Events ==================== + + async def get_events( + self, + address: str, + abi: Optional[Abi] = None, + event_name: Optional[str] = None, + from_block: Optional[Union[int, str]] = None, + to_block: Optional[Union[int, str]] = None, + filter_values: Optional[Dict[str, Any]] = None, + ) -> List[DecodedEvent]: + """ + Get historical events. + + Args: + address: Contract address + abi: Contract ABI (required for decoding) + event_name: Event name (optional) + from_block: From block + to_block: To block + filter_values: Indexed filter values + + Returns: + Array of decoded events + """ + body: Dict[str, Any] = { + "address": address, + "fromBlock": from_block or "earliest", + "toBlock": to_block or "latest", + } + if abi: + body["abi"] = [e.to_dict() for e in abi] + if event_name: + body["eventName"] = event_name + if filter_values: + body["filter"] = filter_values + + response = await self._request("POST", "/contracts/events", body) + return [DecodedEvent.from_dict(e) for e in response["events"]] + + async def get_logs( + self, + address: str, + abi: Optional[Abi] = None, + event_name: Optional[str] = None, + from_block: Optional[Union[int, str]] = None, + to_block: Optional[Union[int, str]] = None, + filter_values: Optional[Dict[str, Any]] = None, + ) -> List[EventLog]: + """ + Get raw event logs. + + Args: + address: Contract address + abi: Contract ABI + event_name: Event name (optional) + from_block: From block + to_block: To block + filter_values: Indexed filter values + + Returns: + Array of raw logs + """ + body: Dict[str, Any] = { + "address": address, + "fromBlock": from_block or "earliest", + "toBlock": to_block or "latest", + } + if abi: + body["abi"] = [e.to_dict() for e in abi] + if event_name: + body["eventName"] = event_name + if filter_values: + body["filter"] = filter_values + + response = await self._request("POST", "/contracts/logs", body) + return [EventLog.from_dict(l) for l in response["logs"]] + + async def decode_log(self, log: EventLog, abi: Abi) -> DecodedEvent: + """ + Decode a raw event log. + + Args: + log: Raw log + abi: Contract ABI + + Returns: + Decoded event + """ + response = await self._request( + "POST", + "/contracts/decode-log", + {"log": log.to_dict(), "abi": [e.to_dict() for e in abi]}, + ) + return DecodedEvent.from_dict(response["event"]) + + # ==================== ABI Utilities ==================== + + def load_abi(self, abi: Abi) -> ContractInterface: + """ + Load and parse a contract ABI. + + Args: + abi: Contract ABI + + Returns: + Parsed contract interface + """ + functions: Dict[str, AbiEntry] = {} + events: Dict[str, AbiEntry] = {} + errors: Dict[str, AbiEntry] = {} + + for entry in abi: + if entry.type == "function" and entry.name: + functions[entry.name] = entry + elif entry.type == "event" and entry.name: + events[entry.name] = entry + elif entry.type == "error" and entry.name: + errors[entry.name] = entry + + return ContractInterface( + abi=abi, + functions=functions, + events=events, + errors=errors, + ) + + async def encode_call( + self, method: str, args: List[Any], abi: Abi + ) -> str: + """ + Encode a function call. + + Args: + method: Method name + args: Arguments + abi: Contract ABI + + Returns: + Encoded calldata (hex) + """ + response = await self._request( + "POST", + "/contracts/encode", + {"method": method, "args": args, "abi": [e.to_dict() for e in abi]}, + ) + return response["data"] + + async def decode_result( + self, method: str, data: str, abi: Abi + ) -> Any: + """ + Decode function return data. + + Args: + method: Method name + data: Raw data to decode + abi: Contract ABI + + Returns: + Decoded result + """ + response = await self._request( + "POST", + "/contracts/decode", + {"method": method, "data": data, "abi": [e.to_dict() for e in abi]}, + ) + return response["result"] + + async def get_function_selector(self, signature: str) -> str: + """ + Get function selector (first 4 bytes of keccak256 hash). + + Args: + signature: Function signature (e.g., "transfer(address,uint256)") + + Returns: + Function selector (hex) + """ + response = await self._request( + "POST", "/contracts/selector", {"signature": signature} + ) + return response["selector"] + + # ==================== Gas Estimation ==================== + + async def estimate_gas( + self, + address: Optional[str] = None, + method: Optional[str] = None, + args: Optional[List[Any]] = None, + abi: Optional[Abi] = None, + bytecode: Optional[str] = None, + value: Optional[str] = None, + from_address: Optional[str] = None, + ) -> GasEstimation: + """ + Estimate gas for a contract call or deployment. + + Args: + address: Contract address (for calls) + method: Method name + args: Arguments + abi: Contract ABI + bytecode: Bytecode (for deployment) + value: Value to send + from_address: Sender address + + Returns: + Gas estimation + """ + body: Dict[str, Any] = {} + if address: + body["address"] = address + if method: + body["method"] = method + if args: + body["args"] = args + if abi: + body["abi"] = [e.to_dict() for e in abi] + if bytecode: + body["bytecode"] = bytecode + if value: + body["value"] = value + if from_address: + body["from"] = from_address + + response = await self._request("POST", "/contracts/estimate-gas", body) + return GasEstimation.from_dict(response["estimation"]) + + # ==================== Contract Info ==================== + + async def get_bytecode(self, address: str) -> BytecodeInfo: + """ + Get contract bytecode. + + Args: + address: Contract address + + Returns: + Bytecode info + """ + response = await self._request("GET", f"/contracts/{address}/bytecode") + return BytecodeInfo.from_dict(response) + + async def is_contract(self, address: str) -> bool: + """ + Check if an address is a contract. + + Args: + address: Address to check + + Returns: + True if address has code + """ + response = await self._request("GET", f"/contracts/{address}/is-contract") + return response["isContract"] + + async def read_storage( + self, + address: str, + slot: str, + block_number: Optional[Union[int, str]] = None, + ) -> str: + """ + Read contract storage slot. + + Args: + address: Contract address + slot: Storage slot + block_number: Block number + + Returns: + Storage value (hex) + """ + response = await self._request( + "POST", + "/contracts/storage", + { + "address": address, + "slot": slot, + "blockNumber": block_number or "latest", + }, + ) + return response["value"] + + # ==================== Verification ==================== + + async def verify_contract( + self, + address: str, + source_code: str, + compiler_version: str, + constructor_arguments: Optional[str] = None, + optimization: bool = False, + optimization_runs: int = 200, + contract_name: Optional[str] = None, + ) -> VerificationResult: + """ + Submit contract for verification. + + Args: + address: Contract address + source_code: Source code + compiler_version: Compiler version + constructor_arguments: Constructor arguments (hex) + optimization: Optimization enabled + optimization_runs: Optimization runs + contract_name: Contract name + + Returns: + Verification result + """ + body: Dict[str, Any] = { + "address": address, + "sourceCode": source_code, + "compilerVersion": compiler_version, + "optimization": optimization, + "optimizationRuns": optimization_runs, + } + if constructor_arguments: + body["constructorArguments"] = constructor_arguments + if contract_name: + body["contractName"] = contract_name + + response = await self._request("POST", "/contracts/verify", body) + return VerificationResult.from_dict(response) + + async def get_verification_status(self, address: str) -> VerificationResult: + """Check verification status.""" + response = await self._request("GET", f"/contracts/{address}/verification") + return VerificationResult.from_dict(response) + + async def get_verified_abi(self, address: str) -> Optional[Abi]: + """Get verified contract ABI.""" + try: + response = await self._request("GET", f"/contracts/{address}/abi") + return [AbiEntry.from_dict(e) for e in response["abi"]] + except ContractError: + return None + + # ==================== Lifecycle ==================== + + async def close(self) -> None: + """Close the client.""" + self._closed = True + await self._client.aclose() + + def is_closed(self) -> bool: + """Check if client is closed.""" + return self._closed + + async def health_check(self) -> bool: + """Health check.""" + try: + response = await self._request("GET", "/health") + return response.get("status") == "healthy" + except ContractError: + return False + + async def __aenter__(self) -> "SynorContract": + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + await self.close() + + # ==================== Private Methods ==================== + + async def _request( + self, + method: str, + path: str, + body: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """Make HTTP request with retries.""" + if self._closed: + raise ContractError("Client has been closed") + + last_error: Optional[Exception] = None + + for attempt in range(self.config.retries): + try: + return await self._do_request(method, path, body) + except ContractError as e: + if self.config.debug: + print(f"Attempt {attempt + 1} failed: {e}") + last_error = e + if attempt < self.config.retries - 1: + await asyncio.sleep(2**attempt) + + if last_error: + raise last_error + raise ContractError("Unknown error after retries") + + async def _do_request( + self, + method: str, + path: str, + body: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """Execute HTTP request.""" + try: + response = await self._client.request( + method, + path, + json=body if body else None, + ) + + if response.status_code >= 400: + error_body = response.json() if response.content else {} + message = ( + error_body.get("message") + or error_body.get("error") + or f"HTTP {response.status_code}" + ) + raise ContractError( + message, + code=error_body.get("code"), + status_code=response.status_code, + details=error_body, + ) + + return response.json() + except httpx.TimeoutException as e: + raise ContractError(f"Request timeout: {e}") + except httpx.RequestError as e: + raise ContractError(f"Request failed: {e}") diff --git a/sdk/python/src/synor_contract/types.py b/sdk/python/src/synor_contract/types.py new file mode 100644 index 0000000..6d50348 --- /dev/null +++ b/sdk/python/src/synor_contract/types.py @@ -0,0 +1,373 @@ +""" +Synor Contract SDK Types + +Smart contract types, ABI definitions, and transaction results. +""" + +from dataclasses import dataclass, field +from typing import Optional, List, Dict, Any, Literal, Union, Callable + + +# ==================== Config Types ==================== + +DEFAULT_CONTRACT_ENDPOINT = "https://contract.synor.cc/api/v1" + + +@dataclass +class ContractConfig: + """Contract SDK configuration.""" + api_key: str + endpoint: str = DEFAULT_CONTRACT_ENDPOINT + timeout: float = 30.0 + retries: int = 3 + debug: bool = False + default_gas_limit: Optional[str] = None + default_gas_price: Optional[str] = None + + +class ContractError(Exception): + """Contract-specific error.""" + + def __init__( + self, + message: str, + code: Optional[str] = None, + status_code: Optional[int] = None, + details: Optional[Dict[str, Any]] = None, + ): + super().__init__(message) + self.message = message + self.code = code + self.status_code = status_code + self.details = details or {} + + +# ==================== ABI Types ==================== + +AbiEntryType = Literal["function", "constructor", "event", "error", "fallback", "receive"] +StateMutability = Literal["pure", "view", "nonpayable", "payable"] + + +@dataclass +class AbiParameter: + """ABI input/output parameter.""" + name: str + type: str + indexed: bool = False + components: Optional[List["AbiParameter"]] = None + internal_type: Optional[str] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AbiParameter": + components = None + if "components" in data: + components = [AbiParameter.from_dict(c) for c in data["components"]] + return cls( + name=data.get("name", ""), + type=data["type"], + indexed=data.get("indexed", False), + components=components, + internal_type=data.get("internalType", data.get("internal_type")), + ) + + def to_dict(self) -> Dict[str, Any]: + d: Dict[str, Any] = {"name": self.name, "type": self.type} + if self.indexed: + d["indexed"] = self.indexed + if self.components: + d["components"] = [c.to_dict() for c in self.components] + if self.internal_type: + d["internalType"] = self.internal_type + return d + + +@dataclass +class AbiEntry: + """ABI entry.""" + type: AbiEntryType + name: Optional[str] = None + inputs: Optional[List[AbiParameter]] = None + outputs: Optional[List[AbiParameter]] = None + state_mutability: Optional[StateMutability] = None + anonymous: bool = False + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AbiEntry": + inputs = None + outputs = None + if "inputs" in data: + inputs = [AbiParameter.from_dict(i) for i in data["inputs"]] + if "outputs" in data: + outputs = [AbiParameter.from_dict(o) for o in data["outputs"]] + return cls( + type=data["type"], + name=data.get("name"), + inputs=inputs, + outputs=outputs, + state_mutability=data.get("stateMutability", data.get("state_mutability")), + anonymous=data.get("anonymous", False), + ) + + def to_dict(self) -> Dict[str, Any]: + d: Dict[str, Any] = {"type": self.type} + if self.name: + d["name"] = self.name + if self.inputs: + d["inputs"] = [i.to_dict() for i in self.inputs] + if self.outputs: + d["outputs"] = [o.to_dict() for o in self.outputs] + if self.state_mutability: + d["stateMutability"] = self.state_mutability + if self.anonymous: + d["anonymous"] = self.anonymous + return d + + +# Type alias for ABI +Abi = List[AbiEntry] + + +# ==================== Deployment Types ==================== + +@dataclass +class DeploymentResult: + """Deployment result.""" + address: str + transaction_hash: str + block_number: int + gas_used: str + effective_gas_price: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DeploymentResult": + return cls( + address=data["address"], + transaction_hash=data.get("transactionHash", data.get("transaction_hash", "")), + block_number=data.get("blockNumber", data.get("block_number", 0)), + gas_used=data.get("gasUsed", data.get("gas_used", "")), + effective_gas_price=data.get("effectiveGasPrice", data.get("effective_gas_price", "")), + ) + + +# ==================== Transaction Types ==================== + +TransactionStatus = Literal["success", "reverted"] + + +@dataclass +class EventLog: + """Event log.""" + log_index: int + address: str + topics: List[str] + data: str + block_number: int + transaction_hash: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "EventLog": + return cls( + log_index=data.get("logIndex", data.get("log_index", 0)), + address=data["address"], + topics=data["topics"], + data=data["data"], + block_number=data.get("blockNumber", data.get("block_number", 0)), + transaction_hash=data.get("transactionHash", data.get("transaction_hash", "")), + ) + + def to_dict(self) -> Dict[str, Any]: + return { + "logIndex": self.log_index, + "address": self.address, + "topics": self.topics, + "data": self.data, + "blockNumber": self.block_number, + "transactionHash": self.transaction_hash, + } + + +@dataclass +class DecodedEvent: + """Decoded event.""" + name: str + signature: str + args: Dict[str, Any] + log: EventLog + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DecodedEvent": + return cls( + name=data["name"], + signature=data["signature"], + args=data["args"], + log=EventLog.from_dict(data["log"]), + ) + + +@dataclass +class TransactionResult: + """Transaction result.""" + transaction_hash: str + block_number: int + block_hash: str + gas_used: str + effective_gas_price: str + status: TransactionStatus + logs: List[EventLog] + return_value: Optional[Any] = None + revert_reason: Optional[str] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "TransactionResult": + return cls( + transaction_hash=data.get("transactionHash", data.get("transaction_hash", "")), + block_number=data.get("blockNumber", data.get("block_number", 0)), + block_hash=data.get("blockHash", data.get("block_hash", "")), + gas_used=data.get("gasUsed", data.get("gas_used", "")), + effective_gas_price=data.get("effectiveGasPrice", data.get("effective_gas_price", "")), + status=data["status"], + logs=[EventLog.from_dict(l) for l in data.get("logs", [])], + return_value=data.get("returnValue", data.get("return_value")), + revert_reason=data.get("revertReason", data.get("revert_reason")), + ) + + +# ==================== Contract Interface Types ==================== + +@dataclass +class ContractInterface: + """Contract interface (from ABI).""" + abi: List[AbiEntry] + functions: Dict[str, AbiEntry] + events: Dict[str, AbiEntry] + errors: Dict[str, AbiEntry] + + +# ==================== Gas Types ==================== + +@dataclass +class GasEstimation: + """Gas estimation result.""" + gas_limit: str + gas_price: str + estimated_cost: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "GasEstimation": + return cls( + gas_limit=data.get("gasLimit", data.get("gas_limit", "")), + gas_price=data.get("gasPrice", data.get("gas_price", "")), + estimated_cost=data.get("estimatedCost", data.get("estimated_cost", "")), + ) + + +# ==================== Bytecode Types ==================== + +@dataclass +class BytecodeMetadata: + """Bytecode metadata.""" + compiler: str + language: str + sources: List[str] + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "BytecodeMetadata": + return cls( + compiler=data.get("compiler", ""), + language=data.get("language", ""), + sources=data.get("sources", []), + ) + + +@dataclass +class BytecodeInfo: + """Contract bytecode info.""" + bytecode: str + deployed_bytecode: Optional[str] = None + abi: Optional[List[AbiEntry]] = None + metadata: Optional[BytecodeMetadata] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "BytecodeInfo": + abi = None + if "abi" in data: + abi = [AbiEntry.from_dict(e) for e in data["abi"]] + metadata = None + if "metadata" in data: + metadata = BytecodeMetadata.from_dict(data["metadata"]) + return cls( + bytecode=data["bytecode"], + deployed_bytecode=data.get("deployedBytecode", data.get("deployed_bytecode")), + abi=abi, + metadata=metadata, + ) + + +# ==================== Verification Types ==================== + +VerificationStatus = Literal["verified", "pending", "failed"] + + +@dataclass +class VerificationResult: + """Contract verification result.""" + status: VerificationStatus + message: str + abi: Optional[List[AbiEntry]] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "VerificationResult": + abi = None + if "abi" in data: + abi = [AbiEntry.from_dict(e) for e in data["abi"]] + return cls( + status=data["status"], + message=data["message"], + abi=abi, + ) + + +# ==================== Multicall Types ==================== + +@dataclass +class MulticallRequest: + """Multicall request.""" + address: str + call_data: str + allow_failure: bool = False + + def to_dict(self) -> Dict[str, Any]: + return { + "address": self.address, + "callData": self.call_data, + "allowFailure": self.allow_failure, + } + + +@dataclass +class MulticallResult: + """Multicall result.""" + success: bool + return_data: str + decoded: Optional[Any] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "MulticallResult": + return cls( + success=data["success"], + return_data=data.get("returnData", data.get("return_data", "")), + decoded=data.get("decoded"), + ) + + +# ==================== Event Subscription Types ==================== + +# Callback type for event subscription +EventCallback = Callable[[DecodedEvent], None] + + +@dataclass +class EventSubscription: + """Event subscription handle.""" + id: str + unsubscribe: Callable[[], None] diff --git a/sdk/python/src/synor_privacy/__init__.py b/sdk/python/src/synor_privacy/__init__.py new file mode 100644 index 0000000..a7e7abf --- /dev/null +++ b/sdk/python/src/synor_privacy/__init__.py @@ -0,0 +1,63 @@ +""" +Synor Privacy SDK + +Privacy-enhancing features for the Synor blockchain: +- Confidential transactions with hidden amounts +- Ring signatures for anonymous signing +- Stealth addresses for unlinkable payments +- Pedersen commitments and Bulletproof range proofs +""" + +from .types import ( + PrivacyConfig, + PrivacyError, + PublicKey, + PrivateKey, + ConfidentialUTXO, + ConfidentialOutput, + ConfidentialTransaction, + ConfidentialTxInput, + ConfidentialTxOutput, + OutputFeatures, + TransactionKernel, + VerifyConfidentialTxResult, + RingSignature, + VerifyRingSignatureResult, + StealthAddress, + StealthKeypair, + OneTimeAddress, + SharedSecret, + Commitment, + CommitmentWithBlinding, + RangeProof, + VerifyRangeProofResult, + DEFAULT_PRIVACY_ENDPOINT, +) + +from .client import SynorPrivacy + +__all__ = [ + "SynorPrivacy", + "PrivacyConfig", + "PrivacyError", + "PublicKey", + "PrivateKey", + "ConfidentialUTXO", + "ConfidentialOutput", + "ConfidentialTransaction", + "ConfidentialTxInput", + "ConfidentialTxOutput", + "OutputFeatures", + "TransactionKernel", + "VerifyConfidentialTxResult", + "RingSignature", + "VerifyRingSignatureResult", + "StealthAddress", + "StealthKeypair", + "OneTimeAddress", + "SharedSecret", + "Commitment", + "CommitmentWithBlinding", + "RangeProof", + "VerifyRangeProofResult", +] diff --git a/sdk/python/src/synor_privacy/client.py b/sdk/python/src/synor_privacy/client.py new file mode 100644 index 0000000..c511836 --- /dev/null +++ b/sdk/python/src/synor_privacy/client.py @@ -0,0 +1,609 @@ +""" +Synor Privacy SDK Client + +Confidential transactions, ring signatures, stealth addresses, and commitments. +""" + +import asyncio +from typing import Optional, List, Dict, Any +import httpx + +from .types import ( + PrivacyConfig, + PrivacyError, + ConfidentialUTXO, + ConfidentialOutput, + ConfidentialTransaction, + VerifyConfidentialTxResult, + RingSignature, + VerifyRingSignatureResult, + StealthAddress, + StealthKeypair, + OneTimeAddress, + SharedSecret, + Commitment, + CommitmentWithBlinding, + RangeProof, + VerifyRangeProofResult, + DEFAULT_PRIVACY_ENDPOINT, +) + + +class SynorPrivacy: + """ + Synor Privacy Client. + + Provides privacy-enhancing features including confidential transactions, + ring signatures, stealth addresses, and Pedersen commitments. + + Example: + async with SynorPrivacy(api_key="sk_...") as privacy: + # Generate stealth keypair + keypair = await privacy.generate_stealth_keypair() + print(f"Stealth address: {keypair.address.address}") + + # Create ring signature + signature = await privacy.create_ring_signature( + message="Hello, World!", + ring=[pub_key1, pub_key2, my_pub_key, pub_key3], + private_key=my_private_key, + ) + """ + + def __init__( + self, + api_key: str, + endpoint: str = DEFAULT_PRIVACY_ENDPOINT, + timeout: float = 30.0, + retries: int = 3, + debug: bool = False, + ): + self.config = PrivacyConfig( + api_key=api_key, + endpoint=endpoint, + timeout=timeout, + retries=retries, + debug=debug, + ) + self._client = httpx.AsyncClient( + base_url=endpoint, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "X-SDK-Version": "python/0.1.0", + }, + timeout=timeout, + ) + self._closed = False + + # ==================== Confidential Transactions ==================== + + async def create_confidential_transaction( + self, + inputs: List[ConfidentialUTXO], + outputs: List[ConfidentialOutput], + fee: Optional[str] = None, + lock_height: Optional[int] = None, + ) -> ConfidentialTransaction: + """ + Create a confidential transaction with hidden amounts. + + Uses Pedersen commitments and Bulletproof range proofs to hide + transaction amounts while proving they are valid (non-negative). + + Args: + inputs: Input UTXOs with commitments + outputs: Outputs to create + fee: Transaction fee + lock_height: Lock height for the transaction + + Returns: + Confidential transaction ready for broadcast + """ + body: Dict[str, Any] = { + "inputs": [i.to_dict() for i in inputs], + "outputs": [o.to_dict() for o in outputs], + } + if fee: + body["fee"] = fee + if lock_height is not None: + body["lockHeight"] = lock_height + + response = await self._request("POST", "/transactions/confidential", body) + return ConfidentialTransaction.from_dict(response["transaction"]) + + async def verify_confidential_transaction( + self, transaction: ConfidentialTransaction + ) -> VerifyConfidentialTxResult: + """ + Verify a confidential transaction. + + Checks that: + - Commitments balance (inputs = outputs + fee) + - All range proofs are valid (no negative amounts) + - Kernel signature is valid + - No duplicate inputs + + Args: + transaction: Transaction to verify + + Returns: + Verification result with details + """ + response = await self._request( + "POST", + "/transactions/confidential/verify", + {"transaction": transaction.raw}, + ) + return VerifyConfidentialTxResult.from_dict(response) + + async def decode_confidential_output( + self, commitment: str, blinding: str + ) -> str: + """ + Decode a confidential transaction output. + + Args: + commitment: Output commitment + blinding: Blinding factor + + Returns: + Decoded amount + """ + response = await self._request( + "POST", + "/transactions/confidential/decode", + {"commitment": commitment, "blinding": blinding}, + ) + return response["amount"] + + # ==================== Ring Signatures ==================== + + async def create_ring_signature( + self, + message: str, + ring: List[str], + private_key: str, + signer_index: Optional[int] = None, + ) -> RingSignature: + """ + Create a ring signature. + + Ring signatures allow signing a message as "one of N" without + revealing which member of the ring actually signed. + + Args: + message: Message to sign (hex or string) + ring: Ring of public keys (must include signer's key) + private_key: Signer's private key + signer_index: Optional index of signer's key in ring + + Returns: + Ring signature + """ + body: Dict[str, Any] = { + "message": message, + "ring": ring, + "privateKey": private_key, + } + if signer_index is not None: + body["signerIndex"] = signer_index + + response = await self._request("POST", "/ring-signatures/create", body) + return RingSignature.from_dict(response["signature"]) + + async def verify_ring_signature( + self, signature: RingSignature, message: str + ) -> VerifyRingSignatureResult: + """ + Verify a ring signature. + + Args: + signature: Ring signature to verify + message: Original message + + Returns: + Verification result + """ + response = await self._request( + "POST", + "/ring-signatures/verify", + {"signature": signature.to_dict(), "message": message}, + ) + return VerifyRingSignatureResult.from_dict(response) + + async def is_key_image_used(self, key_image: str) -> bool: + """ + Check if a key image has been used (double-spend detection). + + Args: + key_image: Key image from ring signature + + Returns: + True if the key image has been seen before + """ + response = await self._request( + "GET", f"/ring-signatures/key-images/{key_image}" + ) + return response["used"] + + async def generate_random_ring( + self, size: int, exclude: Optional[List[str]] = None + ) -> List[str]: + """ + Generate a random ring from available public keys. + + Args: + size: Desired ring size + exclude: Public keys to exclude + + Returns: + Array of public keys for the ring + """ + body: Dict[str, Any] = {"size": size} + if exclude: + body["exclude"] = exclude + + response = await self._request("POST", "/ring-signatures/random-ring", body) + return response["ring"] + + # ==================== Stealth Addresses ==================== + + async def generate_stealth_keypair(self) -> StealthKeypair: + """ + Generate a new stealth address keypair. + + Stealth addresses allow receiving payments at unique one-time + addresses that cannot be linked to the recipient's public address. + + Returns: + Stealth keypair (keep private keys secure!) + """ + response = await self._request("POST", "/stealth/generate") + return StealthKeypair.from_dict(response["keypair"]) + + async def create_one_time_address( + self, stealth_address: StealthAddress + ) -> OneTimeAddress: + """ + Create a one-time address for sending to a stealth address. + + Args: + stealth_address: Recipient's stealth address + + Returns: + One-time address and ephemeral key + """ + response = await self._request( + "POST", + "/stealth/one-time-address", + { + "scanPublicKey": stealth_address.scan_public_key, + "spendPublicKey": stealth_address.spend_public_key, + }, + ) + return OneTimeAddress.from_dict(response["oneTimeAddress"]) + + async def derive_shared_secret( + self, + stealth_address: StealthAddress, + private_key: str, + ephemeral_public_key: str, + ) -> SharedSecret: + """ + Derive the shared secret for a stealth payment. + + Args: + stealth_address: Stealth address + private_key: Private key for derivation + ephemeral_public_key: Ephemeral public key from transaction + + Returns: + Shared secret and derived keys + """ + response = await self._request( + "POST", + "/stealth/derive-secret", + { + "scanPublicKey": stealth_address.scan_public_key, + "spendPublicKey": stealth_address.spend_public_key, + "privateKey": private_key, + "ephemeralPublicKey": ephemeral_public_key, + }, + ) + return SharedSecret.from_dict(response) + + async def scan_for_payments( + self, + scan_private_key: str, + spend_public_key: str, + transactions: List[str], + ) -> List[OneTimeAddress]: + """ + Scan transactions to find stealth payments. + + Args: + scan_private_key: Your scan private key + spend_public_key: Your spend public key + transactions: Transaction IDs to scan + + Returns: + Found payments with one-time addresses + """ + response = await self._request( + "POST", + "/stealth/scan", + { + "scanPrivateKey": scan_private_key, + "spendPublicKey": spend_public_key, + "transactions": transactions, + }, + ) + return [OneTimeAddress.from_dict(p) for p in response["payments"]] + + # ==================== Pedersen Commitments ==================== + + async def create_commitment( + self, value: str, blinding: Optional[str] = None + ) -> CommitmentWithBlinding: + """ + Create a Pedersen commitment. + + C = vG + bH where v is the value and b is the blinding factor. + + Args: + value: Value to commit to + blinding: Blinding factor (generated if not provided) + + Returns: + Commitment and blinding factor + """ + body: Dict[str, Any] = {"value": value} + if blinding: + body["blinding"] = blinding + + response = await self._request("POST", "/commitments/create", body) + return CommitmentWithBlinding.from_dict(response) + + async def open_commitment( + self, commitment: str, value: str, blinding: str + ) -> bool: + """ + Open (verify) a Pedersen commitment. + + Args: + commitment: Commitment to open + value: Claimed value + blinding: Blinding factor + + Returns: + True if the commitment opens correctly + """ + response = await self._request( + "POST", + "/commitments/open", + {"commitment": commitment, "value": value, "blinding": blinding}, + ) + return response["valid"] + + async def add_commitments(self, commitment1: str, commitment2: str) -> str: + """ + Add two commitments. + + Args: + commitment1: First commitment + commitment2: Second commitment + + Returns: + Sum commitment + """ + response = await self._request( + "POST", + "/commitments/add", + {"commitment1": commitment1, "commitment2": commitment2}, + ) + return response["commitment"] + + async def subtract_commitments(self, commitment1: str, commitment2: str) -> str: + """ + Subtract two commitments. + + Args: + commitment1: First commitment + commitment2: Second commitment + + Returns: + Difference commitment + """ + response = await self._request( + "POST", + "/commitments/subtract", + {"commitment1": commitment1, "commitment2": commitment2}, + ) + return response["commitment"] + + async def compute_blinding_sum( + self, positive: List[str], negative: List[str] + ) -> str: + """ + Compute blinding factor sum for transaction balancing. + + Args: + positive: Positive blinding factors (inputs) + negative: Negative blinding factors (outputs) + + Returns: + Net blinding factor + """ + response = await self._request( + "POST", + "/commitments/blinding-sum", + {"positive": positive, "negative": negative}, + ) + return response["sum"] + + async def generate_blinding(self) -> str: + """ + Generate a random blinding factor. + + Returns: + Random 32-byte blinding factor (hex) + """ + response = await self._request("POST", "/commitments/random-blinding") + return response["blinding"] + + # ==================== Range Proofs ==================== + + async def create_range_proof( + self, + value: str, + blinding: str, + message: Optional[str] = None, + bit_length: int = 64, + ) -> RangeProof: + """ + Create a Bulletproof range proof. + + Args: + value: Value to prove is in range + blinding: Blinding factor + message: Optional message to embed in proof + bit_length: Bit length (default 64) + + Returns: + Range proof + """ + body: Dict[str, Any] = { + "value": value, + "blinding": blinding, + "bitLength": bit_length, + } + if message: + body["message"] = message + + response = await self._request("POST", "/range-proofs/create", body) + return RangeProof.from_dict(response["proof"]) + + async def verify_range_proof( + self, commitment: str, proof: str + ) -> VerifyRangeProofResult: + """ + Verify a Bulletproof range proof. + + Args: + commitment: Commitment the proof is for + proof: Range proof data + + Returns: + Verification result + """ + response = await self._request( + "POST", + "/range-proofs/verify", + {"commitment": commitment, "proof": proof}, + ) + return VerifyRangeProofResult.from_dict(response) + + async def create_aggregated_range_proof( + self, outputs: List[Dict[str, str]] + ) -> str: + """ + Create an aggregated range proof for multiple outputs. + + Args: + outputs: Array of {value, blinding} pairs + + Returns: + Aggregated proof + """ + response = await self._request( + "POST", "/range-proofs/aggregate", {"outputs": outputs} + ) + return response["proof"] + + # ==================== Lifecycle ==================== + + async def close(self) -> None: + """Close the client.""" + self._closed = True + await self._client.aclose() + + def is_closed(self) -> bool: + """Check if client is closed.""" + return self._closed + + async def health_check(self) -> bool: + """Health check.""" + try: + response = await self._request("GET", "/health") + return response.get("status") == "healthy" + except PrivacyError: + return False + + async def __aenter__(self) -> "SynorPrivacy": + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + await self.close() + + # ==================== Private Methods ==================== + + async def _request( + self, + method: str, + path: str, + body: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """Make HTTP request with retries.""" + if self._closed: + raise PrivacyError("Client has been closed") + + last_error: Optional[Exception] = None + + for attempt in range(self.config.retries): + try: + return await self._do_request(method, path, body) + except PrivacyError as e: + if self.config.debug: + print(f"Attempt {attempt + 1} failed: {e}") + last_error = e + if attempt < self.config.retries - 1: + await asyncio.sleep(2**attempt) + + if last_error: + raise last_error + raise PrivacyError("Unknown error after retries") + + async def _do_request( + self, + method: str, + path: str, + body: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """Execute HTTP request.""" + try: + response = await self._client.request( + method, + path, + json=body if body else None, + ) + + if response.status_code >= 400: + error_body = response.json() if response.content else {} + message = ( + error_body.get("message") + or error_body.get("error") + or f"HTTP {response.status_code}" + ) + raise PrivacyError( + message, + code=error_body.get("code"), + status_code=response.status_code, + details=error_body, + ) + + return response.json() + except httpx.TimeoutException as e: + raise PrivacyError(f"Request timeout: {e}") + except httpx.RequestError as e: + raise PrivacyError(f"Request failed: {e}") diff --git a/sdk/python/src/synor_privacy/types.py b/sdk/python/src/synor_privacy/types.py new file mode 100644 index 0000000..dfbee30 --- /dev/null +++ b/sdk/python/src/synor_privacy/types.py @@ -0,0 +1,436 @@ +""" +Synor Privacy SDK Types + +Confidential transactions, ring signatures, stealth addresses, and commitments. +""" + +from dataclasses import dataclass, field +from typing import Optional, List, Dict, Any, Literal + + +# ==================== Config Types ==================== + +DEFAULT_PRIVACY_ENDPOINT = "https://privacy.synor.cc/api/v1" + + +@dataclass +class PrivacyConfig: + """Privacy SDK configuration.""" + api_key: str + endpoint: str = DEFAULT_PRIVACY_ENDPOINT + timeout: float = 30.0 + retries: int = 3 + debug: bool = False + + +class PrivacyError(Exception): + """Privacy-specific error.""" + + def __init__( + self, + message: str, + code: Optional[str] = None, + status_code: Optional[int] = None, + details: Optional[Dict[str, Any]] = None, + ): + super().__init__(message) + self.message = message + self.code = code + self.status_code = status_code + self.details = details or {} + + +# ==================== Key Types ==================== + +KeyType = Literal["ed25519", "secp256k1"] + + +@dataclass +class PublicKey: + """Public key representation.""" + key: str + type: KeyType + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "PublicKey": + return cls( + key=data["key"], + type=data["type"], + ) + + +@dataclass +class PrivateKey: + """Private key representation (for signing operations).""" + key: str + type: KeyType + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "PrivateKey": + return cls( + key=data["key"], + type=data["type"], + ) + + +# ==================== Confidential Transaction Types ==================== + +@dataclass +class ConfidentialUTXO: + """UTXO for confidential transaction input.""" + txid: str + vout: int + commitment: str + range_proof: str + blinding: Optional[str] = None + amount: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + d = { + "txid": self.txid, + "vout": self.vout, + "commitment": self.commitment, + "rangeProof": self.range_proof, + } + if self.blinding: + d["blinding"] = self.blinding + if self.amount: + d["amount"] = self.amount + return d + + +@dataclass +class ConfidentialOutput: + """Output for confidential transaction.""" + recipient: str + amount: str + blinding: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + d = { + "recipient": self.recipient, + "amount": self.amount, + } + if self.blinding: + d["blinding"] = self.blinding + return d + + +@dataclass +class OutputFeatures: + """Output features for confidential transactions.""" + flags: int + lock_height: Optional[int] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "OutputFeatures": + return cls( + flags=data["flags"], + lock_height=data.get("lockHeight", data.get("lock_height")), + ) + + +@dataclass +class ConfidentialTxInput: + """Confidential transaction input.""" + output_ref: str + commitment: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ConfidentialTxInput": + return cls( + output_ref=data.get("outputRef", data.get("output_ref", "")), + commitment=data["commitment"], + ) + + +@dataclass +class ConfidentialTxOutput: + """Confidential transaction output.""" + commitment: str + range_proof: str + features: OutputFeatures + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ConfidentialTxOutput": + return cls( + commitment=data["commitment"], + range_proof=data.get("rangeProof", data.get("range_proof", "")), + features=OutputFeatures.from_dict(data["features"]), + ) + + +@dataclass +class TransactionKernel: + """Transaction kernel for confidential transactions.""" + features: int + fee: str + lock_height: int + excess: str + excess_signature: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "TransactionKernel": + return cls( + features=data["features"], + fee=data["fee"], + lock_height=data.get("lockHeight", data.get("lock_height", 0)), + excess=data["excess"], + excess_signature=data.get("excessSignature", data.get("excess_signature", "")), + ) + + +@dataclass +class ConfidentialTransaction: + """Confidential transaction.""" + txid: str + version: int + inputs: List[ConfidentialTxInput] + outputs: List[ConfidentialTxOutput] + kernel: TransactionKernel + offset: str + raw: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ConfidentialTransaction": + return cls( + txid=data["txid"], + version=data["version"], + inputs=[ConfidentialTxInput.from_dict(i) for i in data.get("inputs", [])], + outputs=[ConfidentialTxOutput.from_dict(o) for o in data.get("outputs", [])], + kernel=TransactionKernel.from_dict(data["kernel"]), + offset=data["offset"], + raw=data["raw"], + ) + + +@dataclass +class VerifyConfidentialTxDetails: + """Verification details for confidential transaction.""" + commitments_balance: bool + range_proofs_valid: bool + signature_valid: bool + no_duplicate_inputs: bool + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "VerifyConfidentialTxDetails": + return cls( + commitments_balance=data.get("commitmentsBalance", data.get("commitments_balance", False)), + range_proofs_valid=data.get("rangeProofsValid", data.get("range_proofs_valid", False)), + signature_valid=data.get("signatureValid", data.get("signature_valid", False)), + no_duplicate_inputs=data.get("noDuplicateInputs", data.get("no_duplicate_inputs", False)), + ) + + +@dataclass +class VerifyConfidentialTxResult: + """Confidential transaction verification result.""" + valid: bool + details: VerifyConfidentialTxDetails + error: Optional[str] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "VerifyConfidentialTxResult": + return cls( + valid=data["valid"], + details=VerifyConfidentialTxDetails.from_dict(data["details"]), + error=data.get("error"), + ) + + +# ==================== Ring Signature Types ==================== + +@dataclass +class RingSignatureComponents: + """Ring signature components.""" + c: List[str] + r: List[str] + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RingSignatureComponents": + return cls( + c=data["c"], + r=data["r"], + ) + + +@dataclass +class RingSignature: + """Ring signature.""" + id: str + message_hash: str + ring: List[str] + key_image: str + signature: RingSignatureComponents + ring_size: int + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RingSignature": + return cls( + id=data["id"], + message_hash=data.get("messageHash", data.get("message_hash", "")), + ring=data["ring"], + key_image=data.get("keyImage", data.get("key_image", "")), + signature=RingSignatureComponents.from_dict(data["signature"]), + ring_size=data.get("ringSize", data.get("ring_size", 0)), + ) + + def to_dict(self) -> Dict[str, Any]: + return { + "id": self.id, + "messageHash": self.message_hash, + "ring": self.ring, + "keyImage": self.key_image, + "signature": { + "c": self.signature.c, + "r": self.signature.r, + }, + "ringSize": self.ring_size, + } + + +@dataclass +class VerifyRingSignatureResult: + """Ring signature verification result.""" + valid: bool + key_image: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "VerifyRingSignatureResult": + return cls( + valid=data["valid"], + key_image=data.get("keyImage", data.get("key_image", "")), + ) + + +# ==================== Stealth Address Types ==================== + +@dataclass +class StealthAddress: + """Stealth address.""" + address: str + scan_public_key: str + spend_public_key: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "StealthAddress": + return cls( + address=data["address"], + scan_public_key=data.get("scanPublicKey", data.get("scan_public_key", "")), + spend_public_key=data.get("spendPublicKey", data.get("spend_public_key", "")), + ) + + +@dataclass +class StealthKeypair: + """Stealth address keypair.""" + address: StealthAddress + scan_private_key: str + spend_private_key: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "StealthKeypair": + return cls( + address=StealthAddress.from_dict(data["address"]), + scan_private_key=data.get("scanPrivateKey", data.get("scan_private_key", "")), + spend_private_key=data.get("spendPrivateKey", data.get("spend_private_key", "")), + ) + + +@dataclass +class OneTimeAddress: + """One-time address derived from stealth address.""" + address: str + ephemeral_public_key: str + shared_secret: Optional[str] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "OneTimeAddress": + return cls( + address=data["address"], + ephemeral_public_key=data.get("ephemeralPublicKey", data.get("ephemeral_public_key", "")), + shared_secret=data.get("sharedSecret", data.get("shared_secret")), + ) + + +@dataclass +class SharedSecret: + """Shared secret result.""" + secret: str + one_time_private_key: str + one_time_address: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SharedSecret": + return cls( + secret=data["secret"], + one_time_private_key=data.get("oneTimePrivateKey", data.get("one_time_private_key", "")), + one_time_address=data.get("oneTimeAddress", data.get("one_time_address", "")), + ) + + +# ==================== Commitment Types ==================== + +GeneratorType = Literal["default", "alternate"] + + +@dataclass +class Commitment: + """Pedersen commitment.""" + commitment: str + generator: GeneratorType + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Commitment": + return cls( + commitment=data["commitment"], + generator=data.get("generator", "default"), + ) + + +@dataclass +class CommitmentWithBlinding: + """Commitment with blinding factor.""" + commitment: Commitment + blinding: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CommitmentWithBlinding": + return cls( + commitment=Commitment.from_dict(data["commitment"]), + blinding=data["blinding"], + ) + + +# ==================== Range Proof Types ==================== + +@dataclass +class RangeProof: + """Bulletproof range proof.""" + proof: str + commitment: str + bit_length: int + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RangeProof": + return cls( + proof=data["proof"], + commitment=data["commitment"], + bit_length=data.get("bitLength", data.get("bit_length", 64)), + ) + + +@dataclass +class VerifyRangeProofResult: + """Range proof verification result.""" + valid: bool + min_value: str + max_value: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "VerifyRangeProofResult": + return cls( + valid=data["valid"], + min_value=data.get("minValue", data.get("min_value", "0")), + max_value=data.get("maxValue", data.get("max_value", "")), + ) diff --git a/sdk/ruby/lib/synor_contract.rb b/sdk/ruby/lib/synor_contract.rb new file mode 100644 index 0000000..5354336 --- /dev/null +++ b/sdk/ruby/lib/synor_contract.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative "synor_contract/version" +require_relative "synor_contract/types" +require_relative "synor_contract/client" + +module SynorContract + VERSION = "0.1.0" +end diff --git a/sdk/ruby/lib/synor_contract/client.rb b/sdk/ruby/lib/synor_contract/client.rb new file mode 100644 index 0000000..3b9cad4 --- /dev/null +++ b/sdk/ruby/lib/synor_contract/client.rb @@ -0,0 +1,370 @@ +# frozen_string_literal: true + +require "faraday" +require "json" +require "uri" + +module SynorContract + # Synor Contract SDK client for Ruby. + # Smart contract deployment, interaction, and event handling. + class Client + attr_reader :closed + + def initialize(config) + @config = config + @closed = false + @conn = Faraday.new(url: config.endpoint) do |f| + f.request :json + f.response :json + f.options.timeout = config.timeout + f.headers["Authorization"] = "Bearer #{config.api_key}" + f.headers["Content-Type"] = "application/json" + f.headers["X-SDK-Version"] = "ruby/#{VERSION}" + end + end + + # ==================== Contract Deployment ==================== + + def deploy(options) + body = { + bytecode: options.bytecode + } + body[:abi] = options.abi.map(&:to_h) if options.abi + body[:constructor_args] = options.constructor_args if options.constructor_args + body[:value] = options.value if options.value + body[:gas_limit] = options.gas_limit if options.gas_limit + body[:gas_price] = options.gas_price if options.gas_price + body[:nonce] = options.nonce if options.nonce + + response = post("/contract/deploy", body) + parse_deployment_result(response) + end + + def deploy_create2(options, salt:) + body = { + bytecode: options.bytecode, + salt: salt + } + body[:abi] = options.abi.map(&:to_h) if options.abi + body[:constructor_args] = options.constructor_args if options.constructor_args + body[:value] = options.value if options.value + body[:gas_limit] = options.gas_limit if options.gas_limit + body[:gas_price] = options.gas_price if options.gas_price + + response = post("/contract/deploy/create2", body) + parse_deployment_result(response) + end + + def predict_address(bytecode:, salt:, deployer: nil) + body = { bytecode: bytecode, salt: salt } + body[:deployer] = deployer if deployer + response = post("/contract/predict-address", body) + response["address"] + end + + # ==================== Contract Interaction ==================== + + def call(options) + body = { + contract: options.contract, + method: options.method, + args: options.args, + abi: options.abi.map(&:to_h) + } + post("/contract/call", body) + end + + def send(options) + body = { + contract: options.contract, + method: options.method, + args: options.args, + abi: options.abi.map(&:to_h) + } + body[:value] = options.value if options.value + body[:gas_limit] = options.gas_limit if options.gas_limit + body[:gas_price] = options.gas_price if options.gas_price + body[:nonce] = options.nonce if options.nonce + + response = post("/contract/send", body) + parse_transaction_result(response) + end + + # ==================== Events ==================== + + def get_events(filter) + body = { contract: filter.contract } + body[:event] = filter.event if filter.event + body[:from_block] = filter.from_block if filter.from_block + body[:to_block] = filter.to_block if filter.to_block + body[:topics] = filter.topics if filter.topics + body[:abi] = filter.abi.map(&:to_h) if filter.abi + + response = post("/contract/events", body) + response.map { |e| parse_decoded_event(e) } + end + + def get_logs(contract, from_block: nil, to_block: nil) + path = "/contract/logs?contract=#{encode(contract)}" + path += "&from_block=#{from_block}" if from_block + path += "&to_block=#{to_block}" if to_block + + response = get(path) + response.map { |l| parse_event_log(l) } + end + + def decode_logs(logs:, abi:) + body = { + logs: logs.map(&:to_h), + abi: abi.map(&:to_h) + } + response = post("/contract/decode-logs", body) + response.map { |e| parse_decoded_event(e) } + end + + # ==================== ABI Utilities ==================== + + def encode_call(options) + body = { + method: options.method, + args: options.args, + abi: options.abi.map(&:to_h) + } + response = post("/contract/encode", body) + response["data"] + end + + def decode_result(options) + body = { + data: options.data, + method: options.method, + abi: options.abi.map(&:to_h) + } + response = post("/contract/decode", body) + response["result"] + end + + def get_selector(signature) + response = get("/contract/selector?signature=#{encode(signature)}") + response["selector"] + end + + # ==================== Gas Estimation ==================== + + def estimate_gas(options) + body = { + contract: options.contract, + method: options.method, + args: options.args, + abi: options.abi.map(&:to_h) + } + body[:value] = options.value if options.value + + response = post("/contract/estimate-gas", body) + parse_gas_estimation(response) + end + + # ==================== Contract Information ==================== + + def get_bytecode(address) + response = get("/contract/#{encode(address)}/bytecode") + parse_bytecode_info(response) + end + + def verify(options) + body = { + address: options.address, + source_code: options.source_code, + compiler_version: options.compiler_version + } + body[:constructor_args] = options.constructor_args if options.constructor_args + body[:optimization] = options.optimization if options.optimization + body[:optimization_runs] = options.optimization_runs if options.optimization_runs + body[:license] = options.license if options.license + + response = post("/contract/verify", body) + parse_verification_result(response) + end + + def get_verification_status(address) + response = get("/contract/#{encode(address)}/verification") + parse_verification_result(response) + end + + # ==================== Multicall ==================== + + def multicall(requests) + body = { calls: requests.map(&:to_h) } + response = post("/contract/multicall", body) + response.map { |r| parse_multicall_result(r) } + end + + # ==================== Storage ==================== + + def read_storage(options) + path = "/contract/storage?contract=#{encode(options.contract)}&slot=#{encode(options.slot)}" + path += "&block=#{options.block_number}" if options.block_number + response = get(path) + response["value"] + end + + # ==================== Lifecycle ==================== + + def health_check + response = get("/health") + response["status"] == "healthy" + rescue StandardError + false + end + + def close + @closed = true + @conn.close if @conn.respond_to?(:close) + end + + private + + def get(path, params = {}) + execute { @conn.get(path, params).body } + end + + def post(path, body) + execute { @conn.post(path, body).body } + end + + def execute + raise ClientClosedError, "Client has been closed" if @closed + + last_error = nil + @config.retries.times do |attempt| + begin + response = yield + check_error(response) if response.is_a?(Hash) + return response + rescue StandardError => e + last_error = e + sleep(2**attempt) if attempt < @config.retries - 1 + end + end + raise last_error + end + + def check_error(response) + return unless response["error"] || (response["code"] && response["message"]) + + message = response["message"] || response["error"] || "Unknown error" + code = response["code"] + status = response["status_code"] || 0 + raise HttpError.new(message, status_code: status, code: code) + end + + def encode(str) + URI.encode_www_form_component(str.to_s) + end + + def parse_deployment_result(data) + DeploymentResult.new( + contract_address: data["contract_address"], + transaction_hash: data["transaction_hash"], + deployer: data["deployer"], + gas_used: data["gas_used"], + block_number: data["block_number"], + block_hash: data["block_hash"] + ) + end + + def parse_transaction_result(data) + TransactionResult.new( + transaction_hash: data["transaction_hash"], + block_number: data["block_number"], + block_hash: data["block_hash"], + gas_used: data["gas_used"], + effective_gas_price: data["effective_gas_price"], + status: data["status"], + logs: (data["logs"] || []).map { |l| parse_event_log(l) } + ) + end + + def parse_event_log(data) + EventLog.new( + address: data["address"], + topics: data["topics"], + data: data["data"], + block_number: data["block_number"], + transaction_hash: data["transaction_hash"], + log_index: data["log_index"], + block_hash: data["block_hash"], + removed: data["removed"] + ) + end + + def parse_decoded_event(data) + DecodedEvent.new( + name: data["name"], + signature: data["signature"], + args: data["args"], + log: data["log"] ? parse_event_log(data["log"]) : nil + ) + end + + def parse_gas_estimation(data) + GasEstimation.new( + gas_limit: data["gas_limit"], + gas_price: data["gas_price"], + max_fee_per_gas: data["max_fee_per_gas"], + max_priority_fee_per_gas: data["max_priority_fee_per_gas"], + estimated_cost: data["estimated_cost"] + ) + end + + def parse_bytecode_info(data) + BytecodeInfo.new( + bytecode: data["bytecode"], + deployed_bytecode: data["deployed_bytecode"], + size: data["size"], + is_contract: data["is_contract"] + ) + end + + def parse_verification_result(data) + VerificationResult.new( + verified: data["verified"], + address: data["address"], + compiler_version: data["compiler_version"], + optimization: data["optimization"], + optimization_runs: data["optimization_runs"], + license: data["license"], + abi: data["abi"]&.map { |e| parse_abi_entry(e) }, + source_code: data["source_code"] + ) + end + + def parse_abi_entry(data) + AbiEntry.new( + type: data["type"], + name: data["name"], + inputs: data["inputs"]&.map { |p| parse_abi_parameter(p) }, + outputs: data["outputs"]&.map { |p| parse_abi_parameter(p) }, + state_mutability: data["stateMutability"], + anonymous: data["anonymous"] + ) + end + + def parse_abi_parameter(data) + AbiParameter.new( + name: data["name"], + type: data["type"], + indexed: data["indexed"], + components: data["components"]&.map { |c| parse_abi_parameter(c) } + ) + end + + def parse_multicall_result(data) + MulticallResult.new( + success: data["success"], + result: data["result"], + error: data["error"] + ) + end + end +end diff --git a/sdk/ruby/lib/synor_contract/types.rb b/sdk/ruby/lib/synor_contract/types.rb new file mode 100644 index 0000000..b227447 --- /dev/null +++ b/sdk/ruby/lib/synor_contract/types.rb @@ -0,0 +1,326 @@ +# frozen_string_literal: true + +module SynorContract + # Configuration for the Contract SDK. + class Config + attr_accessor :api_key, :endpoint, :timeout, :retries + + def initialize(api_key:, endpoint: "https://contract.synor.io", timeout: 30, retries: 3) + @api_key = api_key + @endpoint = endpoint + @timeout = timeout + @retries = retries + end + end + + # ABI parameter. + class AbiParameter + attr_accessor :name, :type, :indexed, :components + + def initialize(name: nil, type:, indexed: nil, components: nil) + @name = name + @type = type + @indexed = indexed + @components = components + end + + def to_h + h = { type: @type } + h[:name] = @name if @name + h[:indexed] = @indexed unless @indexed.nil? + h[:components] = @components.map(&:to_h) if @components + h + end + end + + # ABI entry. + class AbiEntry + attr_accessor :type, :name, :inputs, :outputs, :state_mutability, :anonymous + + def initialize(type:, name: nil, inputs: nil, outputs: nil, state_mutability: nil, anonymous: nil) + @type = type + @name = name + @inputs = inputs + @outputs = outputs + @state_mutability = state_mutability + @anonymous = anonymous + end + + def to_h + h = { type: @type } + h[:name] = @name if @name + h[:inputs] = @inputs.map(&:to_h) if @inputs + h[:outputs] = @outputs.map(&:to_h) if @outputs + h[:stateMutability] = @state_mutability if @state_mutability + h[:anonymous] = @anonymous unless @anonymous.nil? + h + end + end + + # Deploy contract options. + class DeployContractOptions + attr_accessor :bytecode, :abi, :constructor_args, :value, :gas_limit, :gas_price, :nonce + + def initialize(bytecode:, abi: nil, constructor_args: nil, value: nil, gas_limit: nil, gas_price: nil, nonce: nil) + @bytecode = bytecode + @abi = abi + @constructor_args = constructor_args + @value = value + @gas_limit = gas_limit + @gas_price = gas_price + @nonce = nonce + end + end + + # Deployment result. + class DeploymentResult + attr_accessor :contract_address, :transaction_hash, :deployer, :gas_used, :block_number, :block_hash + + def initialize(contract_address:, transaction_hash:, deployer: nil, gas_used: nil, block_number: nil, block_hash: nil) + @contract_address = contract_address + @transaction_hash = transaction_hash + @deployer = deployer + @gas_used = gas_used + @block_number = block_number + @block_hash = block_hash + end + end + + # Call contract options. + class CallContractOptions + attr_accessor :contract, :method, :args, :abi + + def initialize(contract:, method:, args: [], abi:) + @contract = contract + @method = method + @args = args + @abi = abi + end + end + + # Send contract options. + class SendContractOptions + attr_accessor :contract, :method, :args, :abi, :value, :gas_limit, :gas_price, :nonce + + def initialize(contract:, method:, args: [], abi:, value: nil, gas_limit: nil, gas_price: nil, nonce: nil) + @contract = contract + @method = method + @args = args + @abi = abi + @value = value + @gas_limit = gas_limit + @gas_price = gas_price + @nonce = nonce + end + end + + # Event log. + class EventLog + attr_accessor :address, :topics, :data, :block_number, :transaction_hash, :log_index, :block_hash, :removed + + def initialize(address:, topics:, data:, block_number: nil, transaction_hash: nil, log_index: nil, block_hash: nil, removed: nil) + @address = address + @topics = topics + @data = data + @block_number = block_number + @transaction_hash = transaction_hash + @log_index = log_index + @block_hash = block_hash + @removed = removed + end + + def to_h + h = { address: @address, topics: @topics, data: @data } + h[:block_number] = @block_number if @block_number + h[:transaction_hash] = @transaction_hash if @transaction_hash + h[:log_index] = @log_index if @log_index + h[:block_hash] = @block_hash if @block_hash + h[:removed] = @removed unless @removed.nil? + h + end + end + + # Transaction result. + class TransactionResult + attr_accessor :transaction_hash, :block_number, :block_hash, :gas_used, :effective_gas_price, :status, :logs + + def initialize(transaction_hash:, block_number: nil, block_hash: nil, gas_used: nil, effective_gas_price: nil, status: nil, logs: nil) + @transaction_hash = transaction_hash + @block_number = block_number + @block_hash = block_hash + @gas_used = gas_used + @effective_gas_price = effective_gas_price + @status = status + @logs = logs + end + end + + # Decoded event. + class DecodedEvent + attr_accessor :name, :signature, :args, :log + + def initialize(name:, signature: nil, args: nil, log: nil) + @name = name + @signature = signature + @args = args + @log = log + end + end + + # Event filter. + class EventFilter + attr_accessor :contract, :event, :from_block, :to_block, :topics, :abi + + def initialize(contract:, event: nil, from_block: nil, to_block: nil, topics: nil, abi: nil) + @contract = contract + @event = event + @from_block = from_block + @to_block = to_block + @topics = topics + @abi = abi + end + end + + # Encode call options. + class EncodeCallOptions + attr_accessor :method, :args, :abi + + def initialize(method:, args: [], abi:) + @method = method + @args = args + @abi = abi + end + end + + # Decode result options. + class DecodeResultOptions + attr_accessor :data, :method, :abi + + def initialize(data:, method:, abi:) + @data = data + @method = method + @abi = abi + end + end + + # Estimate gas options. + class EstimateGasOptions + attr_accessor :contract, :method, :args, :abi, :value + + def initialize(contract:, method:, args: [], abi:, value: nil) + @contract = contract + @method = method + @args = args + @abi = abi + @value = value + end + end + + # Gas estimation result. + class GasEstimation + attr_accessor :gas_limit, :gas_price, :max_fee_per_gas, :max_priority_fee_per_gas, :estimated_cost + + def initialize(gas_limit:, gas_price: nil, max_fee_per_gas: nil, max_priority_fee_per_gas: nil, estimated_cost: nil) + @gas_limit = gas_limit + @gas_price = gas_price + @max_fee_per_gas = max_fee_per_gas + @max_priority_fee_per_gas = max_priority_fee_per_gas + @estimated_cost = estimated_cost + end + end + + # Bytecode info. + class BytecodeInfo + attr_accessor :bytecode, :deployed_bytecode, :size, :is_contract + + def initialize(bytecode:, deployed_bytecode: nil, size: nil, is_contract: nil) + @bytecode = bytecode + @deployed_bytecode = deployed_bytecode + @size = size + @is_contract = is_contract + end + end + + # Verify contract options. + class VerifyContractOptions + attr_accessor :address, :source_code, :compiler_version, :constructor_args, :optimization, :optimization_runs, :license + + def initialize(address:, source_code:, compiler_version:, constructor_args: nil, optimization: nil, optimization_runs: nil, license: nil) + @address = address + @source_code = source_code + @compiler_version = compiler_version + @constructor_args = constructor_args + @optimization = optimization + @optimization_runs = optimization_runs + @license = license + end + end + + # Verification result. + class VerificationResult + attr_accessor :verified, :address, :compiler_version, :optimization, :optimization_runs, :license, :abi, :source_code + + def initialize(verified:, address: nil, compiler_version: nil, optimization: nil, optimization_runs: nil, license: nil, abi: nil, source_code: nil) + @verified = verified + @address = address + @compiler_version = compiler_version + @optimization = optimization + @optimization_runs = optimization_runs + @license = license + @abi = abi + @source_code = source_code + end + end + + # Multicall request. + class MulticallRequest + attr_accessor :contract, :method, :args, :abi + + def initialize(contract:, method:, args: [], abi:) + @contract = contract + @method = method + @args = args + @abi = abi + end + + def to_h + { contract: @contract, method: @method, args: @args, abi: @abi.map(&:to_h) } + end + end + + # Multicall result. + class MulticallResult + attr_accessor :success, :result, :error + + def initialize(success:, result: nil, error: nil) + @success = success + @result = result + @error = error + end + end + + # Read storage options. + class ReadStorageOptions + attr_accessor :contract, :slot, :block_number + + def initialize(contract:, slot:, block_number: nil) + @contract = contract + @slot = slot + @block_number = block_number + end + end + + # Client closed error. + class ClientClosedError < StandardError; end + + # HTTP error. + class HttpError < StandardError + attr_reader :status_code, :code + + def initialize(message, status_code: nil, code: nil) + super(message) + @status_code = status_code + @code = code + end + end +end diff --git a/sdk/ruby/lib/synor_contract/version.rb b/sdk/ruby/lib/synor_contract/version.rb new file mode 100644 index 0000000..e555c06 --- /dev/null +++ b/sdk/ruby/lib/synor_contract/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module SynorContract + VERSION = "0.1.0" +end diff --git a/sdk/ruby/lib/synor_privacy.rb b/sdk/ruby/lib/synor_privacy.rb new file mode 100644 index 0000000..197d572 --- /dev/null +++ b/sdk/ruby/lib/synor_privacy.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative "synor_privacy/version" +require_relative "synor_privacy/types" +require_relative "synor_privacy/client" + +module SynorPrivacy + VERSION = "0.1.0" +end diff --git a/sdk/ruby/lib/synor_privacy/client.rb b/sdk/ruby/lib/synor_privacy/client.rb new file mode 100644 index 0000000..f006b88 --- /dev/null +++ b/sdk/ruby/lib/synor_privacy/client.rb @@ -0,0 +1,269 @@ +# frozen_string_literal: true + +require "faraday" +require "json" +require "uri" + +module SynorPrivacy + # Synor Privacy SDK client for Ruby. + # Confidential transactions, ring signatures, stealth addresses, and cryptographic commitments. + class Client + attr_reader :closed + + def initialize(config) + @config = config + @closed = false + @conn = Faraday.new(url: config.endpoint) do |f| + f.request :json + f.response :json + f.options.timeout = config.timeout + f.headers["Authorization"] = "Bearer #{config.api_key}" + f.headers["Content-Type"] = "application/json" + f.headers["X-SDK-Version"] = "ruby/#{VERSION}" + end + end + + # ==================== Confidential Transactions ==================== + + def create_confidential_tx(inputs:, outputs:) + body = { inputs: inputs.map(&:to_h), outputs: outputs.map(&:to_h) } + response = post("/privacy/confidential/create", body) + parse_confidential_transaction(response) + end + + def verify_confidential_tx(tx:) + body = { transaction: tx.to_h } + response = post("/privacy/confidential/verify", body) + response["valid"] == true + end + + def create_commitment(value:, blinding_factor:) + body = { value: value, blinding_factor: blinding_factor } + response = post("/privacy/commitment/create", body) + Commitment.new( + commitment: response["commitment"], + blinding_factor: response["blinding_factor"] + ) + end + + def verify_commitment(commitment:, value:, blinding_factor:) + body = { commitment: commitment, value: value, blinding_factor: blinding_factor } + response = post("/privacy/commitment/verify", body) + response["valid"] == true + end + + def create_range_proof(value:, blinding_factor:, min_value:, max_value:) + body = { + value: value, + blinding_factor: blinding_factor, + min_value: min_value, + max_value: max_value + } + response = post("/privacy/range-proof/create", body) + RangeProof.new( + proof: response["proof"], + commitment: response["commitment"], + min_value: response["min_value"], + max_value: response["max_value"] + ) + end + + def verify_range_proof(proof:) + body = { proof: proof.to_h } + response = post("/privacy/range-proof/verify", body) + response["valid"] == true + end + + # ==================== Ring Signatures ==================== + + def create_ring_signature(message:, ring:, signer_index:, private_key:) + body = { + message: message, + ring: ring, + signer_index: signer_index, + private_key: private_key + } + response = post("/privacy/ring/sign", body) + RingSignature.new( + c0: response["c0"], + s: response["s"], + key_image: response["key_image"], + ring: response["ring"] + ) + end + + def verify_ring_signature(signature:, message:) + body = { signature: signature.to_h, message: message } + response = post("/privacy/ring/verify", body) + response["valid"] == true + end + + def generate_decoys(count, exclude_key: nil) + path = "/privacy/ring/decoys?count=#{count}" + path += "&exclude=#{encode(exclude_key)}" if exclude_key + get(path) + end + + def check_key_image(key_image) + response = get("/privacy/ring/key-image/#{key_image}") + response["spent"] == true + end + + # ==================== Stealth Addresses ==================== + + def generate_stealth_keypair + response = post("/privacy/stealth/generate", {}) + StealthKeyPair.new( + spend_public_key: response["spend_public_key"], + spend_private_key: response["spend_private_key"], + view_public_key: response["view_public_key"], + view_private_key: response["view_private_key"] + ) + end + + def derive_stealth_address(spend_public_key:, view_public_key:) + body = { spend_public_key: spend_public_key, view_public_key: view_public_key } + response = post("/privacy/stealth/derive", body) + StealthAddress.new( + address: response["address"], + ephemeral_public_key: response["ephemeral_public_key"], + tx_public_key: response["tx_public_key"] + ) + end + + def recover_stealth_private_key(stealth_address:, view_private_key:, spend_private_key:) + body = { + stealth_address: stealth_address, + view_private_key: view_private_key, + spend_private_key: spend_private_key + } + response = post("/privacy/stealth/recover", body) + response["private_key"] + end + + def scan_outputs(view_private_key:, spend_public_key:, from_block:, to_block: nil) + body = { + view_private_key: view_private_key, + spend_public_key: spend_public_key, + from_block: from_block + } + body[:to_block] = to_block if to_block + response = post("/privacy/stealth/scan", body) + response.map { |o| parse_stealth_output(o) } + end + + # ==================== Blinding ==================== + + def generate_blinding_factor + response = post("/privacy/blinding/generate", {}) + response["blinding_factor"] + end + + def blind_value(value:, blinding_factor:) + body = { value: value, blinding_factor: blinding_factor } + response = post("/privacy/blinding/blind", body) + response["blinded_value"] + end + + def unblind_value(blinded_value:, blinding_factor:) + body = { blinded_value: blinded_value, blinding_factor: blinding_factor } + response = post("/privacy/blinding/unblind", body) + response["value"] + end + + # ==================== Lifecycle ==================== + + def health_check + response = get("/health") + response["status"] == "healthy" + rescue StandardError + false + end + + def close + @closed = true + @conn.close if @conn.respond_to?(:close) + end + + private + + def get(path, params = {}) + execute { @conn.get(path, params).body } + end + + def post(path, body) + execute { @conn.post(path, body).body } + end + + def execute + raise ClientClosedError, "Client has been closed" if @closed + + last_error = nil + @config.retries.times do |attempt| + begin + response = yield + check_error(response) if response.is_a?(Hash) + return response + rescue StandardError => e + last_error = e + sleep(2**attempt) if attempt < @config.retries - 1 + end + end + raise last_error + end + + def check_error(response) + return unless response["error"] || (response["code"] && response["message"]) + + message = response["message"] || response["error"] || "Unknown error" + code = response["code"] + status = response["status_code"] || 0 + raise HttpError.new(message, status_code: status, code: code) + end + + def encode(str) + URI.encode_www_form_component(str) + end + + def parse_confidential_transaction(data) + ConfidentialTransaction.new( + id: data["id"], + inputs: (data["inputs"] || []).map { |i| parse_confidential_input(i) }, + outputs: (data["outputs"] || []).map { |o| parse_confidential_output(o) }, + fee: data["fee"], + excess: data["excess"], + excess_sig: data["excess_sig"], + kernel_offset: data["kernel_offset"] + ) + end + + def parse_confidential_input(data) + ConfidentialTxInput.new( + commitment: data["commitment"], + blinding_factor: data["blinding_factor"], + value: data["value"], + key_image: data["key_image"] + ) + end + + def parse_confidential_output(data) + ConfidentialTxOutput.new( + commitment: data["commitment"], + blinding_factor: data["blinding_factor"], + value: data["value"], + recipient_public_key: data["recipient_public_key"], + range_proof: data["range_proof"] + ) + end + + def parse_stealth_output(data) + StealthOutput.new( + tx_hash: data["tx_hash"], + output_index: data["output_index"], + stealth_address: data["stealth_address"], + amount: data["amount"], + block_height: data["block_height"] + ) + end + end +end diff --git a/sdk/ruby/lib/synor_privacy/types.rb b/sdk/ruby/lib/synor_privacy/types.rb new file mode 100644 index 0000000..36765bf --- /dev/null +++ b/sdk/ruby/lib/synor_privacy/types.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +module SynorPrivacy + # Configuration for the Privacy SDK. + class Config + attr_accessor :api_key, :endpoint, :timeout, :retries + + def initialize(api_key:, endpoint: "https://privacy.synor.io", timeout: 30, retries: 3) + @api_key = api_key + @endpoint = endpoint + @timeout = timeout + @retries = retries + end + end + + # Confidential transaction input. + class ConfidentialTxInput + attr_accessor :commitment, :blinding_factor, :value, :key_image + + def initialize(commitment:, blinding_factor:, value:, key_image: nil) + @commitment = commitment + @blinding_factor = blinding_factor + @value = value + @key_image = key_image + end + + def to_h + h = { + commitment: @commitment, + blinding_factor: @blinding_factor, + value: @value + } + h[:key_image] = @key_image if @key_image + h + end + end + + # Confidential transaction output. + class ConfidentialTxOutput + attr_accessor :commitment, :blinding_factor, :value, :recipient_public_key, :range_proof + + def initialize(commitment:, blinding_factor:, value:, recipient_public_key:, range_proof: nil) + @commitment = commitment + @blinding_factor = blinding_factor + @value = value + @recipient_public_key = recipient_public_key + @range_proof = range_proof + end + + def to_h + h = { + commitment: @commitment, + blinding_factor: @blinding_factor, + value: @value, + recipient_public_key: @recipient_public_key + } + h[:range_proof] = @range_proof if @range_proof + h + end + end + + # Confidential transaction. + class ConfidentialTransaction + attr_accessor :id, :inputs, :outputs, :fee, :excess, :excess_sig, :kernel_offset + + def initialize(id:, inputs:, outputs:, fee:, excess:, excess_sig:, kernel_offset: nil) + @id = id + @inputs = inputs + @outputs = outputs + @fee = fee + @excess = excess + @excess_sig = excess_sig + @kernel_offset = kernel_offset + end + + def to_h + h = { + id: @id, + inputs: @inputs.map(&:to_h), + outputs: @outputs.map(&:to_h), + fee: @fee, + excess: @excess, + excess_sig: @excess_sig + } + h[:kernel_offset] = @kernel_offset if @kernel_offset + h + end + end + + # Pedersen commitment. + class Commitment + attr_accessor :commitment, :blinding_factor + + def initialize(commitment:, blinding_factor:) + @commitment = commitment + @blinding_factor = blinding_factor + end + + def to_h + { commitment: @commitment, blinding_factor: @blinding_factor } + end + end + + # Bulletproof range proof. + class RangeProof + attr_accessor :proof, :commitment, :min_value, :max_value + + def initialize(proof:, commitment:, min_value:, max_value:) + @proof = proof + @commitment = commitment + @min_value = min_value + @max_value = max_value + end + + def to_h + { + proof: @proof, + commitment: @commitment, + min_value: @min_value, + max_value: @max_value + } + end + end + + # Ring signature for anonymous signing. + class RingSignature + attr_accessor :c0, :s, :key_image, :ring + + def initialize(c0:, s:, key_image:, ring:) + @c0 = c0 + @s = s + @key_image = key_image + @ring = ring + end + + def to_h + { c0: @c0, s: @s, key_image: @key_image, ring: @ring } + end + end + + # Stealth address key pair. + class StealthKeyPair + attr_accessor :spend_public_key, :spend_private_key, :view_public_key, :view_private_key + + def initialize(spend_public_key:, spend_private_key:, view_public_key:, view_private_key:) + @spend_public_key = spend_public_key + @spend_private_key = spend_private_key + @view_public_key = view_public_key + @view_private_key = view_private_key + end + end + + # Derived stealth address. + class StealthAddress + attr_accessor :address, :ephemeral_public_key, :tx_public_key + + def initialize(address:, ephemeral_public_key:, tx_public_key: nil) + @address = address + @ephemeral_public_key = ephemeral_public_key + @tx_public_key = tx_public_key + end + end + + # Scanned stealth output. + class StealthOutput + attr_accessor :tx_hash, :output_index, :stealth_address, :amount, :block_height + + def initialize(tx_hash:, output_index:, stealth_address:, amount:, block_height:) + @tx_hash = tx_hash + @output_index = output_index + @stealth_address = stealth_address + @amount = amount + @block_height = block_height + end + end + + # Client closed error. + class ClientClosedError < StandardError; end + + # HTTP error. + class HttpError < StandardError + attr_reader :status_code, :code + + def initialize(message, status_code: nil, code: nil) + super(message) + @status_code = status_code + @code = code + end + end +end diff --git a/sdk/ruby/lib/synor_privacy/version.rb b/sdk/ruby/lib/synor_privacy/version.rb new file mode 100644 index 0000000..b45bf4a --- /dev/null +++ b/sdk/ruby/lib/synor_privacy/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module SynorPrivacy + VERSION = "0.1.0" +end diff --git a/sdk/rust/src/contract/client.rs b/sdk/rust/src/contract/client.rs new file mode 100644 index 0000000..43c4d8d --- /dev/null +++ b/sdk/rust/src/contract/client.rs @@ -0,0 +1,471 @@ +//! Synor Contract SDK client + +use reqwest::Client as HttpClient; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +use super::error::{ContractError, Result}; +use super::types::*; + +/// Contract SDK configuration +#[derive(Debug, Clone)] +pub struct ContractConfig { + /// API key for authentication + pub api_key: String, + /// API endpoint URL + pub endpoint: String, + /// Request timeout in milliseconds + pub timeout_ms: u64, + /// Number of retry attempts + pub retries: u32, +} + +impl ContractConfig { + /// Create a new configuration with the given API key + pub fn new(api_key: impl Into) -> Self { + Self { + api_key: api_key.into(), + endpoint: "https://contract.synor.io".to_string(), + timeout_ms: 30000, + retries: 3, + } + } + + /// Set the endpoint URL + pub fn endpoint(mut self, endpoint: impl Into) -> Self { + self.endpoint = endpoint.into(); + self + } + + /// Set the timeout in milliseconds + pub fn timeout_ms(mut self, timeout_ms: u64) -> Self { + self.timeout_ms = timeout_ms; + self + } + + /// Set the number of retries + pub fn retries(mut self, retries: u32) -> Self { + self.retries = retries; + self + } +} + +/// Synor Contract SDK client +pub struct SynorContract { + config: ContractConfig, + client: HttpClient, + closed: Arc, +} + +impl SynorContract { + /// Create a new Contract client + pub fn new(config: ContractConfig) -> Result { + let client = HttpClient::builder() + .timeout(std::time::Duration::from_millis(config.timeout_ms)) + .build() + .map_err(|e| ContractError::Request(e.to_string()))?; + + Ok(Self { + config, + client, + closed: Arc::new(AtomicBool::new(false)), + }) + } + + fn check_closed(&self) -> Result<()> { + if self.closed.load(Ordering::SeqCst) { + return Err(ContractError::Closed); + } + Ok(()) + } + + async fn request( + &self, + method: reqwest::Method, + path: &str, + body: Option, + ) -> Result { + self.check_closed()?; + + let url = format!("{}{}", self.config.endpoint, path); + let mut last_error = None; + + for attempt in 0..self.config.retries { + let mut req = self + .client + .request(method.clone(), &url) + .header("Authorization", format!("Bearer {}", self.config.api_key)) + .header("Content-Type", "application/json") + .header("X-SDK-Version", format!("rust/{}", env!("CARGO_PKG_VERSION"))); + + if let Some(ref b) = body { + req = req.json(b); + } + + match req.send().await { + Ok(response) => { + let status = response.status(); + let text = response.text().await.map_err(|e| ContractError::Response(e.to_string()))?; + + if status.is_success() { + return serde_json::from_str(&text).map_err(ContractError::from); + } + + // Try to parse error response + if let Ok(error_response) = serde_json::from_str::(&text) { + let message = error_response + .get("message") + .or_else(|| error_response.get("error")) + .and_then(|v| v.as_str()) + .unwrap_or("Unknown error") + .to_string(); + let code = error_response + .get("code") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + last_error = Some(ContractError::Api { + message, + code, + status_code: Some(status.as_u16()), + }); + } else { + last_error = Some(ContractError::Response(text)); + } + } + Err(e) => { + last_error = Some(ContractError::Request(e.to_string())); + } + } + + if attempt < self.config.retries - 1 { + tokio::time::sleep(std::time::Duration::from_millis( + 2u64.pow(attempt) * 1000, + )) + .await; + } + } + + Err(last_error.unwrap_or_else(|| ContractError::Request("Unknown error".to_string()))) + } + + async fn get(&self, path: &str) -> Result { + self.request(reqwest::Method::GET, path, None).await + } + + async fn post( + &self, + path: &str, + body: serde_json::Value, + ) -> Result { + self.request(reqwest::Method::POST, path, Some(body)).await + } + + // ==================== Contract Deployment ==================== + + /// Deploy a new smart contract + pub async fn deploy(&self, options: DeployContractOptions) -> Result { + let mut body = serde_json::json!({ + "bytecode": options.bytecode, + }); + + if let Some(abi) = &options.abi { + body["abi"] = serde_json::to_value(abi)?; + } + if let Some(args) = &options.constructor_args { + body["constructor_args"] = serde_json::to_value(args)?; + } + if let Some(value) = &options.value { + body["value"] = serde_json::json!(value); + } + if let Some(gas_limit) = options.gas_limit { + body["gas_limit"] = serde_json::json!(gas_limit); + } + if let Some(gas_price) = &options.gas_price { + body["gas_price"] = serde_json::json!(gas_price); + } + if let Some(nonce) = options.nonce { + body["nonce"] = serde_json::json!(nonce); + } + + self.post("/contract/deploy", body).await + } + + /// Deploy a contract using CREATE2 for deterministic addresses + pub async fn deploy_create2(&self, options: DeployContractOptions, salt: &str) -> Result { + let mut body = serde_json::json!({ + "bytecode": options.bytecode, + "salt": salt, + }); + + if let Some(abi) = &options.abi { + body["abi"] = serde_json::to_value(abi)?; + } + if let Some(args) = &options.constructor_args { + body["constructor_args"] = serde_json::to_value(args)?; + } + if let Some(value) = &options.value { + body["value"] = serde_json::json!(value); + } + if let Some(gas_limit) = options.gas_limit { + body["gas_limit"] = serde_json::json!(gas_limit); + } + if let Some(gas_price) = &options.gas_price { + body["gas_price"] = serde_json::json!(gas_price); + } + + self.post("/contract/deploy/create2", body).await + } + + /// Predict the address from a CREATE2 deployment + pub async fn predict_address(&self, bytecode: &str, salt: &str, deployer: Option<&str>) -> Result { + let mut body = serde_json::json!({ + "bytecode": bytecode, + "salt": salt, + }); + + if let Some(d) = deployer { + body["deployer"] = serde_json::json!(d); + } + + let response: serde_json::Value = self.post("/contract/predict-address", body).await?; + response["address"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| ContractError::Response("Missing address in response".to_string())) + } + + // ==================== Contract Interaction ==================== + + /// Call a view/pure function (read-only, no gas) + pub async fn call(&self, options: CallContractOptions) -> Result { + let body = serde_json::json!({ + "contract": options.contract, + "method": options.method, + "args": options.args, + "abi": options.abi, + }); + + self.post("/contract/call", body).await + } + + /// Send a state-changing transaction + pub async fn send(&self, options: SendContractOptions) -> Result { + let mut body = serde_json::json!({ + "contract": options.contract, + "method": options.method, + "args": options.args, + "abi": options.abi, + }); + + if let Some(value) = &options.value { + body["value"] = serde_json::json!(value); + } + if let Some(gas_limit) = options.gas_limit { + body["gas_limit"] = serde_json::json!(gas_limit); + } + if let Some(gas_price) = &options.gas_price { + body["gas_price"] = serde_json::json!(gas_price); + } + if let Some(nonce) = options.nonce { + body["nonce"] = serde_json::json!(nonce); + } + + self.post("/contract/send", body).await + } + + // ==================== Events ==================== + + /// Get events from a contract + pub async fn get_events(&self, filter: EventFilter) -> Result> { + let mut body = serde_json::json!({ + "contract": filter.contract, + }); + + if let Some(event) = &filter.event { + body["event"] = serde_json::json!(event); + } + if let Some(from_block) = filter.from_block { + body["from_block"] = serde_json::json!(from_block); + } + if let Some(to_block) = filter.to_block { + body["to_block"] = serde_json::json!(to_block); + } + if let Some(topics) = &filter.topics { + body["topics"] = serde_json::to_value(topics)?; + } + if let Some(abi) = &filter.abi { + body["abi"] = serde_json::to_value(abi)?; + } + + self.post("/contract/events", body).await + } + + /// Get the logs for a contract + pub async fn get_logs(&self, contract: &str, from_block: Option, to_block: Option) -> Result> { + let mut params = format!("contract={}", contract); + if let Some(from) = from_block { + params.push_str(&format!("&from_block={}", from)); + } + if let Some(to) = to_block { + params.push_str(&format!("&to_block={}", to)); + } + + self.get(&format!("/contract/logs?{}", params)).await + } + + /// Decode event logs using an ABI + pub async fn decode_logs(&self, logs: &[EventLog], abi: &Abi) -> Result> { + let body = serde_json::json!({ + "logs": logs, + "abi": abi, + }); + + self.post("/contract/decode-logs", body).await + } + + // ==================== ABI Utilities ==================== + + /// Encode a function call + pub async fn encode_call(&self, options: EncodeCallOptions) -> Result { + let body = serde_json::json!({ + "method": options.method, + "args": options.args, + "abi": options.abi, + }); + + let response: serde_json::Value = self.post("/contract/encode", body).await?; + response["data"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| ContractError::Response("Missing data in response".to_string())) + } + + /// Decode a function result + pub async fn decode_result(&self, options: DecodeResultOptions) -> Result { + let body = serde_json::json!({ + "data": options.data, + "method": options.method, + "abi": options.abi, + }); + + let response: serde_json::Value = self.post("/contract/decode", body).await?; + Ok(response["result"].clone()) + } + + /// Get the function selector for a method + pub async fn get_selector(&self, signature: &str) -> Result { + let response: serde_json::Value = self + .get(&format!("/contract/selector?signature={}", urlencoding::encode(signature))) + .await?; + response["selector"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| ContractError::Response("Missing selector in response".to_string())) + } + + // ==================== Gas Estimation ==================== + + /// Estimate gas for a contract interaction + pub async fn estimate_gas(&self, options: EstimateGasOptions) -> Result { + let mut body = serde_json::json!({ + "contract": options.contract, + "method": options.method, + "args": options.args, + "abi": options.abi, + }); + + if let Some(value) = &options.value { + body["value"] = serde_json::json!(value); + } + + self.post("/contract/estimate-gas", body).await + } + + // ==================== Contract Information ==================== + + /// Get bytecode deployed at an address + pub async fn get_bytecode(&self, address: &str) -> Result { + self.get(&format!("/contract/{}/bytecode", address)).await + } + + /// Verify contract source code + pub async fn verify(&self, options: VerifyContractOptions) -> Result { + let mut body = serde_json::json!({ + "address": options.address, + "source_code": options.source_code, + "compiler_version": options.compiler_version, + }); + + if let Some(ctor_args) = &options.constructor_args { + body["constructor_args"] = serde_json::json!(ctor_args); + } + if let Some(optimization) = options.optimization { + body["optimization"] = serde_json::json!(optimization); + } + if let Some(runs) = options.optimization_runs { + body["optimization_runs"] = serde_json::json!(runs); + } + if let Some(license) = &options.license { + body["license"] = serde_json::json!(license); + } + + self.post("/contract/verify", body).await + } + + /// Get verification status for a contract + pub async fn get_verification_status(&self, address: &str) -> Result { + self.get(&format!("/contract/{}/verification", address)).await + } + + // ==================== Multicall ==================== + + /// Execute multiple calls in a single request + pub async fn multicall(&self, requests: &[MulticallRequest]) -> Result> { + let body = serde_json::json!({ + "calls": requests, + }); + + self.post("/contract/multicall", body).await + } + + // ==================== Storage ==================== + + /// Read a storage slot from a contract + pub async fn read_storage(&self, options: ReadStorageOptions) -> Result { + let mut params = format!("contract={}&slot={}", options.contract, options.slot); + if let Some(block) = options.block_number { + params.push_str(&format!("&block={}", block)); + } + + let response: serde_json::Value = self.get(&format!("/contract/storage?{}", params)).await?; + response["value"] + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| ContractError::Response("Missing value in response".to_string())) + } + + // ==================== Lifecycle ==================== + + /// Check if the service is healthy + pub async fn health_check(&self) -> bool { + if self.closed.load(Ordering::SeqCst) { + return false; + } + + match self.get::("/health").await { + Ok(response) => response.get("status").and_then(|s| s.as_str()) == Some("healthy"), + Err(_) => false, + } + } + + /// Close the client + pub fn close(&self) { + self.closed.store(true, Ordering::SeqCst); + } + + /// Check if the client is closed + pub fn is_closed(&self) -> bool { + self.closed.load(Ordering::SeqCst) + } +} diff --git a/sdk/rust/src/contract/error.rs b/sdk/rust/src/contract/error.rs new file mode 100644 index 0000000..61335d4 --- /dev/null +++ b/sdk/rust/src/contract/error.rs @@ -0,0 +1,60 @@ +//! Contract SDK errors + +use std::fmt; + +/// Contract SDK result type +pub type Result = std::result::Result; + +/// Contract SDK error +#[derive(Debug)] +pub enum ContractError { + /// HTTP request error + Request(String), + /// Invalid response + Response(String), + /// Serialization error + Serialization(String), + /// API error + Api { + message: String, + code: Option, + status_code: Option, + }, + /// Client is closed + Closed, +} + +impl fmt::Display for ContractError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ContractError::Request(msg) => write!(f, "Request error: {}", msg), + ContractError::Response(msg) => write!(f, "Response error: {}", msg), + ContractError::Serialization(msg) => write!(f, "Serialization error: {}", msg), + ContractError::Api { message, code, status_code } => { + write!(f, "API error: {}", message)?; + if let Some(c) = code { + write!(f, " (code: {})", c)?; + } + if let Some(s) = status_code { + write!(f, " (status: {})", s)?; + } + Ok(()) + } + ContractError::Closed => write!(f, "Client has been closed"), + } + } +} + +impl std::error::Error for ContractError {} + +impl From for ContractError { + fn from(err: reqwest::Error) -> Self { + ContractError::Request(err.to_string()) + } +} + +impl From for ContractError { + fn from(err: serde_json::Error) -> Self { + ContractError::Serialization(err.to_string()) + } +} diff --git a/sdk/rust/src/contract/mod.rs b/sdk/rust/src/contract/mod.rs new file mode 100644 index 0000000..c43b42c --- /dev/null +++ b/sdk/rust/src/contract/mod.rs @@ -0,0 +1,16 @@ +//! Synor Contract SDK for Rust +//! +//! Smart contract deployment, interaction, and event handling: +//! - Deploy contracts (standard and CREATE2) +//! - Call view/pure functions +//! - Send state-changing transactions +//! - Get events and logs +//! - ABI encoding/decoding utilities + +mod types; +mod error; +mod client; + +pub use types::*; +pub use error::*; +pub use client::*; diff --git a/sdk/rust/src/contract/types.rs b/sdk/rust/src/contract/types.rs new file mode 100644 index 0000000..25a6a53 --- /dev/null +++ b/sdk/rust/src/contract/types.rs @@ -0,0 +1,316 @@ +//! Contract SDK types + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Default contract endpoint +pub const DEFAULT_CONTRACT_ENDPOINT: &str = "https://contract.synor.cc/api/v1"; + +/// Contract client configuration +#[derive(Clone, Debug)] +pub struct ContractConfig { + pub api_key: String, + pub endpoint: String, + pub timeout_secs: u64, + pub retries: u32, + pub debug: bool, + pub default_gas_limit: Option, + pub default_gas_price: Option, +} + +impl Default for ContractConfig { + fn default() -> Self { + Self { + api_key: String::new(), + endpoint: DEFAULT_CONTRACT_ENDPOINT.to_string(), + timeout_secs: 30, + retries: 3, + debug: false, + default_gas_limit: None, + default_gas_price: None, + } + } +} + +/// ABI entry type +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum AbiEntryType { + Function, + Constructor, + Event, + Error, + Fallback, + Receive, +} + +/// State mutability +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum StateMutability { + Pure, + View, + Nonpayable, + Payable, +} + +/// Transaction status +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum TransactionStatus { + Success, + Reverted, +} + +/// Verification status +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum VerificationStatus { + Verified, + Pending, + Failed, +} + +/// ABI parameter +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AbiParameter { + pub name: String, + #[serde(rename = "type")] + pub param_type: String, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub indexed: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub components: Option>, + #[serde(rename = "internalType", skip_serializing_if = "Option::is_none")] + pub internal_type: Option, +} + +/// ABI entry +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AbiEntry { + #[serde(rename = "type")] + pub entry_type: AbiEntryType, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub inputs: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub outputs: Option>, + #[serde(rename = "stateMutability", skip_serializing_if = "Option::is_none")] + pub state_mutability: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub anonymous: bool, +} + +/// Contract ABI +pub type Abi = Vec; + +/// Deployment result +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DeploymentResult { + pub address: String, + #[serde(rename = "transactionHash")] + pub transaction_hash: String, + #[serde(rename = "blockNumber")] + pub block_number: u64, + #[serde(rename = "gasUsed")] + pub gas_used: String, + #[serde(rename = "effectiveGasPrice")] + pub effective_gas_price: String, +} + +/// Event log +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EventLog { + #[serde(rename = "logIndex")] + pub log_index: u32, + pub address: String, + pub topics: Vec, + pub data: String, + #[serde(rename = "blockNumber")] + pub block_number: u64, + #[serde(rename = "transactionHash")] + pub transaction_hash: String, +} + +/// Decoded event +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DecodedEvent { + pub name: String, + pub signature: String, + pub args: HashMap, + pub log: EventLog, +} + +/// Transaction result +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TransactionResult { + #[serde(rename = "transactionHash")] + pub transaction_hash: String, + #[serde(rename = "blockNumber")] + pub block_number: u64, + #[serde(rename = "blockHash")] + pub block_hash: String, + #[serde(rename = "gasUsed")] + pub gas_used: String, + #[serde(rename = "effectiveGasPrice")] + pub effective_gas_price: String, + pub status: TransactionStatus, + pub logs: Vec, + #[serde(rename = "returnValue", skip_serializing_if = "Option::is_none")] + pub return_value: Option, + #[serde(rename = "revertReason", skip_serializing_if = "Option::is_none")] + pub revert_reason: Option, +} + +/// Contract interface +#[derive(Clone, Debug)] +pub struct ContractInterface { + pub abi: Abi, + pub functions: HashMap, + pub events: HashMap, + pub errors: HashMap, +} + +/// Gas estimation +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GasEstimation { + #[serde(rename = "gasLimit")] + pub gas_limit: String, + #[serde(rename = "gasPrice")] + pub gas_price: String, + #[serde(rename = "estimatedCost")] + pub estimated_cost: String, +} + +/// Bytecode metadata +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BytecodeMetadata { + pub compiler: String, + pub language: String, + pub sources: Vec, +} + +/// Bytecode info +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BytecodeInfo { + pub bytecode: String, + #[serde(rename = "deployedBytecode", skip_serializing_if = "Option::is_none")] + pub deployed_bytecode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub abi: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// Verification result +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct VerificationResult { + pub status: VerificationStatus, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub abi: Option, +} + +/// Multicall request +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MulticallRequest { + pub address: String, + #[serde(rename = "callData")] + pub call_data: String, + #[serde(rename = "allowFailure", default)] + pub allow_failure: bool, +} + +/// Multicall result +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MulticallResult { + pub success: bool, + #[serde(rename = "returnData")] + pub return_data: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub decoded: Option, +} + +/// Deploy options +#[derive(Clone, Debug, Default)] +pub struct DeployOptions { + pub bytecode: String, + pub abi: Option, + pub args: Option>, + pub gas_limit: Option, + pub gas_price: Option, + pub value: Option, + pub salt: Option, +} + +/// Call options +#[derive(Clone, Debug)] +pub struct CallOptions { + pub address: String, + pub method: String, + pub abi: Abi, + pub args: Option>, + pub block_number: Option, +} + +/// Send options +#[derive(Clone, Debug)] +pub struct SendOptions { + pub address: String, + pub method: String, + pub abi: Abi, + pub args: Option>, + pub gas_limit: Option, + pub gas_price: Option, + pub value: Option, +} + +/// Block number specifier +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum BlockNumber { + Number(u64), + Tag(String), +} + +impl Default for BlockNumber { + fn default() -> Self { + BlockNumber::Tag("latest".to_string()) + } +} + +/// Event filter +#[derive(Clone, Debug, Default)] +pub struct EventFilter { + pub address: String, + pub event_name: Option, + pub abi: Option, + pub from_block: Option, + pub to_block: Option, + pub filter: Option>, +} + +/// Estimate gas options +#[derive(Clone, Debug, Default)] +pub struct EstimateGasOptions { + pub address: Option, + pub method: Option, + pub args: Option>, + pub abi: Option, + pub bytecode: Option, + pub value: Option, + pub from: Option, +} + +/// Verify contract options +#[derive(Clone, Debug)] +pub struct VerifyContractOptions { + pub address: String, + pub source_code: String, + pub compiler_version: String, + pub constructor_arguments: Option, + pub optimization: bool, + pub optimization_runs: u32, + pub contract_name: Option, +} diff --git a/sdk/rust/src/lib.rs b/sdk/rust/src/lib.rs index 5aa10c9..f4b3950 100644 --- a/sdk/rust/src/lib.rs +++ b/sdk/rust/src/lib.rs @@ -54,6 +54,11 @@ pub mod storage; pub mod database; pub mod hosting; pub mod bridge; +pub mod economics; +pub mod governance; +pub mod mining; +pub mod privacy; +pub mod contract; #[cfg(test)] mod tests; diff --git a/sdk/rust/src/privacy/client.rs b/sdk/rust/src/privacy/client.rs new file mode 100644 index 0000000..83a015a --- /dev/null +++ b/sdk/rust/src/privacy/client.rs @@ -0,0 +1,626 @@ +//! Privacy client implementation + +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use super::error::{PrivacyError, Result}; +use super::types::*; + +/// Synor Privacy Client +pub struct PrivacyClient { + config: PrivacyConfig, + http_client: reqwest::Client, + closed: Arc, +} + +impl PrivacyClient { + /// Create a new privacy client + pub fn new(config: PrivacyConfig) -> Self { + let mut headers = HeaderMap::new(); + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {}", config.api_key)).unwrap(), + ); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert("X-SDK-Version", HeaderValue::from_static("rust/0.1.0")); + + let http_client = reqwest::Client::builder() + .default_headers(headers) + .timeout(Duration::from_secs(config.timeout_secs)) + .build() + .expect("Failed to create HTTP client"); + + Self { + config, + http_client, + closed: Arc::new(AtomicBool::new(false)), + } + } + + // ==================== Confidential Transactions ==================== + + /// Create a confidential transaction + pub async fn create_confidential_transaction( + &self, + opts: CreateConfidentialTxOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + inputs: Vec, + outputs: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + fee: Option, + #[serde(rename = "lockHeight", skip_serializing_if = "Option::is_none")] + lock_height: Option, + } + + #[derive(Deserialize)] + struct Response { + transaction: ConfidentialTransaction, + } + + let resp: Response = self.request( + "POST", + "/transactions/confidential", + Some(Request { + inputs: opts.inputs, + outputs: opts.outputs, + fee: opts.fee, + lock_height: opts.lock_height, + }), + ).await?; + Ok(resp.transaction) + } + + /// Verify a confidential transaction + pub async fn verify_confidential_transaction( + &self, + tx: &ConfidentialTransaction, + ) -> Result { + #[derive(Serialize)] + struct Request { + transaction: String, + } + self.request( + "POST", + "/transactions/confidential/verify", + Some(Request { transaction: tx.raw.clone() }), + ).await + } + + /// Decode a confidential output + pub async fn decode_confidential_output( + &self, + commitment: &str, + blinding: &str, + ) -> Result { + #[derive(Serialize)] + struct Request { + commitment: String, + blinding: String, + } + #[derive(Deserialize)] + struct Response { + amount: String, + } + let resp: Response = self.request( + "POST", + "/transactions/confidential/decode", + Some(Request { + commitment: commitment.to_string(), + blinding: blinding.to_string(), + }), + ).await?; + Ok(resp.amount) + } + + // ==================== Ring Signatures ==================== + + /// Create a ring signature + pub async fn create_ring_signature( + &self, + opts: CreateRingSignatureOptions, + ) -> Result { + #[derive(Serialize)] + struct Request { + message: String, + ring: Vec, + #[serde(rename = "privateKey")] + private_key: String, + #[serde(rename = "signerIndex", skip_serializing_if = "Option::is_none")] + signer_index: Option, + } + #[derive(Deserialize)] + struct Response { + signature: RingSignature, + } + let resp: Response = self.request( + "POST", + "/ring-signatures/create", + Some(Request { + message: opts.message, + ring: opts.ring, + private_key: opts.private_key, + signer_index: opts.signer_index, + }), + ).await?; + Ok(resp.signature) + } + + /// Verify a ring signature + pub async fn verify_ring_signature( + &self, + signature: &RingSignature, + message: &str, + ) -> Result { + #[derive(Serialize)] + struct Request<'a> { + signature: &'a RingSignature, + message: String, + } + self.request( + "POST", + "/ring-signatures/verify", + Some(Request { + signature, + message: message.to_string(), + }), + ).await + } + + /// Check if a key image has been used + pub async fn is_key_image_used(&self, key_image: &str) -> Result { + #[derive(Deserialize)] + struct Response { + used: bool, + } + let resp: Response = self.request( + "GET", + &format!("/ring-signatures/key-images/{}", urlencoding::encode(key_image)), + Option::<()>::None, + ).await?; + Ok(resp.used) + } + + /// Generate a random ring + pub async fn generate_random_ring( + &self, + size: u32, + exclude: Option>, + ) -> Result> { + #[derive(Serialize)] + struct Request { + size: u32, + #[serde(skip_serializing_if = "Option::is_none")] + exclude: Option>, + } + #[derive(Deserialize)] + struct Response { + ring: Vec, + } + let resp: Response = self.request( + "POST", + "/ring-signatures/random-ring", + Some(Request { size, exclude }), + ).await?; + Ok(resp.ring) + } + + // ==================== Stealth Addresses ==================== + + /// Generate a stealth keypair + pub async fn generate_stealth_keypair(&self) -> Result { + #[derive(Deserialize)] + struct Response { + keypair: StealthKeypair, + } + let resp: Response = self.request("POST", "/stealth/generate", Option::<()>::None).await?; + Ok(resp.keypair) + } + + /// Create a one-time address + pub async fn create_one_time_address( + &self, + stealth_address: &StealthAddress, + ) -> Result { + #[derive(Serialize)] + struct Request { + #[serde(rename = "scanPublicKey")] + scan_public_key: String, + #[serde(rename = "spendPublicKey")] + spend_public_key: String, + } + #[derive(Deserialize)] + struct Response { + #[serde(rename = "oneTimeAddress")] + one_time_address: OneTimeAddress, + } + let resp: Response = self.request( + "POST", + "/stealth/one-time-address", + Some(Request { + scan_public_key: stealth_address.scan_public_key.clone(), + spend_public_key: stealth_address.spend_public_key.clone(), + }), + ).await?; + Ok(resp.one_time_address) + } + + /// Derive shared secret + pub async fn derive_shared_secret( + &self, + stealth_address: &StealthAddress, + private_key: &str, + ephemeral_public_key: &str, + ) -> Result { + #[derive(Serialize)] + struct Request { + #[serde(rename = "scanPublicKey")] + scan_public_key: String, + #[serde(rename = "spendPublicKey")] + spend_public_key: String, + #[serde(rename = "privateKey")] + private_key: String, + #[serde(rename = "ephemeralPublicKey")] + ephemeral_public_key: String, + } + self.request( + "POST", + "/stealth/derive-secret", + Some(Request { + scan_public_key: stealth_address.scan_public_key.clone(), + spend_public_key: stealth_address.spend_public_key.clone(), + private_key: private_key.to_string(), + ephemeral_public_key: ephemeral_public_key.to_string(), + }), + ).await + } + + /// Scan for payments + pub async fn scan_for_payments( + &self, + scan_private_key: &str, + spend_public_key: &str, + transactions: Vec, + ) -> Result> { + #[derive(Serialize)] + struct Request { + #[serde(rename = "scanPrivateKey")] + scan_private_key: String, + #[serde(rename = "spendPublicKey")] + spend_public_key: String, + transactions: Vec, + } + #[derive(Deserialize)] + struct Response { + payments: Vec, + } + let resp: Response = self.request( + "POST", + "/stealth/scan", + Some(Request { + scan_private_key: scan_private_key.to_string(), + spend_public_key: spend_public_key.to_string(), + transactions, + }), + ).await?; + Ok(resp.payments) + } + + // ==================== Commitments ==================== + + /// Create a commitment + pub async fn create_commitment( + &self, + value: &str, + blinding: Option<&str>, + ) -> Result { + #[derive(Serialize)] + struct Request { + value: String, + #[serde(skip_serializing_if = "Option::is_none")] + blinding: Option, + } + self.request( + "POST", + "/commitments/create", + Some(Request { + value: value.to_string(), + blinding: blinding.map(|s| s.to_string()), + }), + ).await + } + + /// Open (verify) a commitment + pub async fn open_commitment( + &self, + commitment: &str, + value: &str, + blinding: &str, + ) -> Result { + #[derive(Serialize)] + struct Request { + commitment: String, + value: String, + blinding: String, + } + #[derive(Deserialize)] + struct Response { + valid: bool, + } + let resp: Response = self.request( + "POST", + "/commitments/open", + Some(Request { + commitment: commitment.to_string(), + value: value.to_string(), + blinding: blinding.to_string(), + }), + ).await?; + Ok(resp.valid) + } + + /// Add two commitments + pub async fn add_commitments( + &self, + commitment1: &str, + commitment2: &str, + ) -> Result { + #[derive(Serialize)] + struct Request { + commitment1: String, + commitment2: String, + } + #[derive(Deserialize)] + struct Response { + commitment: String, + } + let resp: Response = self.request( + "POST", + "/commitments/add", + Some(Request { + commitment1: commitment1.to_string(), + commitment2: commitment2.to_string(), + }), + ).await?; + Ok(resp.commitment) + } + + /// Subtract two commitments + pub async fn subtract_commitments( + &self, + commitment1: &str, + commitment2: &str, + ) -> Result { + #[derive(Serialize)] + struct Request { + commitment1: String, + commitment2: String, + } + #[derive(Deserialize)] + struct Response { + commitment: String, + } + let resp: Response = self.request( + "POST", + "/commitments/subtract", + Some(Request { + commitment1: commitment1.to_string(), + commitment2: commitment2.to_string(), + }), + ).await?; + Ok(resp.commitment) + } + + /// Compute blinding sum + pub async fn compute_blinding_sum( + &self, + positive: Vec, + negative: Vec, + ) -> Result { + #[derive(Serialize)] + struct Request { + positive: Vec, + negative: Vec, + } + #[derive(Deserialize)] + struct Response { + sum: String, + } + let resp: Response = self.request( + "POST", + "/commitments/blinding-sum", + Some(Request { positive, negative }), + ).await?; + Ok(resp.sum) + } + + /// Generate a random blinding factor + pub async fn generate_blinding(&self) -> Result { + #[derive(Deserialize)] + struct Response { + blinding: String, + } + let resp: Response = self.request("POST", "/commitments/random-blinding", Option::<()>::None).await?; + Ok(resp.blinding) + } + + // ==================== Range Proofs ==================== + + /// Create a range proof + pub async fn create_range_proof(&self, opts: CreateRangeProofOptions) -> Result { + #[derive(Serialize)] + struct Request { + value: String, + blinding: String, + #[serde(skip_serializing_if = "Option::is_none")] + message: Option, + #[serde(rename = "bitLength")] + bit_length: u32, + } + #[derive(Deserialize)] + struct Response { + proof: RangeProof, + } + let resp: Response = self.request( + "POST", + "/range-proofs/create", + Some(Request { + value: opts.value, + blinding: opts.blinding, + message: opts.message, + bit_length: opts.bit_length.unwrap_or(64), + }), + ).await?; + Ok(resp.proof) + } + + /// Verify a range proof + pub async fn verify_range_proof( + &self, + commitment: &str, + proof: &str, + ) -> Result { + #[derive(Serialize)] + struct Request { + commitment: String, + proof: String, + } + self.request( + "POST", + "/range-proofs/verify", + Some(Request { + commitment: commitment.to_string(), + proof: proof.to_string(), + }), + ).await + } + + /// Create aggregated range proof + pub async fn create_aggregated_range_proof( + &self, + outputs: Vec<(String, String)>, + ) -> Result { + #[derive(Serialize)] + struct Output { + value: String, + blinding: String, + } + #[derive(Serialize)] + struct Request { + outputs: Vec, + } + #[derive(Deserialize)] + struct Response { + proof: String, + } + let resp: Response = self.request( + "POST", + "/range-proofs/aggregate", + Some(Request { + outputs: outputs.into_iter().map(|(v, b)| Output { value: v, blinding: b }).collect(), + }), + ).await?; + Ok(resp.proof) + } + + // ==================== Lifecycle ==================== + + /// Health check + pub async fn health_check(&self) -> bool { + #[derive(Deserialize)] + struct Response { + status: String, + } + match self.request::<_, Response>("GET", "/health", Option::<()>::None).await { + Ok(resp) => resp.status == "healthy", + Err(_) => false, + } + } + + /// Close the client + pub fn close(&self) { + self.closed.store(true, Ordering::SeqCst); + } + + /// Check if client is closed + pub fn is_closed(&self) -> bool { + self.closed.load(Ordering::SeqCst) + } + + // ==================== Private ==================== + + async fn request(&self, method: &str, path: &str, body: Option) -> Result + where + B: Serialize, + R: DeserializeOwned, + { + if self.closed.load(Ordering::SeqCst) { + return Err(PrivacyError::Closed); + } + + let mut last_error = None; + for attempt in 0..self.config.retries { + match self.do_request(method, path, &body).await { + Ok(result) => return Ok(result), + Err(e) => { + last_error = Some(e); + if attempt < self.config.retries - 1 { + tokio::time::sleep(Duration::from_secs(1 << attempt)).await; + } + } + } + } + Err(last_error.unwrap_or(PrivacyError::Request("Unknown error".to_string()))) + } + + async fn do_request(&self, method: &str, path: &str, body: &Option) -> Result + where + B: Serialize, + R: DeserializeOwned, + { + let url = format!("{}{}", self.config.endpoint, path); + + let mut req = match method { + "GET" => self.http_client.get(&url), + "POST" => self.http_client.post(&url), + "PUT" => self.http_client.put(&url), + "DELETE" => self.http_client.delete(&url), + _ => return Err(PrivacyError::Request(format!("Unknown method: {}", method))), + }; + + if let Some(b) = body { + req = req.json(b); + } + + let response = req.send().await?; + let status = response.status(); + + if status.is_client_error() || status.is_server_error() { + #[derive(Deserialize)] + struct ErrorResponse { + message: Option, + error: Option, + code: Option, + } + let error_body: ErrorResponse = response.json().await.unwrap_or(ErrorResponse { + message: None, + error: None, + code: None, + }); + let message = error_body.message + .or(error_body.error) + .unwrap_or_else(|| format!("HTTP {}", status)); + return Err(PrivacyError::Api { + message, + code: error_body.code, + status_code: Some(status.as_u16()), + }); + } + + response.json().await.map_err(|e| PrivacyError::Response(e.to_string())) + } +} diff --git a/sdk/rust/src/privacy/error.rs b/sdk/rust/src/privacy/error.rs new file mode 100644 index 0000000..0e7a981 --- /dev/null +++ b/sdk/rust/src/privacy/error.rs @@ -0,0 +1,60 @@ +//! Privacy SDK errors + +use std::fmt; + +/// Privacy SDK result type +pub type Result = std::result::Result; + +/// Privacy SDK error +#[derive(Debug)] +pub enum PrivacyError { + /// HTTP request error + Request(String), + /// Invalid response + Response(String), + /// Serialization error + Serialization(String), + /// API error + Api { + message: String, + code: Option, + status_code: Option, + }, + /// Client is closed + Closed, +} + +impl fmt::Display for PrivacyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PrivacyError::Request(msg) => write!(f, "Request error: {}", msg), + PrivacyError::Response(msg) => write!(f, "Response error: {}", msg), + PrivacyError::Serialization(msg) => write!(f, "Serialization error: {}", msg), + PrivacyError::Api { message, code, status_code } => { + write!(f, "API error: {}", message)?; + if let Some(c) = code { + write!(f, " (code: {})", c)?; + } + if let Some(s) = status_code { + write!(f, " (status: {})", s)?; + } + Ok(()) + } + PrivacyError::Closed => write!(f, "Client has been closed"), + } + } +} + +impl std::error::Error for PrivacyError {} + +impl From for PrivacyError { + fn from(err: reqwest::Error) -> Self { + PrivacyError::Request(err.to_string()) + } +} + +impl From for PrivacyError { + fn from(err: serde_json::Error) -> Self { + PrivacyError::Serialization(err.to_string()) + } +} diff --git a/sdk/rust/src/privacy/mod.rs b/sdk/rust/src/privacy/mod.rs new file mode 100644 index 0000000..1866588 --- /dev/null +++ b/sdk/rust/src/privacy/mod.rs @@ -0,0 +1,15 @@ +//! Synor Privacy SDK for Rust +//! +//! Privacy-enhancing features for the Synor blockchain: +//! - Confidential transactions with hidden amounts +//! - Ring signatures for anonymous signing +//! - Stealth addresses for unlinkable payments +//! - Pedersen commitments and Bulletproof range proofs + +mod types; +mod error; +mod client; + +pub use types::*; +pub use error::*; +pub use client::*; diff --git a/sdk/rust/src/privacy/types.rs b/sdk/rust/src/privacy/types.rs new file mode 100644 index 0000000..a6b98aa --- /dev/null +++ b/sdk/rust/src/privacy/types.rs @@ -0,0 +1,290 @@ +//! Privacy SDK types + +use serde::{Deserialize, Serialize}; + +/// Default privacy endpoint +pub const DEFAULT_PRIVACY_ENDPOINT: &str = "https://privacy.synor.cc/api/v1"; + +/// Privacy client configuration +#[derive(Clone, Debug)] +pub struct PrivacyConfig { + pub api_key: String, + pub endpoint: String, + pub timeout_secs: u64, + pub retries: u32, + pub debug: bool, +} + +impl Default for PrivacyConfig { + fn default() -> Self { + Self { + api_key: String::new(), + endpoint: DEFAULT_PRIVACY_ENDPOINT.to_string(), + timeout_secs: 30, + retries: 3, + debug: false, + } + } +} + +/// Key type +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum KeyType { + Ed25519, + Secp256k1, +} + +/// Generator type for commitments +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum GeneratorType { + Default, + Alternate, +} + +impl Default for GeneratorType { + fn default() -> Self { + Self::Default + } +} + +/// Public key +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PublicKey { + pub key: String, + #[serde(rename = "type")] + pub key_type: KeyType, +} + +/// Private key +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PrivateKey { + pub key: String, + #[serde(rename = "type")] + pub key_type: KeyType, +} + +/// UTXO for confidential transactions +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ConfidentialUtxo { + pub txid: String, + pub vout: u32, + pub commitment: String, + #[serde(rename = "rangeProof")] + pub range_proof: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub blinding: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub amount: Option, +} + +/// Output for confidential transactions +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ConfidentialOutput { + pub recipient: String, + pub amount: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub blinding: Option, +} + +/// Output features +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OutputFeatures { + pub flags: u32, + #[serde(rename = "lockHeight", skip_serializing_if = "Option::is_none")] + pub lock_height: Option, +} + +/// Confidential transaction input +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ConfidentialTxInput { + #[serde(rename = "outputRef")] + pub output_ref: String, + pub commitment: String, +} + +/// Confidential transaction output +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ConfidentialTxOutput { + pub commitment: String, + #[serde(rename = "rangeProof")] + pub range_proof: String, + pub features: OutputFeatures, +} + +/// Transaction kernel +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TransactionKernel { + pub features: u32, + pub fee: String, + #[serde(rename = "lockHeight")] + pub lock_height: u64, + pub excess: String, + #[serde(rename = "excessSignature")] + pub excess_signature: String, +} + +/// Confidential transaction +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ConfidentialTransaction { + pub txid: String, + pub version: u32, + pub inputs: Vec, + pub outputs: Vec, + pub kernel: TransactionKernel, + pub offset: String, + pub raw: String, +} + +/// Verification details for confidential transaction +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct VerifyConfidentialTxDetails { + #[serde(rename = "commitmentsBalance")] + pub commitments_balance: bool, + #[serde(rename = "rangeProofsValid")] + pub range_proofs_valid: bool, + #[serde(rename = "signatureValid")] + pub signature_valid: bool, + #[serde(rename = "noDuplicateInputs")] + pub no_duplicate_inputs: bool, +} + +/// Verification result for confidential transaction +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct VerifyConfidentialTxResult { + pub valid: bool, + pub details: VerifyConfidentialTxDetails, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +/// Ring signature components +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RingSignatureComponents { + pub c: Vec, + pub r: Vec, +} + +/// Ring signature +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RingSignature { + pub id: String, + #[serde(rename = "messageHash")] + pub message_hash: String, + pub ring: Vec, + #[serde(rename = "keyImage")] + pub key_image: String, + pub signature: RingSignatureComponents, + #[serde(rename = "ringSize")] + pub ring_size: u32, +} + +/// Ring signature verification result +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct VerifyRingSignatureResult { + pub valid: bool, + #[serde(rename = "keyImage")] + pub key_image: String, +} + +/// Stealth address +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StealthAddress { + pub address: String, + #[serde(rename = "scanPublicKey")] + pub scan_public_key: String, + #[serde(rename = "spendPublicKey")] + pub spend_public_key: String, +} + +/// Stealth keypair +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StealthKeypair { + pub address: StealthAddress, + #[serde(rename = "scanPrivateKey")] + pub scan_private_key: String, + #[serde(rename = "spendPrivateKey")] + pub spend_private_key: String, +} + +/// One-time address +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OneTimeAddress { + pub address: String, + #[serde(rename = "ephemeralPublicKey")] + pub ephemeral_public_key: String, + #[serde(rename = "sharedSecret", skip_serializing_if = "Option::is_none")] + pub shared_secret: Option, +} + +/// Shared secret +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SharedSecret { + pub secret: String, + #[serde(rename = "oneTimePrivateKey")] + pub one_time_private_key: String, + #[serde(rename = "oneTimeAddress")] + pub one_time_address: String, +} + +/// Pedersen commitment +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Commitment { + pub commitment: String, + #[serde(default)] + pub generator: GeneratorType, +} + +/// Commitment with blinding factor +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CommitmentWithBlinding { + pub commitment: Commitment, + pub blinding: String, +} + +/// Range proof +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RangeProof { + pub proof: String, + pub commitment: String, + #[serde(rename = "bitLength")] + pub bit_length: u32, +} + +/// Range proof verification result +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct VerifyRangeProofResult { + pub valid: bool, + #[serde(rename = "minValue")] + pub min_value: String, + #[serde(rename = "maxValue")] + pub max_value: String, +} + +/// Options for creating a confidential transaction +#[derive(Clone, Debug, Default)] +pub struct CreateConfidentialTxOptions { + pub inputs: Vec, + pub outputs: Vec, + pub fee: Option, + pub lock_height: Option, +} + +/// Options for creating a ring signature +#[derive(Clone, Debug)] +pub struct CreateRingSignatureOptions { + pub message: String, + pub ring: Vec, + pub private_key: String, + pub signer_index: Option, +} + +/// Options for creating a range proof +#[derive(Clone, Debug)] +pub struct CreateRangeProofOptions { + pub value: String, + pub blinding: String, + pub message: Option, + pub bit_length: Option, +} diff --git a/sdk/swift/Sources/SynorContract/ContractClient.swift b/sdk/swift/Sources/SynorContract/ContractClient.swift new file mode 100644 index 0000000..b7cdbad --- /dev/null +++ b/sdk/swift/Sources/SynorContract/ContractClient.swift @@ -0,0 +1,314 @@ +import Foundation + +/// Synor Contract SDK client for Swift. +/// Smart contract deployment, interaction, and event handling. +public actor ContractClient { + public static let version = "0.1.0" + + private let config: ContractConfig + private let session: URLSession + private var isClosed = false + + public init(config: ContractConfig) { + self.config = config + let sessionConfig = URLSessionConfiguration.default + sessionConfig.timeoutIntervalForRequest = TimeInterval(config.timeoutMs) / 1000.0 + self.session = URLSession(configuration: sessionConfig) + } + + // MARK: - Contract Deployment + + public func deploy(_ options: DeployContractOptions) async throws -> DeploymentResult { + var body: [String: Any] = ["bytecode": options.bytecode] + if let abi = options.abi { body["abi"] = abi.map { $0.toDictionary() } } + if let args = options.constructorArgs { body["constructor_args"] = args } + if let value = options.value { body["value"] = value } + if let gasLimit = options.gasLimit { body["gas_limit"] = gasLimit } + if let gasPrice = options.gasPrice { body["gas_price"] = gasPrice } + if let nonce = options.nonce { body["nonce"] = nonce } + return try await post("/contract/deploy", body: body) + } + + public func deployCreate2(_ options: DeployContractOptions, salt: String) async throws -> DeploymentResult { + var body: [String: Any] = ["bytecode": options.bytecode, "salt": salt] + if let abi = options.abi { body["abi"] = abi.map { $0.toDictionary() } } + if let args = options.constructorArgs { body["constructor_args"] = args } + if let value = options.value { body["value"] = value } + if let gasLimit = options.gasLimit { body["gas_limit"] = gasLimit } + if let gasPrice = options.gasPrice { body["gas_price"] = gasPrice } + return try await post("/contract/deploy/create2", body: body) + } + + public func predictAddress(bytecode: String, salt: String, deployer: String? = nil) async throws -> String { + var body: [String: Any] = ["bytecode": bytecode, "salt": salt] + if let deployer = deployer { body["deployer"] = deployer } + let response: [String: Any] = try await post("/contract/predict-address", body: body) + guard let address = response["address"] as? String else { + throw ContractError.response("Missing address") + } + return address + } + + // MARK: - Contract Interaction + + public func call(_ options: CallContractOptions) async throws -> Any { + let body: [String: Any] = [ + "contract": options.contract, + "method": options.method, + "args": options.args, + "abi": options.abi.map { $0.toDictionary() } + ] + return try await post("/contract/call", body: body) as [String: Any] + } + + public func send(_ options: SendContractOptions) async throws -> TransactionResult { + var body: [String: Any] = [ + "contract": options.contract, + "method": options.method, + "args": options.args, + "abi": options.abi.map { $0.toDictionary() } + ] + if let value = options.value { body["value"] = value } + if let gasLimit = options.gasLimit { body["gas_limit"] = gasLimit } + if let gasPrice = options.gasPrice { body["gas_price"] = gasPrice } + if let nonce = options.nonce { body["nonce"] = nonce } + return try await post("/contract/send", body: body) + } + + // MARK: - Events + + public func getEvents(_ filter: EventFilter) async throws -> [DecodedEvent] { + var body: [String: Any] = ["contract": filter.contract] + if let event = filter.event { body["event"] = event } + if let fromBlock = filter.fromBlock { body["from_block"] = fromBlock } + if let toBlock = filter.toBlock { body["to_block"] = toBlock } + if let topics = filter.topics { body["topics"] = topics } + if let abi = filter.abi { body["abi"] = abi.map { $0.toDictionary() } } + return try await post("/contract/events", body: body) + } + + public func getLogs(contract: String, fromBlock: Int64? = nil, toBlock: Int64? = nil) async throws -> [EventLog] { + var path = "/contract/logs?contract=\(contract.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? contract)" + if let fromBlock = fromBlock { path += "&from_block=\(fromBlock)" } + if let toBlock = toBlock { path += "&to_block=\(toBlock)" } + return try await get(path) + } + + public func decodeLogs(_ logs: [EventLog], abi: [AbiEntry]) async throws -> [DecodedEvent] { + let body: [String: Any] = [ + "logs": logs.map { $0.toDictionary() }, + "abi": abi.map { $0.toDictionary() } + ] + return try await post("/contract/decode-logs", body: body) + } + + // MARK: - ABI Utilities + + public func encodeCall(_ options: EncodeCallOptions) async throws -> String { + let body: [String: Any] = [ + "method": options.method, + "args": options.args, + "abi": options.abi.map { $0.toDictionary() } + ] + let response: [String: Any] = try await post("/contract/encode", body: body) + guard let data = response["data"] as? String else { + throw ContractError.response("Missing data") + } + return data + } + + public func decodeResult(_ options: DecodeResultOptions) async throws -> Any { + let body: [String: Any] = [ + "data": options.data, + "method": options.method, + "abi": options.abi.map { $0.toDictionary() } + ] + let response: [String: Any] = try await post("/contract/decode", body: body) + return response["result"] as Any + } + + public func getSelector(_ signature: String) async throws -> String { + let encoded = signature.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? signature + let response: [String: Any] = try await get("/contract/selector?signature=\(encoded)") + guard let selector = response["selector"] as? String else { + throw ContractError.response("Missing selector") + } + return selector + } + + // MARK: - Gas Estimation + + public func estimateGas(_ options: EstimateGasOptions) async throws -> GasEstimation { + var body: [String: Any] = [ + "contract": options.contract, + "method": options.method, + "args": options.args, + "abi": options.abi.map { $0.toDictionary() } + ] + if let value = options.value { body["value"] = value } + return try await post("/contract/estimate-gas", body: body) + } + + // MARK: - Contract Information + + public func getBytecode(_ address: String) async throws -> BytecodeInfo { + let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? address + return try await get("/contract/\(encoded)/bytecode") + } + + public func verify(_ options: VerifyContractOptions) async throws -> VerificationResult { + var body: [String: Any] = [ + "address": options.address, + "source_code": options.sourceCode, + "compiler_version": options.compilerVersion + ] + if let args = options.constructorArgs { body["constructor_args"] = args } + if let optimization = options.optimization { body["optimization"] = optimization } + if let runs = options.optimizationRuns { body["optimization_runs"] = runs } + if let license = options.license { body["license"] = license } + return try await post("/contract/verify", body: body) + } + + public func getVerificationStatus(_ address: String) async throws -> VerificationResult { + let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? address + return try await get("/contract/\(encoded)/verification") + } + + // MARK: - Multicall + + public func multicall(_ requests: [MulticallRequest]) async throws -> [MulticallResult] { + let body: [String: Any] = ["calls": requests.map { $0.toDictionary() }] + return try await post("/contract/multicall", body: body) + } + + // MARK: - Storage + + public func readStorage(_ options: ReadStorageOptions) async throws -> String { + let contract = options.contract.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? options.contract + let slot = options.slot.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? options.slot + var path = "/contract/storage?contract=\(contract)&slot=\(slot)" + if let block = options.blockNumber { path += "&block=\(block)" } + let response: [String: Any] = try await get(path) + guard let value = response["value"] as? String else { + throw ContractError.response("Missing value") + } + return value + } + + // MARK: - Lifecycle + + public func healthCheck() async -> Bool { + if isClosed { return false } + do { + let response: [String: Any] = try await get("/health") + return response["status"] as? String == "healthy" + } catch { + return false + } + } + + public func close() { + isClosed = true + session.invalidateAndCancel() + } + + // MARK: - HTTP Helpers + + private func get(_ path: String) async throws -> T { + try checkClosed() + return try await executeWithRetry { + let url = URL(string: "\(self.config.endpoint)\(path)")! + var request = URLRequest(url: url) + request.httpMethod = "GET" + self.addHeaders(to: &request) + return try await self.execute(request) + } + } + + private func post(_ path: String, body: [String: Any]) async throws -> T { + try checkClosed() + return try await executeWithRetry { + let url = URL(string: "\(self.config.endpoint)\(path)")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = try JSONSerialization.data(withJSONObject: body) + self.addHeaders(to: &request) + return try await self.execute(request) + } + } + + private func addHeaders(to request: inout URLRequest) { + request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("swift/\(Self.version)", forHTTPHeaderField: "X-SDK-Version") + } + + private func execute(_ request: URLRequest) async throws -> T { + let (data, response) = try await session.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw ContractError.request("Invalid response") + } + + if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 { + return try JSONDecoder().decode(T.self, from: data) + } + + if let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + let message = errorJson["message"] as? String ?? errorJson["error"] as? String ?? "Unknown error" + let code = errorJson["code"] as? String + throw ContractError.api(message: message, code: code, statusCode: httpResponse.statusCode) + } + + throw ContractError.response("HTTP \(httpResponse.statusCode)") + } + + private func executeWithRetry(_ block: () async throws -> T) async throws -> T { + var lastError: Error? + for attempt in 0.. [String: Any] { + var dict: [String: Any] = ["type": type] + if let name = name { dict["name"] = name } + if let indexed = indexed { dict["indexed"] = indexed } + if let components = components { dict["components"] = components.map { $0.toDictionary() } } + return dict + } +} + +public struct AbiEntry: Codable { + public let type: String + public let name: String? + public let inputs: [AbiParameter]? + public let outputs: [AbiParameter]? + public let stateMutability: String? + public let anonymous: Bool? + + public init(type: String, name: String? = nil, inputs: [AbiParameter]? = nil, outputs: [AbiParameter]? = nil, stateMutability: String? = nil, anonymous: Bool? = nil) { + self.type = type + self.name = name + self.inputs = inputs + self.outputs = outputs + self.stateMutability = stateMutability + self.anonymous = anonymous + } + + func toDictionary() -> [String: Any] { + var dict: [String: Any] = ["type": type] + if let name = name { dict["name"] = name } + if let inputs = inputs { dict["inputs"] = inputs.map { $0.toDictionary() } } + if let outputs = outputs { dict["outputs"] = outputs.map { $0.toDictionary() } } + if let stateMutability = stateMutability { dict["stateMutability"] = stateMutability } + if let anonymous = anonymous { dict["anonymous"] = anonymous } + return dict + } +} + +// MARK: - Deployment + +public struct DeployContractOptions { + public let bytecode: String + public let abi: [AbiEntry]? + public let constructorArgs: [Any]? + public let value: String? + public let gasLimit: Int64? + public let gasPrice: String? + public let nonce: Int64? + + public init(bytecode: String, abi: [AbiEntry]? = nil, constructorArgs: [Any]? = nil, value: String? = nil, gasLimit: Int64? = nil, gasPrice: String? = nil, nonce: Int64? = nil) { + self.bytecode = bytecode + self.abi = abi + self.constructorArgs = constructorArgs + self.value = value + self.gasLimit = gasLimit + self.gasPrice = gasPrice + self.nonce = nonce + } +} + +public struct DeploymentResult: Codable { + public let contractAddress: String + public let transactionHash: String + public let deployer: String? + public let gasUsed: Int64? + public let blockNumber: Int64? + public let blockHash: String? + + enum CodingKeys: String, CodingKey { + case contractAddress = "contract_address" + case transactionHash = "transaction_hash" + case deployer + case gasUsed = "gas_used" + case blockNumber = "block_number" + case blockHash = "block_hash" + } +} + +// MARK: - Contract Interaction + +public struct CallContractOptions { + public let contract: String + public let method: String + public let args: [Any] + public let abi: [AbiEntry] + + public init(contract: String, method: String, args: [Any] = [], abi: [AbiEntry]) { + self.contract = contract + self.method = method + self.args = args + self.abi = abi + } +} + +public struct SendContractOptions { + public let contract: String + public let method: String + public let args: [Any] + public let abi: [AbiEntry] + public let value: String? + public let gasLimit: Int64? + public let gasPrice: String? + public let nonce: Int64? + + public init(contract: String, method: String, args: [Any] = [], abi: [AbiEntry], value: String? = nil, gasLimit: Int64? = nil, gasPrice: String? = nil, nonce: Int64? = nil) { + self.contract = contract + self.method = method + self.args = args + self.abi = abi + self.value = value + self.gasLimit = gasLimit + self.gasPrice = gasPrice + self.nonce = nonce + } +} + +public struct TransactionResult: Codable { + public let transactionHash: String + public let blockNumber: Int64? + public let blockHash: String? + public let gasUsed: Int64? + public let effectiveGasPrice: String? + public let status: String? + public let logs: [EventLog]? + + enum CodingKeys: String, CodingKey { + case transactionHash = "transaction_hash" + case blockNumber = "block_number" + case blockHash = "block_hash" + case gasUsed = "gas_used" + case effectiveGasPrice = "effective_gas_price" + case status + case logs + } +} + +// MARK: - Events + +public struct EventLog: Codable { + public let address: String + public let topics: [String] + public let data: String + public let blockNumber: Int64? + public let transactionHash: String? + public let logIndex: Int? + public let blockHash: String? + public let removed: Bool? + + enum CodingKeys: String, CodingKey { + case address, topics, data + case blockNumber = "block_number" + case transactionHash = "transaction_hash" + case logIndex = "log_index" + case blockHash = "block_hash" + case removed + } + + func toDictionary() -> [String: Any] { + var dict: [String: Any] = [ + "address": address, + "topics": topics, + "data": data + ] + if let blockNumber = blockNumber { dict["block_number"] = blockNumber } + if let transactionHash = transactionHash { dict["transaction_hash"] = transactionHash } + if let logIndex = logIndex { dict["log_index"] = logIndex } + if let blockHash = blockHash { dict["block_hash"] = blockHash } + if let removed = removed { dict["removed"] = removed } + return dict + } +} + +public struct DecodedEvent: Codable { + public let name: String + public let signature: String? + public let args: [String: AnyCodable]? + public let log: EventLog? +} + +public struct EventFilter { + public let contract: String + public let event: String? + public let fromBlock: Int64? + public let toBlock: Int64? + public let topics: [String?]? + public let abi: [AbiEntry]? + + public init(contract: String, event: String? = nil, fromBlock: Int64? = nil, toBlock: Int64? = nil, topics: [String?]? = nil, abi: [AbiEntry]? = nil) { + self.contract = contract + self.event = event + self.fromBlock = fromBlock + self.toBlock = toBlock + self.topics = topics + self.abi = abi + } +} + +// MARK: - ABI Utilities + +public struct EncodeCallOptions { + public let method: String + public let args: [Any] + public let abi: [AbiEntry] + + public init(method: String, args: [Any] = [], abi: [AbiEntry]) { + self.method = method + self.args = args + self.abi = abi + } +} + +public struct DecodeResultOptions { + public let data: String + public let method: String + public let abi: [AbiEntry] + + public init(data: String, method: String, abi: [AbiEntry]) { + self.data = data + self.method = method + self.abi = abi + } +} + +// MARK: - Gas Estimation + +public struct EstimateGasOptions { + public let contract: String + public let method: String + public let args: [Any] + public let abi: [AbiEntry] + public let value: String? + + public init(contract: String, method: String, args: [Any] = [], abi: [AbiEntry], value: String? = nil) { + self.contract = contract + self.method = method + self.args = args + self.abi = abi + self.value = value + } +} + +public struct GasEstimation: Codable { + public let gasLimit: Int64 + public let gasPrice: String? + public let maxFeePerGas: String? + public let maxPriorityFeePerGas: String? + public let estimatedCost: String? + + enum CodingKeys: String, CodingKey { + case gasLimit = "gas_limit" + case gasPrice = "gas_price" + case maxFeePerGas = "max_fee_per_gas" + case maxPriorityFeePerGas = "max_priority_fee_per_gas" + case estimatedCost = "estimated_cost" + } +} + +// MARK: - Contract Information + +public struct BytecodeInfo: Codable { + public let bytecode: String + public let deployedBytecode: String? + public let size: Int? + public let isContract: Bool? + + enum CodingKeys: String, CodingKey { + case bytecode + case deployedBytecode = "deployed_bytecode" + case size + case isContract = "is_contract" + } +} + +public struct VerifyContractOptions { + public let address: String + public let sourceCode: String + public let compilerVersion: String + public let constructorArgs: String? + public let optimization: Bool? + public let optimizationRuns: Int? + public let license: String? + + public init(address: String, sourceCode: String, compilerVersion: String, constructorArgs: String? = nil, optimization: Bool? = nil, optimizationRuns: Int? = nil, license: String? = nil) { + self.address = address + self.sourceCode = sourceCode + self.compilerVersion = compilerVersion + self.constructorArgs = constructorArgs + self.optimization = optimization + self.optimizationRuns = optimizationRuns + self.license = license + } +} + +public struct VerificationResult: Codable { + public let verified: Bool + public let address: String? + public let compilerVersion: String? + public let optimization: Bool? + public let optimizationRuns: Int? + public let license: String? + public let abi: [AbiEntry]? + public let sourceCode: String? + + enum CodingKeys: String, CodingKey { + case verified, address + case compilerVersion = "compiler_version" + case optimization + case optimizationRuns = "optimization_runs" + case license, abi + case sourceCode = "source_code" + } +} + +// MARK: - Multicall + +public struct MulticallRequest { + public let contract: String + public let method: String + public let args: [Any] + public let abi: [AbiEntry] + + public init(contract: String, method: String, args: [Any] = [], abi: [AbiEntry]) { + self.contract = contract + self.method = method + self.args = args + self.abi = abi + } + + func toDictionary() -> [String: Any] { + return [ + "contract": contract, + "method": method, + "args": args, + "abi": abi.map { $0.toDictionary() } + ] + } +} + +public struct MulticallResult: Codable { + public let success: Bool + public let result: AnyCodable? + public let error: String? +} + +// MARK: - Storage + +public struct ReadStorageOptions { + public let contract: String + public let slot: String + public let blockNumber: Int64? + + public init(contract: String, slot: String, blockNumber: Int64? = nil) { + self.contract = contract + self.slot = slot + self.blockNumber = blockNumber + } +} + +// MARK: - AnyCodable Helper + +public struct AnyCodable: Codable { + public let value: Any + + public init(_ value: Any) { + self.value = value + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let intVal = try? container.decode(Int.self) { + value = intVal + } else if let doubleVal = try? container.decode(Double.self) { + value = doubleVal + } else if let boolVal = try? container.decode(Bool.self) { + value = boolVal + } else if let stringVal = try? container.decode(String.self) { + value = stringVal + } else if let arrayVal = try? container.decode([AnyCodable].self) { + value = arrayVal.map { $0.value } + } else if let dictVal = try? container.decode([String: AnyCodable].self) { + value = dictVal.mapValues { $0.value } + } else { + value = NSNull() + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch value { + case let intVal as Int: + try container.encode(intVal) + case let doubleVal as Double: + try container.encode(doubleVal) + case let boolVal as Bool: + try container.encode(boolVal) + case let stringVal as String: + try container.encode(stringVal) + case let arrayVal as [Any]: + try container.encode(arrayVal.map { AnyCodable($0) }) + case let dictVal as [String: Any]: + try container.encode(dictVal.mapValues { AnyCodable($0) }) + default: + try container.encodeNil() + } + } +} diff --git a/sdk/swift/Sources/SynorPrivacy/PrivacyClient.swift b/sdk/swift/Sources/SynorPrivacy/PrivacyClient.swift new file mode 100644 index 0000000..e69586d --- /dev/null +++ b/sdk/swift/Sources/SynorPrivacy/PrivacyClient.swift @@ -0,0 +1,314 @@ +import Foundation + +/// Synor Privacy SDK client for Swift. +/// Confidential transactions, ring signatures, stealth addresses, and cryptographic commitments. +public actor PrivacyClient { + public static let version = "0.1.0" + + private let config: PrivacyConfig + private let session: URLSession + private var isClosed = false + + public init(config: PrivacyConfig) { + self.config = config + let sessionConfig = URLSessionConfiguration.default + sessionConfig.timeoutIntervalForRequest = TimeInterval(config.timeoutMs) / 1000.0 + self.session = URLSession(configuration: sessionConfig) + } + + // MARK: - Confidential Transactions + + public func createConfidentialTx( + inputs: [ConfidentialTxInput], + outputs: [ConfidentialTxOutput] + ) async throws -> ConfidentialTransaction { + let body: [String: Any] = [ + "inputs": inputs.map { $0.toDictionary() }, + "outputs": outputs.map { $0.toDictionary() } + ] + return try await post("/privacy/confidential/create", body: body) + } + + public func verifyConfidentialTx(_ tx: ConfidentialTransaction) async throws -> Bool { + let body: [String: Any] = ["transaction": tx.toDictionary()] + let response: [String: Any] = try await post("/privacy/confidential/verify", body: body) + return response["valid"] as? Bool ?? false + } + + public func createCommitment(value: String, blindingFactor: String) async throws -> Commitment { + let body: [String: Any] = [ + "value": value, + "blinding_factor": blindingFactor + ] + return try await post("/privacy/commitment/create", body: body) + } + + public func verifyCommitment(commitment: String, value: String, blindingFactor: String) async throws -> Bool { + let body: [String: Any] = [ + "commitment": commitment, + "value": value, + "blinding_factor": blindingFactor + ] + let response: [String: Any] = try await post("/privacy/commitment/verify", body: body) + return response["valid"] as? Bool ?? false + } + + public func createRangeProof( + value: String, + blindingFactor: String, + minValue: Int64, + maxValue: Int64 + ) async throws -> RangeProof { + let body: [String: Any] = [ + "value": value, + "blinding_factor": blindingFactor, + "min_value": minValue, + "max_value": maxValue + ] + return try await post("/privacy/range-proof/create", body: body) + } + + public func verifyRangeProof(_ proof: RangeProof) async throws -> Bool { + let body: [String: Any] = ["proof": proof.toDictionary()] + let response: [String: Any] = try await post("/privacy/range-proof/verify", body: body) + return response["valid"] as? Bool ?? false + } + + // MARK: - Ring Signatures + + public func createRingSignature( + message: String, + ring: [String], + signerIndex: Int, + privateKey: String + ) async throws -> RingSignature { + let body: [String: Any] = [ + "message": message, + "ring": ring, + "signer_index": signerIndex, + "private_key": privateKey + ] + return try await post("/privacy/ring/sign", body: body) + } + + public func verifyRingSignature(_ signature: RingSignature, message: String) async throws -> Bool { + let body: [String: Any] = [ + "signature": signature.toDictionary(), + "message": message + ] + let response: [String: Any] = try await post("/privacy/ring/verify", body: body) + return response["valid"] as? Bool ?? false + } + + public func generateDecoys(count: Int, excludeKey: String? = nil) async throws -> [String] { + var path = "/privacy/ring/decoys?count=\(count)" + if let excludeKey = excludeKey { + path += "&exclude=\(excludeKey.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? excludeKey)" + } + return try await get(path) + } + + public func checkKeyImage(_ keyImage: String) async throws -> Bool { + let response: [String: Any] = try await get("/privacy/ring/key-image/\(keyImage)") + return response["spent"] as? Bool ?? false + } + + // MARK: - Stealth Addresses + + public func generateStealthKeyPair() async throws -> StealthKeyPair { + return try await post("/privacy/stealth/generate", body: [:]) + } + + public func deriveStealthAddress(spendPublicKey: String, viewPublicKey: String) async throws -> StealthAddress { + let body: [String: Any] = [ + "spend_public_key": spendPublicKey, + "view_public_key": viewPublicKey + ] + return try await post("/privacy/stealth/derive", body: body) + } + + public func recoverStealthPrivateKey( + stealthAddress: String, + viewPrivateKey: String, + spendPrivateKey: String + ) async throws -> String { + let body: [String: Any] = [ + "stealth_address": stealthAddress, + "view_private_key": viewPrivateKey, + "spend_private_key": spendPrivateKey + ] + let response: [String: Any] = try await post("/privacy/stealth/recover", body: body) + guard let privateKey = response["private_key"] as? String else { + throw PrivacyError.response("Missing private_key") + } + return privateKey + } + + public func scanOutputs( + viewPrivateKey: String, + spendPublicKey: String, + fromBlock: Int64, + toBlock: Int64? = nil + ) async throws -> [StealthOutput] { + var body: [String: Any] = [ + "view_private_key": viewPrivateKey, + "spend_public_key": spendPublicKey, + "from_block": fromBlock + ] + if let toBlock = toBlock { + body["to_block"] = toBlock + } + return try await post("/privacy/stealth/scan", body: body) + } + + // MARK: - Blinding + + public func generateBlindingFactor() async throws -> String { + let response: [String: Any] = try await post("/privacy/blinding/generate", body: [:]) + guard let factor = response["blinding_factor"] as? String else { + throw PrivacyError.response("Missing blinding_factor") + } + return factor + } + + public func blindValue(value: String, blindingFactor: String) async throws -> String { + let body: [String: Any] = [ + "value": value, + "blinding_factor": blindingFactor + ] + let response: [String: Any] = try await post("/privacy/blinding/blind", body: body) + guard let blindedValue = response["blinded_value"] as? String else { + throw PrivacyError.response("Missing blinded_value") + } + return blindedValue + } + + public func unblindValue(blindedValue: String, blindingFactor: String) async throws -> String { + let body: [String: Any] = [ + "blinded_value": blindedValue, + "blinding_factor": blindingFactor + ] + let response: [String: Any] = try await post("/privacy/blinding/unblind", body: body) + guard let value = response["value"] as? String else { + throw PrivacyError.response("Missing value") + } + return value + } + + // MARK: - Lifecycle + + public func healthCheck() async -> Bool { + if isClosed { return false } + do { + let response: [String: Any] = try await get("/health") + return response["status"] as? String == "healthy" + } catch { + return false + } + } + + public func close() { + isClosed = true + session.invalidateAndCancel() + } + + // MARK: - HTTP Helpers + + private func get(_ path: String) async throws -> T { + try checkClosed() + return try await executeWithRetry { + let url = URL(string: "\(self.config.endpoint)\(path)")! + var request = URLRequest(url: url) + request.httpMethod = "GET" + self.addHeaders(to: &request) + return try await self.execute(request) + } + } + + private func post(_ path: String, body: [String: Any]) async throws -> T { + try checkClosed() + return try await executeWithRetry { + let url = URL(string: "\(self.config.endpoint)\(path)")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = try JSONSerialization.data(withJSONObject: body) + self.addHeaders(to: &request) + return try await self.execute(request) + } + } + + private func addHeaders(to request: inout URLRequest) { + request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("swift/\(Self.version)", forHTTPHeaderField: "X-SDK-Version") + } + + private func execute(_ request: URLRequest) async throws -> T { + let (data, response) = try await session.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw PrivacyError.request("Invalid response") + } + + if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 { + return try JSONDecoder().decode(T.self, from: data) + } + + if let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { + let message = errorJson["message"] as? String ?? errorJson["error"] as? String ?? "Unknown error" + let code = errorJson["code"] as? String + throw PrivacyError.api(message: message, code: code, statusCode: httpResponse.statusCode) + } + + throw PrivacyError.response("HTTP \(httpResponse.statusCode)") + } + + private func executeWithRetry(_ block: () async throws -> T) async throws -> T { + var lastError: Error? + for attempt in 0.. [String: Any] { + var dict: [String: Any] = [ + "commitment": commitment, + "blinding_factor": blindingFactor, + "value": value + ] + if let keyImage = keyImage { + dict["key_image"] = keyImage + } + return dict + } +} + +public struct ConfidentialTxOutput: Codable { + public let commitment: String + public let blindingFactor: String + public let value: String + public let recipientPublicKey: String + public let rangeProof: String? + + public init(commitment: String, blindingFactor: String, value: String, recipientPublicKey: String, rangeProof: String? = nil) { + self.commitment = commitment + self.blindingFactor = blindingFactor + self.value = value + self.recipientPublicKey = recipientPublicKey + self.rangeProof = rangeProof + } + + enum CodingKeys: String, CodingKey { + case commitment + case blindingFactor = "blinding_factor" + case value + case recipientPublicKey = "recipient_public_key" + case rangeProof = "range_proof" + } + + func toDictionary() -> [String: Any] { + var dict: [String: Any] = [ + "commitment": commitment, + "blinding_factor": blindingFactor, + "value": value, + "recipient_public_key": recipientPublicKey + ] + if let rangeProof = rangeProof { + dict["range_proof"] = rangeProof + } + return dict + } +} + +public struct ConfidentialTransaction: Codable { + public let id: String + public let inputs: [ConfidentialTxInput] + public let outputs: [ConfidentialTxOutput] + public let fee: String + public let excess: String + public let excessSig: String + public let kernelOffset: String? + + enum CodingKeys: String, CodingKey { + case id, inputs, outputs, fee, excess + case excessSig = "excess_sig" + case kernelOffset = "kernel_offset" + } + + func toDictionary() -> [String: Any] { + var dict: [String: Any] = [ + "id": id, + "inputs": inputs.map { $0.toDictionary() }, + "outputs": outputs.map { $0.toDictionary() }, + "fee": fee, + "excess": excess, + "excess_sig": excessSig + ] + if let kernelOffset = kernelOffset { + dict["kernel_offset"] = kernelOffset + } + return dict + } +} + +// MARK: - Commitments + +public struct Commitment: Codable { + public let commitment: String + public let blindingFactor: String + + enum CodingKeys: String, CodingKey { + case commitment + case blindingFactor = "blinding_factor" + } +} + +public struct RangeProof: Codable { + public let proof: String + public let commitment: String + public let minValue: Int64 + public let maxValue: Int64 + + enum CodingKeys: String, CodingKey { + case proof, commitment + case minValue = "min_value" + case maxValue = "max_value" + } + + func toDictionary() -> [String: Any] { + return [ + "proof": proof, + "commitment": commitment, + "min_value": minValue, + "max_value": maxValue + ] + } +} + +// MARK: - Ring Signatures + +public struct RingSignature: Codable { + public let c0: String + public let s: [String] + public let keyImage: String + public let ring: [String] + + enum CodingKeys: String, CodingKey { + case c0, s, ring + case keyImage = "key_image" + } + + func toDictionary() -> [String: Any] { + return [ + "c0": c0, + "s": s, + "key_image": keyImage, + "ring": ring + ] + } +} + +// MARK: - Stealth Addresses + +public struct StealthKeyPair: Codable { + public let spendPublicKey: String + public let spendPrivateKey: String + public let viewPublicKey: String + public let viewPrivateKey: String + + enum CodingKeys: String, CodingKey { + case spendPublicKey = "spend_public_key" + case spendPrivateKey = "spend_private_key" + case viewPublicKey = "view_public_key" + case viewPrivateKey = "view_private_key" + } +} + +public struct StealthAddress: Codable { + public let address: String + public let ephemeralPublicKey: String + public let txPublicKey: String? + + enum CodingKeys: String, CodingKey { + case address + case ephemeralPublicKey = "ephemeral_public_key" + case txPublicKey = "tx_public_key" + } +} + +public struct StealthOutput: Codable { + public let txHash: String + public let outputIndex: Int + public let stealthAddress: String + public let amount: String + public let blockHeight: Int64 + + enum CodingKeys: String, CodingKey { + case txHash = "tx_hash" + case outputIndex = "output_index" + case stealthAddress = "stealth_address" + case amount + case blockHeight = "block_height" + } +}