laksa 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,94 @@
1
+ # Laksa
2
+
3
+ Laksa -- Zilliqa Blockchain Ruby Library
4
+
5
+ The project is still under development.
6
+
7
+ ## Requirement
8
+
9
+ Ruby(2.5.3)
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'laksa'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install laksa
26
+
27
+ ## Usage
28
+
29
+ ### Generate A new address
30
+ ```ruby
31
+ private_key = Laksa::Crypto::KeyTool.generate_private_key
32
+ public_key = Laksa::Crypto::KeyTool.get_public_key_from_private_key(private_key)
33
+ address = Laksa::Crypto::KeyTool.get_address_from_private_key(private_key)
34
+ ```
35
+
36
+ ### Validate an address
37
+ ```ruby
38
+ address = '2624B9EA4B1CD740630F6BF2FEA82AAC0067070B'
39
+ Laksa::Util::Validator.address?(address)
40
+ ```
41
+
42
+ ### Validate checksum address
43
+ ```ruby
44
+ checksum_address = '0x4BAF5faDA8e5Db92C3d3242618c5B47133AE003C'
45
+ Laksa::Util::Validator.checksum_address?(checksum_address)
46
+ ```
47
+
48
+ ### Deploy and Call a transaction
49
+ ```ruby
50
+ private_key = "e19d05c5452598e24caad4a0d85a49146f7be089515c905ae6a19e8a578a6930"
51
+
52
+ provider = Laksa::Jsonrpc::Provider.new('https://dev-api.zilliqa.com')
53
+ wallet = Laksa::Account::Wallet.new(provider)
54
+ address = wallet.add_by_private_key(private_key)
55
+
56
+ factory = Laksa::Contract::ContractFactory.new(provider, wallet)
57
+
58
+ contract = factory.new_contract(TEST_CONTRACT, [
59
+ {
60
+ vname: 'owner',
61
+ type: 'ByStr20',
62
+ value: '0x124567890124567890124567890124567890',
63
+ },
64
+ ],
65
+ ABI,
66
+ )
67
+
68
+ deploy_params = Laksa::Contract::DeployParams.new(nil, Laksa::Util.pack(8, 8), nil, 1000, 1000, nil)
69
+ tx, deployed = contract.deploy(deploy_params)
70
+
71
+ assert tx.confirmed?
72
+ assert deployed.deployed?
73
+ assert_equal Laksa::Contract::ContractStatus::DEPLOYED, deployed.status
74
+
75
+ assert /[A-F0-9]+/ =~ contract.address
76
+
77
+ # call a deployed contract
78
+ call_tx = deployed.call(
79
+ 'setHello',
80
+ [
81
+ { vname: 'msg', type: 'String', value: 'Hello World!' },
82
+ ],
83
+ {
84
+ version: Laksa::Util.pack(8, 8),
85
+ amount: 0,
86
+ gasPrice: 1000,
87
+ gasLimit: 1000
88
+ })
89
+
90
+
91
+ receipt = call_tx.tx_params.receipt
92
+ ```
93
+
94
+ the definition of [TEST_CONTRACT](https://github.com/FireStack-Lab/LaksaRuby/blob/master/test/contract/test_contract.rb) and [ABI](https://github.com/FireStack-Lab/LaksaRuby/blob/master/test/contract/test_abi.rb) can be found in this folder.
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "laksa"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,46 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "laksa/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "laksa"
8
+ spec.version = Laksa::VERSION
9
+ spec.authors = ["cenyongh"]
10
+ spec.email = ["cenyongh@gmail.com"]
11
+
12
+ spec.summary = %q{LaksaRuby -- Zilliqa Blockchain Library}
13
+ spec.description = %q{LaksaRuby -- Zilliqa Blockchain Library}
14
+ spec.homepage = "https://github.com/FireStack-Lab/LaksaRuby"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = "https://github.com/FireStack-Lab/LaksaRuby"
22
+ spec.metadata["changelog_uri"] = "https://github.com/FireStack-Lab/LaksaRuby"
23
+ else
24
+ raise "RubyGems 2.0 or newer is required to protect against " \
25
+ "public gem pushes."
26
+ end
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
31
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ end
33
+ spec.bindir = "exe"
34
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.add_development_dependency "bundler", "~> 1.16"
38
+ spec.add_development_dependency "rake", "~> 10.0"
39
+ spec.add_development_dependency "minitest", "~> 5.0"
40
+ spec.add_dependency "bitcoin-secp256k1"
41
+ spec.add_dependency "scrypt"
42
+ spec.add_dependency "pbkdf2-ruby"
43
+ spec.add_dependency "jsonrpc-client"
44
+ spec.add_dependency 'google-protobuf'
45
+ spec.add_dependency 'bitcoin-ruby'
46
+ end
@@ -0,0 +1,21 @@
1
+ require "laksa/version"
2
+ require 'laksa/crypto/key_tool'
3
+ require 'laksa/crypto/key_store'
4
+ require 'laksa/crypto/schnorr'
5
+ require 'laksa/jsonrpc/provider'
6
+ require 'laksa/account/account'
7
+ require 'laksa/account/wallet'
8
+ require 'laksa/account/transaction_factory'
9
+ require 'laksa/account/transaction'
10
+ require 'laksa/proto/message_pb'
11
+ require 'laksa/contract/contract_factory'
12
+ require 'laksa/contract/contract'
13
+ require 'laksa/util/validator'
14
+ require 'laksa/util/util'
15
+ require 'laksa/util/unit'
16
+ require 'laksa/util/bech32'
17
+
18
+
19
+ module Laksa
20
+
21
+ end
@@ -0,0 +1,34 @@
1
+ module Laksa
2
+ module Account
3
+ class Account
4
+ attr_reader :private_key, :public_key, :address
5
+ def initialize(private_key)
6
+ @private_key = private_key
7
+ @public_key = Laksa::Crypto::KeyTool.get_public_key_from_private_key(private_key, true)
8
+ @address = Laksa::Crypto::KeyTool.get_address_from_public_key(@public_key)
9
+ end
10
+
11
+ # Takes a JSON-encoded keystore and passphrase, returning a fully
12
+ # instantiated Account instance.
13
+ def self.from_file(file, passphrase)
14
+ key_store = Laksa::Crypto::KeyStore.new
15
+ private_key = key_store.decrypt_private_key(file, passphrase)
16
+ Account.new(private_key)
17
+ end
18
+
19
+ # Convert an Account instance to a JSON-encoded keystore.
20
+ def to_file(passphrase, type)
21
+ key_store = Laksa::Crypto::KeyStore.new
22
+ json = key_store.encrypt_private_key(@private_key, passphrase, type);
23
+ end
24
+
25
+ # sign the passed in transaction with the account's private and public key
26
+ def sign_transaction(tx)
27
+ message = tx.bytes
28
+ message_hex = Util.encode_hex(message)
29
+
30
+ Laksa::Crypto::Schnorr.sign(message_hex, @private_key, @public_key)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,184 @@
1
+ module Laksa
2
+ module Account
3
+ #
4
+ # Transaction
5
+ #
6
+ # Transaction is a functor. Its purpose is to encode the possible states a
7
+ # Transaction can be in: Confirmed, Rejected, Pending, or Initialised (i.e., not broadcasted).
8
+ class Transaction
9
+ attr_accessor :id, :version, :nonce, :amount, :gas_price, :gas_limit, :signature, :receipt, :sender_pub_key, :to_addr, :code, :data, :to_ds
10
+ attr_accessor :provider, :status
11
+
12
+ GET_TX_ATTEMPTS = 33
13
+
14
+ def initialize(tx_params, provider, status = TxStatus::INITIALIZED, to_ds = false)
15
+ if tx_params
16
+ @version = tx_params.version;
17
+ @nonce = tx_params.nonce
18
+ @amount = tx_params.amount
19
+ @gas_price = tx_params.gas_price
20
+ @gas_limit = tx_params.gas_limit
21
+ @signature = tx_params.signature
22
+ @receipt = tx_params.receipt
23
+ @sender_pub_key = tx_params.sender_pub_key
24
+ @to_addr = tx_params.to_addr.downcase
25
+ @code = tx_params.code
26
+ @data = tx_params.data
27
+ end
28
+
29
+ @provider = provider
30
+ @status = status
31
+ @to_ds = to_ds
32
+ end
33
+
34
+ # constructs an already-confirmed transaction.
35
+ def self.confirm(tx_params, provider)
36
+ Transaction.new(tx_params, provider, TxStatus::CONFIRMED)
37
+ end
38
+
39
+ # constructs an already-rejected transaction.
40
+ def self.reject(tx_params, provider)
41
+ Transaction.new(tx_params, provider, TxStatus::REJECTED)
42
+ end
43
+
44
+ def bytes
45
+ protocol = Laksa::Proto::ProtoTransactionCoreInfo.new
46
+ protocol.version = self.version
47
+ protocol.nonce = self.nonce
48
+ protocol.toaddr = Util.decode_hex(self.to_addr.downcase.sub('0x',''))
49
+ protocol.senderpubkey = Laksa::Proto::ByteArray.new(data: Util.decode_hex(self.sender_pub_key))
50
+
51
+ raise 'standard length exceeded for value' if self.amount.to_i > 2 ** 128 - 1
52
+
53
+ protocol.amount = Laksa::Proto::ByteArray.new(data: bigint_to_bytes(self.amount.to_i))
54
+ protocol.gasprice = Laksa::Proto::ByteArray.new(data: bigint_to_bytes(self.gas_price.to_i))
55
+ protocol.gaslimit = self.gas_limit
56
+ protocol.code = self.code if self.code
57
+ protocol.data = self.data if self.data
58
+
59
+ Laksa::Proto::ProtoTransactionCoreInfo.encode(protocol)
60
+ end
61
+
62
+ def tx_params
63
+ tx_params = TxParams.new
64
+
65
+ tx_params.id = self.id
66
+ tx_params.version = self.version
67
+ tx_params.nonce = self.nonce
68
+ tx_params.amount = self.amount
69
+ tx_params.gas_price = self.gas_price
70
+ tx_params.gas_limit = self.gas_limit
71
+ tx_params.signature = self.signature
72
+ tx_params.receipt = self.receipt
73
+ tx_params.sender_pub_key = self.sender_pub_key
74
+ tx_params.to_addr = Wallet.to_checksum_address(self.to_addr)
75
+ tx_params.code = self.code
76
+ tx_params.data = self.data
77
+
78
+ tx_params
79
+ end
80
+
81
+ def to_payload
82
+ {
83
+ version: self.version.to_i,
84
+ nonce: self.nonce.to_i,
85
+ toAddr: Wallet.to_checksum_address(self.to_addr),
86
+ amount: self.amount,
87
+ pubKey: self.sender_pub_key,
88
+ gasPrice: self.gas_price,
89
+ gasLimit: self.gas_limit,
90
+ code: self.code,
91
+ data: self.data,
92
+ signature: self.signature
93
+ }
94
+ end
95
+
96
+ def pending?
97
+ @status == TxStatus::PENDING
98
+ end
99
+
100
+ def initialised?
101
+ @status === TxStatus::INITIALIZED
102
+ end
103
+
104
+ def confirmed?
105
+ @status === TxStatus::CONFIRMED;
106
+ end
107
+
108
+ def rejected?
109
+ @status === TxStatus::REJECTED;
110
+ end
111
+
112
+ # This sets the Transaction instance to a state
113
+ # of pending. Calling this function kicks off a passive loop that polls the
114
+ # lookup node for confirmation on the txHash.
115
+ #
116
+ # The polls are performed with a linear backoff:
117
+ #
118
+ # This is a low-level method that you should generally not have to use
119
+ # directly.
120
+ def confirm(tx_hash, max_attempts = GET_TX_ATTEMPTS, interval = 1)
121
+ @status = TxStatus::PENDING
122
+ 1.upto(max_attempts) do
123
+ if self.track_tx(tx_hash)
124
+ return self
125
+ else
126
+ sleep(interval)
127
+ end
128
+ end
129
+
130
+ self.status = TxStatus::REJECTED
131
+ throw 'The transaction is still not confirmed after ${maxAttempts} attempts.'
132
+ end
133
+
134
+ def track_tx(tx_hash)
135
+ puts "tracking transaction: #{tx_hash}"
136
+
137
+ begin
138
+ response = @provider.GetTransaction(tx_hash)
139
+ rescue Exception => e
140
+ puts "transaction not confirmed yet"
141
+ puts e
142
+ end
143
+
144
+ if response['error']
145
+ puts "transaction not confirmed yet"
146
+ return false;
147
+ end
148
+
149
+ self.id = response['result']['ID']
150
+ self.receipt = response['result']['receipt']
151
+ self.receipt['cumulative_gas'] = response['result']['receipt']['cumulative_gas'].to_i
152
+
153
+ if self.receipt && self.receipt['success']
154
+ puts "Transaction confirmed!"
155
+ self.status = TxStatus::CONFIRMED
156
+ else
157
+ puts "Transaction rejected!"
158
+ self.status = TxStatus::REJECTED
159
+ end
160
+
161
+ true
162
+ end
163
+
164
+ private
165
+ def bigint_to_bytes(value)
166
+ raise 'standard length exceeded for value' if value > 2 ** 128 - 1
167
+ bs = [value / (2 ** 64), value % (2 ** 64)].pack('Q>*')
168
+ end
169
+ end
170
+
171
+ class TxParams
172
+ attr_accessor :id, :version, :nonce, :amount, :gas_price, :gas_limit, :signature, :receipt, :sender_pub_key, :to_addr, :code, :data
173
+ def initialize
174
+ end
175
+ end
176
+
177
+ class TxStatus
178
+ INITIALIZED = 0
179
+ PENDING = 1
180
+ CONFIRMED = 2
181
+ REJECTED = 3
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,16 @@
1
+ module Laksa
2
+ module Account
3
+ class TransactionFactory
4
+ attr_reader :provider, :signer
5
+
6
+ def initialize(provider, signer)
7
+ @provider = provider
8
+ @signer = signer
9
+ end
10
+
11
+ def new(tx_params, to_ds = false)
12
+ Transaction.new(tx_params, @provider, TxStatus::INITIALIZED, to_ds)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,129 @@
1
+ require 'digest'
2
+
3
+ module Laksa
4
+ module Account
5
+ class Wallet
6
+ # Takes an array of Account objects and instantiates a Wallet instance.
7
+ def initialize(provider = nil, accounts = {})
8
+ @provider = provider
9
+ @accounts = accounts
10
+ if accounts.length > 0
11
+ @default_account = accounts[0]
12
+ else
13
+ @default_account = nil
14
+ end
15
+ end
16
+
17
+ # Creates a new keypair with a randomly-generated private key. The new
18
+ # account is accessible by address.
19
+ def create
20
+ private_key = Laksa::Crypto::KeyTool.generate_private_key
21
+ account = Laksa::Account::Account.new(private_key)
22
+
23
+ @accounts[account.address] = account
24
+
25
+ @default_account = account unless @default_account
26
+
27
+ account.address
28
+ end
29
+
30
+ # Adds an account to the wallet by private key.
31
+ def add_by_private_key(private_key)
32
+ account = Laksa::Account::Account.new(private_key)
33
+
34
+ @accounts[account.address] = account
35
+
36
+ @default_account = account unless @default_account
37
+
38
+ account.address
39
+ end
40
+
41
+
42
+ # Adds an account by keystore
43
+ def add_by_keystore(keystore, passphrase)
44
+ account = Laksa::Account::Account.from_file(keystore, passphrase)
45
+
46
+ @accounts[account.address] = account
47
+
48
+ @default_account = account unless @default_account
49
+
50
+ account.address
51
+ end
52
+
53
+ # Removes an account from the wallet and returns boolean to indicate
54
+ # failure or success.
55
+
56
+ def remove(address)
57
+ if @accounts.has_key?(address)
58
+ @accounts.delete(address)
59
+
60
+ true
61
+ else
62
+ false
63
+ end
64
+ end
65
+
66
+ # Sets the default account of the wallet.
67
+ def set_default(address)
68
+ @default_account = @accounts[address]
69
+ end
70
+
71
+ # to_checksum_address
72
+ #
73
+ # takes hex-encoded string and returns the corresponding address
74
+ #
75
+ # @param {string} address
76
+ # @returns {string}
77
+ def self.to_checksum_address(address)
78
+ address = address.downcase.gsub('0x', '')
79
+
80
+ s1 = Digest::SHA256.hexdigest(Util.decode_hex(address))
81
+ v = s1.to_i(base=16)
82
+
83
+ ret = ['0x']
84
+ address.each_char.each_with_index do |c, idx|
85
+ if '1234567890'.include?(c)
86
+ ret << c
87
+ else
88
+ ret << ((v & (2 ** (255 - 6 * idx))) < 1 ? c.downcase : c.upcase)
89
+ end
90
+ end
91
+
92
+ ret.join
93
+ end
94
+
95
+ # signs an unsigned transaction with the default account.
96
+ def sign(tx)
97
+ tx_params = tx.tx_params
98
+ if tx_params.sender_pub_key
99
+ # attempt to find the address
100
+ address = Laksa::Crypto::KeyTool.get_address_from_public_key(tx_params.sender_pub_key)
101
+ account = @accounts[address]
102
+ raise 'Could not sign the transaction with address as it does not exist' unless account
103
+
104
+ self.sign_with(tx, address)
105
+ else
106
+ raise 'This wallet has no default account.' unless @default_account
107
+
108
+ self.sign_with(tx, @default_account.address)
109
+ end
110
+ end
111
+
112
+ def sign_with(tx, address)
113
+ account = @accounts[address]
114
+
115
+ raise 'The selected account does not exist on this Wallet instance.' unless account
116
+
117
+ if tx.nonce == nil
118
+ result = @provider.GetBalance(account.address)
119
+ tx.nonce = result['nonce'].to_i + 1
120
+ end
121
+
122
+ tx.sender_pub_key = account.public_key
123
+ sig = account.sign_transaction(tx)
124
+ tx.signature = sig.to_s
125
+ tx
126
+ end
127
+ end
128
+ end
129
+ end