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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +62 -0
- data/LICENSE.txt +21 -0
- data/README.md +172 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/rochain +5 -0
- data/lib/record_on_chain/cli.rb +140 -0
- data/lib/record_on_chain/command_loader.rb +20 -0
- data/lib/record_on_chain/commands/abstract_command.rb +55 -0
- data/lib/record_on_chain/commands/help.rb +50 -0
- data/lib/record_on_chain/commands/init.rb +150 -0
- data/lib/record_on_chain/commands/mod_command.rb +38 -0
- data/lib/record_on_chain/commands/record.rb +163 -0
- data/lib/record_on_chain/commands/secret.rb +57 -0
- data/lib/record_on_chain/config.rb +12 -0
- data/lib/record_on_chain/constants.rb +21 -0
- data/lib/record_on_chain/crypto/aes.rb +51 -0
- data/lib/record_on_chain/crypto/cryptor.rb +60 -0
- data/lib/record_on_chain/crypto/default_cryptor.rb +12 -0
- data/lib/record_on_chain/datafile_base.rb +101 -0
- data/lib/record_on_chain/keyfile.rb +14 -0
- data/lib/record_on_chain/nem_controller.rb +113 -0
- data/lib/record_on_chain/utils.rb +44 -0
- data/lib/record_on_chain/version.rb +3 -0
- data/lib/record_on_chain.rb +21 -0
- data/lib/resources/preset_mainnet_nodes +45 -0
- data/lib/resources/preset_testnet_nodes +1 -0
- data/record_on_chain.gemspec +46 -0
- metadata +166 -0
@@ -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
|