dbchain_client 0.6.0 → 0.6.2
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/dbchain_client/aes.rb +49 -0
- data/lib/dbchain_client/key.rb +54 -0
- data/lib/dbchain_client/key_escrow.rb +97 -0
- data/lib/dbchain_client/message_generator.rb +19 -0
- data/lib/dbchain_client/mnemonics.rb +34 -0
- data/lib/dbchain_client/reader.rb +27 -0
- data/lib/dbchain_client/rest_lib.rb +27 -0
- data/lib/dbchain_client/transaction.rb +87 -0
- data/lib/dbchain_client/writer.rb +42 -0
- data/lib/dbchain_client.rb +13 -3
- metadata +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f300331aa2995762eb461fee85ea02a12e736319679a0ceea696be1917dfb71d
|
4
|
+
data.tar.gz: f78d01c152f1ad80c72f35cf7d1454fcc3b6b0808d4d300b13d77c978547daee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d40306d4be159d83f9aee1d49899232523235b0d9b396d4350e1f5c8d7243255f0990659f6487e41742466fcafc12886a8dbe3eaa329aae5ad08ab2bc00ea388
|
7
|
+
data.tar.gz: 3a1507e51851413873b544624569b0d665159986d487bfbf59db531f7e6f890dd93f48f1cea2ebd3887792b0c8c790de90fdf4436d9060aa686817aad638bc18
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'digest'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module DbchainClient
|
6
|
+
class AESCrypt
|
7
|
+
def initialize
|
8
|
+
@cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
9
|
+
end
|
10
|
+
|
11
|
+
def encrypt(password, clear_data)
|
12
|
+
iv = generate_iv
|
13
|
+
|
14
|
+
@cipher.encrypt
|
15
|
+
@cipher.key = Digest::SHA256.digest(password)
|
16
|
+
@cipher.iv = iv
|
17
|
+
|
18
|
+
encrypted = @cipher.update(clear_data) + @cipher.final
|
19
|
+
encrypted_with_iv = prefix_iv(iv, encrypted)
|
20
|
+
Base64.strict_encode64(encrypted_with_iv)
|
21
|
+
end
|
22
|
+
|
23
|
+
def decrypt(password, secret_data_with_iv)
|
24
|
+
iv_encrypted = Base64::decode64(secret_data_with_iv)
|
25
|
+
iv, encrypted = extract_iv_and_encrypted(iv_encrypted)
|
26
|
+
|
27
|
+
@cipher.decrypt
|
28
|
+
@cipher.key = Digest::SHA256.digest(password)
|
29
|
+
@cipher.iv = iv
|
30
|
+
@cipher.update(encrypted) + @cipher.final
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def prefix_iv(iv, encrypted)
|
36
|
+
iv + encrypted
|
37
|
+
end
|
38
|
+
|
39
|
+
def generate_iv
|
40
|
+
OpenSSL::Random.random_bytes(16)
|
41
|
+
end
|
42
|
+
|
43
|
+
def extract_iv_and_encrypted(iv_encrypted)
|
44
|
+
iv = iv_encrypted[0..15]
|
45
|
+
encrypted = iv_encrypted[16..-1]
|
46
|
+
[iv, encrypted]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#require "bitcoin"
|
2
|
+
require "secp256k1"
|
3
|
+
|
4
|
+
module DbchainClient
|
5
|
+
class PublicKey
|
6
|
+
def initialize(pub_key) # hex or raw public key
|
7
|
+
if pub_key.instance_of?(Secp256k1::PublicKey)
|
8
|
+
@public_key = pub_key
|
9
|
+
else
|
10
|
+
raw_pub_key = Secp256k1::Utils.decode_hex(pub_key)
|
11
|
+
@public_key = Secp256k1::PublicKey.new(pubkey: raw_pub_key, raw: true)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def public_key_hex
|
16
|
+
@public_key.serialize.unpack('H*')[0]
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_raw
|
20
|
+
@public_key.serialize
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
public_key_hex
|
25
|
+
end
|
26
|
+
|
27
|
+
def address
|
28
|
+
@address ||= Mnemonics.public_key_to_address(public_key_hex)
|
29
|
+
end
|
30
|
+
|
31
|
+
def verify(message, signature)
|
32
|
+
compact_sig = Secp256k1::Utils.decode_hex(signature)
|
33
|
+
raw_sig = @public_key.ecdsa_deserialize_compact(compact_sig)
|
34
|
+
@public_key.ecdsa_verify(message, raw_sig)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class PrivateKey
|
39
|
+
def initialize(private_key_hex)
|
40
|
+
raw_key = Secp256k1::Utils.decode_hex(private_key_hex)
|
41
|
+
@private_key = Secp256k1::PrivateKey.new(privkey: raw_key)
|
42
|
+
end
|
43
|
+
|
44
|
+
def public_key
|
45
|
+
@public_key ||= PublicKey.new(@private_key.pubkey)
|
46
|
+
end
|
47
|
+
|
48
|
+
def sign(message)
|
49
|
+
raw_sig = @private_key.ecdsa_sign(message)
|
50
|
+
compact_sig = @private_key.ecdsa_serialize_compact(raw_sig)
|
51
|
+
Secp256k1::Utils.encode_hex(compact_sig)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'digest'
|
4
|
+
|
5
|
+
module DbchainClient
|
6
|
+
class KeyEscrow
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
SUFFIX_SECRET = "secret"
|
10
|
+
SUFFIX_PRIVATE = "private"
|
11
|
+
|
12
|
+
def create_and_save_private_key_with_password(username, password, key_store_obj)
|
13
|
+
private_key = DbchainClient::Mnemonics.generate_private_key
|
14
|
+
save_private_key(username, password, private_key, key_store_obj)
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_private_key_with_password(username, password, key_store_obj)
|
18
|
+
load_private_key(username, password, key_store_obj)
|
19
|
+
end
|
20
|
+
|
21
|
+
def save_private_key_with_recovery_phrase(username, recovery_phrase, private_key, key_store_obj)
|
22
|
+
save_private_key(username, recovery_phrase, private_key, key_store_obj)
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_private_key_by_recovery_phrase(username, recovery_phrase, key_store_obj)
|
26
|
+
load_private_key(username, recovery_phrase, key_store_obj)
|
27
|
+
end
|
28
|
+
|
29
|
+
def reset_password_from_recovery_phrase(username, recovery_phrase, new_password, key_store_obj)
|
30
|
+
private_key = load_private_key(username, recovery_phrase, key_store_obj) or raise "Failed to retrieve private key"
|
31
|
+
save_private_key(username, new_password, private_key, key_store_obj)
|
32
|
+
end
|
33
|
+
|
34
|
+
def reset_password_from_old(username, old_password, new_password, key_store_obj)
|
35
|
+
private_key = load_private_key(username, old_password, key_store_obj) or raise "Failed to retrive private key"
|
36
|
+
save_private_key(username, new_password, private_key, key_store_obj)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def save_private_key(username, password_or_recovery_phrase, private_key, key_store_obj)
|
42
|
+
seed = random_seed()
|
43
|
+
aes = DbchainClient::AESCrypt.new
|
44
|
+
encrypted_private_key = aes.encrypt(f1(seed, password_or_recovery_phrase), private_key)
|
45
|
+
secret = aes.encrypt(f2(username, password_or_recovery_phrase), seed)
|
46
|
+
key_of_secret = hash1(username, password_or_recovery_phrase)
|
47
|
+
key_of_private = hash2(username, password_or_recovery_phrase)
|
48
|
+
key_store_obj.save(key_of_private, encrypted_private_key) && key_store_obj.save(key_of_secret, secret)
|
49
|
+
end
|
50
|
+
|
51
|
+
def load_private_key(username, password_or_recovery_phrase, key_store_obj)
|
52
|
+
key_of_secret = hash1(username, password_or_recovery_phrase)
|
53
|
+
key_of_private = hash2(username, password_or_recovery_phrase)
|
54
|
+
secret = key_store_obj.load(key_of_secret)
|
55
|
+
encrypted_private_key = key_store_obj.load(key_of_private)
|
56
|
+
|
57
|
+
aes = DbchainClient::AESCrypt.new
|
58
|
+
seed = aes.decrypt(f2(username, password_or_recovery_phrase), secret)
|
59
|
+
aes.decrypt(f1(seed, password_or_recovery_phrase), encrypted_private_key)
|
60
|
+
end
|
61
|
+
|
62
|
+
def f1(str1, str2)
|
63
|
+
Digest::SHA256.digest(str1 + str2)
|
64
|
+
end
|
65
|
+
|
66
|
+
def f2(str1, str2)
|
67
|
+
f1(str1, str2)
|
68
|
+
end
|
69
|
+
|
70
|
+
def hash1(str1, str2)
|
71
|
+
Digest::SHA256.digest(str1 + str2 + SUFFIX_SECRET)
|
72
|
+
end
|
73
|
+
|
74
|
+
def hash2(str1, str2)
|
75
|
+
Digest::SHA256.digest(str1 + str2 + SUFFIX_PRIVATE)
|
76
|
+
end
|
77
|
+
|
78
|
+
def random_seed()
|
79
|
+
SecureRandom.random_bytes(32)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# This key store is for test purpose. Developers are supposed to implement their own key store for production.
|
84
|
+
class KeyStore
|
85
|
+
def initialize
|
86
|
+
@h = {}
|
87
|
+
end
|
88
|
+
|
89
|
+
def save(key, value)
|
90
|
+
@h[key] = value
|
91
|
+
end
|
92
|
+
|
93
|
+
def load(key)
|
94
|
+
@h[key]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DbchainClient
|
2
|
+
class MessageGenerator
|
3
|
+
def initialize(from_address)
|
4
|
+
@from_address = from_address
|
5
|
+
end
|
6
|
+
|
7
|
+
def run(message_name, message_hash)
|
8
|
+
if message_name == 'MsgSend'
|
9
|
+
type = 'cosmos-sdk/MsgSend'
|
10
|
+
message_hash[:from_address] = @from_address
|
11
|
+
else
|
12
|
+
type = "dbchain/#{message_name}"
|
13
|
+
message_hash[:owner] = @from_address
|
14
|
+
end
|
15
|
+
|
16
|
+
return { type: type, value: message_hash }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "bitcoin"
|
2
|
+
|
3
|
+
module DbchainClient
|
4
|
+
class Mnemonics
|
5
|
+
class << self
|
6
|
+
def generate_mnemonic(strength_bits = 128)
|
7
|
+
Bitcoin::Trezor::Mnemonic.generate(strength_bits)
|
8
|
+
end
|
9
|
+
|
10
|
+
def mnemonic_to_master_key(mnemonic)
|
11
|
+
seed = Bitcoin::Trezor::Mnemonic.to_seed(mnemonic)
|
12
|
+
Bitcoin::ExtKey.generate_master(seed.htb)
|
13
|
+
end
|
14
|
+
|
15
|
+
def master_key_to_cosmos_key_pair(master_key)
|
16
|
+
# m/44'/118'/0'/0/0"
|
17
|
+
key = master_key.derive(2**31 + 44).derive(2**31 + 118).derive(2**31).derive(0 ).derive(0)
|
18
|
+
[key.priv, key.pub]
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate_private_key
|
22
|
+
mnemonic = generate_mnemonic
|
23
|
+
master_key = mnemonic_to_master_key(mnemonic)
|
24
|
+
master_key_to_cosmos_key_pair(master_key)[0]
|
25
|
+
end
|
26
|
+
|
27
|
+
def public_key_to_address(pub_key)
|
28
|
+
hash160 = Bitcoin.hash160(pub_key)
|
29
|
+
words = Bitcoin::Bech32.convert_bits(hash160.htb.unpack("C*"), from_bits: 8, to_bits: 5, pad: true)
|
30
|
+
Bitcoin::Bech32.encode("cosmos", words)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'base58'
|
3
|
+
|
4
|
+
module DbchainClient
|
5
|
+
class Reader
|
6
|
+
def initialize(base_url, private_key, address=nil)
|
7
|
+
@rest_lib = DbchainClient::RestLib.new(base_url)
|
8
|
+
|
9
|
+
if private_key.instance_of? String
|
10
|
+
@private_key = PrivateKey.new(private_key)
|
11
|
+
else
|
12
|
+
@private_key = private_key
|
13
|
+
end
|
14
|
+
|
15
|
+
@public_key = @private_key.public_key
|
16
|
+
@from_address = address || @public_key.address
|
17
|
+
end
|
18
|
+
|
19
|
+
def access_code(time=nil)
|
20
|
+
encoded_public_key = Base58.binary_to_base58(@public_key.to_raw, :bitcoin)
|
21
|
+
time ||= (Time.now.to_f * 1000).to_i.to_s
|
22
|
+
signature = @private_key.sign(time)
|
23
|
+
encoded_signature = Base58.binary_to_base58([signature].pack("H*"), :bitcoin)
|
24
|
+
"#{encoded_public_key}:#{time}:#{encoded_signature}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'base64'
|
3
|
+
require 'net/http'
|
4
|
+
require_relative 'message_generator'
|
5
|
+
|
6
|
+
module DbchainClient
|
7
|
+
class RestLib
|
8
|
+
def initialize(base_url)
|
9
|
+
@base_url = base_url
|
10
|
+
end
|
11
|
+
|
12
|
+
def rest_get(url)
|
13
|
+
uri = URI(@base_url + url)
|
14
|
+
res = Net::HTTP.get_response(uri)
|
15
|
+
res
|
16
|
+
end
|
17
|
+
|
18
|
+
def rest_post(url, data)
|
19
|
+
uri = URI(@base_url + url)
|
20
|
+
req = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
|
21
|
+
req.body = data
|
22
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
23
|
+
res = http.request(req)
|
24
|
+
res.body
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'base64'
|
3
|
+
require 'net/http'
|
4
|
+
require_relative 'message_generator'
|
5
|
+
|
6
|
+
module DbchainClient
|
7
|
+
class Transaction
|
8
|
+
def initialize(base_url, chain_id, private_key_hex)
|
9
|
+
@rest_lib = DbchainClient::RestLib.new(base_url)
|
10
|
+
@chain_id = chain_id
|
11
|
+
@private_key = PrivateKey.new(private_key_hex)
|
12
|
+
@from_address = @private_key.public_key.address
|
13
|
+
end
|
14
|
+
|
15
|
+
def sign_and_broadcast(messages, gas: '99999999', memo: '')
|
16
|
+
tx = {
|
17
|
+
fee: {
|
18
|
+
amount: [],
|
19
|
+
gas: gas
|
20
|
+
},
|
21
|
+
memo: memo,
|
22
|
+
msg: messages
|
23
|
+
}
|
24
|
+
|
25
|
+
sign_message = make_sign_message(tx, messages)
|
26
|
+
signature = @private_key.sign(sign_message)
|
27
|
+
signed_tx = {
|
28
|
+
signature: Base64.strict_encode64([signature].pack("H*")),
|
29
|
+
pub_key: {
|
30
|
+
type: 'tendermint/PubKeySecp256k1',
|
31
|
+
value: Base64.strict_encode64([@private_key.public_key.public_key_hex].pack("H*"))
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
tx[:signatures] = [signed_tx]
|
36
|
+
broadcastBody = {
|
37
|
+
tx: tx,
|
38
|
+
mode: 'async'
|
39
|
+
}.to_json
|
40
|
+
|
41
|
+
response = rest_post("/txs", broadcastBody)
|
42
|
+
response#.data.txhash
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def make_sign_message(tx, messages)
|
48
|
+
account = get_account
|
49
|
+
sign_obj = {
|
50
|
+
account_number: account["account_number"],
|
51
|
+
chain_id: @chain_id,
|
52
|
+
fee: tx[:fee],
|
53
|
+
memo: tx[:memo],
|
54
|
+
msgs: tx[:msg],
|
55
|
+
sequence: account["sequence"]
|
56
|
+
}
|
57
|
+
to_deep_sorted_json(sign_obj)
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_account
|
61
|
+
response = rest_get("/auth/accounts/#{@from_address}")
|
62
|
+
account = JSON.parse(response.body)
|
63
|
+
return account['result']['value']
|
64
|
+
end
|
65
|
+
|
66
|
+
def rest_get(url)
|
67
|
+
@rest_lib.rest_get(url)
|
68
|
+
end
|
69
|
+
|
70
|
+
def rest_post(url, data)
|
71
|
+
@rest_lib.rest_post(url, data)
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_deep_sorted_json(obj)
|
75
|
+
if obj.instance_of?(Array)
|
76
|
+
return '[' + obj.map{|o|to_deep_sorted_json(o)}.join(',') + ']'
|
77
|
+
end
|
78
|
+
|
79
|
+
if obj.instance_of?(Hash)
|
80
|
+
keys = obj.keys.sort
|
81
|
+
return '{' + keys.map{|k| JSON.generate(k) + ':' + to_deep_sorted_json(obj[k])}.join(',') + '}'
|
82
|
+
end
|
83
|
+
|
84
|
+
JSON.generate(obj)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'base64'
|
3
|
+
require 'net/http'
|
4
|
+
require_relative 'message_generator'
|
5
|
+
|
6
|
+
module DbchainClient
|
7
|
+
class Writer
|
8
|
+
def initialize(base_url, chain_id, private_key_hex, address=nil)
|
9
|
+
@transaction = DbchainClient::Transaction.new(base_url, chain_id, private_key_hex)
|
10
|
+
from_address = address || PrivateKey.new(private_key_hex).public_key.address
|
11
|
+
@message_generator = DbchainClient::MessageGenerator.new(from_address)
|
12
|
+
end
|
13
|
+
|
14
|
+
def send_token(to_address, amount)
|
15
|
+
message = generate_message('MsgSend',
|
16
|
+
to_address: to_address,
|
17
|
+
amount: [{denom: 'dbctoken', amount: amount.to_string}]
|
18
|
+
)
|
19
|
+
sign_and_broadcast([message])
|
20
|
+
end
|
21
|
+
|
22
|
+
def insert_row(app_code, table_name, fields)
|
23
|
+
fields_str = Base64.strict_encode64(fields.to_json)
|
24
|
+
message = generate_message('InsertRow',
|
25
|
+
app_code: app_code,
|
26
|
+
table_name: table_name,
|
27
|
+
fields: fields_str
|
28
|
+
)
|
29
|
+
sign_and_broadcast([message])
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def generate_message(message_type, message_data)
|
35
|
+
@message_generator.run(message_type, message_data)
|
36
|
+
end
|
37
|
+
|
38
|
+
def sign_and_broadcast(messages)
|
39
|
+
@transaction.sign_and_broadcast([message])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/dbchain_client.rb
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
module DbchainClient
|
2
|
+
autoload :Mnemonics, "dbchain_client/mnemonics"
|
3
|
+
autoload :PrivateKey, "dbchain_client/key"
|
4
|
+
autoload :PublicKey, "dbchain_client/key"
|
5
|
+
autoload :KeyStore, "dbchain_client/key_escrow"
|
6
|
+
autoload :MessageGenerator, "dbchain_client/message_generator"
|
7
|
+
autoload :RestLib, "dbchain_client/rest_lib"
|
8
|
+
autoload :Transaction, "dbchain_client/transaction"
|
9
|
+
autoload :Writer, "dbchain_client/writer"
|
10
|
+
autoload :Reader, "dbchain_client/reader"
|
11
|
+
autoload :KeyEscrow, "dbchain_client/key_escrow"
|
12
|
+
autoload :AESCrypt, "dbchain_client/aes"
|
13
|
+
end
|
metadata
CHANGED
@@ -1,22 +1,33 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dbchain_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ethan Zhang
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-12-
|
11
|
+
date: 2021-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description: This is the ruby client of
|
13
|
+
description: This is the ruby client of DBChain. DBChain is a blockchain based relational
|
14
|
+
database. Developers can use traditional ways to quickly develop blockchain applications
|
15
|
+
with DBChain.
|
14
16
|
email: yzhang.wa@gmail.com
|
15
17
|
executables: []
|
16
18
|
extensions: []
|
17
19
|
extra_rdoc_files: []
|
18
20
|
files:
|
19
21
|
- lib/dbchain_client.rb
|
22
|
+
- lib/dbchain_client/aes.rb
|
23
|
+
- lib/dbchain_client/key.rb
|
24
|
+
- lib/dbchain_client/key_escrow.rb
|
25
|
+
- lib/dbchain_client/message_generator.rb
|
26
|
+
- lib/dbchain_client/mnemonics.rb
|
27
|
+
- lib/dbchain_client/reader.rb
|
28
|
+
- lib/dbchain_client/rest_lib.rb
|
29
|
+
- lib/dbchain_client/transaction.rb
|
30
|
+
- lib/dbchain_client/writer.rb
|
20
31
|
homepage: https://rubygems.org/gems/dbchain_client
|
21
32
|
licenses:
|
22
33
|
- MIT
|