feat: implement Privacy and Contract SDKs for all 12 languages (Phase 5)
Privacy SDK features: - Confidential transactions with Pedersen commitments - Bulletproof range proofs for value validation - Ring signatures for anonymous signing with key images - Stealth addresses for unlinkable payments - Blinding factor generation and value operations Contract SDK features: - Smart contract deployment (standard and CREATE2) - Call (view/pure) and Send (state-changing) operations - Event log filtering, subscription, and decoding - ABI encoding/decoding utilities - Gas estimation and contract verification - Multicall for batched operations - Storage slot reading Languages implemented: - JavaScript/TypeScript - Python (async with httpx) - Go - Rust (async with reqwest/tokio) - Java (async with OkHttp) - Kotlin (coroutines with Ktor) - Swift (async/await with URLSession) - Flutter/Dart - C (header-only interface) - C++ (header-only with std::future) - C#/.NET (async with HttpClient) - Ruby (Faraday HTTP client) All SDKs follow consistent patterns: - Configuration with API key, endpoint, timeout, retries - Custom exception types with error codes - Retry logic with exponential backoff - Health check endpoints - Closed state management
This commit is contained in:
parent
6607223c9e
commit
e65ea40af2
59 changed files with 15760 additions and 0 deletions
383
sdk/c/src/contract/synor_contract.h
Normal file
383
sdk/c/src/contract/synor_contract.h
Normal file
|
|
@ -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 <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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 */
|
||||
310
sdk/c/src/privacy/synor_privacy.h
Normal file
310
sdk/c/src/privacy/synor_privacy.h
Normal file
|
|
@ -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 <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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 */
|
||||
297
sdk/cpp/include/synor/contract.hpp
Normal file
297
sdk/cpp/include/synor/contract.hpp
Normal file
|
|
@ -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 <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
#include <future>
|
||||
#include <stdexcept>
|
||||
#include <cstdint>
|
||||
#include <any>
|
||||
#include <map>
|
||||
|
||||
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<int> status_code = std::nullopt,
|
||||
std::optional<std::string> code = std::nullopt)
|
||||
: std::runtime_error(message), status_code_(status_code), code_(code) {}
|
||||
|
||||
std::optional<int> status_code() const { return status_code_; }
|
||||
std::optional<std::string> code() const { return code_; }
|
||||
|
||||
private:
|
||||
std::optional<int> status_code_;
|
||||
std::optional<std::string> 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<std::string> name;
|
||||
std::string type;
|
||||
std::optional<bool> indexed;
|
||||
std::optional<std::vector<AbiParameter>> components;
|
||||
};
|
||||
|
||||
struct AbiEntry {
|
||||
std::string type;
|
||||
std::optional<std::string> name;
|
||||
std::optional<std::vector<AbiParameter>> inputs;
|
||||
std::optional<std::vector<AbiParameter>> outputs;
|
||||
std::optional<std::string> state_mutability;
|
||||
std::optional<bool> anonymous;
|
||||
};
|
||||
|
||||
using Abi = std::vector<AbiEntry>;
|
||||
|
||||
// ==================== Deployment ====================
|
||||
|
||||
struct DeployContractOptions {
|
||||
std::string bytecode;
|
||||
std::optional<Abi> abi;
|
||||
std::optional<std::vector<std::any>> constructor_args;
|
||||
std::optional<std::string> value;
|
||||
std::optional<int64_t> gas_limit;
|
||||
std::optional<std::string> gas_price;
|
||||
std::optional<int64_t> nonce;
|
||||
};
|
||||
|
||||
struct DeploymentResult {
|
||||
std::string contract_address;
|
||||
std::string transaction_hash;
|
||||
std::optional<std::string> deployer;
|
||||
std::optional<int64_t> gas_used;
|
||||
std::optional<int64_t> block_number;
|
||||
std::optional<std::string> block_hash;
|
||||
};
|
||||
|
||||
// ==================== Contract Interaction ====================
|
||||
|
||||
struct CallContractOptions {
|
||||
std::string contract;
|
||||
std::string method;
|
||||
std::vector<std::any> args;
|
||||
Abi abi;
|
||||
};
|
||||
|
||||
struct SendContractOptions {
|
||||
std::string contract;
|
||||
std::string method;
|
||||
std::vector<std::any> args;
|
||||
Abi abi;
|
||||
std::optional<std::string> value;
|
||||
std::optional<int64_t> gas_limit;
|
||||
std::optional<std::string> gas_price;
|
||||
std::optional<int64_t> nonce;
|
||||
};
|
||||
|
||||
struct EventLog {
|
||||
std::string address;
|
||||
std::vector<std::string> topics;
|
||||
std::string data;
|
||||
std::optional<int64_t> block_number;
|
||||
std::optional<std::string> transaction_hash;
|
||||
std::optional<int> log_index;
|
||||
std::optional<std::string> block_hash;
|
||||
std::optional<bool> removed;
|
||||
};
|
||||
|
||||
struct TransactionResult {
|
||||
std::string transaction_hash;
|
||||
std::optional<int64_t> block_number;
|
||||
std::optional<std::string> block_hash;
|
||||
std::optional<int64_t> gas_used;
|
||||
std::optional<std::string> effective_gas_price;
|
||||
std::optional<std::string> status;
|
||||
std::optional<std::vector<EventLog>> logs;
|
||||
};
|
||||
|
||||
// ==================== Events ====================
|
||||
|
||||
struct DecodedEvent {
|
||||
std::string name;
|
||||
std::optional<std::string> signature;
|
||||
std::optional<std::map<std::string, std::any>> args;
|
||||
std::optional<EventLog> log;
|
||||
};
|
||||
|
||||
struct EventFilter {
|
||||
std::string contract;
|
||||
std::optional<std::string> event;
|
||||
std::optional<int64_t> from_block;
|
||||
std::optional<int64_t> to_block;
|
||||
std::optional<std::vector<std::optional<std::string>>> topics;
|
||||
std::optional<Abi> abi;
|
||||
};
|
||||
|
||||
// ==================== ABI Utilities ====================
|
||||
|
||||
struct EncodeCallOptions {
|
||||
std::string method;
|
||||
std::vector<std::any> 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<std::any> args;
|
||||
Abi abi;
|
||||
std::optional<std::string> value;
|
||||
};
|
||||
|
||||
struct GasEstimation {
|
||||
int64_t gas_limit;
|
||||
std::optional<std::string> gas_price;
|
||||
std::optional<std::string> max_fee_per_gas;
|
||||
std::optional<std::string> max_priority_fee_per_gas;
|
||||
std::optional<std::string> estimated_cost;
|
||||
};
|
||||
|
||||
// ==================== Contract Information ====================
|
||||
|
||||
struct BytecodeInfo {
|
||||
std::string bytecode;
|
||||
std::optional<std::string> deployed_bytecode;
|
||||
std::optional<int> size;
|
||||
std::optional<bool> is_contract;
|
||||
};
|
||||
|
||||
struct VerifyContractOptions {
|
||||
std::string address;
|
||||
std::string source_code;
|
||||
std::string compiler_version;
|
||||
std::optional<std::string> constructor_args;
|
||||
std::optional<bool> optimization;
|
||||
std::optional<int> optimization_runs;
|
||||
std::optional<std::string> license;
|
||||
};
|
||||
|
||||
struct VerificationResult {
|
||||
bool verified;
|
||||
std::optional<std::string> address;
|
||||
std::optional<std::string> compiler_version;
|
||||
std::optional<bool> optimization;
|
||||
std::optional<int> optimization_runs;
|
||||
std::optional<std::string> license;
|
||||
std::optional<Abi> abi;
|
||||
std::optional<std::string> source_code;
|
||||
};
|
||||
|
||||
// ==================== Multicall ====================
|
||||
|
||||
struct MulticallRequest {
|
||||
std::string contract;
|
||||
std::string method;
|
||||
std::vector<std::any> args;
|
||||
Abi abi;
|
||||
};
|
||||
|
||||
struct MulticallResult {
|
||||
bool success;
|
||||
std::optional<std::any> result;
|
||||
std::optional<std::string> error;
|
||||
};
|
||||
|
||||
// ==================== Storage ====================
|
||||
|
||||
struct ReadStorageOptions {
|
||||
std::string contract;
|
||||
std::string slot;
|
||||
std::optional<int64_t> 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<DeploymentResult> deploy(const DeployContractOptions& options);
|
||||
std::future<DeploymentResult> deploy_create2(const DeployContractOptions& options, const std::string& salt);
|
||||
std::future<std::string> predict_address(const std::string& bytecode, const std::string& salt,
|
||||
const std::optional<std::string>& deployer = std::nullopt);
|
||||
|
||||
// Contract Interaction
|
||||
std::future<std::any> call(const CallContractOptions& options);
|
||||
std::future<TransactionResult> send(const SendContractOptions& options);
|
||||
|
||||
// Events
|
||||
std::future<std::vector<DecodedEvent>> get_events(const EventFilter& filter);
|
||||
std::future<std::vector<EventLog>> get_logs(const std::string& contract,
|
||||
std::optional<int64_t> from_block = std::nullopt,
|
||||
std::optional<int64_t> to_block = std::nullopt);
|
||||
std::future<std::vector<DecodedEvent>> decode_logs(const std::vector<EventLog>& logs, const Abi& abi);
|
||||
|
||||
// ABI Utilities
|
||||
std::future<std::string> encode_call(const EncodeCallOptions& options);
|
||||
std::future<std::any> decode_result(const DecodeResultOptions& options);
|
||||
std::future<std::string> get_selector(const std::string& signature);
|
||||
|
||||
// Gas Estimation
|
||||
std::future<GasEstimation> estimate_gas(const EstimateGasOptions& options);
|
||||
|
||||
// Contract Information
|
||||
std::future<BytecodeInfo> get_bytecode(const std::string& address);
|
||||
std::future<VerificationResult> verify(const VerifyContractOptions& options);
|
||||
std::future<VerificationResult> get_verification_status(const std::string& address);
|
||||
|
||||
// Multicall
|
||||
std::future<std::vector<MulticallResult>> multicall(const std::vector<MulticallRequest>& requests);
|
||||
|
||||
// Storage
|
||||
std::future<std::string> read_storage(const ReadStorageOptions& options);
|
||||
|
||||
// Lifecycle
|
||||
std::future<bool> health_check();
|
||||
void close();
|
||||
bool is_closed() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace synor::contract
|
||||
|
||||
#endif // SYNOR_CONTRACT_HPP
|
||||
229
sdk/cpp/include/synor/privacy.hpp
Normal file
229
sdk/cpp/include/synor/privacy.hpp
Normal file
|
|
@ -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 <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
#include <future>
|
||||
#include <stdexcept>
|
||||
#include <cstdint>
|
||||
|
||||
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<int> status_code = std::nullopt,
|
||||
std::optional<std::string> code = std::nullopt)
|
||||
: std::runtime_error(message), status_code_(status_code), code_(code) {}
|
||||
|
||||
std::optional<int> status_code() const { return status_code_; }
|
||||
std::optional<std::string> code() const { return code_; }
|
||||
|
||||
private:
|
||||
std::optional<int> status_code_;
|
||||
std::optional<std::string> 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<std::string> key_image;
|
||||
};
|
||||
|
||||
struct ConfidentialTxOutput {
|
||||
std::string commitment;
|
||||
std::string blinding_factor;
|
||||
std::string value;
|
||||
std::string recipient_public_key;
|
||||
std::optional<std::string> range_proof;
|
||||
};
|
||||
|
||||
struct ConfidentialTransaction {
|
||||
std::string id;
|
||||
std::vector<ConfidentialTxInput> inputs;
|
||||
std::vector<ConfidentialTxOutput> outputs;
|
||||
std::string fee;
|
||||
std::string excess;
|
||||
std::string excess_sig;
|
||||
std::optional<std::string> 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<std::string> s;
|
||||
std::string key_image;
|
||||
std::vector<std::string> 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<std::string> 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<ConfidentialTransaction> create_confidential_tx(
|
||||
const std::vector<ConfidentialTxInput>& inputs,
|
||||
const std::vector<ConfidentialTxOutput>& outputs
|
||||
);
|
||||
|
||||
std::future<bool> verify_confidential_tx(const ConfidentialTransaction& tx);
|
||||
|
||||
std::future<Commitment> create_commitment(
|
||||
const std::string& value,
|
||||
const std::string& blinding_factor
|
||||
);
|
||||
|
||||
std::future<bool> verify_commitment(
|
||||
const std::string& commitment,
|
||||
const std::string& value,
|
||||
const std::string& blinding_factor
|
||||
);
|
||||
|
||||
std::future<RangeProof> create_range_proof(
|
||||
const std::string& value,
|
||||
const std::string& blinding_factor,
|
||||
int64_t min_value,
|
||||
int64_t max_value
|
||||
);
|
||||
|
||||
std::future<bool> verify_range_proof(const RangeProof& proof);
|
||||
|
||||
// Ring Signatures
|
||||
std::future<RingSignature> create_ring_signature(
|
||||
const std::string& message,
|
||||
const std::vector<std::string>& ring,
|
||||
int signer_index,
|
||||
const std::string& private_key
|
||||
);
|
||||
|
||||
std::future<bool> verify_ring_signature(
|
||||
const RingSignature& signature,
|
||||
const std::string& message
|
||||
);
|
||||
|
||||
std::future<std::vector<std::string>> generate_decoys(
|
||||
int count,
|
||||
const std::optional<std::string>& exclude_key = std::nullopt
|
||||
);
|
||||
|
||||
std::future<bool> check_key_image(const std::string& key_image);
|
||||
|
||||
// Stealth Addresses
|
||||
std::future<StealthKeyPair> generate_stealth_keypair();
|
||||
|
||||
std::future<StealthAddress> derive_stealth_address(
|
||||
const std::string& spend_public_key,
|
||||
const std::string& view_public_key
|
||||
);
|
||||
|
||||
std::future<std::string> recover_stealth_private_key(
|
||||
const std::string& stealth_address,
|
||||
const std::string& view_private_key,
|
||||
const std::string& spend_private_key
|
||||
);
|
||||
|
||||
std::future<std::vector<StealthOutput>> scan_outputs(
|
||||
const std::string& view_private_key,
|
||||
const std::string& spend_public_key,
|
||||
int64_t from_block,
|
||||
std::optional<int64_t> to_block = std::nullopt
|
||||
);
|
||||
|
||||
// Blinding
|
||||
std::future<std::string> generate_blinding_factor();
|
||||
|
||||
std::future<std::string> blind_value(
|
||||
const std::string& value,
|
||||
const std::string& blinding_factor
|
||||
);
|
||||
|
||||
std::future<std::string> unblind_value(
|
||||
const std::string& blinded_value,
|
||||
const std::string& blinding_factor
|
||||
);
|
||||
|
||||
// Lifecycle
|
||||
std::future<bool> health_check();
|
||||
void close();
|
||||
bool is_closed() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace synor::privacy
|
||||
|
||||
#endif // SYNOR_PRIVACY_HPP
|
||||
307
sdk/csharp/src/Synor.Contract/ContractClient.cs
Normal file
307
sdk/csharp/src/Synor.Contract/ContractClient.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Synor Contract SDK client for C#/.NET.
|
||||
/// Smart contract deployment, interaction, and event handling.
|
||||
/// </summary>
|
||||
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<DeploymentResult> DeployAsync(DeployContractOptions options, CancellationToken ct = default)
|
||||
{
|
||||
return await PostAsync<DeploymentResult>("/contract/deploy", options, ct);
|
||||
}
|
||||
|
||||
public async Task<DeploymentResult> DeployCreate2Async(DeployContractOptions options, string salt, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object?>
|
||||
{
|
||||
["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<DeploymentResult>("/contract/deploy/create2", body, ct);
|
||||
}
|
||||
|
||||
public async Task<string> PredictAddressAsync(string bytecode, string salt, string? deployer = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new { bytecode, salt, deployer };
|
||||
var response = await PostAsync<Dictionary<string, object>>("/contract/predict-address", body, ct);
|
||||
return response["address"]?.ToString() ?? throw new ContractException("Missing address");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Contract Interaction
|
||||
|
||||
public async Task<JsonElement> CallAsync(CallContractOptions options, CancellationToken ct = default)
|
||||
{
|
||||
return await PostAsync<JsonElement>("/contract/call", options, ct);
|
||||
}
|
||||
|
||||
public async Task<TransactionResult> SendAsync(SendContractOptions options, CancellationToken ct = default)
|
||||
{
|
||||
return await PostAsync<TransactionResult>("/contract/send", options, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
public async Task<DecodedEvent[]> GetEventsAsync(EventFilter filter, CancellationToken ct = default)
|
||||
{
|
||||
return await PostAsync<DecodedEvent[]>("/contract/events", filter, ct);
|
||||
}
|
||||
|
||||
public async Task<EventLog[]> 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<EventLog[]>(path, ct);
|
||||
}
|
||||
|
||||
public async Task<DecodedEvent[]> DecodeLogsAsync(IEnumerable<EventLog> logs, IEnumerable<AbiEntry> abi, CancellationToken ct = default)
|
||||
{
|
||||
var body = new { logs, abi };
|
||||
return await PostAsync<DecodedEvent[]>("/contract/decode-logs", body, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ABI Utilities
|
||||
|
||||
public async Task<string> EncodeCallAsync(EncodeCallOptions options, CancellationToken ct = default)
|
||||
{
|
||||
var response = await PostAsync<Dictionary<string, object>>("/contract/encode", options, ct);
|
||||
return response["data"]?.ToString() ?? throw new ContractException("Missing data");
|
||||
}
|
||||
|
||||
public async Task<JsonElement> DecodeResultAsync(DecodeResultOptions options, CancellationToken ct = default)
|
||||
{
|
||||
var response = await PostAsync<Dictionary<string, JsonElement>>("/contract/decode", options, ct);
|
||||
return response["result"];
|
||||
}
|
||||
|
||||
public async Task<string> GetSelectorAsync(string signature, CancellationToken ct = default)
|
||||
{
|
||||
var response = await GetAsync<Dictionary<string, object>>($"/contract/selector?signature={HttpUtility.UrlEncode(signature)}", ct);
|
||||
return response["selector"]?.ToString() ?? throw new ContractException("Missing selector");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Gas Estimation
|
||||
|
||||
public async Task<GasEstimation> EstimateGasAsync(EstimateGasOptions options, CancellationToken ct = default)
|
||||
{
|
||||
return await PostAsync<GasEstimation>("/contract/estimate-gas", options, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Contract Information
|
||||
|
||||
public async Task<BytecodeInfo> GetBytecodeAsync(string address, CancellationToken ct = default)
|
||||
{
|
||||
return await GetAsync<BytecodeInfo>($"/contract/{HttpUtility.UrlEncode(address)}/bytecode", ct);
|
||||
}
|
||||
|
||||
public async Task<VerificationResult> VerifyAsync(VerifyContractOptions options, CancellationToken ct = default)
|
||||
{
|
||||
return await PostAsync<VerificationResult>("/contract/verify", options, ct);
|
||||
}
|
||||
|
||||
public async Task<VerificationResult> GetVerificationStatusAsync(string address, CancellationToken ct = default)
|
||||
{
|
||||
return await GetAsync<VerificationResult>($"/contract/{HttpUtility.UrlEncode(address)}/verification", ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Multicall
|
||||
|
||||
public async Task<MulticallResult[]> MulticallAsync(IEnumerable<MulticallRequest> requests, CancellationToken ct = default)
|
||||
{
|
||||
var body = new { calls = requests };
|
||||
return await PostAsync<MulticallResult[]>("/contract/multicall", body, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Storage
|
||||
|
||||
public async Task<string> 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<Dictionary<string, object>>(path, ct);
|
||||
return response["value"]?.ToString() ?? throw new ContractException("Missing value");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_closed) return false;
|
||||
try
|
||||
{
|
||||
var response = await GetAsync<Dictionary<string, object>>("/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<T> GetAsync<T>(string path, CancellationToken ct)
|
||||
{
|
||||
ThrowIfClosed();
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var response = await _httpClient.GetAsync(path, ct);
|
||||
return await HandleResponseAsync<T>(response, ct);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<T> PostAsync<T>(string path, object body, CancellationToken ct)
|
||||
{
|
||||
ThrowIfClosed();
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct);
|
||||
return await HandleResponseAsync<T>(response, ct);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<T> HandleResponseAsync<T>(HttpResponseMessage response, CancellationToken ct)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(content, _jsonOptions)
|
||||
?? throw new ContractException("Failed to deserialize response");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var error = JsonSerializer.Deserialize<Dictionary<string, object>>(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<T> ExecuteWithRetryAsync<T>(Func<Task<T>> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
394
sdk/csharp/src/Synor.Contract/ContractTypes.cs
Normal file
394
sdk/csharp/src/Synor.Contract/ContractTypes.cs
Normal file
|
|
@ -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<string, JsonElement>? 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; }
|
||||
}
|
||||
}
|
||||
335
sdk/csharp/src/Synor.Privacy/PrivacyClient.cs
Normal file
335
sdk/csharp/src/Synor.Privacy/PrivacyClient.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Synor Privacy SDK client for C#/.NET.
|
||||
/// Confidential transactions, ring signatures, stealth addresses, and cryptographic commitments.
|
||||
/// </summary>
|
||||
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<ConfidentialTransaction> CreateConfidentialTxAsync(
|
||||
IEnumerable<ConfidentialTxInput> inputs,
|
||||
IEnumerable<ConfidentialTxOutput> outputs,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var body = new { inputs, outputs };
|
||||
return await PostAsync<ConfidentialTransaction>("/privacy/confidential/create", body, ct);
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyConfidentialTxAsync(ConfidentialTransaction tx, CancellationToken ct = default)
|
||||
{
|
||||
var body = new { transaction = tx };
|
||||
var response = await PostAsync<Dictionary<string, object>>("/privacy/confidential/verify", body, ct);
|
||||
return response.TryGetValue("valid", out var valid) && valid is JsonElement elem && elem.GetBoolean();
|
||||
}
|
||||
|
||||
public async Task<Commitment> CreateCommitmentAsync(
|
||||
string value,
|
||||
string blindingFactor,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var body = new { value, blinding_factor = blindingFactor };
|
||||
return await PostAsync<Commitment>("/privacy/commitment/create", body, ct);
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyCommitmentAsync(
|
||||
string commitment,
|
||||
string value,
|
||||
string blindingFactor,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var body = new { commitment, value, blinding_factor = blindingFactor };
|
||||
var response = await PostAsync<Dictionary<string, object>>("/privacy/commitment/verify", body, ct);
|
||||
return response.TryGetValue("valid", out var valid) && valid is JsonElement elem && elem.GetBoolean();
|
||||
}
|
||||
|
||||
public async Task<RangeProof> 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<RangeProof>("/privacy/range-proof/create", body, ct);
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyRangeProofAsync(RangeProof proof, CancellationToken ct = default)
|
||||
{
|
||||
var body = new { proof };
|
||||
var response = await PostAsync<Dictionary<string, object>>("/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<RingSignature> CreateRingSignatureAsync(
|
||||
string message,
|
||||
IEnumerable<string> ring,
|
||||
int signerIndex,
|
||||
string privateKey,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var body = new { message, ring, signer_index = signerIndex, private_key = privateKey };
|
||||
return await PostAsync<RingSignature>("/privacy/ring/sign", body, ct);
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyRingSignatureAsync(RingSignature signature, string message, CancellationToken ct = default)
|
||||
{
|
||||
var body = new { signature, message };
|
||||
var response = await PostAsync<Dictionary<string, object>>("/privacy/ring/verify", body, ct);
|
||||
return response.TryGetValue("valid", out var valid) && valid is JsonElement elem && elem.GetBoolean();
|
||||
}
|
||||
|
||||
public async Task<string[]> 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<string[]>(path, ct);
|
||||
}
|
||||
|
||||
public async Task<bool> CheckKeyImageAsync(string keyImage, CancellationToken ct = default)
|
||||
{
|
||||
var response = await GetAsync<Dictionary<string, object>>($"/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<StealthKeyPair> GenerateStealthKeyPairAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await PostAsync<StealthKeyPair>("/privacy/stealth/generate", new { }, ct);
|
||||
}
|
||||
|
||||
public async Task<StealthAddress> DeriveStealthAddressAsync(
|
||||
string spendPublicKey,
|
||||
string viewPublicKey,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var body = new { spend_public_key = spendPublicKey, view_public_key = viewPublicKey };
|
||||
return await PostAsync<StealthAddress>("/privacy/stealth/derive", body, ct);
|
||||
}
|
||||
|
||||
public async Task<string> 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<Dictionary<string, object>>("/privacy/stealth/recover", body, ct);
|
||||
return response["private_key"]?.ToString() ?? throw new PrivacyException("Missing private_key");
|
||||
}
|
||||
|
||||
public async Task<StealthOutput[]> ScanOutputsAsync(
|
||||
string viewPrivateKey,
|
||||
string spendPublicKey,
|
||||
long fromBlock,
|
||||
long? toBlock = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object>
|
||||
{
|
||||
["view_private_key"] = viewPrivateKey,
|
||||
["spend_public_key"] = spendPublicKey,
|
||||
["from_block"] = fromBlock
|
||||
};
|
||||
if (toBlock.HasValue) body["to_block"] = toBlock.Value;
|
||||
return await PostAsync<StealthOutput[]>("/privacy/stealth/scan", body, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Blinding
|
||||
|
||||
public async Task<string> GenerateBlindingFactorAsync(CancellationToken ct = default)
|
||||
{
|
||||
var response = await PostAsync<Dictionary<string, object>>("/privacy/blinding/generate", new { }, ct);
|
||||
return response["blinding_factor"]?.ToString() ?? throw new PrivacyException("Missing blinding_factor");
|
||||
}
|
||||
|
||||
public async Task<string> BlindValueAsync(string value, string blindingFactor, CancellationToken ct = default)
|
||||
{
|
||||
var body = new { value, blinding_factor = blindingFactor };
|
||||
var response = await PostAsync<Dictionary<string, object>>("/privacy/blinding/blind", body, ct);
|
||||
return response["blinded_value"]?.ToString() ?? throw new PrivacyException("Missing blinded_value");
|
||||
}
|
||||
|
||||
public async Task<string> UnblindValueAsync(string blindedValue, string blindingFactor, CancellationToken ct = default)
|
||||
{
|
||||
var body = new { blinded_value = blindedValue, blinding_factor = blindingFactor };
|
||||
var response = await PostAsync<Dictionary<string, object>>("/privacy/blinding/unblind", body, ct);
|
||||
return response["value"]?.ToString() ?? throw new PrivacyException("Missing value");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_closed) return false;
|
||||
try
|
||||
{
|
||||
var response = await GetAsync<Dictionary<string, object>>("/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<T> GetAsync<T>(string path, CancellationToken ct)
|
||||
{
|
||||
ThrowIfClosed();
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var response = await _httpClient.GetAsync(path, ct);
|
||||
return await HandleResponseAsync<T>(response, ct);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<T> PostAsync<T>(string path, object body, CancellationToken ct)
|
||||
{
|
||||
ThrowIfClosed();
|
||||
return await ExecuteWithRetryAsync(async () =>
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct);
|
||||
return await HandleResponseAsync<T>(response, ct);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<T> HandleResponseAsync<T>(HttpResponseMessage response, CancellationToken ct)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(content, _jsonOptions)
|
||||
?? throw new PrivacyException("Failed to deserialize response");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var error = JsonSerializer.Deserialize<Dictionary<string, object>>(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<T> ExecuteWithRetryAsync<T>(Func<Task<T>> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
153
sdk/csharp/src/Synor.Privacy/PrivacyTypes.cs
Normal file
153
sdk/csharp/src/Synor.Privacy/PrivacyTypes.cs
Normal file
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
320
sdk/flutter/lib/src/contract/contract_client.dart
Normal file
320
sdk/flutter/lib/src/contract/contract_client.dart
Normal file
|
|
@ -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<DeploymentResult> 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<DeploymentResult> 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<String> 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<dynamic> 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<TransactionResult> 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<List<DecodedEvent>> 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<List<EventLog>> 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<List<DecodedEvent>> decodeLogs(List<EventLog> logs, List<AbiEntry> 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<String> 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<dynamic> 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<String> getSelector(String signature) async {
|
||||
final response = await _get('/contract/selector?signature=${Uri.encodeComponent(signature)}');
|
||||
return response['selector'] as String;
|
||||
}
|
||||
|
||||
// ==================== Gas Estimation ====================
|
||||
|
||||
Future<GasEstimation> 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<BytecodeInfo> getBytecode(String address) async {
|
||||
final response = await _get('/contract/${Uri.encodeComponent(address)}/bytecode');
|
||||
return BytecodeInfo.fromJson(response);
|
||||
}
|
||||
|
||||
Future<VerificationResult> 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<VerificationResult> getVerificationStatus(String address) async {
|
||||
final response = await _get('/contract/${Uri.encodeComponent(address)}/verification');
|
||||
return VerificationResult.fromJson(response);
|
||||
}
|
||||
|
||||
// ==================== Multicall ====================
|
||||
|
||||
Future<List<MulticallResult>> multicall(List<MulticallRequest> 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<String> 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<bool> 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<dynamic> _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<Map<String, dynamic>> _post(String path, Map<String, dynamic> 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<String, String> 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<T> _executeWithRetry<T>(Future<T> 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)';
|
||||
}
|
||||
496
sdk/flutter/lib/src/contract/contract_types.dart
Normal file
496
sdk/flutter/lib/src/contract/contract_types.dart
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
// ==================== ABI Types ====================
|
||||
|
||||
class AbiParameter {
|
||||
final String? name;
|
||||
final String type;
|
||||
final bool? indexed;
|
||||
final List<AbiParameter>? components;
|
||||
|
||||
AbiParameter({
|
||||
this.name,
|
||||
required this.type,
|
||||
this.indexed,
|
||||
this.components,
|
||||
});
|
||||
|
||||
factory AbiParameter.fromJson(Map<String, dynamic> 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<String, dynamic> 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<AbiParameter>? inputs;
|
||||
final List<AbiParameter>? 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<String, dynamic> 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<String, dynamic> 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<AbiEntry>? abi;
|
||||
final List<dynamic>? 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<String, dynamic> 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<dynamic> args;
|
||||
final List<AbiEntry> abi;
|
||||
|
||||
CallContractOptions({
|
||||
required this.contract,
|
||||
required this.method,
|
||||
this.args = const [],
|
||||
required this.abi,
|
||||
});
|
||||
}
|
||||
|
||||
class SendContractOptions {
|
||||
final String contract;
|
||||
final String method;
|
||||
final List<dynamic> args;
|
||||
final List<AbiEntry> 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<EventLog>? logs;
|
||||
|
||||
TransactionResult({
|
||||
required this.transactionHash,
|
||||
this.blockNumber,
|
||||
this.blockHash,
|
||||
this.gasUsed,
|
||||
this.effectiveGasPrice,
|
||||
this.status,
|
||||
this.logs,
|
||||
});
|
||||
|
||||
factory TransactionResult.fromJson(Map<String, dynamic> 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<String> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic>? args;
|
||||
final EventLog? log;
|
||||
|
||||
DecodedEvent({
|
||||
required this.name,
|
||||
this.signature,
|
||||
this.args,
|
||||
this.log,
|
||||
});
|
||||
|
||||
factory DecodedEvent.fromJson(Map<String, dynamic> json) {
|
||||
return DecodedEvent(
|
||||
name: json['name'] as String,
|
||||
signature: json['signature'] as String?,
|
||||
args: json['args'] as Map<String, dynamic>?,
|
||||
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<String?>? topics;
|
||||
final List<AbiEntry>? abi;
|
||||
|
||||
EventFilter({
|
||||
required this.contract,
|
||||
this.event,
|
||||
this.fromBlock,
|
||||
this.toBlock,
|
||||
this.topics,
|
||||
this.abi,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== ABI Utilities ====================
|
||||
|
||||
class EncodeCallOptions {
|
||||
final String method;
|
||||
final List<dynamic> args;
|
||||
final List<AbiEntry> abi;
|
||||
|
||||
EncodeCallOptions({
|
||||
required this.method,
|
||||
this.args = const [],
|
||||
required this.abi,
|
||||
});
|
||||
}
|
||||
|
||||
class DecodeResultOptions {
|
||||
final String data;
|
||||
final String method;
|
||||
final List<AbiEntry> abi;
|
||||
|
||||
DecodeResultOptions({
|
||||
required this.data,
|
||||
required this.method,
|
||||
required this.abi,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Gas Estimation ====================
|
||||
|
||||
class EstimateGasOptions {
|
||||
final String contract;
|
||||
final String method;
|
||||
final List<dynamic> args;
|
||||
final List<AbiEntry> 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<String, dynamic> 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<String, dynamic> 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<AbiEntry>? 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<String, dynamic> 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<dynamic> args;
|
||||
final List<AbiEntry> abi;
|
||||
|
||||
MulticallRequest({
|
||||
required this.contract,
|
||||
required this.method,
|
||||
this.args = const [],
|
||||
required this.abi,
|
||||
});
|
||||
|
||||
Map<String, dynamic> 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<String, dynamic> 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,
|
||||
});
|
||||
}
|
||||
317
sdk/flutter/lib/src/privacy/privacy_client.dart
Normal file
317
sdk/flutter/lib/src/privacy/privacy_client.dart
Normal file
|
|
@ -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<ConfidentialTransaction> createConfidentialTx({
|
||||
required List<ConfidentialTxInput> inputs,
|
||||
required List<ConfidentialTxOutput> 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<bool> verifyConfidentialTx(ConfidentialTransaction tx) async {
|
||||
final body = {'transaction': tx.toJson()};
|
||||
final response = await _post('/privacy/confidential/verify', body);
|
||||
return response['valid'] as bool? ?? false;
|
||||
}
|
||||
|
||||
Future<Commitment> 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<bool> 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<RangeProof> 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<bool> 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<RingSignature> createRingSignature({
|
||||
required String message,
|
||||
required List<String> 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<bool> 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<List<String>> 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<bool> checkKeyImage(String keyImage) async {
|
||||
final response = await _get('/privacy/ring/key-image/$keyImage');
|
||||
return response['spent'] as bool? ?? false;
|
||||
}
|
||||
|
||||
// ==================== Stealth Addresses ====================
|
||||
|
||||
Future<StealthKeyPair> generateStealthKeyPair() async {
|
||||
final response = await _post('/privacy/stealth/generate', {});
|
||||
return StealthKeyPair.fromJson(response);
|
||||
}
|
||||
|
||||
Future<StealthAddress> 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<String> 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<List<StealthOutput>> 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<String> generateBlindingFactor() async {
|
||||
final response = await _post('/privacy/blinding/generate', {});
|
||||
return response['blinding_factor'] as String;
|
||||
}
|
||||
|
||||
Future<String> 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<String> 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<bool> 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<dynamic> _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<Map<String, dynamic>> _post(String path, Map<String, dynamic> 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<String, String> 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<T> _executeWithRetry<T>(Future<T> 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)';
|
||||
}
|
||||
265
sdk/flutter/lib/src/privacy/privacy_types.dart
Normal file
265
sdk/flutter/lib/src/privacy/privacy_types.dart
Normal file
|
|
@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<ConfidentialTxInput> inputs;
|
||||
final List<ConfidentialTxOutput> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
|
||||
return Commitment(
|
||||
commitment: json['commitment'] as String,
|
||||
blindingFactor: json['blinding_factor'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() => {
|
||||
'proof': proof,
|
||||
'commitment': commitment,
|
||||
'min_value': minValue,
|
||||
'max_value': maxValue,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Ring Signatures ====================
|
||||
|
||||
class RingSignature {
|
||||
final String c0;
|
||||
final List<String> s;
|
||||
final String keyImage;
|
||||
final List<String> ring;
|
||||
|
||||
RingSignature({
|
||||
required this.c0,
|
||||
required this.s,
|
||||
required this.keyImage,
|
||||
required this.ring,
|
||||
});
|
||||
|
||||
factory RingSignature.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
810
sdk/go/contract/contract.go
Normal file
810
sdk/go/contract/contract.go
Normal file
|
|
@ -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<<attempt) * time.Second)
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}, result interface{}) error {
|
||||
url := c.config.Endpoint + path
|
||||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return &Error{Message: fmt.Sprintf("failed to marshal body: %v", err)}
|
||||
}
|
||||
bodyReader = bytes.NewReader(jsonBody)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
||||
if err != nil {
|
||||
return &Error{Message: fmt.Sprintf("failed to create request: %v", err)}
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-SDK-Version", "go/0.1.0")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return &Error{Message: fmt.Sprintf("request failed: %v", err)}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return &Error{Message: fmt.Sprintf("failed to read response: %v", err)}
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 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
|
||||
}
|
||||
682
sdk/go/privacy/privacy.go
Normal file
682
sdk/go/privacy/privacy.go
Normal file
|
|
@ -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<<attempt) * time.Second)
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}, result interface{}) error {
|
||||
url := c.config.Endpoint + path
|
||||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return &Error{Message: fmt.Sprintf("failed to marshal body: %v", err)}
|
||||
}
|
||||
bodyReader = bytes.NewReader(jsonBody)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
||||
if err != nil {
|
||||
return &Error{Message: fmt.Sprintf("failed to create request: %v", err)}
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-SDK-Version", "go/0.1.0")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return &Error{Message: fmt.Sprintf("request failed: %v", err)}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return &Error{Message: fmt.Sprintf("failed to read response: %v", err)}
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 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
|
||||
}
|
||||
324
sdk/java/src/main/java/io/synor/contract/ContractClient.java
Normal file
324
sdk/java/src/main/java/io/synor/contract/ContractClient.java
Normal file
|
|
@ -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<DeploymentResult> 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<DeploymentResult> 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<String> 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<Object> 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<TransactionResult> 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<List<DecodedEvent>> 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<List<DecodedEvent>>(){}.getType();
|
||||
return post("/contract/events", body, listType);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<EventLog>> 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<List<EventLog>>(){}.getType();
|
||||
return get(path.toString(), listType);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<DecodedEvent>> decodeLogs(List<EventLog> logs, List<AbiEntry> abi) {
|
||||
JsonObject body = new JsonObject();
|
||||
body.add("logs", gson.toJsonTree(logs));
|
||||
body.add("abi", gson.toJsonTree(abi));
|
||||
|
||||
Type listType = new TypeToken<List<DecodedEvent>>(){}.getType();
|
||||
return post("/contract/decode-logs", body, listType);
|
||||
}
|
||||
|
||||
// ==================== ABI Utilities ====================
|
||||
|
||||
public CompletableFuture<String> 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<Object> 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<String> getSelector(String signature) {
|
||||
return get("/contract/selector?signature=" + encode(signature), JsonObject.class)
|
||||
.thenApply(response -> response.get("selector").getAsString());
|
||||
}
|
||||
|
||||
// ==================== Gas Estimation ====================
|
||||
|
||||
public CompletableFuture<GasEstimation> 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<BytecodeInfo> getBytecode(String address) {
|
||||
return get("/contract/" + encode(address) + "/bytecode", BytecodeInfo.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<VerificationResult> 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<VerificationResult> getVerificationStatus(String address) {
|
||||
return get("/contract/" + encode(address) + "/verification", VerificationResult.class);
|
||||
}
|
||||
|
||||
// ==================== Multicall ====================
|
||||
|
||||
public CompletableFuture<List<MulticallResult>> multicall(List<MulticallRequest> requests) {
|
||||
JsonObject body = new JsonObject();
|
||||
body.add("calls", gson.toJsonTree(requests));
|
||||
|
||||
Type listType = new TypeToken<List<MulticallResult>>(){}.getType();
|
||||
return post("/contract/multicall", body, listType);
|
||||
}
|
||||
|
||||
// ==================== Storage ====================
|
||||
|
||||
public CompletableFuture<String> 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<Boolean> 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 <T> CompletableFuture<T> get(String path, Type type) {
|
||||
return executeRequest(path, null, type);
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> post(String path, JsonObject body, Type type) {
|
||||
return executeRequest(path, body, type);
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> executeRequest(String path, JsonObject body, Type type) {
|
||||
if (closed.get()) {
|
||||
CompletableFuture<T> future = new CompletableFuture<>();
|
||||
future.completeExceptionally(new ContractException("Client has been closed"));
|
||||
return future;
|
||||
}
|
||||
|
||||
CompletableFuture<T> 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 <T> void executeWithRetry(Request request, int remainingRetries, int attempt, CompletableFuture<T> 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 <T> void scheduleRetry(Request request, int remainingRetries, int attempt, CompletableFuture<T> future, Type type) {
|
||||
long delay = (long) Math.pow(2, attempt) * 1000;
|
||||
CompletableFuture.delayedExecutor(delay, TimeUnit.MILLISECONDS)
|
||||
.execute(() -> executeWithRetry(request, remainingRetries, attempt, future, type));
|
||||
}
|
||||
}
|
||||
49
sdk/java/src/main/java/io/synor/contract/ContractConfig.java
Normal file
49
sdk/java/src/main/java/io/synor/contract/ContractConfig.java
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
294
sdk/java/src/main/java/io/synor/contract/ContractTypes.java
Normal file
294
sdk/java/src/main/java/io/synor/contract/ContractTypes.java
Normal file
|
|
@ -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<AbiParameter> components;
|
||||
}
|
||||
|
||||
public static class AbiEntry {
|
||||
@SerializedName("type")
|
||||
public String type;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("inputs")
|
||||
public List<AbiParameter> inputs;
|
||||
|
||||
@SerializedName("outputs")
|
||||
public List<AbiParameter> outputs;
|
||||
|
||||
@SerializedName("stateMutability")
|
||||
public String stateMutability;
|
||||
|
||||
@SerializedName("anonymous")
|
||||
public Boolean anonymous;
|
||||
}
|
||||
|
||||
// ==================== Deployment ====================
|
||||
|
||||
public static class DeployContractOptions {
|
||||
public String bytecode;
|
||||
public List<AbiEntry> 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<AbiEntry> abi;
|
||||
}
|
||||
|
||||
public static class SendContractOptions {
|
||||
public String contract;
|
||||
public String method;
|
||||
public Object[] args;
|
||||
public List<AbiEntry> 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<EventLog> logs;
|
||||
}
|
||||
|
||||
// ==================== Events ====================
|
||||
|
||||
public static class EventLog {
|
||||
@SerializedName("address")
|
||||
public String address;
|
||||
|
||||
@SerializedName("topics")
|
||||
public List<String> 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<String, Object> args;
|
||||
|
||||
@SerializedName("log")
|
||||
public EventLog log;
|
||||
}
|
||||
|
||||
public static class EventFilter {
|
||||
public String contract;
|
||||
public String event;
|
||||
public Long fromBlock;
|
||||
public Long toBlock;
|
||||
public List<String> topics;
|
||||
public List<AbiEntry> abi;
|
||||
}
|
||||
|
||||
// ==================== ABI Utilities ====================
|
||||
|
||||
public static class EncodeCallOptions {
|
||||
public String method;
|
||||
public Object[] args;
|
||||
public List<AbiEntry> abi;
|
||||
}
|
||||
|
||||
public static class DecodeResultOptions {
|
||||
public String data;
|
||||
public String method;
|
||||
public List<AbiEntry> abi;
|
||||
}
|
||||
|
||||
// ==================== Gas Estimation ====================
|
||||
|
||||
public static class EstimateGasOptions {
|
||||
public String contract;
|
||||
public String method;
|
||||
public Object[] args;
|
||||
public List<AbiEntry> 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<AbiEntry> abi;
|
||||
|
||||
@SerializedName("source_code")
|
||||
public String sourceCode;
|
||||
}
|
||||
|
||||
// ==================== Multicall ====================
|
||||
|
||||
public static class MulticallRequest {
|
||||
public String contract;
|
||||
public String method;
|
||||
public Object[] args;
|
||||
public List<AbiEntry> 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;
|
||||
}
|
||||
}
|
||||
273
sdk/java/src/main/java/io/synor/privacy/PrivacyClient.java
Normal file
273
sdk/java/src/main/java/io/synor/privacy/PrivacyClient.java
Normal file
|
|
@ -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<ConfidentialTransaction> 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<Boolean> 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<Commitment> 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<Boolean> 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<RangeProof> 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<Boolean> 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<RingSignature> 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<Boolean> 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<String[]> generateDecoys(int count, String excludeKey) {
|
||||
String path = "/privacy/ring/decoys?count=" + count;
|
||||
if (excludeKey != null) {
|
||||
path += "&exclude=" + excludeKey;
|
||||
}
|
||||
return get(path, new TypeToken<String[]>(){}.getType());
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> checkKeyImage(String keyImage) {
|
||||
return get("/privacy/ring/key-image/" + keyImage, JsonObject.class)
|
||||
.thenApply(response -> response.get("spent").getAsBoolean());
|
||||
}
|
||||
|
||||
// ==================== Stealth Addresses ====================
|
||||
|
||||
public CompletableFuture<StealthKeyPair> generateStealthKeyPair() {
|
||||
return post("/privacy/stealth/generate", new JsonObject(), StealthKeyPair.class);
|
||||
}
|
||||
|
||||
public CompletableFuture<StealthAddress> 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<String> 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<StealthOutput[]> 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<String> generateBlindingFactor() {
|
||||
return post("/privacy/blinding/generate", new JsonObject(), JsonObject.class)
|
||||
.thenApply(response -> response.get("blinding_factor").getAsString());
|
||||
}
|
||||
|
||||
public CompletableFuture<String> 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<String> 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<Boolean> 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 <T> CompletableFuture<T> get(String path, Type type) {
|
||||
return executeRequest(path, null, type);
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> post(String path, JsonObject body, Type type) {
|
||||
return executeRequest(path, body, type);
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> executeRequest(String path, JsonObject body, Type type) {
|
||||
if (closed.get()) {
|
||||
CompletableFuture<T> future = new CompletableFuture<>();
|
||||
future.completeExceptionally(new PrivacyException("Client has been closed"));
|
||||
return future;
|
||||
}
|
||||
|
||||
CompletableFuture<T> 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 <T> void executeWithRetry(Request request, int remainingRetries, int attempt, CompletableFuture<T> 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 <T> void scheduleRetry(Request request, int remainingRetries, int attempt, CompletableFuture<T> future, Type type) {
|
||||
long delay = (long) Math.pow(2, attempt) * 1000;
|
||||
CompletableFuture.delayedExecutor(delay, TimeUnit.MILLISECONDS)
|
||||
.execute(() -> executeWithRetry(request, remainingRetries, attempt, future, type));
|
||||
}
|
||||
}
|
||||
49
sdk/java/src/main/java/io/synor/privacy/PrivacyConfig.java
Normal file
49
sdk/java/src/main/java/io/synor/privacy/PrivacyConfig.java
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
149
sdk/java/src/main/java/io/synor/privacy/PrivacyTypes.java
Normal file
149
sdk/java/src/main/java/io/synor/privacy/PrivacyTypes.java
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
583
sdk/js/src/contract/client.ts
Normal file
583
sdk/js/src/contract/client.ts
Normal file
|
|
@ -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<Omit<ContractConfig, 'defaultGasLimit' | 'defaultGasPrice'>> & {
|
||||
defaultGasLimit?: string;
|
||||
defaultGasPrice?: string;
|
||||
};
|
||||
private closed = false;
|
||||
private subscriptions = new Map<string, WebSocket>();
|
||||
|
||||
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<DeploymentResult> {
|
||||
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<DeploymentResult> {
|
||||
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<string> {
|
||||
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<unknown> {
|
||||
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<TransactionResult> {
|
||||
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<string, Abi>
|
||||
): Promise<MulticallResult[]> {
|
||||
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<DecodedEvent[]> {
|
||||
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<EventLog[]> {
|
||||
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<EventSubscription> {
|
||||
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<DecodedEvent> {
|
||||
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<string, AbiEntry> = {};
|
||||
const events: Record<string, AbiEntry> = {};
|
||||
const errors: Record<string, AbiEntry> = {};
|
||||
|
||||
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<string> {
|
||||
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<unknown> {
|
||||
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<string> {
|
||||
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<GasEstimation> {
|
||||
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<BytecodeInfo> {
|
||||
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<boolean> {
|
||||
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<string> {
|
||||
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<VerificationResult> {
|
||||
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<VerificationResult> {
|
||||
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<Abi | null> {
|
||||
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<boolean> {
|
||||
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<any> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
42
sdk/js/src/contract/index.ts
Normal file
42
sdk/js/src/contract/index.ts
Normal file
|
|
@ -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';
|
||||
332
sdk/js/src/contract/types.ts
Normal file
332
sdk/js/src/contract/types.ts
Normal file
|
|
@ -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<string, unknown>;
|
||||
/** 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<string, unknown>;
|
||||
}
|
||||
|
||||
/** Event subscription callback */
|
||||
export type EventCallback = (event: DecodedEvent) => void;
|
||||
|
||||
/** Event subscription */
|
||||
export interface EventSubscription {
|
||||
/** Subscription ID */
|
||||
id: string;
|
||||
/** Unsubscribe */
|
||||
unsubscribe: () => Promise<void>;
|
||||
}
|
||||
|
||||
/** Contract interface (from ABI) */
|
||||
export interface ContractInterface {
|
||||
/** Contract ABI */
|
||||
abi: Abi;
|
||||
/** Function signatures */
|
||||
functions: Record<string, AbiEntry>;
|
||||
/** Event signatures */
|
||||
events: Record<string, AbiEntry>;
|
||||
/** Error signatures */
|
||||
errors: Record<string, AbiEntry>;
|
||||
}
|
||||
|
||||
/** 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';
|
||||
}
|
||||
530
sdk/js/src/privacy/client.ts
Normal file
530
sdk/js/src/privacy/client.ts
Normal file
|
|
@ -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<PrivacyConfig>;
|
||||
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<ConfidentialTransaction> {
|
||||
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<VerifyConfidentialTxResult> {
|
||||
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<string> {
|
||||
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<RingSignature> {
|
||||
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<VerifyRingSignatureResult> {
|
||||
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<boolean> {
|
||||
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<string[]> {
|
||||
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<StealthKeypair> {
|
||||
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<OneTimeAddress> {
|
||||
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<SharedSecret> {
|
||||
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<OneTimeAddress[]> {
|
||||
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<CommitmentWithBlinding> {
|
||||
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<boolean> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<RangeProof> {
|
||||
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<VerifyRangeProofResult> {
|
||||
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<string> {
|
||||
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<boolean> {
|
||||
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<any> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
44
sdk/js/src/privacy/index.ts
Normal file
44
sdk/js/src/privacy/index.ts
Normal file
|
|
@ -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';
|
||||
321
sdk/js/src/privacy/types.ts
Normal file
321
sdk/js/src/privacy/types.ts
Normal file
|
|
@ -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[];
|
||||
}
|
||||
292
sdk/kotlin/src/main/kotlin/io/synor/contract/ContractClient.kt
Normal file
292
sdk/kotlin/src/main/kotlin/io/synor/contract/ContractClient.kt
Normal file
|
|
@ -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<DecodedEvent> {
|
||||
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<EventLog> {
|
||||
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<EventLog>, abi: List<AbiEntry>): List<DecodedEvent> {
|
||||
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<MulticallRequest>): List<MulticallResult> {
|
||||
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 <reified T> get(path: String): T {
|
||||
checkClosed()
|
||||
return executeWithRetry { client.get("${config.endpoint}$path").body() }
|
||||
}
|
||||
|
||||
private suspend inline fun <reified T> post(path: String, body: JsonObject): T {
|
||||
checkClosed()
|
||||
return executeWithRetry {
|
||||
client.post("${config.endpoint}$path") {
|
||||
setBody(body)
|
||||
}.body()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <T> 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)
|
||||
199
sdk/kotlin/src/main/kotlin/io/synor/contract/ContractTypes.kt
Normal file
199
sdk/kotlin/src/main/kotlin/io/synor/contract/ContractTypes.kt
Normal file
|
|
@ -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<AbiParameter>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AbiEntry(
|
||||
val type: String,
|
||||
val name: String? = null,
|
||||
val inputs: List<AbiParameter>? = null,
|
||||
val outputs: List<AbiParameter>? = null,
|
||||
val stateMutability: String? = null,
|
||||
val anonymous: Boolean? = null
|
||||
)
|
||||
|
||||
// ==================== Deployment ====================
|
||||
|
||||
data class DeployContractOptions(
|
||||
val bytecode: String,
|
||||
val abi: List<AbiEntry>? = null,
|
||||
val constructorArgs: List<Any?>? = 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<Any?> = emptyList(),
|
||||
val abi: List<AbiEntry>
|
||||
)
|
||||
|
||||
data class SendContractOptions(
|
||||
val contract: String,
|
||||
val method: String,
|
||||
val args: List<Any?> = emptyList(),
|
||||
val abi: List<AbiEntry>,
|
||||
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<EventLog>? = null
|
||||
)
|
||||
|
||||
// ==================== Events ====================
|
||||
|
||||
@Serializable
|
||||
data class EventLog(
|
||||
val address: String,
|
||||
val topics: List<String>,
|
||||
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<String, JsonElement>? = 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<String?>? = null,
|
||||
val abi: List<AbiEntry>? = null
|
||||
)
|
||||
|
||||
// ==================== ABI Utilities ====================
|
||||
|
||||
data class EncodeCallOptions(
|
||||
val method: String,
|
||||
val args: List<Any?> = emptyList(),
|
||||
val abi: List<AbiEntry>
|
||||
)
|
||||
|
||||
data class DecodeResultOptions(
|
||||
val data: String,
|
||||
val method: String,
|
||||
val abi: List<AbiEntry>
|
||||
)
|
||||
|
||||
// ==================== Gas Estimation ====================
|
||||
|
||||
data class EstimateGasOptions(
|
||||
val contract: String,
|
||||
val method: String,
|
||||
val args: List<Any?> = emptyList(),
|
||||
val abi: List<AbiEntry>,
|
||||
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<AbiEntry>? = null,
|
||||
@SerialName("source_code") val sourceCode: String? = null
|
||||
)
|
||||
|
||||
// ==================== Multicall ====================
|
||||
|
||||
@Serializable
|
||||
data class MulticallRequest(
|
||||
val contract: String,
|
||||
val method: String,
|
||||
val args: List<JsonElement> = emptyList(),
|
||||
val abi: List<AbiEntry>
|
||||
)
|
||||
|
||||
@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
|
||||
)
|
||||
286
sdk/kotlin/src/main/kotlin/io/synor/privacy/PrivacyClient.kt
Normal file
286
sdk/kotlin/src/main/kotlin/io/synor/privacy/PrivacyClient.kt
Normal file
|
|
@ -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<ConfidentialTxInput>,
|
||||
outputs: List<ConfidentialTxOutput>
|
||||
): 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<String>,
|
||||
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<String> {
|
||||
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<StealthOutput> {
|
||||
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 <reified T> get(path: String): T {
|
||||
checkClosed()
|
||||
return executeWithRetry { client.get("${config.endpoint}$path").body() }
|
||||
}
|
||||
|
||||
private suspend inline fun <reified T> post(path: String, body: JsonObject): T {
|
||||
checkClosed()
|
||||
return executeWithRetry {
|
||||
client.post("${config.endpoint}$path") {
|
||||
setBody(body)
|
||||
}.body()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <T> 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)
|
||||
86
sdk/kotlin/src/main/kotlin/io/synor/privacy/PrivacyTypes.kt
Normal file
86
sdk/kotlin/src/main/kotlin/io/synor/privacy/PrivacyTypes.kt
Normal file
|
|
@ -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<ConfidentialTxInput>,
|
||||
val outputs: List<ConfidentialTxOutput>,
|
||||
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<String>,
|
||||
@SerialName("key_image") val keyImage: String,
|
||||
val ring: List<String>
|
||||
)
|
||||
|
||||
// ==================== 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
|
||||
)
|
||||
50
sdk/python/src/synor_contract/__init__.py
Normal file
50
sdk/python/src/synor_contract/__init__.py
Normal file
|
|
@ -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",
|
||||
]
|
||||
711
sdk/python/src/synor_contract/client.py
Normal file
711
sdk/python/src/synor_contract/client.py
Normal file
|
|
@ -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}")
|
||||
373
sdk/python/src/synor_contract/types.py
Normal file
373
sdk/python/src/synor_contract/types.py
Normal file
|
|
@ -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]
|
||||
63
sdk/python/src/synor_privacy/__init__.py
Normal file
63
sdk/python/src/synor_privacy/__init__.py
Normal file
|
|
@ -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",
|
||||
]
|
||||
609
sdk/python/src/synor_privacy/client.py
Normal file
609
sdk/python/src/synor_privacy/client.py
Normal file
|
|
@ -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}")
|
||||
436
sdk/python/src/synor_privacy/types.py
Normal file
436
sdk/python/src/synor_privacy/types.py
Normal file
|
|
@ -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", "")),
|
||||
)
|
||||
9
sdk/ruby/lib/synor_contract.rb
Normal file
9
sdk/ruby/lib/synor_contract.rb
Normal file
|
|
@ -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
|
||||
370
sdk/ruby/lib/synor_contract/client.rb
Normal file
370
sdk/ruby/lib/synor_contract/client.rb
Normal file
|
|
@ -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
|
||||
326
sdk/ruby/lib/synor_contract/types.rb
Normal file
326
sdk/ruby/lib/synor_contract/types.rb
Normal file
|
|
@ -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
|
||||
5
sdk/ruby/lib/synor_contract/version.rb
Normal file
5
sdk/ruby/lib/synor_contract/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorContract
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
9
sdk/ruby/lib/synor_privacy.rb
Normal file
9
sdk/ruby/lib/synor_privacy.rb
Normal file
|
|
@ -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
|
||||
269
sdk/ruby/lib/synor_privacy/client.rb
Normal file
269
sdk/ruby/lib/synor_privacy/client.rb
Normal file
|
|
@ -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
|
||||
190
sdk/ruby/lib/synor_privacy/types.rb
Normal file
190
sdk/ruby/lib/synor_privacy/types.rb
Normal file
|
|
@ -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
|
||||
5
sdk/ruby/lib/synor_privacy/version.rb
Normal file
5
sdk/ruby/lib/synor_privacy/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorPrivacy
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
471
sdk/rust/src/contract/client.rs
Normal file
471
sdk/rust/src/contract/client.rs
Normal file
|
|
@ -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<String>) -> 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<String>) -> 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<AtomicBool>,
|
||||
}
|
||||
|
||||
impl SynorContract {
|
||||
/// Create a new Contract client
|
||||
pub fn new(config: ContractConfig) -> Result<Self> {
|
||||
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<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
method: reqwest::Method,
|
||||
path: &str,
|
||||
body: Option<serde_json::Value>,
|
||||
) -> Result<T> {
|
||||
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::<serde_json::Value>(&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<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
|
||||
self.request(reqwest::Method::GET, path, None).await
|
||||
}
|
||||
|
||||
async fn post<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: serde_json::Value,
|
||||
) -> Result<T> {
|
||||
self.request(reqwest::Method::POST, path, Some(body)).await
|
||||
}
|
||||
|
||||
// ==================== Contract Deployment ====================
|
||||
|
||||
/// Deploy a new smart contract
|
||||
pub async fn deploy(&self, options: DeployContractOptions) -> Result<DeploymentResult> {
|
||||
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<DeploymentResult> {
|
||||
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<String> {
|
||||
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<serde_json::Value> {
|
||||
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<TransactionResult> {
|
||||
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<Vec<DecodedEvent>> {
|
||||
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<u64>, to_block: Option<u64>) -> Result<Vec<EventLog>> {
|
||||
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<Vec<DecodedEvent>> {
|
||||
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<String> {
|
||||
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<serde_json::Value> {
|
||||
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<String> {
|
||||
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<GasEstimation> {
|
||||
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<BytecodeInfo> {
|
||||
self.get(&format!("/contract/{}/bytecode", address)).await
|
||||
}
|
||||
|
||||
/// Verify contract source code
|
||||
pub async fn verify(&self, options: VerifyContractOptions) -> Result<VerificationResult> {
|
||||
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<VerificationResult> {
|
||||
self.get(&format!("/contract/{}/verification", address)).await
|
||||
}
|
||||
|
||||
// ==================== Multicall ====================
|
||||
|
||||
/// Execute multiple calls in a single request
|
||||
pub async fn multicall(&self, requests: &[MulticallRequest]) -> Result<Vec<MulticallResult>> {
|
||||
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<String> {
|
||||
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::<serde_json::Value>("/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)
|
||||
}
|
||||
}
|
||||
60
sdk/rust/src/contract/error.rs
Normal file
60
sdk/rust/src/contract/error.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
//! Contract SDK errors
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Contract SDK result type
|
||||
pub type Result<T> = std::result::Result<T, ContractError>;
|
||||
|
||||
/// 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<String>,
|
||||
status_code: Option<u16>,
|
||||
},
|
||||
/// 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<reqwest::Error> for ContractError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
ContractError::Request(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ContractError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ContractError::Serialization(err.to_string())
|
||||
}
|
||||
}
|
||||
16
sdk/rust/src/contract/mod.rs
Normal file
16
sdk/rust/src/contract/mod.rs
Normal file
|
|
@ -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::*;
|
||||
316
sdk/rust/src/contract/types.rs
Normal file
316
sdk/rust/src/contract/types.rs
Normal file
|
|
@ -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<String>,
|
||||
pub default_gas_price: Option<String>,
|
||||
}
|
||||
|
||||
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<Vec<AbiParameter>>,
|
||||
#[serde(rename = "internalType", skip_serializing_if = "Option::is_none")]
|
||||
pub internal_type: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub inputs: Option<Vec<AbiParameter>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub outputs: Option<Vec<AbiParameter>>,
|
||||
#[serde(rename = "stateMutability", skip_serializing_if = "Option::is_none")]
|
||||
pub state_mutability: Option<StateMutability>,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub anonymous: bool,
|
||||
}
|
||||
|
||||
/// Contract ABI
|
||||
pub type Abi = Vec<AbiEntry>;
|
||||
|
||||
/// 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<String>,
|
||||
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<String, serde_json::Value>,
|
||||
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<EventLog>,
|
||||
#[serde(rename = "returnValue", skip_serializing_if = "Option::is_none")]
|
||||
pub return_value: Option<serde_json::Value>,
|
||||
#[serde(rename = "revertReason", skip_serializing_if = "Option::is_none")]
|
||||
pub revert_reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Contract interface
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ContractInterface {
|
||||
pub abi: Abi,
|
||||
pub functions: HashMap<String, AbiEntry>,
|
||||
pub events: HashMap<String, AbiEntry>,
|
||||
pub errors: HashMap<String, AbiEntry>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub abi: Option<Abi>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<BytecodeMetadata>,
|
||||
}
|
||||
|
||||
/// 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<Abi>,
|
||||
}
|
||||
|
||||
/// 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<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Deploy options
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DeployOptions {
|
||||
pub bytecode: String,
|
||||
pub abi: Option<Abi>,
|
||||
pub args: Option<Vec<serde_json::Value>>,
|
||||
pub gas_limit: Option<String>,
|
||||
pub gas_price: Option<String>,
|
||||
pub value: Option<String>,
|
||||
pub salt: Option<String>,
|
||||
}
|
||||
|
||||
/// Call options
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CallOptions {
|
||||
pub address: String,
|
||||
pub method: String,
|
||||
pub abi: Abi,
|
||||
pub args: Option<Vec<serde_json::Value>>,
|
||||
pub block_number: Option<BlockNumber>,
|
||||
}
|
||||
|
||||
/// Send options
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SendOptions {
|
||||
pub address: String,
|
||||
pub method: String,
|
||||
pub abi: Abi,
|
||||
pub args: Option<Vec<serde_json::Value>>,
|
||||
pub gas_limit: Option<String>,
|
||||
pub gas_price: Option<String>,
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
pub abi: Option<Abi>,
|
||||
pub from_block: Option<BlockNumber>,
|
||||
pub to_block: Option<BlockNumber>,
|
||||
pub filter: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
/// Estimate gas options
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct EstimateGasOptions {
|
||||
pub address: Option<String>,
|
||||
pub method: Option<String>,
|
||||
pub args: Option<Vec<serde_json::Value>>,
|
||||
pub abi: Option<Abi>,
|
||||
pub bytecode: Option<String>,
|
||||
pub value: Option<String>,
|
||||
pub from: Option<String>,
|
||||
}
|
||||
|
||||
/// Verify contract options
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VerifyContractOptions {
|
||||
pub address: String,
|
||||
pub source_code: String,
|
||||
pub compiler_version: String,
|
||||
pub constructor_arguments: Option<String>,
|
||||
pub optimization: bool,
|
||||
pub optimization_runs: u32,
|
||||
pub contract_name: Option<String>,
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
626
sdk/rust/src/privacy/client.rs
Normal file
626
sdk/rust/src/privacy/client.rs
Normal file
|
|
@ -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<AtomicBool>,
|
||||
}
|
||||
|
||||
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<ConfidentialTransaction> {
|
||||
#[derive(Serialize)]
|
||||
struct Request {
|
||||
inputs: Vec<ConfidentialUtxo>,
|
||||
outputs: Vec<ConfidentialOutput>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
fee: Option<String>,
|
||||
#[serde(rename = "lockHeight", skip_serializing_if = "Option::is_none")]
|
||||
lock_height: Option<u64>,
|
||||
}
|
||||
|
||||
#[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<VerifyConfidentialTxResult> {
|
||||
#[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<String> {
|
||||
#[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<RingSignature> {
|
||||
#[derive(Serialize)]
|
||||
struct Request {
|
||||
message: String,
|
||||
ring: Vec<String>,
|
||||
#[serde(rename = "privateKey")]
|
||||
private_key: String,
|
||||
#[serde(rename = "signerIndex", skip_serializing_if = "Option::is_none")]
|
||||
signer_index: Option<u32>,
|
||||
}
|
||||
#[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<VerifyRingSignatureResult> {
|
||||
#[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<bool> {
|
||||
#[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<Vec<String>>,
|
||||
) -> Result<Vec<String>> {
|
||||
#[derive(Serialize)]
|
||||
struct Request {
|
||||
size: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
exclude: Option<Vec<String>>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Response {
|
||||
ring: Vec<String>,
|
||||
}
|
||||
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<StealthKeypair> {
|
||||
#[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<OneTimeAddress> {
|
||||
#[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<SharedSecret> {
|
||||
#[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<String>,
|
||||
) -> Result<Vec<OneTimeAddress>> {
|
||||
#[derive(Serialize)]
|
||||
struct Request {
|
||||
#[serde(rename = "scanPrivateKey")]
|
||||
scan_private_key: String,
|
||||
#[serde(rename = "spendPublicKey")]
|
||||
spend_public_key: String,
|
||||
transactions: Vec<String>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct Response {
|
||||
payments: Vec<OneTimeAddress>,
|
||||
}
|
||||
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<CommitmentWithBlinding> {
|
||||
#[derive(Serialize)]
|
||||
struct Request {
|
||||
value: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
blinding: Option<String>,
|
||||
}
|
||||
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<bool> {
|
||||
#[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<String> {
|
||||
#[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<String> {
|
||||
#[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<String>,
|
||||
negative: Vec<String>,
|
||||
) -> Result<String> {
|
||||
#[derive(Serialize)]
|
||||
struct Request {
|
||||
positive: Vec<String>,
|
||||
negative: Vec<String>,
|
||||
}
|
||||
#[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<String> {
|
||||
#[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<RangeProof> {
|
||||
#[derive(Serialize)]
|
||||
struct Request {
|
||||
value: String,
|
||||
blinding: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
message: Option<String>,
|
||||
#[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<VerifyRangeProofResult> {
|
||||
#[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<String> {
|
||||
#[derive(Serialize)]
|
||||
struct Output {
|
||||
value: String,
|
||||
blinding: String,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct Request {
|
||||
outputs: Vec<Output>,
|
||||
}
|
||||
#[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<B, R>(&self, method: &str, path: &str, body: Option<B>) -> Result<R>
|
||||
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<B, R>(&self, method: &str, path: &str, body: &Option<B>) -> Result<R>
|
||||
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<String>,
|
||||
error: Option<String>,
|
||||
code: Option<String>,
|
||||
}
|
||||
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()))
|
||||
}
|
||||
}
|
||||
60
sdk/rust/src/privacy/error.rs
Normal file
60
sdk/rust/src/privacy/error.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
//! Privacy SDK errors
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Privacy SDK result type
|
||||
pub type Result<T> = std::result::Result<T, PrivacyError>;
|
||||
|
||||
/// 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<String>,
|
||||
status_code: Option<u16>,
|
||||
},
|
||||
/// 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<reqwest::Error> for PrivacyError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
PrivacyError::Request(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for PrivacyError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
PrivacyError::Serialization(err.to_string())
|
||||
}
|
||||
}
|
||||
15
sdk/rust/src/privacy/mod.rs
Normal file
15
sdk/rust/src/privacy/mod.rs
Normal file
|
|
@ -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::*;
|
||||
290
sdk/rust/src/privacy/types.rs
Normal file
290
sdk/rust/src/privacy/types.rs
Normal file
|
|
@ -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<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub amount: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
/// 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<u64>,
|
||||
}
|
||||
|
||||
/// 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<ConfidentialTxInput>,
|
||||
pub outputs: Vec<ConfidentialTxOutput>,
|
||||
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<String>,
|
||||
}
|
||||
|
||||
/// Ring signature components
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RingSignatureComponents {
|
||||
pub c: Vec<String>,
|
||||
pub r: Vec<String>,
|
||||
}
|
||||
|
||||
/// Ring signature
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RingSignature {
|
||||
pub id: String,
|
||||
#[serde(rename = "messageHash")]
|
||||
pub message_hash: String,
|
||||
pub ring: Vec<String>,
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
/// 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<ConfidentialUtxo>,
|
||||
pub outputs: Vec<ConfidentialOutput>,
|
||||
pub fee: Option<String>,
|
||||
pub lock_height: Option<u64>,
|
||||
}
|
||||
|
||||
/// Options for creating a ring signature
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CreateRingSignatureOptions {
|
||||
pub message: String,
|
||||
pub ring: Vec<String>,
|
||||
pub private_key: String,
|
||||
pub signer_index: Option<u32>,
|
||||
}
|
||||
|
||||
/// Options for creating a range proof
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CreateRangeProofOptions {
|
||||
pub value: String,
|
||||
pub blinding: String,
|
||||
pub message: Option<String>,
|
||||
pub bit_length: Option<u32>,
|
||||
}
|
||||
314
sdk/swift/Sources/SynorContract/ContractClient.swift
Normal file
314
sdk/swift/Sources/SynorContract/ContractClient.swift
Normal file
|
|
@ -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<T: Decodable>(_ 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<T: Decodable>(_ 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<T: Decodable>(_ 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<T>(_ block: () async throws -> T) async throws -> T {
|
||||
var lastError: Error?
|
||||
for attempt in 0..<config.retries {
|
||||
do {
|
||||
return try await block()
|
||||
} catch {
|
||||
lastError = error
|
||||
if attempt < config.retries - 1 {
|
||||
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError ?? ContractError.request("Request failed")
|
||||
}
|
||||
|
||||
private func checkClosed() throws {
|
||||
if isClosed {
|
||||
throw ContractError.closed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contract SDK configuration.
|
||||
public struct ContractConfig {
|
||||
public let apiKey: String
|
||||
public var endpoint: String
|
||||
public var timeoutMs: Int
|
||||
public var retries: Int
|
||||
|
||||
public init(
|
||||
apiKey: String,
|
||||
endpoint: String = "https://contract.synor.io",
|
||||
timeoutMs: Int = 30000,
|
||||
retries: Int = 3
|
||||
) {
|
||||
self.apiKey = apiKey
|
||||
self.endpoint = endpoint
|
||||
self.timeoutMs = timeoutMs
|
||||
self.retries = retries
|
||||
}
|
||||
}
|
||||
|
||||
/// Contract SDK errors.
|
||||
public enum ContractError: Error {
|
||||
case request(String)
|
||||
case response(String)
|
||||
case api(message: String, code: String?, statusCode: Int)
|
||||
case closed
|
||||
}
|
||||
422
sdk/swift/Sources/SynorContract/ContractTypes.swift
Normal file
422
sdk/swift/Sources/SynorContract/ContractTypes.swift
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
import Foundation
|
||||
|
||||
// MARK: - ABI Types
|
||||
|
||||
public struct AbiParameter: Codable {
|
||||
public let name: String?
|
||||
public let type: String
|
||||
public let indexed: Bool?
|
||||
public let components: [AbiParameter]?
|
||||
|
||||
public init(name: String? = nil, type: String, indexed: Bool? = nil, components: [AbiParameter]? = nil) {
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.indexed = indexed
|
||||
self.components = components
|
||||
}
|
||||
|
||||
func toDictionary() -> [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()
|
||||
}
|
||||
}
|
||||
}
|
||||
314
sdk/swift/Sources/SynorPrivacy/PrivacyClient.swift
Normal file
314
sdk/swift/Sources/SynorPrivacy/PrivacyClient.swift
Normal file
|
|
@ -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<T: Decodable>(_ 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<T: Decodable>(_ 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<T: Decodable>(_ 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<T>(_ block: () async throws -> T) async throws -> T {
|
||||
var lastError: Error?
|
||||
for attempt in 0..<config.retries {
|
||||
do {
|
||||
return try await block()
|
||||
} catch {
|
||||
lastError = error
|
||||
if attempt < config.retries - 1 {
|
||||
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError ?? PrivacyError.request("Request failed")
|
||||
}
|
||||
|
||||
private func checkClosed() throws {
|
||||
if isClosed {
|
||||
throw PrivacyError.closed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Privacy SDK configuration.
|
||||
public struct PrivacyConfig {
|
||||
public let apiKey: String
|
||||
public var endpoint: String
|
||||
public var timeoutMs: Int
|
||||
public var retries: Int
|
||||
|
||||
public init(
|
||||
apiKey: String,
|
||||
endpoint: String = "https://privacy.synor.io",
|
||||
timeoutMs: Int = 30000,
|
||||
retries: Int = 3
|
||||
) {
|
||||
self.apiKey = apiKey
|
||||
self.endpoint = endpoint
|
||||
self.timeoutMs = timeoutMs
|
||||
self.retries = retries
|
||||
}
|
||||
}
|
||||
|
||||
/// Privacy SDK errors.
|
||||
public enum PrivacyError: Error {
|
||||
case request(String)
|
||||
case response(String)
|
||||
case api(message: String, code: String?, statusCode: Int)
|
||||
case closed
|
||||
}
|
||||
205
sdk/swift/Sources/SynorPrivacy/PrivacyTypes.swift
Normal file
205
sdk/swift/Sources/SynorPrivacy/PrivacyTypes.swift
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import Foundation
|
||||
|
||||
// MARK: - Confidential Transactions
|
||||
|
||||
public struct ConfidentialTxInput: Codable {
|
||||
public let commitment: String
|
||||
public let blindingFactor: String
|
||||
public let value: String
|
||||
public let keyImage: String?
|
||||
|
||||
public init(commitment: String, blindingFactor: String, value: String, keyImage: String? = nil) {
|
||||
self.commitment = commitment
|
||||
self.blindingFactor = blindingFactor
|
||||
self.value = value
|
||||
self.keyImage = keyImage
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case commitment
|
||||
case blindingFactor = "blinding_factor"
|
||||
case value
|
||||
case keyImage = "key_image"
|
||||
}
|
||||
|
||||
func toDictionary() -> [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"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue