zilliqa 0.1.1
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/.circleci/config.yml +30 -0
- data/.gitignore +8 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +50 -0
- data/LICENSE.txt +674 -0
- data/README.md +175 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/zilliqa.rb +21 -0
- data/lib/zilliqa/account/account.rb +34 -0
- data/lib/zilliqa/account/transaction.rb +158 -0
- data/lib/zilliqa/account/transaction_factory.rb +16 -0
- data/lib/zilliqa/account/wallet.rb +146 -0
- data/lib/zilliqa/contract/contract.rb +150 -0
- data/lib/zilliqa/contract/contract_factory.rb +47 -0
- data/lib/zilliqa/crypto/key_store.rb +113 -0
- data/lib/zilliqa/crypto/key_tool.rb +61 -0
- data/lib/zilliqa/crypto/schnorr.rb +147 -0
- data/lib/zilliqa/jsonrpc/provider.rb +31 -0
- data/lib/zilliqa/proto/message.proto +44 -0
- data/lib/zilliqa/proto/message_pb.rb +46 -0
- data/lib/zilliqa/util/bech32.rb +28 -0
- data/lib/zilliqa/util/unit.rb +37 -0
- data/lib/zilliqa/util/util.rb +17 -0
- data/lib/zilliqa/util/validator.rb +41 -0
- data/lib/zilliqa/version.rb +3 -0
- data/zilliqa.gemspec +46 -0
- metadata +204 -0
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module Zilliqa
|
4
|
+
module Account
|
5
|
+
class Wallet
|
6
|
+
MAINNET = 65_537
|
7
|
+
|
8
|
+
# Takes an array of Account objects and instantiates a Wallet instance.
|
9
|
+
def initialize(provider = nil, accounts = {})
|
10
|
+
@provider = provider
|
11
|
+
@accounts = accounts
|
12
|
+
if accounts.length > 0
|
13
|
+
@default_account = accounts[0]
|
14
|
+
else
|
15
|
+
@default_account = nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Creates a new keypair with a randomly-generated private key. The new
|
20
|
+
# account is accessible by address.
|
21
|
+
def create
|
22
|
+
private_key = Zilliqa::Crypto::KeyTool.generate_private_key
|
23
|
+
account = Zilliqa::Account::Account.new(private_key)
|
24
|
+
|
25
|
+
@accounts[account.address] = account
|
26
|
+
|
27
|
+
@default_account = account unless @default_account
|
28
|
+
|
29
|
+
account.address
|
30
|
+
end
|
31
|
+
|
32
|
+
# Adds an account to the wallet by private key.
|
33
|
+
def add_by_private_key(private_key)
|
34
|
+
account = Zilliqa::Account::Account.new(private_key)
|
35
|
+
|
36
|
+
@accounts[account.address] = account
|
37
|
+
|
38
|
+
@default_account = account unless @default_account
|
39
|
+
|
40
|
+
account.address
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Adds an account by keystore
|
45
|
+
def add_by_keystore(keystore, passphrase)
|
46
|
+
account = Zilliqa::Account::Account.from_file(keystore, passphrase)
|
47
|
+
|
48
|
+
@accounts[account.address] = account
|
49
|
+
|
50
|
+
@default_account = account unless @default_account
|
51
|
+
|
52
|
+
account.address
|
53
|
+
end
|
54
|
+
|
55
|
+
# Removes an account from the wallet and returns boolean to indicate
|
56
|
+
# failure or success.
|
57
|
+
|
58
|
+
def remove(address)
|
59
|
+
if @accounts.has_key?(address)
|
60
|
+
@accounts.delete(address)
|
61
|
+
|
62
|
+
true
|
63
|
+
else
|
64
|
+
false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Sets the default account of the wallet.
|
69
|
+
def set_default(address)
|
70
|
+
@default_account = @accounts[address]
|
71
|
+
end
|
72
|
+
|
73
|
+
# to_checksum_address
|
74
|
+
#
|
75
|
+
# takes hex-encoded string and returns the corresponding address
|
76
|
+
#
|
77
|
+
# @param {string} address
|
78
|
+
# @returns {string}
|
79
|
+
def self.to_checksum_address(address)
|
80
|
+
return Zilliqa::Util::Bech32.from_bech32(address) if Zilliqa::Util::Validator.bech32?(address)
|
81
|
+
address = address.downcase.gsub('0x', '')
|
82
|
+
|
83
|
+
s1 = Digest::SHA256.hexdigest(Util.decode_hex(address))
|
84
|
+
v = s1.to_i(base=16)
|
85
|
+
|
86
|
+
ret = ['0x']
|
87
|
+
address.each_char.each_with_index do |c, idx|
|
88
|
+
if '1234567890'.include?(c)
|
89
|
+
ret << c
|
90
|
+
else
|
91
|
+
ret << ((v & (2 ** (255 - 6 * idx))) < 1 ? c.downcase : c.upcase)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
ret.join
|
96
|
+
end
|
97
|
+
|
98
|
+
def transfer(to_addr, amount)
|
99
|
+
gas_price = Integer(@provider.GetMinimumGasPrice)
|
100
|
+
gas_limit = 1
|
101
|
+
|
102
|
+
tx = sign(Zilliqa::Account::Transaction.new({
|
103
|
+
version: MAINNET,
|
104
|
+
amount: amount.to_s,
|
105
|
+
to_addr: to_addr,
|
106
|
+
gas_price: gas_price.to_s,
|
107
|
+
gas_limit: gas_limit
|
108
|
+
}, @provider))
|
109
|
+
tx.submit!
|
110
|
+
end
|
111
|
+
|
112
|
+
# signs an unsigned transaction with the default account.
|
113
|
+
def sign(tx)
|
114
|
+
if tx.sender_pub_key
|
115
|
+
# attempt to find the address
|
116
|
+
address = Zilliqa::Crypto::KeyTool.get_address_from_public_key(tx.sender_pub_key)
|
117
|
+
account = @accounts[address]
|
118
|
+
raise 'Could not sign the transaction with address as it does not exist' unless account
|
119
|
+
|
120
|
+
sign_with(tx, address)
|
121
|
+
else
|
122
|
+
raise 'This wallet has no default account.' unless @default_account
|
123
|
+
|
124
|
+
sign_with(tx, @default_account.address)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def sign_with(tx, address)
|
129
|
+
account = @accounts[address]
|
130
|
+
address = @provider.testnet? ? Zilliqa::Util::Bech32.from_bech32(account.address) : account.address
|
131
|
+
|
132
|
+
raise 'The selected account does not exist on this Wallet instance.' unless account
|
133
|
+
|
134
|
+
if tx.nonce.nil?
|
135
|
+
result = @provider.GetBalance(address)
|
136
|
+
tx.nonce = result['nonce'].to_i + 1
|
137
|
+
end
|
138
|
+
|
139
|
+
tx.sender_pub_key = account.public_key
|
140
|
+
sig = account.sign_transaction(tx)
|
141
|
+
tx.signature = sig.to_s
|
142
|
+
tx
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Zilliqa
|
6
|
+
module Contract
|
7
|
+
class Contract
|
8
|
+
include Account
|
9
|
+
|
10
|
+
NIL_ADDRESS = '0000000000000000000000000000000000000000'
|
11
|
+
|
12
|
+
attr_reader :factory, :provider, :signer, :code, :abi, :init, :state, :address, :status
|
13
|
+
|
14
|
+
def initialize(factory, code, abi, address, init, state)
|
15
|
+
@factory = factory
|
16
|
+
@provider = factory.provider
|
17
|
+
@signer = factory.signer
|
18
|
+
|
19
|
+
@code = code
|
20
|
+
@abi = abi
|
21
|
+
@init = init
|
22
|
+
@state = state
|
23
|
+
|
24
|
+
if address && !address.empty?
|
25
|
+
@address = address
|
26
|
+
@status = ContractStatus::DEPLOYED
|
27
|
+
else
|
28
|
+
@status = ContractStatus::INITIALISED
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialised?
|
33
|
+
@status == ContractStatus::INITIALISED
|
34
|
+
end
|
35
|
+
|
36
|
+
def deployed?
|
37
|
+
@status == ContractStatus::DEPLOYED
|
38
|
+
end
|
39
|
+
|
40
|
+
def rejected?
|
41
|
+
@status == ContractStatus::REJECTED
|
42
|
+
end
|
43
|
+
|
44
|
+
def deploy(deploy_params, attempts = 33, interval = 1000, _to_ds = false)
|
45
|
+
raise 'Cannot deploy without code or initialisation parameters.' if @code.nil? || @code == ''
|
46
|
+
raise 'Cannot deploy without code or initialisation parameters.' if @init.nil? || @init.length.zero?
|
47
|
+
|
48
|
+
tx_params = {
|
49
|
+
id: deploy_params.id,
|
50
|
+
version: deploy_params.version,
|
51
|
+
nonce: deploy_params.nonce,
|
52
|
+
sender_pub_key: deploy_params.sender_pub_key,
|
53
|
+
gas_price: deploy_params.gas_price,
|
54
|
+
gas_limit: deploy_params.gas_limit,
|
55
|
+
to_addr: NIL_ADDRESS,
|
56
|
+
amount: '0',
|
57
|
+
code: @code.gsub('/\\', ''),
|
58
|
+
data: @init.to_json.gsub('\\"', '"')
|
59
|
+
}
|
60
|
+
|
61
|
+
tx = Transaction.new(tx_params, @provider)
|
62
|
+
|
63
|
+
tx = prepare_tx(tx, attempts, interval)
|
64
|
+
|
65
|
+
if tx.rejected?
|
66
|
+
@status = ContractStatus::REJECTED
|
67
|
+
|
68
|
+
return [tx, self]
|
69
|
+
end
|
70
|
+
|
71
|
+
@status = ContractStatus::DEPLOYED
|
72
|
+
@address = ContractFactory.get_address_for_contract(tx)
|
73
|
+
|
74
|
+
[tx, self]
|
75
|
+
end
|
76
|
+
|
77
|
+
def call(transition, args, params, attempts = 33, interval = 1000, to_ds = false)
|
78
|
+
data = {
|
79
|
+
_tag: transition,
|
80
|
+
params: args
|
81
|
+
}
|
82
|
+
|
83
|
+
return 'Contract has not been deployed!' unless @address
|
84
|
+
|
85
|
+
tx_params = {
|
86
|
+
id: params['id'],
|
87
|
+
version: params['version'],
|
88
|
+
nonce: params['nonce'],
|
89
|
+
sender_pub_key: params['sender_pub_key'],
|
90
|
+
gas_price: params['gas_price'],
|
91
|
+
gas_limit: params['gas_limit'],
|
92
|
+
to_addr: @address,
|
93
|
+
data: JSON.generate(data)
|
94
|
+
}
|
95
|
+
|
96
|
+
tx = Transaction.new(tx_params, @provider, Zilliqa::Account::Transaction::TX_STATUSES[:initialized], to_ds)
|
97
|
+
|
98
|
+
prepare_tx(tx, attempts, interval)
|
99
|
+
end
|
100
|
+
|
101
|
+
def state
|
102
|
+
return [] unless deployed
|
103
|
+
|
104
|
+
response = @provider.GetSmartContractState(@address)
|
105
|
+
response.result
|
106
|
+
end
|
107
|
+
|
108
|
+
def prepare_tx(tx, attempts, interval)
|
109
|
+
tx = @signer.sign(tx)
|
110
|
+
|
111
|
+
response = @provider.CreateTransaction(tx.to_payload)
|
112
|
+
|
113
|
+
if response['error']
|
114
|
+
tx.status = Zilliqa::Account::Transaction::TX_STATUSES[:rejected]
|
115
|
+
else
|
116
|
+
tx.confirm(response['result']['TranID'], attempts, interval)
|
117
|
+
end
|
118
|
+
|
119
|
+
tx
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class ContractStatus
|
124
|
+
DEPLOYED = 0
|
125
|
+
REJECTED = 1
|
126
|
+
INITIALISED = 2
|
127
|
+
end
|
128
|
+
|
129
|
+
class Value
|
130
|
+
attr_reader :vname, :type, :value
|
131
|
+
def initialize(vname, type, value)
|
132
|
+
@vname = vname
|
133
|
+
@type = type
|
134
|
+
@value = value
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class DeployParams
|
139
|
+
attr_reader :id, :version, :nonce, :gas_price, :gas_limit, :sender_pub_key
|
140
|
+
def initialize(id, version, nonce, gas_price, gas_limit, sender_pub_key)
|
141
|
+
@id = id
|
142
|
+
@version = version
|
143
|
+
@nonce = nonce
|
144
|
+
@gas_price = gas_price
|
145
|
+
@gas_limit = gas_limit
|
146
|
+
@sender_pub_key = sender_pub_key
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module Zilliqa
|
4
|
+
module Contract
|
5
|
+
# ContractFactory
|
6
|
+
#
|
7
|
+
# individual `Contract` instances are instead obtained by
|
8
|
+
# calling `ContractFactory.at` (for an already-deployed contract) and
|
9
|
+
# `ContractFactory.new` (to deploy a new contract).
|
10
|
+
class ContractFactory
|
11
|
+
|
12
|
+
attr_reader :provider, :signer
|
13
|
+
|
14
|
+
def initialize(provider, signer)
|
15
|
+
@provider = provider
|
16
|
+
@signer = signer
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.get_address_for_contract(tx)
|
20
|
+
sha256 = Digest::SHA256.new
|
21
|
+
|
22
|
+
sender_address = Zilliqa::Crypto::KeyTool.get_address_from_public_key(tx.sender_pub_key)
|
23
|
+
|
24
|
+
sha256 << Util.decode_hex(sender_address)
|
25
|
+
|
26
|
+
nonce = 0
|
27
|
+
if tx.nonce
|
28
|
+
nonce = tx.nonce.to_i - 1
|
29
|
+
end
|
30
|
+
|
31
|
+
nonce_hex = [nonce].pack('Q>*')
|
32
|
+
|
33
|
+
sha256 << nonce_hex
|
34
|
+
|
35
|
+
sha256.hexdigest[24..-1]
|
36
|
+
end
|
37
|
+
|
38
|
+
def new_contract(code, init, abi)
|
39
|
+
Contract.new(self, code, abi, nil, init, nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
def at_contract(address, code, init, abi)
|
43
|
+
Contract.new(self, code, abi, address, init, nil)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'pbkdf2'
|
2
|
+
require 'scrypt'
|
3
|
+
require 'openssl'
|
4
|
+
require 'digest'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Zilliqa
|
8
|
+
module Crypto
|
9
|
+
class KeyStore
|
10
|
+
T_PBKDF2 = 'pbkdf2'
|
11
|
+
T_SCRYPT = 'scrypt'
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
end
|
15
|
+
|
16
|
+
# encryptPrivateKey
|
17
|
+
#
|
18
|
+
# Encodes and encrypts an account in the format specified by
|
19
|
+
# https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition.
|
20
|
+
# However, note that, in keeping with the hash function used by Zilliqa's
|
21
|
+
# core protocol, the MAC is generated using sha256 instead of keccak.
|
22
|
+
#
|
23
|
+
# NOTE: only scrypt and pbkdf2 are supported.
|
24
|
+
#
|
25
|
+
# @param {string} private_key - hex-encoded private key
|
26
|
+
# @param {string} password - a password used for encryption
|
27
|
+
# @param {KDF} kdf_type - the key derivation function to be used
|
28
|
+
def encrypt_private_key(private_key, password, kdf_type)
|
29
|
+
address = KeyTool.get_address_from_private_key(private_key)
|
30
|
+
|
31
|
+
iv = KeyTool.generate_random_bytes(16)
|
32
|
+
salt = KeyTool.generate_random_bytes(32)
|
33
|
+
|
34
|
+
case kdf_type
|
35
|
+
when T_PBKDF2
|
36
|
+
derived_key = PBKDF2.new(password: password, salt: salt, key_length: 32, iterations: 262144).value
|
37
|
+
when T_SCRYPT
|
38
|
+
derived_key = SCrypt::Engine.scrypt(password, salt, 8192, 8, 1, 32)
|
39
|
+
end
|
40
|
+
|
41
|
+
encrypt_key = derived_key[0..15]
|
42
|
+
|
43
|
+
cipher = OpenSSL::Cipher.new('aes-128-ctr')
|
44
|
+
cipher.encrypt
|
45
|
+
cipher.iv = iv
|
46
|
+
cipher.key = encrypt_key
|
47
|
+
cipher.padding = 0
|
48
|
+
|
49
|
+
ciphertext = cipher.update(Util.decode_hex(private_key)) + cipher.final
|
50
|
+
|
51
|
+
mac = generate_mac(derived_key, ciphertext)
|
52
|
+
|
53
|
+
datas = {address: address,
|
54
|
+
crypto: {
|
55
|
+
cipher: 'aes-128-ctr',
|
56
|
+
cipherparams: {'iv': Util.encode_hex(iv)},
|
57
|
+
ciphertext: Util.encode_hex(ciphertext),
|
58
|
+
kdf: kdf_type,
|
59
|
+
kdfparams: {n: 8192, c:262144, r:8, p:1, dklen: 32, salt: salt.bytes},
|
60
|
+
mac: mac
|
61
|
+
},
|
62
|
+
id: SecureRandom.uuid,
|
63
|
+
version: 3
|
64
|
+
}
|
65
|
+
|
66
|
+
datas.to_json
|
67
|
+
end
|
68
|
+
|
69
|
+
# decrypt_private_key
|
70
|
+
#
|
71
|
+
# Recovers the private key from a keystore file using the given passphrase.
|
72
|
+
#
|
73
|
+
# @param {KeystoreV3} encrypt_json
|
74
|
+
# @param {string} password
|
75
|
+
def decrypt_private_key(encrypt_json, password)
|
76
|
+
datas = JSON.parse(encrypt_json)
|
77
|
+
|
78
|
+
ciphertext = Util.decode_hex(datas['crypto']['ciphertext'])
|
79
|
+
iv = Util.decode_hex(datas['crypto']['cipherparams']['iv'])
|
80
|
+
kdfparams = datas['crypto']['kdfparams']
|
81
|
+
kdf_type = datas['crypto']['kdf']
|
82
|
+
|
83
|
+
case kdf_type
|
84
|
+
when T_PBKDF2
|
85
|
+
derived_key = PBKDF2.new(password: password, salt: kdfparams['salt'].pack('c*'), key_length: kdfparams['dklen'], iterations: kdfparams['c']).value
|
86
|
+
when T_SCRYPT
|
87
|
+
derived_key = SCrypt::Engine.scrypt(password, kdfparams['salt'].pack('c*'), kdfparams['n'], kdfparams['r'], kdfparams['p'], kdfparams['dklen'])
|
88
|
+
end
|
89
|
+
|
90
|
+
encrypt_key = derived_key[0..15]
|
91
|
+
|
92
|
+
mac = generate_mac(derived_key, ciphertext)
|
93
|
+
|
94
|
+
raise 'Failed to decrypt.' if mac.casecmp(datas['crypto']['mac']) != 0
|
95
|
+
|
96
|
+
cipher = OpenSSL::Cipher.new(datas['crypto']['cipher'])
|
97
|
+
cipher.decrypt
|
98
|
+
cipher.iv = iv
|
99
|
+
cipher.key = encrypt_key
|
100
|
+
cipher.padding = 0
|
101
|
+
|
102
|
+
private_key = cipher.update(ciphertext) + cipher.final
|
103
|
+
|
104
|
+
return Util.encode_hex private_key
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
def generate_mac(derived_key, ciphertext)
|
109
|
+
Digest::SHA256.hexdigest(derived_key[16..32] + ciphertext)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|