- Implement SynorStorage class for decentralized storage operations including upload, download, pinning, and CAR file management. - Create supporting types and models for storage operations such as UploadOptions, Pin, and StorageConfig. - Implement SynorWallet class for wallet operations including wallet creation, address generation, transaction signing, and balance queries. - Create supporting types and models for wallet operations such as Wallet, Address, and Transaction. - Introduce error handling for both storage and wallet operations.
244 lines
6 KiB
Ruby
244 lines
6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "faraday"
|
|
require "json"
|
|
|
|
module SynorWallet
|
|
# Synor Wallet SDK Client
|
|
#
|
|
# @example
|
|
# client = SynorWallet::Client.new(api_key: 'your-api-key')
|
|
#
|
|
# # Create wallet
|
|
# result = client.create_wallet
|
|
# puts result.mnemonic
|
|
#
|
|
# # Get balance
|
|
# balance = client.get_balance(address)
|
|
#
|
|
class Client
|
|
attr_reader :config
|
|
|
|
def initialize(api_key: nil, **options)
|
|
@config = Config.new(api_key: api_key, **options)
|
|
raise ArgumentError, "API key is required" unless @config.api_key
|
|
|
|
@conn = Faraday.new(url: @config.base_url) do |f|
|
|
f.request :json
|
|
f.response :json
|
|
f.options.timeout = @config.timeout
|
|
f.headers["Authorization"] = "Bearer #{@config.api_key}"
|
|
f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
|
|
end
|
|
@closed = false
|
|
end
|
|
|
|
# ==================== Wallet Operations ====================
|
|
|
|
# Create a new wallet
|
|
#
|
|
# @param type [String] Wallet type (standard, multisig, hardware)
|
|
# @return [CreateWalletResult]
|
|
def create_wallet(type: WalletType::STANDARD)
|
|
check_closed!
|
|
|
|
body = {
|
|
type: type,
|
|
network: @config.network
|
|
}
|
|
|
|
response = with_retry { @conn.post("/wallets", body) }
|
|
CreateWalletResult.from_hash(response.body)
|
|
end
|
|
|
|
# Import wallet from mnemonic
|
|
#
|
|
# @param mnemonic [String] 12 or 24 word mnemonic phrase
|
|
# @param passphrase [String, nil] Optional passphrase
|
|
# @return [Wallet]
|
|
def import_wallet(mnemonic, passphrase: nil)
|
|
check_closed!
|
|
|
|
body = {
|
|
mnemonic: mnemonic,
|
|
passphrase: passphrase,
|
|
network: @config.network
|
|
}
|
|
|
|
response = with_retry { @conn.post("/wallets/import", body) }
|
|
Wallet.from_hash(response.body)
|
|
end
|
|
|
|
# Get wallet by ID
|
|
#
|
|
# @param wallet_id [String]
|
|
# @return [Wallet]
|
|
def get_wallet(wallet_id)
|
|
check_closed!
|
|
|
|
response = with_retry { @conn.get("/wallets/#{wallet_id}") }
|
|
Wallet.from_hash(response.body)
|
|
end
|
|
|
|
# Generate a new address
|
|
#
|
|
# @param wallet_id [String]
|
|
# @param is_change [Boolean]
|
|
# @return [Address]
|
|
def generate_address(wallet_id, is_change: false)
|
|
check_closed!
|
|
|
|
body = { is_change: is_change }
|
|
response = with_retry { @conn.post("/wallets/#{wallet_id}/addresses", body) }
|
|
Address.from_hash(response.body)
|
|
end
|
|
|
|
# Get stealth address
|
|
#
|
|
# @param wallet_id [String]
|
|
# @return [StealthAddress]
|
|
def get_stealth_address(wallet_id)
|
|
check_closed!
|
|
|
|
response = with_retry { @conn.get("/wallets/#{wallet_id}/stealth-address") }
|
|
StealthAddress.from_hash(response.body)
|
|
end
|
|
|
|
# ==================== Signing Operations ====================
|
|
|
|
# Sign a transaction
|
|
#
|
|
# @param wallet_id [String]
|
|
# @param transaction [Transaction]
|
|
# @return [SignedTransaction]
|
|
def sign_transaction(wallet_id, transaction)
|
|
check_closed!
|
|
|
|
body = {
|
|
wallet_id: wallet_id,
|
|
transaction: transaction.to_h
|
|
}
|
|
|
|
response = with_retry { @conn.post("/transactions/sign", body) }
|
|
SignedTransaction.from_hash(response.body)
|
|
end
|
|
|
|
# Sign a message
|
|
#
|
|
# @param wallet_id [String]
|
|
# @param message [String]
|
|
# @param address_index [Integer]
|
|
# @return [Signature]
|
|
def sign_message(wallet_id, message, address_index: 0)
|
|
check_closed!
|
|
|
|
body = {
|
|
wallet_id: wallet_id,
|
|
message: message,
|
|
address_index: address_index
|
|
}
|
|
|
|
response = with_retry { @conn.post("/messages/sign", body) }
|
|
Signature.from_hash(response.body)
|
|
end
|
|
|
|
# Verify a message signature
|
|
#
|
|
# @param message [String]
|
|
# @param signature [String]
|
|
# @param address [String]
|
|
# @return [Boolean]
|
|
def verify_message(message, signature, address)
|
|
check_closed!
|
|
|
|
body = {
|
|
message: message,
|
|
signature: signature,
|
|
address: address
|
|
}
|
|
|
|
response = with_retry { @conn.post("/messages/verify", body) }
|
|
response.body["valid"]
|
|
end
|
|
|
|
# ==================== Balance & UTXOs ====================
|
|
|
|
# Get balance for an address
|
|
#
|
|
# @param address [String]
|
|
# @return [Balance]
|
|
def get_balance(address)
|
|
check_closed!
|
|
|
|
response = with_retry { @conn.get("/addresses/#{address}/balance") }
|
|
Balance.from_hash(response.body)
|
|
end
|
|
|
|
# Get UTXOs for an address
|
|
#
|
|
# @param address [String]
|
|
# @param min_confirmations [Integer]
|
|
# @return [Array<UTXO>]
|
|
def get_utxos(address, min_confirmations: 1)
|
|
check_closed!
|
|
|
|
response = with_retry do
|
|
@conn.get("/addresses/#{address}/utxos", min_confirmations: min_confirmations)
|
|
end
|
|
response.body.map { |u| UTXO.from_hash(u) }
|
|
end
|
|
|
|
# ==================== Fee Estimation ====================
|
|
|
|
# Estimate fee
|
|
#
|
|
# @param priority [String]
|
|
# @return [Integer] Fee per byte in satoshis
|
|
def estimate_fee(priority: Priority::MEDIUM)
|
|
check_closed!
|
|
|
|
response = with_retry { @conn.get("/fees/estimate", priority: priority) }
|
|
response.body["fee_per_byte"]
|
|
end
|
|
|
|
# ==================== Lifecycle ====================
|
|
|
|
def close
|
|
@closed = true
|
|
@conn.close if @conn.respond_to?(:close)
|
|
end
|
|
|
|
def closed?
|
|
@closed
|
|
end
|
|
|
|
private
|
|
|
|
def check_closed!
|
|
raise ClientClosedError, "Client has been closed" if @closed
|
|
end
|
|
|
|
def with_retry
|
|
attempts = 0
|
|
begin
|
|
attempts += 1
|
|
response = yield
|
|
handle_response(response)
|
|
response
|
|
rescue Faraday::Error, ApiError => e
|
|
if attempts < @config.retries
|
|
sleep(attempts)
|
|
retry
|
|
end
|
|
raise
|
|
end
|
|
end
|
|
|
|
def handle_response(response)
|
|
return if response.success?
|
|
|
|
error_message = response.body["error"] || response.body["message"] || "Unknown error"
|
|
raise ApiError.new(error_message, status_code: response.status)
|
|
end
|
|
end
|
|
end
|