webcash-rb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 95aab40d012c2dcfedd0bc7f300e73c0400b897f59265e03c9ddcea86c91349a
4
+ data.tar.gz: a686a4cc24dafecfc57ef967d1ce862c9e309ab0bd39a98de4a9ec6fae00a093
5
+ SHA512:
6
+ metadata.gz: eb4f02e2446599c0b513c56e020cd16c71ec9242e5bb5f42dbf2fc19634ccecdc8104031a65dcb416dce0cf6ae73294a4935a8948083ed881e24caf683561399
7
+ data.tar.gz: '09fc45f8561423df3abee27795912e9abcab7e667d89f15cf78eaa56e095f28ee18450b7cb1e2963fee2f72124ddebbf0836fda138bb5ed98ebe6cfa08a43f07'
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ inherit_gem:
2
+ rubocop-rails-omakase: rubocop.yml
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-09-06
4
+
5
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 acidtib
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # webcash-rb
2
+
3
+ Webcash is an experimental electronic cash library for decentralized payments.
4
+ Webcash facilitates decentralized, peer-to-peer electronic cash transactions. It allows users to send webcash directly to one another and includes mechanisms for detecting double-spending and maintaining monetary supply integrity.
5
+
6
+ Navigate to https://webcash.org/ for more information, including the Terms of Service.
7
+
8
+ ## Installation
9
+
10
+ Install the gem and add to the application's Gemfile by executing:
11
+
12
+ ```bash
13
+ bundle add webcash-rb
14
+ ```
15
+
16
+ If bundler is not being used to manage dependencies, install the gem by executing:
17
+
18
+ ```bash
19
+ gem install webcash-rb
20
+ ```
21
+
22
+ ## Usage
23
+ ```ruby
24
+ require "webcash"
25
+
26
+ wallet = Webcash::Wallet.new(
27
+ master_secret: "19be5ea41c71836f78b5691fe69af918a5f003719b7a7e2a30533737c04521fc",
28
+ )
29
+
30
+ # Set the legal agreements to true
31
+ wallet.set_legal_agreements_to_true
32
+
33
+ # Recover webcash using the wallet's master secret.
34
+ wallet.recover
35
+
36
+ # Check webcash in wallet. Remove any spent webcash.
37
+ wallet.check
38
+
39
+ # Insert webcash into the wallet.
40
+ wallet.insert("e20:secret:8c2e565a1649b03052833c508b237bacd2c62995c7e8ed5dc9fe644850808192")
41
+
42
+ # Pay webcash from the wallet.
43
+ wallet.pay(4.20)
44
+
45
+ # Get wallet balance
46
+ pp wallet.get_balance
47
+ ```
48
+
49
+ ## Development
50
+
51
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
52
+
53
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
54
+
55
+ ## Contributing
56
+
57
+ Bug reports and pull requests are welcome on GitHub at https://github.com/acidtib/webcash-rb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/acidtib/webcash-rb/blob/master/CODE_OF_CONDUCT.md).
58
+
59
+ ## License
60
+
61
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
62
+
63
+ ## Code of Conduct
64
+
65
+ Everyone interacting in the Webcash project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/acidtib/webcash-rb/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/webcash/helpers.rb
4
+ require "bigdecimal"
5
+ require "securerandom"
6
+ require "base64"
7
+ require "digest"
8
+
9
+ module Webcash
10
+ # Provides helper methods for Webcash.
11
+ module Helpers
12
+ def self.range(start, stop, step = 1)
13
+ return [] if (step.positive? && start >= stop) || (step.negative? && start <= stop)
14
+
15
+ result = []
16
+ i = start
17
+ while step.positive? ? i < stop : i > stop
18
+ result << i
19
+ i += step
20
+ end
21
+
22
+ result
23
+ end
24
+
25
+ def self.chunk_array(array, chunk_size)
26
+ array.each_slice(chunk_size).to_a
27
+ end
28
+
29
+ # Check that the amount has no more than a maximum number of decimal places.
30
+ def self.validate_amount_decimals(amount)
31
+ if amount.is_a?(String)
32
+ amount = parse_amount_from_string(amount)
33
+ elsif amount.is_a?(Float)
34
+ amount = BigDecimal(amount.to_s)
35
+ else
36
+ amount = BigDecimal(amount)
37
+ end
38
+
39
+ raise RangeError, "Amount precision should be at most 8 decimals." unless amount.scale <= 8
40
+
41
+ true
42
+ end
43
+
44
+ def self.deserialize_webcash(webcash)
45
+ raise Error, "Unusable format for webcash." unless webcash.include?(":")
46
+
47
+ parts = webcash.split(":")
48
+ raise Error, "Don't know how to deserialize this webcash." if parts.length < 2
49
+
50
+ amount_raw = parts[0]
51
+ public_or_secret = parts[1]
52
+ value = parts[2]
53
+
54
+ raise Error, "Can't deserialize this webcash, value is missing." if value.nil?
55
+
56
+ unless %w[public secret].include?(public_or_secret)
57
+ raise Error, "Can't deserialize this webcash, needs to be either public/secret."
58
+ end
59
+
60
+ amount = parse_amount_from_string(amount_raw)
61
+
62
+ if public_or_secret == "secret"
63
+ Webcash::Secret.new(amount, value)
64
+ else
65
+ Webcash::Public.new(amount, value)
66
+ end
67
+ end
68
+
69
+ def self.parse_amount_from_string(amount_raw)
70
+ # If there is a colon in the value, then the amount is going to be on the
71
+ # left hand side.
72
+ part1 = amount_raw.split(":")[0]
73
+
74
+ # There can be at most one 'e' in the value, at the beginning.
75
+ count = part1.count("e")
76
+ if count.zero?
77
+ BigDecimal(part1)
78
+ elsif count <= 1
79
+ # should be at the beginning
80
+ raise Error, "Invalid amount format for webcash." unless part1[0] == "e"
81
+ # there needs to be an actual amount
82
+ raise Error, "Invalid amount format for webcash." unless part1 != "e"
83
+
84
+ part2 = part1.split("e")[1]
85
+ BigDecimal(part2)
86
+
87
+ else
88
+ raise Error, "Invalid amount format for webcash."
89
+ end
90
+ end
91
+
92
+ def self.convert_secret_value_to_public_value(secret_value)
93
+ Digest::SHA256.hexdigest(secret_value)
94
+ end
95
+
96
+ # Convert from a string amount to a Decimal value.
97
+ def self.string_amount_to_decimal(amount)
98
+ BigDecimal(amount)
99
+ end
100
+
101
+
102
+ # Convert a decimal amount to a string. This is used for representing
103
+ # different webcash when serializing webcash. When the amount is not known,
104
+ # the string should be "?".
105
+ def self.decimal_amount_to_string(amount)
106
+ return "?" if amount.nil?
107
+
108
+ if amount.frac.zero?
109
+ amount.to_i.to_s
110
+ else
111
+ # Force 8 decimals and trim trailing zeros
112
+ amount_str = format("%.8f", amount)
113
+ amount_str.sub(/\.?0+$/, "")
114
+ end
115
+ end
116
+
117
+ def self.create_webcash_with_random_secret_from_amount(amount)
118
+ "e#{amount.to_s('F')}:secret:#{generate_random_value(32)}"
119
+ end
120
+
121
+ def self.hex_to_padded_bytes(hex, padding_target_length = 32)
122
+ bytes = [ hex.sub(/^0x/, "") ].pack("H*").bytes
123
+ padded_bytes = Array.new(padding_target_length - bytes.length, 0) + bytes
124
+ padded_bytes
125
+ end
126
+
127
+ def self.convert_secret_hex_to_bytes(secret)
128
+ hex_to_padded_bytes(secret)
129
+ end
130
+
131
+ def self.hex_to_bytes(hex)
132
+ hex = hex.sub(/^0x/i, "")
133
+ hex.scan(/../).map { |x| x.hex }
134
+ end
135
+
136
+ def self.padded_bytes(bytes, padding_target_length = 32)
137
+ if bytes.length == padding_target_length
138
+ bytes
139
+ elsif bytes.length > padding_target_length
140
+ raise "Can only handle up to #{padding_target_length} bytes, int too big to convert"
141
+ else
142
+ padding_needed = padding_target_length - bytes.length
143
+ Array.new(padding_needed, 0) + bytes
144
+ end
145
+ end
146
+
147
+ def self.long_to_byte_array(num)
148
+ byte_array = Array.new(8, 0)
149
+
150
+ (0...byte_array.length).each do |index|
151
+ byte_array[index] = num & 0xff
152
+ num >>= 8
153
+ end
154
+
155
+ byte_array
156
+ end
157
+
158
+ def self.assert_is_array(input)
159
+ unless input.is_a?(Array) && input.all? { |e| e.is_a?(Integer) }
160
+ raise "This method only supports number arrays but input was: #{input}"
161
+ end
162
+ end
163
+
164
+ def self.sha256_from_array(array)
165
+ assert_is_array(array)
166
+
167
+ # Convert the array of integers to a binary string
168
+ binary_string = array.pack("C*")
169
+
170
+ # Create and return the SHA256 digest
171
+ Digest::SHA256.digest(binary_string)
172
+ end
173
+
174
+ def self.generate_random_value(length)
175
+ (1..length).map { rand(16).to_s(16) }.join
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/webcash/public.rb
4
+
5
+ module Webcash
6
+ class Public
7
+ attr_reader :amount, :hashed_value
8
+
9
+ def initialize(amount, hashed_value)
10
+ Webcash::Helpers.validate_amount_decimals(amount)
11
+ @amount = amount
12
+ @hashed_value = hashed_value
13
+ end
14
+
15
+ def self.deserialize(webcash)
16
+ Webcash::Helpers.deserialize_webcash(webcash)
17
+ end
18
+
19
+ def is_equal(other)
20
+ if other.is_a?(Webcash::Secret)
21
+ return true if @hashed_value == other.to_public.hashed_value
22
+ elsif other.is_a?(Webcash::Public)
23
+ return true if @hashed_value == other.hashed_value
24
+ end
25
+
26
+ false
27
+ end
28
+
29
+ def to_s
30
+ "e#{@amount.to_s('F').sub(/\.0+$/, '')}:public:#{@hashed_value}"
31
+ end
32
+
33
+ def ==(other)
34
+ other.is_a?(Webcash::Public) && @amount == other.amount && @hashed_value == other.hashed_value
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/webcash/secret.rb
4
+
5
+ module Webcash
6
+ class Secret
7
+ attr_accessor :amount
8
+ attr_reader :amount, :secret_value
9
+
10
+ def initialize(amount, secret_value)
11
+ Webcash::Helpers.validate_amount_decimals(amount)
12
+ @amount = amount
13
+ @secret_value = secret_value
14
+ end
15
+
16
+ def self.deserialize(webcash)
17
+ Webcash::Helpers.deserialize_webcash(webcash)
18
+ end
19
+
20
+ def self.from_amount(amount)
21
+ Webcash::Secret.deserialize(Webcash::Helpers.create_webcash_with_random_secret_from_amount(amount))
22
+ end
23
+
24
+ def is_equal(other)
25
+ if other.is_a?(Webcash::Secret)
26
+ return true if @secret_value == other.secret_value
27
+ elsif other.is_a?(Webcash::Public)
28
+ return true if to_public.hashed_value == other.hashed_value
29
+ end
30
+
31
+ false
32
+ end
33
+
34
+ def to_s
35
+ "e#{@amount.to_s('F').sub(/\.0+$/, '')}:secret:#{@secret_value}"
36
+ end
37
+
38
+ def ==(other)
39
+ other.is_a?(Webcash::Secret) && @amount == other.amount && @secret_value == other.secret_value
40
+ end
41
+
42
+ def to_public
43
+ hashed_value = Webcash::Helpers.convert_secret_value_to_public_value(@secret_value)
44
+ Webcash::Public.new(@amount, hashed_value)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Webcash
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,464 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/webcash/wallet.rb
4
+
5
+ require "httparty"
6
+ require "json"
7
+
8
+ module Webcash
9
+ class Wallet
10
+ attr_accessor :version,
11
+ :legalese,
12
+ :webcash,
13
+ :unconfirmed,
14
+ :log,
15
+ :master_secret,
16
+ :walletdepths
17
+
18
+ DEFAULT_WALLETDEPTHS = {
19
+ "RECEIVE" => 0,
20
+ "PAY" => 0,
21
+ "CHANGE" => 0,
22
+ "MINING" => 0
23
+ }
24
+
25
+ DEFAULT_LEGALESE = { "terms" => nil }
26
+
27
+ CHAIN_CODES = {
28
+ "RECEIVE" => 0,
29
+ "PAY" => 1,
30
+ "CHANGE" => 2,
31
+ "MINING" => 3
32
+ }
33
+
34
+ API_REPLACE = "https://webcash.org/api/v1/replace"
35
+ API_HEALTHCHECK = "https://webcash.org/api/v1/health_check"
36
+
37
+ def initialize(
38
+ version: "1.0",
39
+ legalese: DEFAULT_LEGALESE,
40
+ webcash: [],
41
+ unconfirmed: [],
42
+ log: [],
43
+ master_secret: "",
44
+ walletdepths: DEFAULT_WALLETDEPTHS
45
+ )
46
+ @version = version
47
+ @legalese = legalese
48
+ @webcash = webcash
49
+ @unconfirmed = unconfirmed
50
+ @log = log
51
+ @master_secret = master_secret.empty? ? Webcash::Helpers.generate_random_value(32) : master_secret
52
+ @walletdepths = walletdepths
53
+ end
54
+
55
+ # Check that the legal agreements have been agreed to and acknowledged.
56
+ def check_legal_agreements
57
+ @legalese["terms"] == true
58
+ end
59
+
60
+ # Set the legal agreements to true
61
+ def set_legal_agreements_to_true
62
+ @legalese["terms"] = true
63
+ end
64
+
65
+ # Get all contents of the wallet
66
+ def get_contents
67
+ {
68
+ master_secret: @master_secret,
69
+ walletdepths: @walletdepths,
70
+ webcash: @webcash,
71
+ unconfirmed: @unconfirmed,
72
+ log: @log,
73
+ version: @version,
74
+ legalese: @legalese
75
+ }
76
+ end
77
+
78
+ # Calculate the balance based on the webcash in the wallet
79
+ def get_balance
80
+ @webcash
81
+ .map { |n| Webcash::Secret.deserialize(n).amount }
82
+ .reduce(BigDecimal("0")) { |prev, next_val| prev + next_val }
83
+ end
84
+
85
+ # Generate the next secret based on chain code and seek value
86
+ def generate_next_secret(chain_code, seek = false)
87
+ walletdepth = seek ? seek : @walletdepths[chain_code]
88
+
89
+ master_secret = @master_secret
90
+ master_secret_bytes = Webcash::Helpers.convert_secret_hex_to_bytes(master_secret)
91
+
92
+ chain_coded = CHAIN_CODES[chain_code]
93
+ raise ArgumentError, "Invalid chain code" if chain_coded.nil?
94
+
95
+ # Tag as byte array
96
+ tag = Webcash::Helpers.sha256_from_array([ 119, 101, 98, 99, 97, 115, 104, 119, 97, 108, 108, 101, 116, 118, 49 ])
97
+
98
+ array = []
99
+ tag_numbers = tag.unpack("C*")
100
+ array.concat(tag_numbers)
101
+ array.concat(tag_numbers)
102
+ array.concat(master_secret_bytes)
103
+ array.concat(Webcash::Helpers.long_to_byte_array(chain_coded).reverse)
104
+ array.concat(Webcash::Helpers.long_to_byte_array(walletdepth).reverse)
105
+
106
+ new_secret = Webcash::Helpers.sha256_from_array(array)
107
+
108
+ new_hex_secret = new_secret.unpack1("H*") # Convert binary data to hex string
109
+
110
+ # Update wallet depths if seek is false
111
+ unless seek
112
+ @walletdepths[chain_code] += 1
113
+ end
114
+
115
+ new_hex_secret
116
+ end
117
+
118
+ # Insert webcash into the wallet. Replace the given webcash with new webcash.
119
+ def insert(webcash, memo = "")
120
+ # Deserialize the given webcash if it's a string
121
+ if webcash.is_a?(String)
122
+ webcash = Webcash::Secret.deserialize(webcash)
123
+ end
124
+
125
+ # Create a new secret webcash
126
+ new_webcash = Webcash::Secret.new(webcash.amount, generate_next_secret("RECEIVE"))
127
+
128
+ # Check if the legal agreements have been accepted
129
+ unless check_legal_agreements
130
+ raise "User hasn't agreed to the legal terms."
131
+ end
132
+
133
+ # Prepare the replacement request body
134
+ replace_request_body = {
135
+ webcashes: [ webcash.to_s ],
136
+ new_webcashes: [ new_webcash.to_s ],
137
+ legalese: @legalese
138
+ }
139
+
140
+ # Save the new webcash into the wallet so the value isn't lost if there's a network error
141
+ new_webcash_str = new_webcash.to_s
142
+ @unconfirmed.push(new_webcash_str)
143
+
144
+ # Execute the replacement request
145
+ begin
146
+ # Make the POST request using HTTParty
147
+ response = HTTParty.post(API_REPLACE, body: JSON.dump(replace_request_body), headers: { "Content-Type" => "application/json" })
148
+
149
+ # Log the response
150
+ puts "After replace API call. Response = #{response.body}"
151
+
152
+ # Check if the response was successful
153
+ unless response.success?
154
+ raise "Server returned an error: #{response.body}"
155
+ end
156
+
157
+ rescue => e
158
+ # Handle network or other exceptions, log them, and raise
159
+ puts "Could not successfully call replacement API"
160
+ raise e
161
+ end
162
+
163
+ # Handle existing webcash
164
+ existing_webcash_str = @webcash.find { |w| w == webcash.to_s }
165
+ if existing_webcash_str
166
+ # Replace existing webcash with new webcash
167
+ @webcash.reject! { |item| item == existing_webcash_str }
168
+ end
169
+
170
+ # Remove from unconfirmed
171
+ @unconfirmed.reject! { |item| item == new_webcash_str }
172
+
173
+ # Add the new webcash to the wallet
174
+ @webcash.push(new_webcash.to_s)
175
+
176
+ # Log the operation
177
+ @log.push({
178
+ type: "insert",
179
+ amount: Webcash::Helpers.decimal_amount_to_string(new_webcash.amount),
180
+ webcash: webcash.to_s,
181
+ new_webcash: new_webcash_str,
182
+ memo: memo,
183
+ timestamp: Time.now.to_i.to_s
184
+ })
185
+
186
+ # Return the new webcash
187
+ new_webcash.to_s
188
+ end
189
+
190
+ def pay(amount, memo = "")
191
+ amount = BigDecimal(amount.to_s)
192
+
193
+ # Check legal agreements
194
+ raise "User hasn't agreed to the legal terms." unless check_legal_agreements
195
+
196
+ have_enough = false
197
+ input_webcash = []
198
+
199
+ # Try to satisfy the request with a single payment that matches the size
200
+ @webcash.each do |webcash_str|
201
+ webcash = Webcash::Secret.deserialize(webcash_str)
202
+ if webcash.amount >= amount
203
+ input_webcash.push(webcash)
204
+ have_enough = true
205
+ break
206
+ end
207
+ end
208
+
209
+ unless have_enough
210
+ running_amount = BigDecimal("0")
211
+ running_webcash = []
212
+
213
+ @webcash.each do |webcash_str|
214
+ webcash = Webcash::Secret.deserialize(webcash_str)
215
+ running_amount += webcash.amount
216
+ running_webcash.push(webcash)
217
+ if running_amount >= amount
218
+ input_webcash = running_webcash
219
+ have_enough = true
220
+ break
221
+ end
222
+ end
223
+ end
224
+
225
+ unless have_enough
226
+ raise "Wallet does not have enough funds to make the transfer."
227
+ end
228
+
229
+ found_amount = input_webcash.sum(&:amount)
230
+ change_amount = found_amount - amount
231
+
232
+ new_webcash = []
233
+ if change_amount > BigDecimal("0")
234
+ change_webcash = Webcash::Secret.new(change_amount, generate_next_secret("CHANGE"))
235
+ new_webcash.push(change_webcash.to_s)
236
+ end
237
+
238
+ transfer_webcash = Webcash::Secret.new(amount, generate_next_secret("PAY"))
239
+ new_webcash.push(transfer_webcash.to_s)
240
+
241
+ # Prepare the replacement request body
242
+ replace_request_body = {
243
+ webcashes: input_webcash.map(&:to_s),
244
+ new_webcashes: new_webcash,
245
+ legalese: @legalese
246
+ }
247
+
248
+ # Save the new webcash into the wallet
249
+ @unconfirmed.push(transfer_webcash.to_s)
250
+ @unconfirmed.push(change_webcash.to_s) if change_webcash
251
+
252
+ # Execute the replacement request
253
+ begin
254
+ response = HTTParty.post(API_REPLACE, body: JSON.dump(replace_request_body), headers: { "Content-Type" => "application/json" })
255
+
256
+ raise "Server returned an error: #{response.body}" unless response.success?
257
+ rescue => e
258
+ puts "Could not successfully call the replacement API"
259
+ raise e
260
+ end
261
+
262
+ # Remove the webcash from the wallet
263
+ @webcash.reject! { |item| replace_request_body[:webcashes].include?(item) }
264
+ @unconfirmed.reject! { |item| item == transfer_webcash.to_s || item == change_webcash.to_s }
265
+
266
+ # Record change
267
+ if change_webcash
268
+ @webcash.push(change_webcash.to_s)
269
+ @log.push({
270
+ type: "change",
271
+ amount: Webcash::Helpers.decimal_amount_to_string(change_amount),
272
+ webcash: change_webcash.to_s,
273
+ timestamp: Time.now.to_i.to_s
274
+ })
275
+ end
276
+
277
+ # Record payment
278
+ @log.push({
279
+ type: "payment",
280
+ amount: Webcash::Helpers.decimal_amount_to_string(transfer_webcash.amount),
281
+ webcash: transfer_webcash.to_s,
282
+ memo: memo,
283
+ timestamp: Time.now.to_i.to_s
284
+ })
285
+
286
+ # Return the transfer webcash
287
+ transfer_webcash.to_s
288
+ end
289
+
290
+ def process_healthcheck_results(results, webcashes_map = {})
291
+ results.each do |public_webcash, result|
292
+ hashed_value = Webcash::Public.deserialize(public_webcash).hashed_value
293
+ wallet_cash = Webcash::Secret.deserialize(webcashes_map[hashed_value])
294
+
295
+ if result["spent"] == false
296
+ # Check the amount.
297
+ result_amount = BigDecimal(result["amount"].to_s)
298
+ if result_amount != wallet_cash.amount
299
+ puts "Wallet was mistaken about amount stored by a certain webcash. Updating."
300
+ @webcash.reject! { |item| item == webcashes_map[hashed_value] }
301
+ @webcash.push(Webcash::Secret.new(result_amount, wallet_cash.secret_value).to_s)
302
+ end
303
+ elsif [ nil, true ].include?(result["spent"])
304
+ # Invalid webcash found. Remove from wallet.
305
+ puts "Removing some webcash."
306
+ @webcash.reject! { |item| item == webcashes_map[hashed_value] }
307
+ @unconfirmed.push(webcashes_map[hashed_value])
308
+ else
309
+ raise "Invalid webcash status: #{result["spent"]}"
310
+ end
311
+ end
312
+ end
313
+
314
+ # Check every webcash in the wallet and remove any invalid already-spent
315
+ def check
316
+ webcashes = {}
317
+ @webcash.each do |webcash|
318
+ sk = Webcash::Secret.deserialize(webcash)
319
+ hashed_value = sk.to_public.hashed_value
320
+
321
+ # Detect and remove duplicates.
322
+ if webcashes.key?(hashed_value)
323
+ puts "Duplicate webcash detected in wallet, moving it to unconfirmed"
324
+ @unconfirmed.push(webcash)
325
+
326
+ # Remove all copies
327
+ @webcash.reject! { |item| item == webcash }
328
+
329
+ # Add one copy back for a total of one
330
+ @webcash.push(webcash)
331
+
332
+ save if respond_to?(:save)
333
+ end
334
+
335
+ # Make a map from the hashed value back to the webcash which can
336
+ # be used for lookups when the server gives a response.
337
+ webcashes[hashed_value] = webcash
338
+ end
339
+
340
+ chunks = Webcash::Helpers.chunk_array(@webcash, 25)
341
+
342
+ chunks.each do |chunk|
343
+ health_check_request = chunk.map { |webcash| Webcash::Secret.deserialize(webcash).to_public.to_s }
344
+
345
+ begin
346
+ response = HTTParty.post(
347
+ API_HEALTHCHECK,
348
+ body: JSON.dump(health_check_request),
349
+ headers: { "Content-Type" => "application/json" }
350
+ )
351
+
352
+ response_content = response.body
353
+ if response.code != 200
354
+ raise "Server returned an error: #{response_content}"
355
+ end
356
+
357
+ response_data = JSON.parse(response_content)
358
+ results = response_data["results"]
359
+
360
+ process_healthcheck_results(results, webcashes)
361
+ rescue => e
362
+ puts "Could not successfully call the healthcheck API"
363
+ raise e
364
+ end
365
+ end
366
+ end
367
+
368
+ def recover(gaplimit: 20, sweep_payments: false)
369
+ # Start by healthchecking the contents of the wallet.
370
+ check
371
+
372
+ @walletdepths.each do |chain_code, reported_walletdepth|
373
+ current_walletdepth = 0
374
+ last_used_walletdepth = 0
375
+ has_had_webcash = true
376
+ idx = 0
377
+
378
+ while has_had_webcash
379
+ puts "Checking gaplimit #{gaplimit} secrets for chainCode #{chain_code}, round #{idx}"
380
+
381
+ # Assume this is the last iteration
382
+ has_had_webcash = false
383
+
384
+ # Check the next gaplimit number of secrets
385
+ health_check_request = []
386
+ check_webcashes = {}
387
+ walletdepths = {}
388
+
389
+ Webcash::Helpers.range(current_walletdepth, current_walletdepth + gaplimit).each do |x|
390
+ secret = generate_next_secret(chain_code, x)
391
+ webcash = Webcash::Secret.new(BigDecimal(1), secret)
392
+ public_webcash = webcash.to_public
393
+ check_webcashes[public_webcash.hashed_value] = webcash
394
+ walletdepths[public_webcash.hashed_value] = x
395
+ health_check_request << public_webcash.to_s
396
+ end
397
+
398
+ # Fetch the response from the healthcheck API
399
+ begin
400
+ response = HTTParty.post(
401
+ API_HEALTHCHECK,
402
+ body: JSON.dump(health_check_request),
403
+ headers: { "Content-Type" => "application/json" }
404
+ )
405
+
406
+ response_content = response.body
407
+ if response.code != 200
408
+ raise "Server returned an error: #{response_content}"
409
+ end
410
+
411
+ response_data = JSON.parse(response_content)
412
+ results = response_data["results"]
413
+
414
+ # Use results and check_webcashes to process
415
+ results.each do |public_webcash_str, result|
416
+ public_webcash = Webcash::Public.deserialize(public_webcash_str)
417
+ if result["spent"] != nil
418
+ has_had_webcash = true
419
+ last_used_walletdepth = walletdepths[public_webcash.hashed_value]
420
+ end
421
+
422
+ if result["spent"] == false
423
+ swc = check_webcashes[public_webcash.hashed_value]
424
+ swc.amount = BigDecimal(result["amount"])
425
+
426
+ if sweep_payments || chain_code != "PAY"
427
+ unless @webcash.include?(swc.to_s)
428
+ puts "Recovered webcash: #{Webcash::Helpers.decimal_amount_to_string(swc.amount)}"
429
+ @webcash.push(swc.to_s)
430
+ end
431
+ else
432
+ puts "Found known webcash of amount: #{Webcash::Helpers.decimal_amount_to_string(swc.amount)}"
433
+ end
434
+ end
435
+ end
436
+
437
+ if current_walletdepth < reported_walletdepth
438
+ has_had_webcash = true
439
+ end
440
+
441
+ if has_had_webcash
442
+ current_walletdepth += gaplimit
443
+ end
444
+
445
+ idx += 1
446
+ rescue => e
447
+ puts "Could not successfully call the healthcheck API"
448
+ raise e
449
+ end
450
+ end
451
+
452
+ if reported_walletdepth > last_used_walletdepth + 1
453
+ puts "Something may have gone wrong: reported walletdepth was #{reported_walletdepth} but only found up to #{last_used_walletdepth} depth."
454
+ end
455
+
456
+ if reported_walletdepth < last_used_walletdepth
457
+ @walletdepths[chain_code] = last_used_walletdepth + 1
458
+ end
459
+ end
460
+
461
+ save if respond_to?(:save)
462
+ end
463
+ end
464
+ end
data/lib/webcash.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/webcash.rb
4
+ require_relative "webcash/version"
5
+ require_relative "webcash/helpers"
6
+ require_relative "webcash/public"
7
+ require_relative "webcash/secret"
8
+ require_relative "webcash/wallet"
9
+
10
+ module Webcash
11
+ class Error < StandardError; end
12
+ end
data/sig/webcash.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Webcash
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: webcash-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - acidtib
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-09-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bigdecimal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: digest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: base64
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: securerandom
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.3.1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.3.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: httparty
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.22'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.22'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry-byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Webcash facilitates decentralized, peer-to-peer electronic cash transactions.
112
+ It allows users to send webcash directly to one another and includes mechanisms
113
+ for detecting double-spending and maintaining monetary supply integrity.
114
+ email:
115
+ - hello@dainelvera.com
116
+ executables: []
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - ".rspec"
121
+ - ".rubocop.yml"
122
+ - CHANGELOG.md
123
+ - CODE_OF_CONDUCT.md
124
+ - LICENSE.txt
125
+ - README.md
126
+ - Rakefile
127
+ - lib/webcash.rb
128
+ - lib/webcash/helpers.rb
129
+ - lib/webcash/public.rb
130
+ - lib/webcash/secret.rb
131
+ - lib/webcash/version.rb
132
+ - lib/webcash/wallet.rb
133
+ - sig/webcash.rbs
134
+ homepage: https://github.com/acidtib/webcash-rb
135
+ licenses:
136
+ - MIT
137
+ metadata:
138
+ allowed_push_host: https://rubygems.org
139
+ homepage_uri: https://github.com/acidtib/webcash-rb
140
+ source_code_uri: https://github.com/acidtib/webcash-rb
141
+ changelog_uri: https://github.com/acidtib/webcash-rb/blob/main/CHANGELOG.md
142
+ post_install_message:
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: 3.0.0
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubygems_version: 3.5.16
158
+ signing_key:
159
+ specification_version: 4
160
+ summary: Webcash is an experimental electronic cash library for decentralized payments.
161
+ test_files: []