record_on_chain 0.1.0

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.
@@ -0,0 +1,20 @@
1
+ require_relative "./cli"
2
+ require_relative "./constants"
3
+
4
+ module RecordOnChain
5
+ class CommandLoader
6
+ def self.load( name, dirpath= COMMANDS_DIRPATH, argv= ARGV, cli= Cli.new )
7
+ # except abstract sourcefile
8
+ return nil if name.start_with?("abstract","mod")
9
+ # expand command file path
10
+ filepath = File.expand_path( name, dirpath ) << ".rb"
11
+ # check file existance
12
+ return nil unless File.file?( filepath )
13
+ # require command file
14
+ require( filepath )
15
+ # generate object
16
+ class_name = "RecordOnChain::Commands::#{name.capitalize}"
17
+ return Object.const_get( class_name ).new( argv , cli )
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,55 @@
1
+ require "optparse"
2
+ require_relative "../cli"
3
+
4
+ module RecordOnChain
5
+
6
+ module Commands
7
+ # base of command class
8
+ class AbstractCommand
9
+ def initialize( command_name , cli= Cli.new )
10
+ @command_name = command_name
11
+ @cli = cli
12
+ end
13
+
14
+ def start
15
+ raise NotImplementedError.new
16
+ end
17
+
18
+ def squeeze_args_from_argv( val_context= {} , flag_context= {} , argv= ARGV )
19
+ opts = OptionParser.new
20
+ args = {}
21
+ # set to args according to hash_context.
22
+ val_context.each { |key,val| opts.on("#{key} VAL"){|v| args[val] = v} }
23
+ # set to args according to flag_context
24
+ flag_context.each{ |key,val| opts.on("#{key}"){|v| args[val] = true} }
25
+ # raise erro if user set unknown option
26
+ # except first because @argv.first is command name
27
+ opts.parse( argv )
28
+ return args
29
+ end
30
+
31
+ private
32
+
33
+ def roc_exit( code , msg="" )
34
+ known_codes = [:nomal_end,:halt]
35
+ raise "Unknown exit code #{code}" unless known_codes.include?(code)
36
+ # puts exit messages
37
+ case code
38
+ when :nomal_end
39
+ # nomal end
40
+ out = "Exit NOMAL : #{@command_name} command execution succeede.\n"
41
+ out << "#{msg}"
42
+ @cli.puts_success_msg( out )
43
+ exit 0
44
+ when :halt
45
+ # something happen
46
+ err = "Exit ERROR : #{@command_name} command execution failed.\n"
47
+ err << "[ ERROR MESSAGE ]\n"
48
+ err << "#{msg}"
49
+ @cli.puts_error_msg( err )
50
+ exit 1
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ require_relative "./abstract_command"
2
+ require_relative "./init"
3
+ require_relative "./record"
4
+ require_relative "./secret"
5
+ require_relative "../utils"
6
+
7
+ module RecordOnChain
8
+ module Commands
9
+ class Help < AbstractCommand
10
+
11
+ def self.description
12
+ return "display usage"
13
+ end
14
+
15
+ def self.usage; end
16
+
17
+ def initialize( argv= ARGV , cli= Cli.new )
18
+ super( argv.first , cli )
19
+ end
20
+
21
+ def start
22
+ command_names = [ "init", "record", "secret", "help" ]
23
+ # command_name : description
24
+ descriptions = {}
25
+ # command_name : usage
26
+ examples = {}
27
+
28
+ command_names.each do |name|
29
+ klass = Object.const_get( "RecordOnChain::Commands::#{name.capitalize}" )
30
+ description = klass.send( :description )
31
+ descriptions[ name ] = description unless description.nil?
32
+ examples[ name ] = klass.usage unless klass.usage.nil?
33
+ end
34
+
35
+ @cli.puts_enhance_msg( "== Record on Chain HELP ==" )
36
+ @cli.blank_line
37
+ @cli.puts_underline_msg("description")
38
+ @cli.puts_hash( descriptions , nil , 1 )
39
+ @cli.blank_line
40
+
41
+ @cli.puts_underline_msg("usage")
42
+ examples.each do | c_name, usage |
43
+ @cli.out.puts( " [ #{c_name} ]" )
44
+ @cli.out.puts( "#{usage}" )
45
+ @cli.blank_line
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,150 @@
1
+ require "nem"
2
+ require "pathname"
3
+ require_relative "./abstract_command"
4
+ require_relative "../utils"
5
+ require_relative "../keyfile"
6
+ require_relative "../config"
7
+ require_relative "../constants"
8
+ require_relative "../nem_controller"
9
+ require_relative "../crypto/default_cryptor"
10
+
11
+ module RecordOnChain
12
+ module Commands
13
+ class Init < AbstractCommand
14
+
15
+ def self.description
16
+ return "initialize RecordOnChain"
17
+ end
18
+
19
+ def self.usage
20
+ output = <<-EOS
21
+ -p <value> => base path ( default : $HOME )
22
+ -s <value> => 32byte hex secret ( default : random hex )
23
+ -k <value> => keyfile name ( default : default_key.yml )
24
+ -c <value> => configfile name ( defailt : default_config.yml )
25
+ -t => network type ( defailt : false -> mainnet )
26
+
27
+ (e.g.) secret:XXX , keyfile_name:mykey.yml , network_type:testnet
28
+ => $ rochain init -k mykey.yml -s XXX -t
29
+ EOS
30
+ return output
31
+ end
32
+
33
+ def initialize( argv= ARGV , cli= Cli.new )
34
+ super( argv.first , cli )
35
+ val_context = { "-p" => :path,
36
+ "-s" => :secret,
37
+ "-k" => :keyfile_name,
38
+ "-c" => :configfile_name }
39
+ flag_context = { "-t" => :testnet }
40
+
41
+ args = squeeze_args_from_argv( val_context , flag_context , argv )
42
+
43
+ @secret = args[:secret]
44
+ @network_type = args[:testnet] ? "testnet" : "mainnet"
45
+ @maindir_path = get_maindir_path( args[:path] ) # Pathname obj
46
+ @keyfile_path = get_datafile_path( "keyfile" , args[:keyfile_name] ) # Pathname obj
47
+ @configfile_path = get_datafile_path( "configfile" , args[:configfile_name] ) # Pathname obj
48
+ rescue => e
49
+ roc_exit( :halt , "#{e.message}" )
50
+ end
51
+
52
+ # you can specify maindir path with argument.
53
+ def start
54
+ @cli.puts_enhance_msg( "[ Start gnerate keyfile ]" )
55
+ generate_keyfile
56
+
57
+ @cli.puts_enhance_msg( "[ Start gnerate configfile ]" )
58
+ generate_config
59
+
60
+ @cli.blank_line
61
+
62
+ # nomal_end
63
+ roc_exit( :nomal_end )
64
+ rescue => e
65
+ roc_exit( :halt , "#{e.message}" )
66
+ end
67
+
68
+ private
69
+
70
+ def get_maindir_path( base_path )
71
+ # default = homedir
72
+ base_path = base_path ? to_absolute_path( base_path ) : Dir.home
73
+ # dir not found
74
+ raise "#{base_path} directory not found." unless Dir.exist?( base_path )
75
+ # generate maindir
76
+ maindir_path = Pathname.new( base_path ) + MAINDIR_NAME
77
+ # mkdir if not exist
78
+ maindir_path.mkdir unless maindir_path.directory?
79
+ return maindir_path
80
+ end
81
+
82
+ def to_absolute_path( path )
83
+ if Pathname.new(path).absolute? then
84
+ return path # no change
85
+ else
86
+ return File.expand_path( path , Dir.pwd )
87
+ end
88
+ end
89
+
90
+ def get_datafile_path( type , name )
91
+ datafile_name = name ? name : D_DATAFILE_NAME
92
+ # add suffix to prevent conflicts
93
+ datafile_name += RecordOnChain.const_get( "D_#{type.upcase}_SUFFIX" )
94
+ # to path
95
+ return @maindir_path + datafile_name
96
+ end
97
+
98
+ def generate_keyfile
99
+ # skip if already exist
100
+ if @keyfile_path.exist? then
101
+ @cli.out.puts( "#{@keyfile_path.to_s} already exists. skip generate keyfile." )
102
+ return
103
+ end
104
+
105
+ # check hex_str
106
+ validate_secret_form
107
+
108
+ # decide password
109
+ password = decide_password
110
+
111
+ # cretae cryptor
112
+ cryptor = Crypto::DefaultCryptor.generate
113
+
114
+ # create status
115
+ salt = cryptor.generate_salt
116
+ secret = @secret ? @secret : cryptor.generate_secret
117
+ public_key = NemController.public_from_secret( secret )
118
+ address = NemController.address_from_public( public_key, @network_type )
119
+ encrypted_secret = cryptor.encrypt( password, salt, secret )
120
+ # generate
121
+ Keyfile.generate( @keyfile_path.to_s, @network_type, salt, encrypted_secret, public_key, address )
122
+ # display address
123
+ @cli.puts_attention_msg( "New keyfile address is #{address}" )
124
+ end
125
+
126
+ def validate_secret_form
127
+ raise "Illegal secret. Secret should be 32byte-hex_string (64chars)." unless @secret.nil? || Utils.hex_str?( @secret , 32 )
128
+ end
129
+
130
+ def decide_password
131
+ pass = @cli.decide_password
132
+ raise "5 incorrect password attempts." if pass.nil?
133
+ return pass
134
+ end
135
+
136
+ def generate_config
137
+ # skip if already exist
138
+ if @configfile_path.exist? then
139
+ @cli.out.puts( "#{@configfile_path.to_s} already exists. skip generate config file." )
140
+ return
141
+ end
142
+
143
+ # create default_values from loaded keyfile
144
+ keyfile = Keyfile.load( @keyfile_path )
145
+ # generate
146
+ Config.generate( @configfile_path.to_s, @keyfile_path.to_s, keyfile.address, [] )
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,38 @@
1
+ require_relative "../keyfile"
2
+ require_relative "../crypto/default_cryptor"
3
+
4
+ # module set for command
5
+ module RecordOnChain
6
+ module Commands
7
+
8
+ module M_LoadDatafile
9
+ # base method of load_config & load_keyfile
10
+ def load_datafile( datafile_path , class_name )
11
+ # try to load
12
+ full_class_name = "RecordOnChain::#{class_name.capitalize}"
13
+ # get klass => klass.send( :load , path ) => klass.load( path )
14
+ klass = Object.const_get( full_class_name )
15
+ datafile = klass.send( :load , datafile_path )
16
+ # if fail to load becaseu any field is illegal
17
+ raise "Fail to load #{class_name}.Please check the fields of #{class_name}." if datafile.nil?
18
+ # success
19
+ return datafile
20
+ end
21
+ end
22
+
23
+ module M_GetSecret
24
+ # recover secret from keyfile and user password
25
+ def get_secret( cli , keyfile )
26
+ answer = ""
27
+ cryptor = RecordOnChain::Crypto::DefaultCryptor.generate
28
+ decrypt_func = ->( attempt ){ cryptor.decrypt( attempt, keyfile.salt, keyfile.encrypted_secret ) }
29
+ secret = cli.encrypt_with_password( decrypt_func )
30
+ # too many inccorect
31
+ raise "3 incorrect password attempts." if secret.nil?
32
+ # if not nil, success to decrypt
33
+ return secret
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,163 @@
1
+ require "pathname"
2
+ require_relative "./abstract_command"
3
+ require_relative "./mod_command"
4
+ require_relative "../keyfile"
5
+ require_relative "../config"
6
+ require_relative "../nem_controller"
7
+ require_relative "../constants"
8
+ require_relative "../crypto/default_cryptor"
9
+
10
+ module RecordOnChain
11
+ module Commands
12
+ class Record < AbstractCommand
13
+
14
+ def self.description
15
+ return "record message on nem(ver-1) chain"
16
+ end
17
+
18
+ def self.usage
19
+ output = <<-EOS
20
+ -p <value> => base path ( default : $HOME )
21
+ -c <value> => configfile name ( defailt : default_config.yml )
22
+ -m <value> => message you want to record ( *mandatory fields )
23
+
24
+ (e.g.) configfile_name:my_config.yml , message:good_luck!
25
+ => $ rochain record -c my_config.yml -m good_luck!
26
+ EOS
27
+ return output
28
+ end
29
+
30
+ def initialize( argv = ARGV , cli = Cli.new )
31
+ super( argv.first , cli )
32
+ val_context = { "-p" => :path,
33
+ "-c" => :config,
34
+ "-m" => :msg }
35
+ flag_context = {}
36
+
37
+ args = squeeze_args_from_argv( val_context , flag_context , argv )
38
+
39
+ @maindir_path = get_dirpath( args[:path] ) # String
40
+ @config = load_config( args[:config] )
41
+ @keyfile = load_keyfile
42
+ @msg = args[:msg]
43
+ @nem = create_nem_controller
44
+ rescue => e
45
+ roc_exit( :halt , "#{e.message}" )
46
+ end
47
+
48
+ def start
49
+ # send
50
+ result = send_tx
51
+ # exit
52
+ result[:success?] ? roc_exit( :nomal_end, "tx_hash [ #{result[:tx_hash]} ]" ) :
53
+ roc_exit( :halt , "Fail to send tx. #{result[:message]}" )
54
+ rescue => e
55
+ roc_exit( :halt , e.message )
56
+ end
57
+
58
+ private
59
+
60
+ # region get_dirpath
61
+
62
+ def get_dirpath( dir_path )
63
+ # default = homedir
64
+ dir_path = dir_path ? dir_path : Dir.home
65
+ pn = Pathname.new( dir_path )
66
+ # dir not found
67
+ raise "#{dir_path} directory not found." unless pn.directory?
68
+ return ( pn + MAINDIR_NAME ).to_s
69
+ end
70
+
71
+ # region load_config & load_keyfile
72
+ include M_LoadDatafile
73
+
74
+ def load_config( name )
75
+ configfile_name = name ? name : D_CONFIGFILE_NAME
76
+ configfile_path = "#{@maindir_path}/#{configfile_name}"
77
+ return load_datafile( configfile_path , "config" )
78
+ end
79
+
80
+ def load_keyfile
81
+ return load_datafile( @config.keyfile_path , "keyfile" )
82
+ end
83
+
84
+ # region create_nem_controller
85
+
86
+ def create_nem_controller
87
+ node_urls = get_node_urls
88
+ return NemController.new( node_urls , @keyfile.network_type )
89
+ end
90
+
91
+ def get_node_urls
92
+ # testnet? mainnet?
93
+ network_type = @keyfile.network_type.to_s
94
+ preset_filepath = RESOURCES_DIRPATH + "/preset_#{network_type}_nodes"
95
+ # load preset node set
96
+ preset_node_urls = []
97
+ File.foreach( preset_filepath ){ |url| preset_node_urls.push( url.chomp ) }
98
+ # select nodes
99
+ node_count = 10
100
+ nodes = preset_node_urls.sample( node_count )
101
+ # add nodes thas is specified by user
102
+ # These nodes have priority
103
+ add_node = @config.add_node
104
+ # add in fornt
105
+ nodes = add_node + nodes
106
+ return delete_invalid_nodes( nodes )
107
+ end
108
+
109
+ # delete invalid form
110
+ def delete_invalid_nodes( urls )
111
+ # delete empty and including unsafe char
112
+ urls.delete_if{ |u| u.empty? || u.match?( URI::UNSAFE ) }
113
+ # modify
114
+ return urls.map do |u|
115
+ # if not start_with http:// or https://. add prefix
116
+ u.start_with?( "http://" , "https://" ) ? u : "http://" << u
117
+ end
118
+ end
119
+
120
+ # region send_tx
121
+ include M_GetSecret
122
+
123
+ def send_tx
124
+ raise "Massage not found. Nothing to record." if @msg.nil? || @msg.empty?
125
+ # get secret from keyfile and password.
126
+ secret = get_secret( @cli, @keyfile )
127
+ # get address from the secret to use for confirm.
128
+ sender_address = NemController.address_from_secret( secret , @keyfile.network_type )
129
+ # for confirm
130
+ recipient = @config.recipient
131
+ # prepare transfer tx
132
+ @nem.prepare_tx( recipient , @msg )
133
+ # confirm
134
+ confirm_before_send_tx( sender_address , recipient )
135
+ # broadcast tx and return result
136
+ return @nem.send_transfer_tx( secret )
137
+ end
138
+
139
+ def confirm_before_send_tx( sender_address , recipient )
140
+ fee = @nem.calc_fee/1000000.to_f
141
+ address_info = @nem.get_address_status( sender_address )
142
+
143
+ # caoutino if address has too many xem
144
+ balance = address_info[:balance]
145
+ too_many_xem = 1000 * 1000000
146
+ @cli.puts_caution_msg( "Caution! There are too many xems in this address! This warning is displayed when it is more than #{too_many_xem/1000000}xem." ) if balance > too_many_xem
147
+
148
+ # multisig not supported
149
+ raise "Error : Sorry, multisig is not supported." if address_info[:multisig]
150
+
151
+ # confirmation info
152
+ @cli.puts_attention_msg( "!! confirm !!" )
153
+ status = { :sender => sender_address,
154
+ :recipient => recipient,
155
+ :data => @msg,
156
+ :fee => "#{fee} xem"}
157
+ @cli.puts_hash( status , :enhance )
158
+ # if not agree, stop recording
159
+ raise "Stop recording." unless @cli.agree( "record" )
160
+ end
161
+ end
162
+ end
163
+ end