factom-ruby 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3c6d1b2e3ca4aecb01e8a6a24cf30f7bfe4c647b
4
+ data.tar.gz: f06336a6677f61bd94941b0739f872cfbda54dc9
5
+ SHA512:
6
+ metadata.gz: 3978a3a0c594a36396f57c3f94d318b4c49b85b8e66ec4c2b63b0e23228445f861cb76a539e2e139703bbb1bb3a45b9aa4ce3d7c4bf61f17e775551d0ba261f8
7
+ data.tar.gz: 52ca7b596e9c543f66faa0e4c0152635e38b36d4770f2d24fd487d606c056cbbb6d9dec5c3123694d10e2df47a45996b778f63a893a7d93d8fcf1fa81a5a928c
data/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2012, Jan Xie <jan.h.xie@gmail.com>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+ * Neither the name of Jan Xie nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9
+
10
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,75 @@
1
+ # Ruby Client for Factom
2
+
3
+ A ruby client who talks to `factomd` in [Factom](http://factom.org) project.
4
+
5
+ ## Features
6
+
7
+ * Encode/decode messages for you
8
+ * Independent, work without `fctwallet` and `factom-cli`
9
+ * No account management, use `factom-cli` for that purpose
10
+
11
+ ## Install
12
+
13
+ ```
14
+ gem i factom-ruby
15
+ ```
16
+
17
+ or
18
+
19
+ Add it to your Gemfile:
20
+
21
+ ```
22
+ gem 'factom-ruby'
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ require 'factom-ruby'
29
+
30
+ ec_private_key = '0000000000000000000000000000000000000000000000000000000000000000'
31
+ f = Factom::Client.new('http://factomd.node.ip:8088', ec_private_key)
32
+
33
+ #################
34
+ # get some info #
35
+ #################
36
+
37
+ p f.properties
38
+ p f.fee
39
+
40
+ ################
41
+ # get balances #
42
+ ################
43
+
44
+ fa_address = 'FAblahblah...' # Factoid address
45
+ fa_pubkey = f.address_to_pubkey fa_address
46
+ p f.fa_balance_in_decimal(fa_pubkey)
47
+
48
+ f.ec_address # EntryCredit address, calculated automatically from ec_private_key
49
+ p f.ec_balance_in_decimal # default to balance on f.ec_address
50
+
51
+ ###########################################################################
52
+ # create a new chain, remember to choose a unique chain names combination #
53
+ ###########################################################################
54
+
55
+ resp = f.commit_chain %w(three body problem), "the world belongs to ???"
56
+ puts "resp code: #{resp.code} body: #{resp.body}"
57
+ resp = f.reveal_chain %w(three body problem), "the world belongs to ???"
58
+ puts "resp code: #{resp.code} body: #{resp.body}"
59
+
60
+ ######################
61
+ # submit a new entry #
62
+ ######################
63
+
64
+ resp = f.commit_entry(some_chain_id, %w(chapter1), "once upon a time ...")
65
+ puts "resp code: #{resp.code} body: #{resp.body}"
66
+ resp = f.reveal_entry(some_chain_id, %w(chapter1), "once upon a time ...")
67
+ puts "resp code: #{resp.code} body: #{resp.body}"
68
+ ```
69
+
70
+ Check [examples](examples/) directory for more examples.
71
+
72
+ ## License
73
+
74
+ [MIT License](LICENSE)
75
+
@@ -0,0 +1 @@
1
+ require_relative 'factom-ruby/client'
@@ -0,0 +1,275 @@
1
+ require 'bigdecimal'
2
+ require 'bitcoin'
3
+ require 'digest'
4
+ require 'json'
5
+ require 'rbnacl'
6
+ require 'rest-client'
7
+
8
+ module Factom
9
+ module APIv1
10
+ GetBalanceFailed = "Failed to get balance!"
11
+
12
+ VERSION = '00'.freeze
13
+
14
+ DENOMINATION_FACTOID = 100000000
15
+ DENOMINATION_ENTRY_CREDIT = 1
16
+
17
+ def height
18
+ json = get "/v1/directory-block-height/"
19
+ json['Height']
20
+ end
21
+
22
+ def head
23
+ json = get "/v1/directory-block-head/"
24
+ json['KeyMR']
25
+ end
26
+
27
+ def block(keymr)
28
+ get "/v1/directory-block-by-keymr/#{keymr}"
29
+ end
30
+
31
+ def chain_head(id)
32
+ json = get "/v1/chain-head/#{id}"
33
+ json['ChainHead']
34
+ end
35
+
36
+ def entry_block(keymr)
37
+ get "/v1/entry-block-by-keymr/#{keymr}"
38
+ end
39
+
40
+ def raw_data(hash)
41
+ get "/v1/get-raw-data/#{hash}"
42
+ end
43
+
44
+ def entry(hash)
45
+ decode_entry get "/v1/entry-by-hash/#{hash}"
46
+ end
47
+
48
+ def fee
49
+ json = get "/v1/factoid-get-fee/"
50
+ json['Fee']
51
+ end
52
+
53
+ def properties
54
+ get "/v1/properties/"
55
+ end
56
+
57
+ def fa_balance(pubkey)
58
+ json = get "/v1/factoid-balance/#{pubkey}"
59
+ raise GetBalanceFailed unless json['Success']
60
+ json['Response']
61
+ end
62
+
63
+ def fa_balance_in_decimal(pubkey)
64
+ BigDecimal.new(fa_balance(pubkey)) / DENOMINATION_FACTOID
65
+ end
66
+
67
+ def ec_balance(pubkey=ec_public_key)
68
+ json = get "/v1/entry-credit-balance/#{pubkey}"
69
+ raise GetBalanceFailed unless json['Success']
70
+ json['Response']
71
+ end
72
+
73
+ def ec_balance_in_decimal(pubkey=ec_public_key)
74
+ BigDecimal.new(ec_balance(pubkey)) / DENOMINATION_ENTRY_CREDIT
75
+ end
76
+
77
+ # Params:
78
+ # chain_names - chain name combination, must be unique globally. It's
79
+ # first entry's external ids actually.
80
+ # content - content of first entry
81
+ def commit_chain(chain_names, content)
82
+ params = { 'CommitChainMsg' => get_chain_commit(chain_names, content) }
83
+ raw_post "/v1/commit-chain/", params.to_json, content_type: :json
84
+ end
85
+
86
+ def reveal_chain(chain_names, content)
87
+ chain_id = get_chain_id chain_names
88
+ ext_ids = chain_names
89
+
90
+ params = { 'Entry' => build_entry(chain_id, ext_ids, content) }
91
+ raw_post "/v1/reveal-chain/", params.to_json, content_type: :json
92
+ end
93
+
94
+ def commit_entry(chain_id, ext_ids, content)
95
+ params = { 'CommitEntryMsg' => get_entry_commit(chain_id, ext_ids, content) }
96
+ # TODO: will factom make response return json, for a better world?
97
+ raw_post "/v1/commit-entry/", params.to_json, content_type: :json
98
+ end
99
+
100
+ def reveal_entry(chain_id, ext_ids, content)
101
+ params = { 'Entry' => build_entry(chain_id, ext_ids, content) }
102
+ # TODO: the same, replace raw_post with post
103
+ raw_post "/v1/reveal-entry/", params.to_json, content_type: :json
104
+ end
105
+
106
+ private
107
+
108
+ def get_chain_commit(chain_names, content)
109
+ timestamp = (Time.now.to_f*1000).floor
110
+ ts = [ timestamp ].pack('Q>').unpack('H*').first
111
+
112
+ chain_id = get_chain_id chain_names
113
+ chain_id_hash = get_chain_id_hash chain_id
114
+
115
+ first_entry = build_entry chain_id, chain_names, content
116
+ first_entry_hash = get_entry_hash first_entry
117
+
118
+ weld = get_weld chain_id, first_entry_hash
119
+ fee = [ calculate_fee(first_entry)+10 ].pack('C').unpack('H*').first
120
+
121
+ sign "#{VERSION}#{ts[4..-1]}#{chain_id_hash}#{weld}#{first_entry_hash}#{fee}"
122
+ end
123
+
124
+ def get_chain_id_hash(chain_id)
125
+ sha256d [chain_id].pack("H*")
126
+ end
127
+
128
+ def get_chain_id(chain_names)
129
+ pre_id = chain_names.map {|name| Digest::SHA256.digest(name) }.join
130
+ Digest::SHA256.hexdigest(pre_id)
131
+ end
132
+
133
+ def get_weld(chain_id, entry_hash)
134
+ sha256d [entry_hash+chain_id].pack("H*")
135
+ end
136
+
137
+ def get_entry_commit(chain_id, ext_ids, content)
138
+ timestamp = (Time.now.to_f*1000).floor
139
+ ts = [ timestamp ].pack('Q>').unpack('H*').first
140
+
141
+ entry = build_entry(chain_id, ext_ids, content)
142
+ entry_hash = get_entry_hash entry
143
+
144
+ fee = [ calculate_fee(entry) ].pack('C').unpack('H*').first
145
+
146
+ sign "#{VERSION}#{ts[4..-1]}#{entry_hash}#{fee}"
147
+ end
148
+
149
+ def get_entry_hash(entry)
150
+ sha512 = Digest::SHA512.hexdigest([entry].pack('H*')) + entry
151
+ Digest::SHA256.hexdigest [sha512].pack('H*')
152
+ end
153
+
154
+ def build_entry(chain_id, ext_ids, content)
155
+ len = 0
156
+ ext_ids_hex = []
157
+ content_hex = content.unpack('H*').first
158
+
159
+ ext_ids.each do |id|
160
+ len += id.size + 2
161
+ ext_ids_hex.push uint16_to_hex(id.size)
162
+ ext_ids_hex.push id.unpack('H*').first
163
+ end
164
+
165
+ "#{VERSION}#{chain_id}#{uint16_to_hex(len)}#{ext_ids_hex.join}#{content_hex}"
166
+ end
167
+
168
+ def decode_entry(json)
169
+ json['ExtIDs'] = json['ExtIDs'].map {|bin| [bin].pack('H*') }
170
+ json['Content'] = [ json['Content'] ].pack('H*')
171
+ json
172
+ end
173
+
174
+ def calculate_fee(entry)
175
+ fee = entry.size / 2 # count of entry bytes
176
+ fee -= 35 # header doesn't count
177
+ fee = (fee+1023)/1024 # round up and divide, rate = 1 EC/KiB
178
+
179
+ # fee only occupy 1 byte in commit message
180
+ # but the hard limit is 10kB actually, much less than 255
181
+ raise "entry is too large!" if fee > 255
182
+
183
+ fee
184
+ end
185
+
186
+ def uint16_to_hex(i)
187
+ [i].pack('n').unpack('H*').first
188
+ end
189
+
190
+ end
191
+
192
+ class Client
193
+ attr :endpoint
194
+
195
+ PREFIX_FA = 'FA'.freeze
196
+ PREFIX_EC = 'EC'.freeze
197
+ ADDRESS_PREFIX = {
198
+ PREFIX_FA => '5fb1',
199
+ PREFIX_EC => '592a'
200
+ }.freeze
201
+
202
+ def initialize(endpoint, ec_private_key, version='v1')
203
+ @endpoint = endpoint.gsub(/\/\z/, '')
204
+ @ec_private_key = ec_private_key =~ /\A#{PREFIX_EC}/ ? address_to_pubkey(ec_private_key) : ec_private_key
205
+ self.instance_eval { extend ::Factom.const_get("API#{version}", false) }
206
+ end
207
+
208
+ def raw_get(path, params={}, options={})
209
+ uri = "#{endpoint}#{path}"
210
+ options = {accept: :json}.merge(options)
211
+ options[:params] = params
212
+
213
+ RestClient.get uri, options
214
+ end
215
+
216
+ def get(path, params={}, options={})
217
+ JSON.parse raw_get(path, params, options)
218
+ end
219
+
220
+ def raw_post(path, params={}, options={})
221
+ uri = "#{endpoint}#{path}"
222
+ options = {accept: :json}.merge(options)
223
+
224
+ RestClient.post uri, params, options
225
+ end
226
+
227
+ def ec_public_key
228
+ @ec_public_key ||= signing_key.verify_key.to_s.unpack('H*').first
229
+ end
230
+
231
+ def ec_address
232
+ @ec_address ||= pubkey_to_address ADDRESS_PREFIX[PREFIX_EC], ec_public_key
233
+ end
234
+
235
+ # to pubkey in hex, 32 bytes
236
+ def address_to_pubkey(addr)
237
+ return unless addr.size == 52
238
+
239
+ prefix = ADDRESS_PREFIX[addr[0,2]]
240
+ return unless prefix
241
+
242
+ v = Bitcoin.decode_base58(addr)
243
+ return if v[0,4] != prefix
244
+
245
+ bytes = [v[0, 68]].pack('H*')
246
+ return if v[68, 8] != sha256d(bytes)[0, 8]
247
+
248
+ v[4, 64]
249
+ end
250
+
251
+ def pubkey_to_address(prefix, pubkey)
252
+ return unless pubkey.size == 64 # 32 bytes in hex
253
+
254
+ addr = "#{prefix}#{pubkey}"
255
+ bytes = [addr].pack('H*')
256
+
257
+ Bitcoin.encode_base58 "#{addr}#{sha256d(bytes)[0,8]}"
258
+ end
259
+
260
+ def sha256d(bytes)
261
+ Digest::SHA256.hexdigest(Digest::SHA256.digest(bytes))
262
+ end
263
+
264
+ # ed25519 private key
265
+ def signing_key
266
+ @signing_key ||= RbNaCl::SigningKey.new([@ec_private_key].pack('H*'))
267
+ end
268
+
269
+ def sign(message)
270
+ sig = signing_key.sign([message].pack('H*')).unpack('H*').first
271
+ "#{message}#{ec_public_key}#{sig}"
272
+ end
273
+
274
+ end
275
+ end
@@ -0,0 +1,3 @@
1
+ module Factom
2
+ VERSION = '0.1.1'
3
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: factom-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Jan Xie
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bitcoin-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.7
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.7
27
+ - !ruby/object:Gem::Dependency
28
+ name: rbnacl-libsodium
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.8
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.8
41
+ - !ruby/object:Gem::Dependency
42
+ name: rbnacl
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.3.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 3.3.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rest-client
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.8.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.8.0
69
+ description: Ruby client consumes Factom (factom.org) API.
70
+ email:
71
+ - jan.h.xie@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - LICENSE
77
+ - README.md
78
+ - lib/factom-ruby.rb
79
+ - lib/factom-ruby/client.rb
80
+ - lib/factom-ruby/version.rb
81
+ homepage: https://github.com/janx/factom-ruby
82
+ licenses:
83
+ - MIT
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.4.5
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Ruby client consumes Factom (factom.org) API.
105
+ test_files: []