Skip to content

Commit

Permalink
feat: Add address reputation (#221)
Browse files Browse the repository at this point in the history
### What changed? Why?
This adds support for fetching an address' reputation lazily.

Example:
```ruby
risky_address = Coinbase::ExternalAddress.new(:ethereum_mainnet, '0x12846c6Fd6baBFE4bC6F761eB871eFfFDEb26913')

# Returns the reputation of the address
risky_address.reputation
=> Coinbase::AddressReputation{score: '-90', total_transactions: '0', unique_days_active: '0', longest_active_streak: '0', current_active_streak: '0', activity_period_days: '0', token_swaps_performed: '0', bridge_transactions_performed: '0', lend_borrow_stake_transactions: '0', ens_contract_interactions: '0', smart_contract_deployments: '0'}

# Returns whether the address itself is deemed "risky"
risky_address.risky?
=> true
```

#### Qualified Impact
<!-- Please evaluate what components could be affected and what the
impact would be if there was an
error. How would this error be resolved, e.g. rollback a deploy, push a
new fix, disable a feature
flag, etc... -->

---------

Co-authored-by: arpit-srivastava <[email protected]>
  • Loading branch information
alex-stone and arpitsrivastava-cb authored Dec 17, 2024
1 parent 6b92ca0 commit 1ee0882
Show file tree
Hide file tree
Showing 24 changed files with 1,090 additions and 154 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
* Add support for fetching address reputation.

## [0.12.0] - Skipped

## [0.11.0] - 2024-11-27

### Added
Expand Down
3 changes: 2 additions & 1 deletion lib/coinbase.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# frozen_string_literal: true

require_relative 'coinbase/client'
require_relative 'coinbase/address'
require_relative 'coinbase/address/wallet_address'
require_relative 'coinbase/address/external_address'
require_relative 'coinbase/address_reputation'
require_relative 'coinbase/asset'
require_relative 'coinbase/authenticator'
require_relative 'coinbase/correlation'
require_relative 'coinbase/balance'
require_relative 'coinbase/balance_map'
require_relative 'coinbase/historical_balance'
require_relative 'coinbase/client'
require_relative 'coinbase/constants'
require_relative 'coinbase/contract_event'
require_relative 'coinbase/contract_invocation'
Expand Down
25 changes: 22 additions & 3 deletions lib/coinbase/address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ def initialize(network, id)
# Returns a String representation of the Address.
# @return [String] a String representation of the Address
def to_s
Coinbase.pretty_print_object(self.class, id: id, network_id: network.id)
Coinbase.pretty_print_object(
self.class,
**{
id: id,
network_id: network.id,
reputation_score: @reputation.nil? ? nil : reputation.score
}.compact
)
end

# Same as to_s.
Expand All @@ -32,6 +39,18 @@ def can_sign?
false
end

# Returns the reputation of the Address.
# @return [Coinbase::AddressReputation] The reputation of the Address
def reputation
@reputation ||= Coinbase::AddressReputation.fetch(network: network, address_id: id)
end

# Returns wheth the Address's reputation is risky.
# @return [Boolean] true if the Address's reputation is risky
def risky?
reputation.risky?
end

# Returns the balances of the Address.
# @return [BalanceMap] The balances of the Address, keyed by asset ID. Ether balances are denominated
# in ETH.
Expand Down Expand Up @@ -66,7 +85,7 @@ def balance(asset_id)
# @return [Enumerable<Coinbase::HistoricalBalance>] Enumerator that returns historical_balance
def historical_balances(asset_id)
Coinbase::Pagination.enumerate(
->(page) { list_page(asset_id, page) }
->(page) { list_historical_balance_page(asset_id, page) }
) do |historical_balance|
Coinbase::HistoricalBalance.from_model(historical_balance)
end
Expand Down Expand Up @@ -265,7 +284,7 @@ def stake_api
@stake_api ||= Coinbase::Client::StakeApi.new(Coinbase.configuration.api_client)
end

def list_page(asset_id, page)
def list_historical_balance_page(asset_id, page)
balance_history_api.list_address_historical_balance(
network.normalized_id,
id,
Expand Down
67 changes: 67 additions & 0 deletions lib/coinbase/address_reputation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

module Coinbase
# A representation of the reputation of a blockchain address.
class AddressReputation
# A metadata object associated with an address reputation.
Metadata = Struct.new(
*Client::AddressReputationMetadata.attribute_map.keys.map(&:to_sym),
keyword_init: true
) do
def to_s
Coinbase.pretty_print_object(
self.class,
**to_h
)
end
end

class << self
def fetch(address_id:, network: Coinbase.default_network)
network = Coinbase::Network.from_id(network)

model = Coinbase.call_api do
reputation_api.get_address_reputation(network.normalized_id, address_id)
end

new(model)
end

private

def reputation_api
Coinbase::Client::ReputationApi.new(Coinbase.configuration.api_client)
end
end

def initialize(model)
raise ArgumentError, 'must be an AddressReputation object' unless model.is_a?(Coinbase::Client::AddressReputation)

@model = model
end

def score
@model.score
end

def metadata
@metadata ||= Metadata.new(**@model.metadata)
end

def risky?
score.negative?
end

def to_s
Coinbase.pretty_print_object(
self.class,
score: score,
**metadata.to_h
)
end

def inspect
to_s
end
end
end
4 changes: 3 additions & 1 deletion lib/coinbase/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
Coinbase::Client.autoload :AddressList, 'coinbase/client/models/address_list'
Coinbase::Client.autoload :AddressReputation, 'coinbase/client/models/address_reputation'
Coinbase::Client.autoload :AddressReputationMetadata, 'coinbase/client/models/address_reputation_metadata'
Coinbase::Client.autoload :AddressRisk, 'coinbase/client/models/address_risk'
Coinbase::Client.autoload :AddressTransactionList, 'coinbase/client/models/address_transaction_list'
Coinbase::Client.autoload :Asset, 'coinbase/client/models/asset'
Coinbase::Client.autoload :Balance, 'coinbase/client/models/balance'
Expand Down Expand Up @@ -82,6 +81,7 @@
Coinbase::Client.autoload :PayloadSignature, 'coinbase/client/models/payload_signature'
Coinbase::Client.autoload :PayloadSignatureList, 'coinbase/client/models/payload_signature_list'
Coinbase::Client.autoload :ReadContractRequest, 'coinbase/client/models/read_contract_request'
Coinbase::Client.autoload :RegisterSmartContractRequest, 'coinbase/client/models/register_smart_contract_request'
Coinbase::Client.autoload :SeedCreationEvent, 'coinbase/client/models/seed_creation_event'
Coinbase::Client.autoload :SeedCreationEventResult, 'coinbase/client/models/seed_creation_event_result'
Coinbase::Client.autoload :ServerSigner, 'coinbase/client/models/server_signer'
Expand All @@ -93,6 +93,7 @@
Coinbase::Client.autoload :SignatureCreationEventResult, 'coinbase/client/models/signature_creation_event_result'
Coinbase::Client.autoload :SignedVoluntaryExitMessageMetadata, 'coinbase/client/models/signed_voluntary_exit_message_metadata'
Coinbase::Client.autoload :SmartContract, 'coinbase/client/models/smart_contract'
Coinbase::Client.autoload :SmartContractActivityEvent, 'coinbase/client/models/smart_contract_activity_event'
Coinbase::Client.autoload :SmartContractList, 'coinbase/client/models/smart_contract_list'
Coinbase::Client.autoload :SmartContractOptions, 'coinbase/client/models/smart_contract_options'
Coinbase::Client.autoload :SmartContractType, 'coinbase/client/models/smart_contract_type'
Expand Down Expand Up @@ -128,6 +129,7 @@
Coinbase::Client.autoload :WebhookEventType, 'coinbase/client/models/webhook_event_type'
Coinbase::Client.autoload :WebhookEventTypeFilter, 'coinbase/client/models/webhook_event_type_filter'
Coinbase::Client.autoload :WebhookList, 'coinbase/client/models/webhook_list'
Coinbase::Client.autoload :WebhookSmartContractEventFilter, 'coinbase/client/models/webhook_smart_contract_event_filter'
Coinbase::Client.autoload :WebhookWalletActivityFilter, 'coinbase/client/models/webhook_wallet_activity_filter'

# APIs
Expand Down
69 changes: 0 additions & 69 deletions lib/coinbase/client/api/reputation_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,74 +87,5 @@ def get_address_reputation_with_http_info(network_id, address_id, opts = {})
end
return data, status_code, headers
end

# Get the risk of an address
# Get the risk of an address
# @param network_id [String] The ID of the blockchain network.
# @param address_id [String] The ID of the address to fetch the risk for.
# @param [Hash] opts the optional parameters
# @return [AddressRisk]
def get_address_risk(network_id, address_id, opts = {})
data, _status_code, _headers = get_address_risk_with_http_info(network_id, address_id, opts)
data
end

# Get the risk of an address
# Get the risk of an address
# @param network_id [String] The ID of the blockchain network.
# @param address_id [String] The ID of the address to fetch the risk for.
# @param [Hash] opts the optional parameters
# @return [Array<(AddressRisk, Integer, Hash)>] AddressRisk data, response status code and response headers
def get_address_risk_with_http_info(network_id, address_id, opts = {})
if @api_client.config.debugging
@api_client.config.logger.debug 'Calling API: ReputationApi.get_address_risk ...'
end
# verify the required parameter 'network_id' is set
if @api_client.config.client_side_validation && network_id.nil?
fail ArgumentError, "Missing the required parameter 'network_id' when calling ReputationApi.get_address_risk"
end
# verify the required parameter 'address_id' is set
if @api_client.config.client_side_validation && address_id.nil?
fail ArgumentError, "Missing the required parameter 'address_id' when calling ReputationApi.get_address_risk"
end
# resource path
local_var_path = '/v1/networks/{network_id}/addresses/{address_id}/risk'.sub('{' + 'network_id' + '}', CGI.escape(network_id.to_s)).sub('{' + 'address_id' + '}', CGI.escape(address_id.to_s))

# query parameters
query_params = opts[:query_params] || {}

# header parameters
header_params = opts[:header_params] || {}
# HTTP header 'Accept' (if needed)
header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']

# form parameters
form_params = opts[:form_params] || {}

# http body (model)
post_body = opts[:debug_body]

# return_type
return_type = opts[:debug_return_type] || 'AddressRisk'

# auth_names
auth_names = opts[:debug_auth_names] || []

new_options = opts.merge(
:operation => :"ReputationApi.get_address_risk",
:header_params => header_params,
:query_params => query_params,
:form_params => form_params,
:body => post_body,
:auth_names => auth_names,
:return_type => return_type
)

data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
if @api_client.config.debugging
@api_client.config.logger.debug "API called: ReputationApi#get_address_risk\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
end
return data, status_code, headers
end
end
end
107 changes: 87 additions & 20 deletions lib/coinbase/client/api/smart_contracts_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -260,40 +260,31 @@ def get_smart_contract_with_http_info(wallet_id, address_id, smart_contract_id,
return data, status_code, headers
end

# List smart contracts deployed by address
# List all smart contracts deployed by address.
# @param wallet_id [String] The ID of the wallet the address belongs to.
# @param address_id [String] The ID of the address to fetch the smart contracts for.
# List smart contracts
# List smart contracts
# @param [Hash] opts the optional parameters
# @option opts [String] :page Pagination token for retrieving the next set of results
# @return [SmartContractList]
def list_smart_contracts(wallet_id, address_id, opts = {})
data, _status_code, _headers = list_smart_contracts_with_http_info(wallet_id, address_id, opts)
def list_smart_contracts(opts = {})
data, _status_code, _headers = list_smart_contracts_with_http_info(opts)
data
end

# List smart contracts deployed by address
# List all smart contracts deployed by address.
# @param wallet_id [String] The ID of the wallet the address belongs to.
# @param address_id [String] The ID of the address to fetch the smart contracts for.
# List smart contracts
# List smart contracts
# @param [Hash] opts the optional parameters
# @option opts [String] :page Pagination token for retrieving the next set of results
# @return [Array<(SmartContractList, Integer, Hash)>] SmartContractList data, response status code and response headers
def list_smart_contracts_with_http_info(wallet_id, address_id, opts = {})
def list_smart_contracts_with_http_info(opts = {})
if @api_client.config.debugging
@api_client.config.logger.debug 'Calling API: SmartContractsApi.list_smart_contracts ...'
end
# verify the required parameter 'wallet_id' is set
if @api_client.config.client_side_validation && wallet_id.nil?
fail ArgumentError, "Missing the required parameter 'wallet_id' when calling SmartContractsApi.list_smart_contracts"
end
# verify the required parameter 'address_id' is set
if @api_client.config.client_side_validation && address_id.nil?
fail ArgumentError, "Missing the required parameter 'address_id' when calling SmartContractsApi.list_smart_contracts"
end
# resource path
local_var_path = '/v1/wallets/{wallet_id}/addresses/{address_id}/smart_contracts'.sub('{' + 'wallet_id' + '}', CGI.escape(wallet_id.to_s)).sub('{' + 'address_id' + '}', CGI.escape(address_id.to_s))
local_var_path = '/v1/smart_contracts'

# query parameters
query_params = opts[:query_params] || {}
query_params[:'page'] = opts[:'page'] if !opts[:'page'].nil?

# header parameters
header_params = opts[:header_params] || {}
Expand Down Expand Up @@ -408,5 +399,81 @@ def read_contract_with_http_info(network_id, contract_address, read_contract_req
end
return data, status_code, headers
end

# Register a smart contract
# Register a smart contract
# @param network_id [String] The ID of the network to fetch.
# @param contract_address [String] EVM address of the smart contract (42 characters, including &#39;0x&#39;, in lowercase)
# @param [Hash] opts the optional parameters
# @option opts [RegisterSmartContractRequest] :register_smart_contract_request
# @return [SmartContract]
def register_smart_contract(network_id, contract_address, opts = {})
data, _status_code, _headers = register_smart_contract_with_http_info(network_id, contract_address, opts)
data
end

# Register a smart contract
# Register a smart contract
# @param network_id [String] The ID of the network to fetch.
# @param contract_address [String] EVM address of the smart contract (42 characters, including &#39;0x&#39;, in lowercase)
# @param [Hash] opts the optional parameters
# @option opts [RegisterSmartContractRequest] :register_smart_contract_request
# @return [Array<(SmartContract, Integer, Hash)>] SmartContract data, response status code and response headers
def register_smart_contract_with_http_info(network_id, contract_address, opts = {})
if @api_client.config.debugging
@api_client.config.logger.debug 'Calling API: SmartContractsApi.register_smart_contract ...'
end
# verify the required parameter 'network_id' is set
if @api_client.config.client_side_validation && network_id.nil?
fail ArgumentError, "Missing the required parameter 'network_id' when calling SmartContractsApi.register_smart_contract"
end
# verify the required parameter 'contract_address' is set
if @api_client.config.client_side_validation && contract_address.nil?
fail ArgumentError, "Missing the required parameter 'contract_address' when calling SmartContractsApi.register_smart_contract"
end
# resource path
local_var_path = '/v1/networks/{network_id}/smart_contracts/{contract_address}/register'.sub('{' + 'network_id' + '}', CGI.escape(network_id.to_s)).sub('{' + 'contract_address' + '}', CGI.escape(contract_address.to_s))

# query parameters
query_params = opts[:query_params] || {}

# header parameters
header_params = opts[:header_params] || {}
# HTTP header 'Accept' (if needed)
header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
# HTTP header 'Content-Type'
content_type = @api_client.select_header_content_type(['application/json'])
if !content_type.nil?
header_params['Content-Type'] = content_type
end

# form parameters
form_params = opts[:form_params] || {}

# http body (model)
post_body = opts[:debug_body] || @api_client.object_to_http_body(opts[:'register_smart_contract_request'])

# return_type
return_type = opts[:debug_return_type] || 'SmartContract'

# auth_names
auth_names = opts[:debug_auth_names] || []

new_options = opts.merge(
:operation => :"SmartContractsApi.register_smart_contract",
:header_params => header_params,
:query_params => query_params,
:form_params => form_params,
:body => post_body,
:auth_names => auth_names,
:return_type => return_type
)

data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
if @api_client.config.debugging
@api_client.config.logger.debug "API called: SmartContractsApi#register_smart_contract\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
end
return data, status_code, headers
end
end
end
Loading

0 comments on commit 1ee0882

Please sign in to comment.