coinbase-sdk 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/coinbase/address.rb +152 -51
- data/lib/coinbase/asset.rb +2 -1
- data/lib/coinbase/authenticator.rb +52 -0
- data/lib/coinbase/balance_map.rb +2 -2
- data/lib/coinbase/client/api/addresses_api.rb +454 -0
- data/lib/coinbase/client/api/transfers_api.rb +342 -0
- data/lib/coinbase/client/api/users_api.rb +79 -0
- data/lib/coinbase/client/api/wallets_api.rb +348 -0
- data/lib/coinbase/client/api_client.rb +431 -0
- data/lib/coinbase/client/api_error.rb +58 -0
- data/lib/coinbase/client/configuration.rb +375 -0
- data/lib/coinbase/client/models/address.rb +273 -0
- data/lib/coinbase/client/models/address_balance_list.rb +275 -0
- data/lib/coinbase/client/models/address_list.rb +275 -0
- data/lib/coinbase/client/models/asset.rb +260 -0
- data/lib/coinbase/client/models/balance.rb +239 -0
- data/lib/coinbase/client/models/broadcast_transfer_request.rb +222 -0
- data/lib/coinbase/client/models/create_address_request.rb +239 -0
- data/lib/coinbase/client/models/create_transfer_request.rb +273 -0
- data/lib/coinbase/client/models/create_wallet_request.rb +221 -0
- data/lib/coinbase/client/models/error.rb +278 -0
- data/lib/coinbase/client/models/faucet_transaction.rb +222 -0
- data/lib/coinbase/client/models/transfer.rb +413 -0
- data/lib/coinbase/client/models/transfer_list.rb +275 -0
- data/lib/coinbase/client/models/user.rb +231 -0
- data/lib/coinbase/client/models/wallet.rb +241 -0
- data/lib/coinbase/client/models/wallet_list.rb +275 -0
- data/lib/coinbase/client/version.rb +15 -0
- data/lib/coinbase/client.rb +59 -0
- data/lib/coinbase/constants.rb +8 -2
- data/lib/coinbase/errors.rb +120 -0
- data/lib/coinbase/faucet_transaction.rb +42 -0
- data/lib/coinbase/middleware.rb +21 -0
- data/lib/coinbase/network.rb +2 -2
- data/lib/coinbase/transfer.rb +106 -65
- data/lib/coinbase/user.rb +180 -0
- data/lib/coinbase/wallet.rb +168 -52
- data/lib/coinbase.rb +127 -9
- metadata +92 -6
data/lib/coinbase/transfer.rb
CHANGED
@@ -3,15 +3,14 @@
|
|
3
3
|
require_relative 'constants'
|
4
4
|
require 'bigdecimal'
|
5
5
|
require 'eth'
|
6
|
+
require 'json'
|
6
7
|
|
7
8
|
module Coinbase
|
8
9
|
# A representation of a Transfer, which moves an amount of an Asset from
|
9
10
|
# a user-controlled Wallet to another address. The fee is assumed to be paid
|
10
|
-
# in the native Asset of the Network.
|
11
|
-
#
|
11
|
+
# in the native Asset of the Network. Transfers should be created through Wallet#transfer or
|
12
|
+
# Address#transfer.
|
12
13
|
class Transfer
|
13
|
-
attr_reader :network_id, :wallet_id, :from_address_id, :amount, :asset_id, :to_address_id
|
14
|
-
|
15
14
|
# A representation of a Transfer status.
|
16
15
|
module Status
|
17
16
|
# The Transfer is awaiting being broadcast to the Network. At this point, transaction
|
@@ -29,28 +28,83 @@ module Coinbase
|
|
29
28
|
FAILED = :failed
|
30
29
|
end
|
31
30
|
|
32
|
-
# Returns a new Transfer object.
|
33
|
-
#
|
34
|
-
# @param
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
#
|
40
|
-
# @
|
41
|
-
|
42
|
-
|
43
|
-
|
31
|
+
# Returns a new Transfer object. Do not use this method directly. Instead, use Wallet#transfer or
|
32
|
+
# Address#transfer.
|
33
|
+
# @param model [Coinbase::Client::Transfer] The underlying Transfer object
|
34
|
+
def initialize(model)
|
35
|
+
@model = model
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the Transfer ID.
|
39
|
+
# @return [String] The Transfer ID
|
40
|
+
def transfer_id
|
41
|
+
@model.transfer_id
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the Network ID of the Transfer.
|
45
|
+
# @return [Symbol] The Network ID
|
46
|
+
def network_id
|
47
|
+
Coinbase.to_sym(@model.network_id)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the Wallet ID of the Transfer.
|
51
|
+
# @return [String] The Wallet ID
|
52
|
+
def wallet_id
|
53
|
+
@model.wallet_id
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the From Address ID of the Transfer.
|
57
|
+
# @return [String] The From Address ID
|
58
|
+
def from_address_id
|
59
|
+
@model.address_id
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the Destination Address ID of the Transfer.
|
63
|
+
# @return [String] The Destination Address ID
|
64
|
+
def destination_address_id
|
65
|
+
@model.destination
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the Asset ID of the Transfer.
|
69
|
+
# @return [Symbol] The Asset ID
|
70
|
+
def asset_id
|
71
|
+
@model.asset_id.to_sym
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the amount of the asset for the Transfer.
|
75
|
+
# @return [BigDecimal] The amount of the asset
|
76
|
+
def amount
|
77
|
+
case asset_id
|
78
|
+
when :eth
|
79
|
+
BigDecimal(@model.amount) / BigDecimal(Coinbase::WEI_PER_ETHER.to_s)
|
80
|
+
else
|
81
|
+
BigDecimal(@model.amount)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns the link to the transaction on the blockchain explorer.
|
86
|
+
# @return [String] The link to the transaction on the blockchain explorer
|
87
|
+
def transaction_link
|
88
|
+
# TODO: Parameterize this by Network.
|
89
|
+
"https://sepolia.basescan.org/tx/#{transaction_hash}"
|
90
|
+
end
|
44
91
|
|
45
|
-
|
92
|
+
# Returns the Unsigned Payload of the Transfer.
|
93
|
+
# @return [String] The Unsigned Payload
|
94
|
+
def unsigned_payload
|
95
|
+
@model.unsigned_payload
|
96
|
+
end
|
46
97
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
@
|
51
|
-
|
52
|
-
|
53
|
-
|
98
|
+
# Returns the Signed Payload of the Transfer.
|
99
|
+
# @return [String] The Signed Payload
|
100
|
+
def signed_payload
|
101
|
+
@model.signed_payload
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns the Transaction Hash of the Transfer.
|
105
|
+
# @return [String] The Transaction Hash
|
106
|
+
def transaction_hash
|
107
|
+
@model.transaction_hash
|
54
108
|
end
|
55
109
|
|
56
110
|
# Returns the underlying Transfer transaction, creating it if it has not been yet.
|
@@ -58,18 +112,19 @@ module Coinbase
|
|
58
112
|
def transaction
|
59
113
|
return @transaction unless @transaction.nil?
|
60
114
|
|
61
|
-
|
62
|
-
|
115
|
+
raw_payload = [unsigned_payload].pack('H*')
|
116
|
+
parsed_payload = JSON.parse(raw_payload)
|
63
117
|
|
64
118
|
params = {
|
65
|
-
chain_id:
|
66
|
-
nonce: nonce,
|
67
|
-
priority_fee:
|
68
|
-
max_gas_fee:
|
69
|
-
gas_limit:
|
70
|
-
from: Eth::Address.new(
|
71
|
-
to: Eth::Address.new(
|
72
|
-
value: (
|
119
|
+
chain_id: parsed_payload['chainId'].to_i(16),
|
120
|
+
nonce: parsed_payload['nonce'].to_i(16),
|
121
|
+
priority_fee: parsed_payload['maxPriorityFeePerGas'].to_i(16),
|
122
|
+
max_gas_fee: parsed_payload['maxFeePerGas'].to_i(16),
|
123
|
+
gas_limit: parsed_payload['gas'].to_i(16), # TODO: Handle multiple currencies.
|
124
|
+
from: Eth::Address.new(from_address_id),
|
125
|
+
to: Eth::Address.new(parsed_payload['to']),
|
126
|
+
value: parsed_payload['value'].to_i(16),
|
127
|
+
data: parsed_payload['input'] || ''
|
73
128
|
}
|
74
129
|
|
75
130
|
@transaction = Eth::Tx::Eip1559.new(Eth::Tx.validate_eip1559_params(params))
|
@@ -79,15 +134,10 @@ module Coinbase
|
|
79
134
|
# Returns the status of the Transfer.
|
80
135
|
# @return [Symbol] The status
|
81
136
|
def status
|
82
|
-
|
83
|
-
|
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
|
137
|
+
# Check if the transfer has been signed yet.
|
138
|
+
return Status::PENDING if transaction_hash.nil?
|
89
139
|
|
90
|
-
onchain_transaction =
|
140
|
+
onchain_transaction = Coinbase.configuration.base_sepolia_client.eth_getTransactionByHash(transaction_hash)
|
91
141
|
|
92
142
|
if onchain_transaction.nil?
|
93
143
|
# If the transaction has not been broadcast, it is still pending.
|
@@ -97,7 +147,7 @@ module Coinbase
|
|
97
147
|
# broadcast.
|
98
148
|
Status::BROADCAST
|
99
149
|
else
|
100
|
-
transaction_receipt =
|
150
|
+
transaction_receipt = Coinbase.configuration.base_sepolia_client.eth_getTransactionReceipt(transaction_hash)
|
101
151
|
|
102
152
|
if transaction_receipt['status'].to_i(16) == 1
|
103
153
|
Status::COMPLETE
|
@@ -126,28 +176,19 @@ module Coinbase
|
|
126
176
|
self
|
127
177
|
end
|
128
178
|
|
129
|
-
# Returns
|
130
|
-
# @return [String]
|
131
|
-
def
|
132
|
-
"
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
#
|
140
|
-
|
141
|
-
|
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
|
179
|
+
# Returns a String representation of the Transfer.
|
180
|
+
# @return [String] a String representation of the Transfer
|
181
|
+
def to_s
|
182
|
+
"Coinbase::Transfer{transfer_id: '#{transfer_id}', network_id: '#{network_id}', " \
|
183
|
+
"from_address_id: '#{from_address_id}', destination_address_id: '#{destination_address_id}', " \
|
184
|
+
"asset_id: '#{asset_id}', amount: '#{amount}', transaction_hash: '#{transaction_hash}', " \
|
185
|
+
"transaction_link: '#{transaction_link}', status: '#{status}'}"
|
186
|
+
end
|
187
|
+
|
188
|
+
# Same as to_s.
|
189
|
+
# @return [String] a String representation of the Transfer
|
190
|
+
def inspect
|
191
|
+
to_s
|
151
192
|
end
|
152
193
|
end
|
153
194
|
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'client'
|
4
|
+
require_relative 'wallet'
|
5
|
+
|
6
|
+
module Coinbase
|
7
|
+
# A representation of a User. Users have Wallets, which can hold balances of Assets. Access the default User through
|
8
|
+
# Coinbase#default_user.
|
9
|
+
class User
|
10
|
+
# Returns a new User object. Do not use this method directly. Instead, use Coinbase#default_user.
|
11
|
+
# @param model [Coinbase::Client::User] the underlying User object
|
12
|
+
def initialize(model)
|
13
|
+
@model = model
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the User ID.
|
17
|
+
# @return [String] the User ID
|
18
|
+
def user_id
|
19
|
+
@model.id
|
20
|
+
end
|
21
|
+
|
22
|
+
# Creates a new Wallet belonging to the User.
|
23
|
+
# @return [Coinbase::Wallet] the new Wallet
|
24
|
+
def create_wallet
|
25
|
+
create_wallet_request = {
|
26
|
+
wallet: {
|
27
|
+
# TODO: Don't hardcode this.
|
28
|
+
network_id: 'base-sepolia'
|
29
|
+
}
|
30
|
+
}
|
31
|
+
opts = { create_wallet_request: create_wallet_request }
|
32
|
+
|
33
|
+
model = Coinbase.call_api do
|
34
|
+
wallets_api.create_wallet(opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
Wallet.new(model)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Imports a Wallet belonging to the User.
|
41
|
+
# @param data [Coinbase::Wallet::Data] the Wallet data to import
|
42
|
+
# @return [Coinbase::Wallet] the imported Wallet
|
43
|
+
def import_wallet(data)
|
44
|
+
model = Coinbase.call_api do
|
45
|
+
wallets_api.get_wallet(data.wallet_id)
|
46
|
+
end
|
47
|
+
|
48
|
+
address_count = Coinbase.call_api do
|
49
|
+
addresses_api.list_addresses(model.id).total_count
|
50
|
+
end
|
51
|
+
|
52
|
+
Wallet.new(model, seed: data.seed, address_count: address_count)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Lists the IDs of the Wallets belonging to the User.
|
56
|
+
# @return [Array<String>] the IDs of the Wallets belonging to the User
|
57
|
+
def list_wallet_ids
|
58
|
+
wallets = Coinbase.call_api do
|
59
|
+
wallets_api.list_wallets
|
60
|
+
end
|
61
|
+
|
62
|
+
wallets.data.map(&:id)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Saves a wallet to local file system. Wallet saved this way can be re-instantiated with `load_wallets` function,
|
66
|
+
# provided the backup_file is available. This is an insecure method of storing wallet seeds and should only be used
|
67
|
+
# for development purposes. If you call save_wallet twice with wallets containing the same wallet_id, the backup
|
68
|
+
# will be overwritten during the second attempt.
|
69
|
+
# The default backup_file is `seeds.json` in the root folder. It can be configured by changing
|
70
|
+
# Coinbase.configuration.backup_file_path.
|
71
|
+
#
|
72
|
+
# @param wallet [Coinbase::Wallet] The wallet model to save.
|
73
|
+
# @param encrypt [bool] (Optional) Boolean representing whether the backup persisted to local file system should be
|
74
|
+
# encrypted or not. Data is unencrypted by default.
|
75
|
+
# @return [Coinbase::Wallet] the saved wallet.
|
76
|
+
def save_wallet(wallet, encrypt: false)
|
77
|
+
existing_seeds_in_store = existing_seeds
|
78
|
+
data = wallet.export
|
79
|
+
seed_to_store = data.seed
|
80
|
+
auth_tag = ''
|
81
|
+
iv = ''
|
82
|
+
if encrypt
|
83
|
+
shared_secret = store_encryption_key
|
84
|
+
cipher = OpenSSL::Cipher.new('aes-256-gcm').encrypt
|
85
|
+
cipher.key = OpenSSL::Digest.digest('SHA256', shared_secret)
|
86
|
+
iv = cipher.random_iv
|
87
|
+
cipher.iv = iv
|
88
|
+
cipher.auth_data = ''
|
89
|
+
encrypted_data = cipher.update(data.seed) + cipher.final
|
90
|
+
auth_tag = cipher.auth_tag.unpack1('H*')
|
91
|
+
iv = iv.unpack1('H*')
|
92
|
+
seed_to_store = encrypted_data.unpack1('H*')
|
93
|
+
end
|
94
|
+
|
95
|
+
existing_seeds_in_store[data.wallet_id] = {
|
96
|
+
seed: seed_to_store,
|
97
|
+
encrypted: encrypt,
|
98
|
+
auth_tag: auth_tag,
|
99
|
+
iv: iv
|
100
|
+
}
|
101
|
+
|
102
|
+
File.open(Coinbase.configuration.backup_file_path, 'w') do |file|
|
103
|
+
file.write(JSON.pretty_generate(existing_seeds_in_store))
|
104
|
+
end
|
105
|
+
wallet
|
106
|
+
end
|
107
|
+
|
108
|
+
# Loads all wallets belonging to the User with backup persisted to the local file system.
|
109
|
+
# @return [Map<String>Coinbase::Wallet] the map of wallet_ids to the wallets.
|
110
|
+
def load_wallets
|
111
|
+
existing_seeds_in_store = existing_seeds
|
112
|
+
raise ArgumentError, 'Backup file not found' if existing_seeds_in_store == {}
|
113
|
+
|
114
|
+
wallets = {}
|
115
|
+
existing_seeds_in_store.each do |wallet_id, seed_data|
|
116
|
+
seed = seed_data['seed']
|
117
|
+
raise ArgumentError, 'Malformed backup data' if seed.nil? || seed == ''
|
118
|
+
|
119
|
+
if seed_data['encrypted']
|
120
|
+
shared_secret = store_encryption_key
|
121
|
+
raise ArgumentError, 'Malformed encrypted seed data' if seed_data['iv'] == '' ||
|
122
|
+
seed_data['auth_tag'] == ''
|
123
|
+
|
124
|
+
cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
|
125
|
+
cipher.key = OpenSSL::Digest.digest('SHA256', shared_secret)
|
126
|
+
iv = [seed_data['iv']].pack('H*')
|
127
|
+
cipher.iv = iv
|
128
|
+
auth_tag = [seed_data['auth_tag']].pack('H*')
|
129
|
+
cipher.auth_tag = auth_tag
|
130
|
+
cipher.auth_data = ''
|
131
|
+
hex_decoded_data = [seed_data['seed']].pack('H*')
|
132
|
+
seed = cipher.update(hex_decoded_data) + cipher.final
|
133
|
+
end
|
134
|
+
|
135
|
+
data = Coinbase::Wallet::Data.new(wallet_id: wallet_id, seed: seed)
|
136
|
+
wallets[wallet_id] = import_wallet(data)
|
137
|
+
end
|
138
|
+
wallets
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns a string representation of the User.
|
142
|
+
# @return [String] a string representation of the User
|
143
|
+
def to_s
|
144
|
+
"Coinbase::User{user_id: '#{user_id}'}"
|
145
|
+
end
|
146
|
+
|
147
|
+
# Same as to_s.
|
148
|
+
# @return [String] a string representation of the User
|
149
|
+
def inspect
|
150
|
+
to_s
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def addresses_api
|
156
|
+
@addresses_api ||= Coinbase::Client::AddressesApi.new(Coinbase.configuration.api_client)
|
157
|
+
end
|
158
|
+
|
159
|
+
def wallets_api
|
160
|
+
@wallets_api ||= Coinbase::Client::WalletsApi.new(Coinbase.configuration.api_client)
|
161
|
+
end
|
162
|
+
|
163
|
+
def existing_seeds
|
164
|
+
existing_seed_data = '{}'
|
165
|
+
file_path = Coinbase.configuration.backup_file_path
|
166
|
+
existing_seed_data = File.read(file_path) if File.exist?(file_path)
|
167
|
+
output = JSON.parse(existing_seed_data)
|
168
|
+
|
169
|
+
raise ArgumentError, 'Malformed backup data' unless output.is_a?(Hash)
|
170
|
+
|
171
|
+
output
|
172
|
+
end
|
173
|
+
|
174
|
+
def store_encryption_key
|
175
|
+
pk = OpenSSL::PKey.read(Coinbase.configuration.api_key_private_key)
|
176
|
+
public_key = pk.public_key # use own public key to generate the shared secret.
|
177
|
+
pk.dh_compute_key(public_key)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|