coinbase 0.0.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of coinbase might be problematic. Click here for more details.

@@ -1,127 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'balance_map'
4
- require_relative 'constants'
5
- require 'bigdecimal'
6
- require 'eth'
7
- require 'jimson'
8
-
9
- module Coinbase
10
- # A representation of a blockchain Address, which is a user-controlled account on a Network. Addresses are used to
11
- # send and receive Assets, and should be created using {link:Wallet#create_address}. Addresses require a
12
- # {link:Eth::Key} to sign transaction data.
13
- class Address
14
- attr_reader :network_id, :address_id, :wallet_id
15
-
16
- # Returns a new Address object.
17
- # @param network_id [Symbol] The ID of the Network on which the Address exists
18
- # @param address_id [String] The ID of the Address. On EVM Networks, for example, this is a hash of the public key.
19
- # @param wallet_id [String] The ID of the Wallet to which the Address belongs
20
- # @param key [Eth::Key] The key backing the Address
21
- # @param client [Jimson::Client] (Optional) The JSON RPC client to use for interacting with the Network
22
- def initialize(network_id, address_id, wallet_id, key,
23
- client: Jimson::Client.new(ENV.fetch('BASE_SEPOLIA_RPC_URL', nil)))
24
- # TODO: Don't require key.
25
- @network_id = network_id
26
- @address_id = address_id
27
- @wallet_id = wallet_id
28
- @key = key
29
- @client = client
30
- end
31
-
32
- # Returns the balances of the Address. Currently only ETH balances are supported.
33
- # @return [BalanceMap] The balances of the Address, keyed by asset ID. Ether balances are denominated
34
- # in ETH.
35
- def list_balances
36
- # TODO: Handle multiple currencies.
37
- eth_balance_in_wei = BigDecimal(@client.eth_getBalance(@address_id, 'latest').to_i(16).to_s)
38
- eth_balance = BigDecimal(eth_balance_in_wei / BigDecimal(Coinbase::WEI_PER_ETHER.to_s))
39
-
40
- BalanceMap.new({ eth: eth_balance })
41
- end
42
-
43
- # Returns the balance of the provided Asset. Currently only ETH is supported.
44
- # @param asset_id [Symbol] The Asset to retrieve the balance for
45
- # @return [BigDecimal] The balance of the Asset
46
- def get_balance(asset_id)
47
- normalized_asset_id = if %i[wei gwei].include?(asset_id)
48
- :eth
49
- else
50
- asset_id
51
- end
52
-
53
- eth_balance = list_balances[normalized_asset_id] || BigDecimal(0)
54
-
55
- case asset_id
56
- when :eth
57
- eth_balance
58
- when :gwei
59
- eth_balance * Coinbase::GWEI_PER_ETHER
60
- when :wei
61
- eth_balance * Coinbase::WEI_PER_ETHER
62
- else
63
- BigDecimal(0)
64
- end
65
- end
66
-
67
- # Transfers the given amount of the given Asset to the given address. Only same-Network Transfers are supported.
68
- # @param amount [Integer, Float, BigDecimal] The amount of the Asset to send.
69
- # @param asset_id [Symbol] The ID of the Asset to send. For Ether, :eth, :gwei, and :wei are supported.
70
- # @param destination [Wallet | Address | String] The destination of the transfer. If a Wallet, sends to the Wallet's
71
- # default address. If a String, interprets it as the address ID.
72
- # @return [String] The hash of the Transfer transaction.
73
- def transfer(amount, asset_id, destination)
74
- # TODO: Handle multiple currencies.
75
- raise ArgumentError, "Unsupported asset: #{asset_id}" unless Coinbase::SUPPORTED_ASSET_IDS[asset_id]
76
-
77
- if destination.is_a?(Wallet)
78
- raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != @network_id
79
-
80
- destination = destination.default_address.address_id
81
- elsif destination.is_a?(Address)
82
- raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != @network_id
83
-
84
- destination = destination.address_id
85
- end
86
-
87
- current_balance = get_balance(asset_id)
88
- if current_balance < amount
89
- raise ArgumentError, "Insufficient funds: #{amount} requested, but only #{current_balance} available"
90
- end
91
-
92
- transfer = Coinbase::Transfer.new(@network_id, @wallet_id, @address_id, amount, asset_id, destination,
93
- client: @client)
94
-
95
- transaction = transfer.transaction
96
- transaction.sign(@key)
97
- @client.eth_sendRawTransaction("0x#{transaction.hex}")
98
-
99
- transfer
100
- end
101
-
102
- # Returns the address as a string.
103
- # @return [String] The address
104
- def to_s
105
- @address_id
106
- end
107
-
108
- private
109
-
110
- # Normalizes the amount of ETH to send based on the asset ID.
111
- # @param amount [Integer, Float, BigDecimal] The amount to normalize
112
- # @param asset_id [Symbol] The ID of the Asset being transferred
113
- # @return [BigDecimal] The normalized amount in units of ETH
114
- def normalize_eth_amount(amount, asset_id)
115
- case asset_id
116
- when :eth
117
- amount.is_a?(BigDecimal) ? amount : BigDecimal(amount.to_s)
118
- when :gwei
119
- BigDecimal(amount / Coinbase::GWEI_PER_ETHER)
120
- when :wei
121
- BigDecimal(amount / Coinbase::WEI_PER_ETHER)
122
- else
123
- raise ArgumentError, "Unsupported asset: #{asset_id}"
124
- end
125
- end
126
- end
127
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Coinbase
4
- # A representation of an Asset.
5
- class Asset
6
- attr_reader :network_id, :asset_id, :display_name, :address_id
7
-
8
- # Returns a new Asset object.
9
- # @param network_id [Symbol] The ID of the Network to which the Asset belongs
10
- # @param asset_id [Symbol] The Asset ID
11
- # @param display_name [String] The Asset's display name
12
- # @param address_id [String] (Optional) The Asset's address ID, if one exists
13
- def initialize(network_id:, asset_id:, display_name:, address_id: nil)
14
- @network_id = network_id
15
- @asset_id = asset_id
16
- @display_name = display_name
17
- @address_id = address_id
18
- end
19
- end
20
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bigdecimal'
4
-
5
- module Coinbase
6
- # A convenience class for printing out crypto asset balances in a human-readable format.
7
- class BalanceMap < Hash
8
- # Returns a new BalanceMap object.
9
- # @param hash [Map<Symbol, BigDecimal>] The hash to initialize with
10
- def initialize(hash = {})
11
- super()
12
- hash.each do |key, value|
13
- self[key] = value
14
- end
15
- end
16
-
17
- # Returns a string representation of the balance map.
18
- # @return [String] The string representation of the balance
19
- def to_s
20
- to_string
21
- end
22
-
23
- # Returns a string representation of the balance map.
24
- # @return [String] The string representation of the balance
25
- def inspect
26
- to_string
27
- end
28
-
29
- private
30
-
31
- # Returns a string representation of the balance.
32
- # @return [String] The string representation of the balance
33
- def to_string
34
- result = {}
35
-
36
- each do |asset_id, balance|
37
- # Convert to floating-point number (not scientific notation)
38
- str = balance.to_s('F')
39
-
40
- str = balance.to_i.to_s if balance.frac.zero?
41
-
42
- result[asset_id] = str
43
- end
44
-
45
- result
46
- end
47
- end
48
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'asset'
4
- require_relative 'network'
5
-
6
- module Coinbase
7
- # The Assets supported on Base Sepolia by the Coinbase SDK.
8
- ETH = Asset.new(network_id: :base_sepolia, asset_id: :eth, display_name: 'Ether')
9
- USDC = Asset.new(network_id: :base_sepolia, asset_id: :usdc, display_name: 'USD Coin',
10
- address_id: '0x036CbD53842c5426634e7929541eC2318f3dCF7e')
11
-
12
- # The Base Sepolia Network.
13
- BASE_SEPOLIA = Network.new(
14
- network_id: :base_sepolia,
15
- display_name: 'Base Sepolia',
16
- protocol_family: :evm,
17
- is_testnet: true,
18
- assets: [ETH, USDC],
19
- native_asset_id: :eth,
20
- chain_id: 84_532
21
- )
22
-
23
- # The amount of Wei per Ether.
24
- WEI_PER_ETHER = 1_000_000_000_000_000_000
25
-
26
- # The amount of Wei per Gwei.
27
- WEI_PER_GWEI = 1_000_000_000
28
-
29
- # The amount of Gwei per Ether.
30
- GWEI_PER_ETHER = 1_000_000_000
31
-
32
- # A map of supported Asset IDs.
33
- SUPPORTED_ASSET_IDS = {
34
- eth: true, # Ether, the native asset of most EVM networks.
35
- gwei: true, # A medium denomination of Ether, typically used in gas prices.
36
- wei: true # The smallest denomination of Ether.
37
- }.freeze
38
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Coinbase
4
- # A blockchain network.
5
- class Network
6
- attr_reader :chain_id
7
-
8
- # Returns a new Network object.
9
- #
10
- # @param network_id [Symbol] The Network ID
11
- # @param display_name [String] The Network's display name
12
- # @param protocol_family [String] The protocol family to which the Network belongs
13
- # (e.g., "evm")
14
- # @param is_testnet [Boolean] Whether the Network is a testnet
15
- # @param assets [Array<Asset>] The Assets supported by the Network
16
- # @param native_asset_id [String] The ID of the Network's native Asset
17
- # @param chain_id [Integer] The Chain ID of the Network
18
- def initialize(network_id:, display_name:, protocol_family:, is_testnet:, assets:, native_asset_id:, chain_id:)
19
- @network_id = network_id
20
- @display_name = display_name
21
- @protocol_family = protocol_family
22
- @is_testnet = is_testnet
23
- @chain_id = chain_id
24
-
25
- @asset_map = {}
26
- assets.each do |asset|
27
- @asset_map[asset.asset_id] = asset
28
- end
29
-
30
- raise ArgumentError, 'Native Asset not found' unless @asset_map.key?(native_asset_id)
31
-
32
- @native_asset = @asset_map[native_asset_id]
33
- end
34
-
35
- # Lists the Assets supported by the Network.
36
- #
37
- # @return [Array<Asset>] The Assets supported by the Network
38
- def list_assets
39
- @asset_map.values
40
- end
41
-
42
- # Gets the Asset with the given ID.
43
- #
44
- # @param asset_id [Symbol] The ID of the Asset
45
- # @return [Asset] The Asset with the given ID
46
- def get_asset(asset_id)
47
- @asset_map[asset_id]
48
- end
49
-
50
- # Gets the native Asset of the Network.
51
- #
52
- # @return [Asset] The native Asset of the Network
53
- attr_reader :native_asset
54
- end
55
- end
@@ -1,153 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'constants'
4
- require 'bigdecimal'
5
- require 'eth'
6
-
7
- module Coinbase
8
- # A representation of a Transfer, which moves an amount of an Asset from
9
- # a user-controlled Wallet to another address. The fee is assumed to be paid
10
- # in the native Asset of the Network. Currently only ETH transfers are supported. Transfers
11
- # should be created through {link:Wallet#transfer} or {link:Address#transfer}.
12
- class Transfer
13
- attr_reader :network_id, :wallet_id, :from_address_id, :amount, :asset_id, :to_address_id
14
-
15
- # A representation of a Transfer status.
16
- module Status
17
- # The Transfer is awaiting being broadcast to the Network. At this point, transaction
18
- # hashes may not yet be assigned.
19
- PENDING = :pending
20
-
21
- # The Transfer has been broadcast to the Network. At this point, at least the transaction hash
22
- # should be assigned.
23
- BROADCAST = :broadcast
24
-
25
- # The Transfer is complete, and has confirmed on the Network.
26
- COMPLETE = :complete
27
-
28
- # The Transfer has failed for some reason.
29
- FAILED = :failed
30
- end
31
-
32
- # Returns a new Transfer object.
33
- # @param network_id [Symbol] The ID of the Network on which the Transfer originated
34
- # @param wallet_id [String] The ID of the Wallet from which the Transfer originated
35
- # @param from_address_id [String] The ID of the address from which the Transfer originated
36
- # @param amount [Integer, Float, BigDecimal] The amount of the Asset to send. Integers are interpreted as
37
- # the smallest denomination of the Asset (e.g. Wei for Ether). Floats and BigDecimals are interpreted as the Asset
38
- # itself (e.g. Ether).
39
- # @param asset_id [Symbol] The ID of the Asset being transferred. Currently only ETH is supported.
40
- # @param to_address_id [String] The address to which the Transfer is being sent
41
- # @param client [Jimson::Client] (Optional) The JSON RPC client to use for interacting with the Network
42
- def initialize(network_id, wallet_id, from_address_id, amount, asset_id, to_address_id,
43
- client: Jimson::Client.new(ENV.fetch('BASE_SEPOLIA_RPC_URL', nil)))
44
-
45
- raise ArgumentError, "Unsupported asset: #{asset_id}" if asset_id != :eth
46
-
47
- @network_id = network_id
48
- @wallet_id = wallet_id
49
- @from_address_id = from_address_id
50
- @amount = normalize_eth_amount(amount)
51
- @asset_id = asset_id
52
- @to_address_id = to_address_id
53
- @client = client
54
- end
55
-
56
- # Returns the underlying Transfer transaction, creating it if it has not been yet.
57
- # @return [Eth::Tx::Eip1559] The Transfer transaction
58
- def transaction
59
- return @transaction unless @transaction.nil?
60
-
61
- nonce = @client.eth_getTransactionCount(@from_address_id.to_s, 'latest').to_i(16)
62
- gas_price = @client.eth_gasPrice.to_i(16)
63
-
64
- params = {
65
- chain_id: BASE_SEPOLIA.chain_id, # TODO: Don't hardcode Base Sepolia.
66
- nonce: nonce,
67
- priority_fee: gas_price, # TODO: Optimize this.
68
- max_gas_fee: gas_price,
69
- gas_limit: 21_000, # TODO: Handle multiple currencies.
70
- from: Eth::Address.new(@from_address_id),
71
- to: Eth::Address.new(@to_address_id),
72
- value: (@amount * Coinbase::WEI_PER_ETHER).to_i
73
- }
74
-
75
- @transaction = Eth::Tx::Eip1559.new(Eth::Tx.validate_eip1559_params(params))
76
- @transaction
77
- end
78
-
79
- # Returns the status of the Transfer.
80
- # @return [Symbol] The status
81
- def status
82
- begin
83
- # Create the transaction, and attempt to get the hash to see if it has been signed.
84
- transaction.hash
85
- rescue Eth::Signature::SignatureError
86
- # If the transaction has not been signed, it is still pending.
87
- return Status::PENDING
88
- end
89
-
90
- onchain_transaction = @client.eth_getTransactionByHash(transaction_hash)
91
-
92
- if onchain_transaction.nil?
93
- # If the transaction has not been broadcast, it is still pending.
94
- Status::PENDING
95
- elsif onchain_transaction['blockHash'].nil?
96
- # If the transaction has been broadcast but hasn't been included in a block, it is
97
- # broadcast.
98
- Status::BROADCAST
99
- else
100
- transaction_receipt = @client.eth_getTransactionReceipt(transaction_hash)
101
-
102
- if transaction_receipt['status'].to_i(16) == 1
103
- Status::COMPLETE
104
- else
105
- Status::FAILED
106
- end
107
- end
108
- end
109
-
110
- # Waits until the Transfer is completed or failed by polling the Network at the given interval. Raises a
111
- # Timeout::Error if the Transfer takes longer than the given timeout.
112
- # @param interval_seconds [Integer] The interval at which to poll the Network, in seconds
113
- # @param timeout_seconds [Integer] The maximum amount of time to wait for the Transfer to complete, in seconds
114
- # @return [Transfer] The completed Transfer object
115
- def wait!(interval_seconds = 0.2, timeout_seconds = 10)
116
- start_time = Time.now
117
-
118
- loop do
119
- return self if status == Status::COMPLETE || status == Status::FAILED
120
-
121
- raise Timeout::Error, 'Transfer timed out' if Time.now - start_time > timeout_seconds
122
-
123
- self.sleep interval_seconds
124
- end
125
-
126
- self
127
- end
128
-
129
- # Returns the transaction hash of the Transfer, or nil if not yet available.
130
- # @return [String] The transaction hash
131
- def transaction_hash
132
- "0x#{transaction.hash}"
133
- rescue Eth::Signature::SignatureError
134
- nil
135
- end
136
-
137
- private
138
-
139
- # Normalizes the given Ether amount into a BigDecimal.
140
- # @param amount [Integer, Float, BigDecimal] The amount to normalize
141
- # @return [BigDecimal] The normalized amount
142
- def normalize_eth_amount(amount)
143
- case amount
144
- when BigDecimal
145
- amount
146
- when Integer, Float
147
- BigDecimal(amount.to_s)
148
- else
149
- raise ArgumentError, "Invalid amount: #{amount}"
150
- end
151
- end
152
- end
153
- end
@@ -1,160 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'jimson'
4
- require 'money-tree'
5
- require 'securerandom'
6
-
7
- module Coinbase
8
- # A representation of a Wallet. Wallets come with a single default Address, but can expand to have a set of Addresses,
9
- # each of which can hold a balance of one or more Assets. Wallets can create new Addresses, list their addresses,
10
- # list their balances, and transfer Assets to other Addresses.
11
- class Wallet
12
- attr_reader :wallet_id, :network_id
13
-
14
- # Returns a new Wallet object.
15
- # @param seed [Integer] (Optional) The seed to use for the Wallet. Expects a 32-byte hexadecimal. If not provided,
16
- # a new seed will be generated.
17
- # @param address_count [Integer] (Optional) The number of addresses to generate for the Wallet. If not provided,
18
- # a single address will be generated.
19
- # @param client [Jimson::Client] (Optional) The JSON RPC client to use for interacting with the Network
20
- def initialize(seed: nil, address_count: 1, client: Jimson::Client.new(ENV.fetch('BASE_SEPOLIA_RPC_URL', nil)))
21
- raise ArgumentError, 'Seed must be 32 bytes' if !seed.nil? && seed.length != 64
22
- raise ArgumentError, 'Address count must be positive' if address_count < 1
23
-
24
- @master = seed.nil? ? MoneyTree::Master.new : MoneyTree::Master.new(seed_hex: seed)
25
-
26
- @wallet_id = SecureRandom.uuid
27
- # TODO: Make Network an argument to the constructor.
28
- @network_id = :base_sepolia
29
- @addresses = []
30
-
31
- # TODO: Adjust derivation path prefix based on network protocol.
32
- @address_path_prefix = "m/44'/60'/0'/0"
33
- @address_index = 0
34
-
35
- @client = client
36
-
37
- address_count.times { create_address }
38
- end
39
-
40
- # Creates a new Address in the Wallet.
41
- # @return [Address] The new Address
42
- def create_address
43
- # TODO: Register with server.
44
- path = "#{@address_path_prefix}/#{@address_index}"
45
- private_key = @master.node_for_path(path).private_key.to_hex
46
- key = Eth::Key.new(priv: private_key)
47
- address = Address.new(@network_id, key.address.address, @wallet_id, key, client: @client)
48
- @addresses << address
49
- @address_index += 1
50
- address
51
- end
52
-
53
- # Returns the default address of the Wallet.
54
- # @return [Address] The default address
55
- def default_address
56
- @addresses.first
57
- end
58
-
59
- # Returns the Address with the given ID.
60
- # @param address_id [String] The ID of the Address to retrieve
61
- # @return [Address] The Address
62
- def get_address(address_id)
63
- @addresses.find { |address| address.address_id == address_id }
64
- end
65
-
66
- # Returns the list of Addresses in the Wallet.
67
- # @return [Array<Address>] The list of Addresses
68
- def list_addresses
69
- # TODO: Register with server.
70
- @addresses
71
- end
72
-
73
- # Returns the list of balances of this Wallet. Balances are aggregated across all Addresses in the Wallet.
74
- # @return [BalanceMap] The list of balances. The key is the Asset ID, and the value is the balance.
75
- def list_balances
76
- balance_map = BalanceMap.new
77
-
78
- @addresses.each do |address|
79
- address.list_balances.each do |asset_id, balance|
80
- balance_map[asset_id] ||= BigDecimal(0)
81
- current_balance = balance_map[asset_id]
82
- new_balance = balance + current_balance
83
- balance_map[asset_id] = new_balance
84
- end
85
- end
86
-
87
- balance_map
88
- end
89
-
90
- # Returns the balance of the provided Asset. Balances are aggregated across all Addresses in the Wallet.
91
- # @param asset_id [Symbol] The ID of the Asset to retrieve the balance for
92
- # @return [BigDecimal] The balance of the Asset
93
- def get_balance(asset_id)
94
- normalized_asset_id = if %i[wei gwei].include?(asset_id)
95
- :eth
96
- else
97
- asset_id
98
- end
99
-
100
- eth_balance = list_balances[normalized_asset_id] || BigDecimal(0)
101
-
102
- case asset_id
103
- when :eth
104
- eth_balance
105
- when :gwei
106
- eth_balance * Coinbase::GWEI_PER_ETHER
107
- when :wei
108
- eth_balance * Coinbase::WEI_PER_ETHER
109
- else
110
- BigDecimal(0)
111
- end
112
- end
113
-
114
- # Transfers the given amount of the given Asset to the given address. Only same-Network Transfers are supported.
115
- # Currently only the default_address is used to source the Transfer.
116
- # @param amount [Integer, Float, BigDecimal] The amount of the Asset to send
117
- # @param asset_id [Symbol] The ID of the Asset to send
118
- # @param destination [Wallet | Address | String] The destination of the transfer. If a Wallet, sends to the Wallet's
119
- # default address. If a String, interprets it as the address ID.
120
- # @return [Transfer] The hash of the Transfer transaction.
121
- def transfer(amount, asset_id, destination)
122
- if destination.is_a?(Wallet)
123
- raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != @network_id
124
-
125
- destination = destination.default_address.address_id
126
- elsif destination.is_a?(Address)
127
- raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != @network_id
128
-
129
- destination = destination.address_id
130
- end
131
-
132
- default_address.transfer(amount, asset_id, destination)
133
- end
134
-
135
- # Exports the Wallet's data to a WalletData object.
136
- # @return [WalletData] The Wallet data
137
- def export
138
- WalletData.new(@master.seed_hex, @addresses.length)
139
- end
140
-
141
- # The data required to recreate a Wallet.
142
- class WalletData
143
- attr_reader :seed, :address_count
144
-
145
- # Returns a new WalletData object.
146
- # @param seed [String] The seed of the Wallet
147
- # @param address_count [Integer] The number of addresses in the Wallet
148
- def initialize(seed, address_count)
149
- @seed = seed
150
- @address_count = address_count
151
- end
152
- end
153
-
154
- # Returns the data required to recreate the Wallet.
155
- # @return [WalletData] The Wallet data
156
- def to_data
157
- WalletData.new(@master.seed_hex, @addresses.length)
158
- end
159
- end
160
- end