glueby 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-gemset +1 -1
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +3 -2
  5. data/README.md +27 -17
  6. data/glueby.gemspec +1 -1
  7. data/lib/generators/glueby/{initializer_generator.rb → contract/initializer_generator.rb} +0 -0
  8. data/lib/generators/glueby/contract/templates/initializer.rb.erb +3 -0
  9. data/lib/generators/glueby/contract/templates/key_table.rb.erb +15 -0
  10. data/lib/generators/glueby/{templates → contract/templates}/timestamp_table.rb.erb +2 -1
  11. data/lib/generators/glueby/contract/templates/utxo_table.rb.erb +15 -0
  12. data/lib/generators/glueby/contract/templates/wallet_table.rb.erb +10 -0
  13. data/lib/generators/glueby/contract/timestamp_generator.rb +26 -0
  14. data/lib/generators/glueby/contract/wallet_adapter_generator.rb +46 -0
  15. data/lib/glueby.rb +18 -1
  16. data/lib/glueby/contract.rb +3 -14
  17. data/lib/glueby/contract/active_record/timestamp.rb +8 -5
  18. data/lib/glueby/contract/errors.rb +6 -0
  19. data/lib/glueby/contract/payment.rb +54 -0
  20. data/lib/glueby/contract/timestamp.rb +39 -38
  21. data/lib/glueby/contract/token.rb +193 -0
  22. data/lib/glueby/contract/tx_builder.rb +197 -31
  23. data/lib/glueby/generator.rb +5 -0
  24. data/lib/glueby/generator/migrate_generator.rb +38 -0
  25. data/lib/glueby/internal.rb +6 -0
  26. data/lib/glueby/internal/rpc.rb +35 -0
  27. data/lib/glueby/internal/wallet.rb +122 -0
  28. data/lib/glueby/internal/wallet/abstract_wallet_adapter.rb +131 -0
  29. data/lib/glueby/internal/wallet/active_record.rb +15 -0
  30. data/lib/glueby/internal/wallet/active_record/key.rb +72 -0
  31. data/lib/glueby/internal/wallet/active_record/utxo.rb +50 -0
  32. data/lib/glueby/internal/wallet/active_record/wallet.rb +54 -0
  33. data/lib/glueby/internal/wallet/active_record_wallet_adapter.rb +133 -0
  34. data/lib/glueby/internal/wallet/errors.rb +11 -0
  35. data/lib/glueby/internal/wallet/tapyrus_core_wallet_adapter.rb +158 -0
  36. data/lib/glueby/version.rb +1 -1
  37. data/lib/glueby/wallet.rb +51 -0
  38. data/lib/tasks/glueby/contract/timestamp.rake +5 -5
  39. data/lib/tasks/glueby/contract/wallet_adapter.rake +42 -0
  40. metadata +30 -10
  41. data/lib/generators/glueby/templates/initializer.rb.erb +0 -4
  42. data/lib/generators/glueby/timestamp_generator.rb +0 -57
  43. data/lib/glueby/contract/rpc.rb +0 -15
@@ -0,0 +1,6 @@
1
+ module Glueby
2
+ module Internal
3
+ autoload :Wallet, 'glueby/internal/wallet'
4
+ autoload :RPC, 'glueby/internal/rpc'
5
+ end
6
+ end
@@ -0,0 +1,35 @@
1
+ module Glueby
2
+ module Internal
3
+ module RPC
4
+ module_function
5
+
6
+ def client
7
+ @rpc ||= Tapyrus::RPC::TapyrusCoreClient.new(@config)
8
+ end
9
+
10
+ def configure(config)
11
+ @config = config
12
+ end
13
+
14
+ # Perform RPC call on the specific wallet.
15
+ # This method needs block, and pass a client as as block argument. You can call RPCs on the wallet using the
16
+ # client object. See an example below.
17
+ # @param [string] wallet name on Tapyrus Core Wallet
18
+ # @return [Object] The return object of the block
19
+ #
20
+ # ## Example
21
+ # ```ruby
22
+ # perform_as('mywallet') do |client|
23
+ # client.getbalance
24
+ # end
25
+ # ```
26
+ def perform_as(wallet)
27
+ before = client.config[:wallet]
28
+ client.config[:wallet] = wallet
29
+ yield(client)
30
+ ensure
31
+ client.config[:wallet] = before
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,122 @@
1
+ module Glueby
2
+ module Internal
3
+ # # Glueby::Internal::Wallet
4
+ #
5
+ # This module provides the way to deal about wallet that includes key management, address management, getting UTXOs.
6
+ #
7
+ # ## How to use
8
+ #
9
+ # First, you need to configure which wallet implementation is used in Glueby::Internal::Wallet. For now, below wallets are
10
+ # supported.
11
+ #
12
+ # * [Tapyrus Core](https://github.com/chaintope/tapyrus-core)
13
+ #
14
+ # Here shows an example to use Tapyrus Core wallet.
15
+ #
16
+ # ```ruby
17
+ # # Setup Tapyrus Core RPC connection
18
+ # config = {schema: 'http', host: '127.0.0.1', port: 12381, user: 'user', password: 'pass'}
19
+ # Glueby::Internal::RPC.configure(config)
20
+ #
21
+ # # Setup wallet adapter
22
+ # Glueby::Internal::Wallet.wallet_adapter = Glueby::Internal::Wallet::TapyrusCoreWalletAdapter.new
23
+ #
24
+ # # Create wallet
25
+ # wallet = Glueby::Internal::Wallet.create
26
+ # wallet.balance # => 0
27
+ # wallet.list_unspent
28
+ # ```
29
+ class Wallet
30
+ autoload :AbstractWalletAdapter, 'glueby/internal/wallet/abstract_wallet_adapter'
31
+ autoload :AR, 'glueby/internal/wallet/active_record'
32
+ autoload :TapyrusCoreWalletAdapter, 'glueby/internal/wallet/tapyrus_core_wallet_adapter'
33
+ autoload :ActiveRecordWalletAdapter, 'glueby/internal/wallet/active_record_wallet_adapter'
34
+ autoload :Errors, 'glueby/internal/wallet/errors'
35
+
36
+ class << self
37
+ attr_writer :wallet_adapter
38
+
39
+ def create
40
+ new(wallet_adapter.create_wallet)
41
+ end
42
+
43
+ def load(wallet_id)
44
+ begin
45
+ wallet_adapter.load_wallet(wallet_id)
46
+ rescue Errors::WalletAlreadyLoaded => _
47
+ # Ignore when wallet is already loaded.
48
+ end
49
+ new(wallet_id)
50
+ end
51
+
52
+ def wallets
53
+ wallet_adapter.wallets.map { |id| new(id) }
54
+ end
55
+
56
+ def wallet_adapter
57
+ @wallet_adapter or
58
+ raise Errors::ShouldInitializeWalletAdapter, 'You should initialize wallet adapter using `Glueby::Internal::Wallet.wallet_adapter = some wallet adapter instance`.'
59
+ end
60
+ end
61
+
62
+ attr_reader :id
63
+
64
+ def initialize(wallet_id)
65
+ @id = wallet_id
66
+ end
67
+
68
+ def balance(only_finalized = true)
69
+ wallet_adapter.balance(id, only_finalized)
70
+ end
71
+
72
+ def list_unspent(only_finalized = true)
73
+ wallet_adapter.list_unspent(id, only_finalized)
74
+ end
75
+
76
+ def delete
77
+ wallet_adapter.delete_wallet(id)
78
+ end
79
+
80
+ def sign_tx(tx, prev_txs = [])
81
+ wallet_adapter.sign_tx(id, tx, prev_txs)
82
+ end
83
+
84
+ def broadcast(tx)
85
+ wallet_adapter.broadcast(id, tx)
86
+ end
87
+
88
+ def receive_address
89
+ wallet_adapter.receive_address(id)
90
+ end
91
+
92
+ def change_address
93
+ wallet_adapter.change_address(id)
94
+ end
95
+
96
+ def create_pubkey
97
+ wallet_adapter.create_pubkey(id)
98
+ end
99
+
100
+ def collect_uncolored_outputs(amount)
101
+ utxos = list_unspent
102
+
103
+ utxos.inject([0, []]) do |sum, output|
104
+ next sum if output[:color_id]
105
+
106
+ new_sum = sum[0] + output[:amount]
107
+ new_outputs = sum[1] << output
108
+ return [new_sum, new_outputs] if new_sum >= amount
109
+
110
+ [new_sum, new_outputs]
111
+ end
112
+ raise Glueby::Contract::Errors::InsufficientFunds
113
+ end
114
+
115
+ private
116
+
117
+ def wallet_adapter
118
+ self.class.wallet_adapter
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,131 @@
1
+ module Glueby
2
+ module Internal
3
+ class Wallet
4
+ # This is an abstract class for wallet adapters. If you want to use a wallet
5
+ # component with Glueby modules, you can use it by adding subclass of this abstract
6
+ # class for the wallet component.
7
+ class AbstractWalletAdapter
8
+ # Creates a new wallet inside the wallet component and returns `wallet_id`. The created
9
+ # wallet is loaded from at first.
10
+ # @return [String] wallet_id
11
+ def create_wallet
12
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
13
+ end
14
+
15
+ # Delete the wallet inside the wallet component.
16
+ # This method expect that the wallet will be removed completely
17
+ # in the wallet and it cannot restore. It is assumed that the main use-case of
18
+ # this method is for testing.
19
+ #
20
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
21
+ # @return [Boolean] The boolean value whether the deletion was success.
22
+ def delete_wallet(wallet_id)
23
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
24
+ end
25
+
26
+ # Load the wallet inside the wallet component.
27
+ # This method makes the wallet to possible to use on other methods like `balance`,
28
+ # `list_unspent`, etc. All the methods that gets `wallet_id` as a argument needs to
29
+ # load the wallet before use it.
30
+ # If the wallet component doesn't need such as load operation to prepare to use the
31
+ # wallet, `load_wallet` can be empty method.
32
+ #
33
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
34
+ # @raise [Glueby::Internal::Wallet::Errors::WalletAlreadyLoaded] when the specified wallet has been already loaded.
35
+ def load_wallet(wallet_id)
36
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
37
+ end
38
+
39
+ # Unload the wallet inside the wallet component.
40
+ #
41
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
42
+ # @return [Boolean] The boolean value whether the unloading was success.
43
+ def unload_wallet(wallet_id)
44
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
45
+ end
46
+
47
+ # Returns list of loaded wallet_id.
48
+ #
49
+ # @return [Array of String] - Array of wallet_id
50
+ def wallets
51
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
52
+ end
53
+
54
+ # Returns amount of tapyrus that the wallet has.
55
+ #
56
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
57
+ # @param [Boolean] only_finalized - The balance includes only finalized UTXO value if it
58
+ # is true. Default is true.
59
+ # @return [Integer] The balance of the wallet. The unit is 'tapyrus'.
60
+ # 1 TPC equals to 10^8 'tapyrus'.
61
+ def balance(wallet_id, only_finalized = true)
62
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
63
+ end
64
+
65
+ # Returns all the UTXOs that the wallet has.
66
+ #
67
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
68
+ # @param [Boolean] only_finalized - The UTXOs includes only finalized UTXO value if it
69
+ # is true. Default is true.
70
+ # @return [Array of UTXO]
71
+ #
72
+ # ## The UTXO structure
73
+ #
74
+ # - txid: [String] Transaction id
75
+ # - vout: [Integer] Output index
76
+ # - amount: [Integer] Amount of the UTXO as tapyrus unit
77
+ # - finalized: [Boolean] Whether the UTXO is finalized
78
+ def list_unspent(wallet_id, only_finalized = true)
79
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
80
+ end
81
+
82
+ # Sign to the transaction with a key in the wallet.
83
+ #
84
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
85
+ # @param [Tapyrus::Tx] tx - The transaction will be signed.
86
+ # @param [Array] prevtxs array of hash that represents unbroadcasted transaction outputs used by signing tx.
87
+ # Each hash has `txid`, `vout`, `scriptPubKey`, `amount` fields.
88
+ # @return [Tapyrus::Tx]
89
+ def sign_tx(wallet_id, tx, prevtxs = [])
90
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
91
+ end
92
+
93
+ # Broadcast the transaction to the Tapyrus Network.
94
+ #
95
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
96
+ # @param [Tapyrus::Tx] tx - The transaction to be broadcasterd.
97
+ # @return [String] txid
98
+ def broadcast(wallet_id, tx)
99
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
100
+ end
101
+
102
+ # Returns an address to receive coin.
103
+ #
104
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
105
+ # @return [String] P2PKH address
106
+ def receive_address(wallet_id)
107
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
108
+ end
109
+
110
+ # Returns an address to change coin.
111
+ #
112
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
113
+ # @return [String] P2PKH address
114
+ def change_address(wallet_id)
115
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
116
+ end
117
+
118
+ # Returns a new public key.
119
+ #
120
+ # This method is expected to returns a new public key. The key would generate internally. This key is provided
121
+ # for contracts that need public key such as multi sig transaction.
122
+ #
123
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
124
+ # @return [Tapyrus::Key]
125
+ def create_pubkey(wallet_id)
126
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ module Glueby
6
+ module Internal
7
+ class Wallet
8
+ module AR
9
+ autoload :Key, 'glueby/internal/wallet/active_record/key'
10
+ autoload :Utxo, 'glueby/internal/wallet/active_record/utxo'
11
+ autoload :Wallet, 'glueby/internal/wallet/active_record/wallet'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glueby
4
+ module Internal
5
+ class Wallet
6
+ module AR
7
+ class Key < ::ActiveRecord::Base
8
+ before_create :generate_key
9
+ before_create :set_script_pubkey
10
+
11
+ belongs_to :wallet
12
+
13
+ enum purpose: { receive: 0, change: 1 }
14
+
15
+ validates :purpose, presence: true
16
+ validates :private_key, uniqueness: { case_sensitive: false }
17
+
18
+ def to_p2pkh
19
+ Tapyrus::Script.to_p2pkh(Tapyrus.hash160(public_key))
20
+ end
21
+
22
+ def sign(data)
23
+ Tapyrus::Key.new(priv_key: self.private_key).sign(data, algo: :schnorr)
24
+ end
25
+
26
+ def address
27
+ to_p2pkh.addresses.first
28
+ end
29
+
30
+ # Return Glueby::Internal::Wallet::AR::Key object for output.
31
+ # If output is colored output, key is found by corresponding `uncolored` script.
32
+ #
33
+ # @param [Tapyrus::TxOut] output
34
+ # @return [Glueby::Internal::Wallet::AR::Key] key for output
35
+ def self.key_for_output(output)
36
+ key_for_script(output.script_pubkey)
37
+ end
38
+
39
+ # Return Glueby::Internal::Wallet::AR::Key object for script.
40
+ # If script is colored, key is found by corresponding `uncolored` script.
41
+ #
42
+ # @param [Tapyrus::Script] script_pubkey
43
+ # @return [Glueby::Internal::Wallet::AR::Key] key for input
44
+ def self.key_for_script(script_pubkey)
45
+ script_pubkey = if script_pubkey.colored?
46
+ script_pubkey.remove_color.to_hex
47
+ else
48
+ script_pubkey.to_hex
49
+ end
50
+ find_by(script_pubkey: script_pubkey)
51
+ end
52
+
53
+ private
54
+
55
+ def generate_key
56
+ key = if private_key
57
+ Tapyrus::Key.new(priv_key: private_key)
58
+ else
59
+ Tapyrus::Key.generate
60
+ end
61
+ self.private_key ||= key.priv_key
62
+ self.public_key ||= key.pubkey
63
+ end
64
+
65
+ def set_script_pubkey
66
+ self.script_pubkey = to_p2pkh.to_hex
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glueby
4
+ module Internal
5
+ class Wallet
6
+ module AR
7
+ class Utxo < ::ActiveRecord::Base
8
+ belongs_to :key
9
+
10
+ validates :txid, uniqueness: { scope: :index, case_sensitive: false }
11
+
12
+ enum status: { init: 0, broadcasted: 1, finalized: 2 }
13
+
14
+ def color_id
15
+ script = Tapyrus::Script.parse_from_payload(script_pubkey.htb)
16
+ script.color_id&.to_hex
17
+ end
18
+
19
+ # Delete utxo spent by specified tx.
20
+ #
21
+ # @param [Tapyrus::Tx] the spending tx
22
+ def self.destroy_for_inputs(tx)
23
+ tx.inputs.each do |input|
24
+ Utxo.destroy_by(txid: input.out_point.txid, index: input.out_point.index)
25
+ end
26
+ end
27
+
28
+ # Create utxo or update utxo for tx outputs
29
+ # if there is no key for script pubkey in an output, utxo for the output is not created.
30
+ #
31
+ # @param [Tapyrus::Tx] tx
32
+ def self.create_or_update_for_outputs(tx, status: :finalized)
33
+ tx.outputs.each.with_index do |output, index|
34
+ key = Key.key_for_output(output)
35
+ next unless key
36
+
37
+ utxo = Utxo.find_or_initialize_by(txid: tx.txid, index: index)
38
+ utxo.update!(
39
+ script_pubkey: output.script_pubkey.to_hex,
40
+ value: output.value,
41
+ status: status,
42
+ key: key
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ module Glueby
6
+ module Internal
7
+ class Wallet
8
+ module AR
9
+ class Wallet < ::ActiveRecord::Base
10
+ has_many :keys
11
+
12
+ validates :wallet_id, uniqueness: { case_sensitive: false }
13
+
14
+ # @param [Tapyrus::Tx] tx
15
+ # @param [Array] prevtxs array of outputs
16
+ def sign(tx, prevtxs = [])
17
+ tx.inputs.each.with_index do |input, index|
18
+ script_pubkey = script_for_input(input, prevtxs)
19
+ next unless script_pubkey
20
+ key = Key.key_for_script(script_pubkey)
21
+ next unless key
22
+ sign_tx_for_p2pkh(tx, index, key, script_pubkey)
23
+ end
24
+ tx
25
+ end
26
+
27
+ def utxos
28
+ Glueby::Internal::Wallet::AR::Utxo.where(key: keys)
29
+ end
30
+
31
+ private
32
+
33
+ def sign_tx_for_p2pkh(tx, index, key, script_pubkey)
34
+ sighash = tx.sighash_for_input(index, script_pubkey)
35
+ sig = key.sign(sighash) + [Tapyrus::SIGHASH_TYPE[:all]].pack('C')
36
+ script_sig = Tapyrus::Script.parse_from_payload(Tapyrus::Script.pack_pushdata(sig) + Tapyrus::Script.pack_pushdata(key.public_key.htb))
37
+ tx.inputs[index].script_sig = script_sig
38
+ end
39
+
40
+ def script_for_input(input, prevtxs = [])
41
+ out_point = input.out_point
42
+ utxo = Utxo.find_by(txid: out_point.txid, index: out_point.index)
43
+ if utxo
44
+ Tapyrus::Script.parse_from_payload(utxo.script_pubkey.htb)
45
+ else
46
+ output = prevtxs.select { |output| output[:txid] == out_point.txid && output[:vout] == out_point.index }.first
47
+ Tapyrus::Script.parse_from_payload(output[:scriptPubKey].htb) if output
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end