laksa 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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