glueby 0.1.0 → 0.2.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.
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