monerorequest 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4efb26bf1513834ee0b68535033a34350a5f159adba869ff3e8682d8612cdabf
4
- data.tar.gz: 48ee1c05ae0db4195692ecc06a14bc2a26504982a0c81c9ab8ec1ee390832cb9
3
+ metadata.gz: 944c04e247c34be236e6071a5b8f384d11f12a21598b487068f8086ef9fdd7cb
4
+ data.tar.gz: e681abc0a5490f7fa86811a735d53f091b4ab8effd906cfa00b0737db11ba9c5
5
5
  SHA512:
6
- metadata.gz: c27b72a81046b8bf050f0118f5c0cb683b7929a0921ffb592ec45388f99d6bb2c82c9695e9e95225a6a051165f7ac6c2c6bfe6d9520dd2d4f1725d89520d4a1a
7
- data.tar.gz: 65e8f263bf3280a640471114d45658e148725c50dc88d4455c75a3620dc27f0b4e61eced65c9150427ee140f8c72948b7d8899b99a22bc530dcafa3b036605ad
6
+ metadata.gz: de8f025de01a42d3a6a4cb701e40122e6c115eee4683331da0789e5886f27ce82d99e5fd81811443b5a00c5f62c811ea3d2540bd1036579404fbe869301a5b04
7
+ data.tar.gz: c81846b18ec909a1c7b84130a17c1b6a7d7b88d884da06aa6634ee915842b4e14d6eaaf862361fa53af48d5f22b21cf56639b07fd2800d67390e1667711602ff
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  group :development, :test do
9
+ gem "byebug", "~> 11.1"
9
10
  gem "faker", "~> 3.3"
10
11
  gem "rake", "~> 13.0"
11
12
  gem "rspec", "~> 3.0"
@@ -13,3 +14,7 @@ group :development, :test do
13
14
  gem "rubocop-rake", "~> 0.6"
14
15
  gem "rubocop-rspec", "~> 2.29"
15
16
  end
17
+
18
+ group :test do
19
+ gem "simplecov", "~> 0.22", require: false
20
+ end
data/Gemfile.lock CHANGED
@@ -1,14 +1,16 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- monerorequest (0.1.0)
4
+ monerorequest (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  ast (2.4.2)
10
+ byebug (11.1.3)
10
11
  concurrent-ruby (1.2.3)
11
12
  diff-lcs (1.5.1)
13
+ docile (1.4.1)
12
14
  faker (3.3.1)
13
15
  i18n (>= 1.8.11, < 2)
14
16
  i18n (1.14.5)
@@ -64,12 +66,19 @@ GEM
64
66
  rubocop-rspec_rails (2.28.3)
65
67
  rubocop (~> 1.40)
66
68
  ruby-progressbar (1.13.0)
69
+ simplecov (0.22.0)
70
+ docile (~> 1.1)
71
+ simplecov-html (~> 0.11)
72
+ simplecov_json_formatter (~> 0.1)
73
+ simplecov-html (0.13.1)
74
+ simplecov_json_formatter (0.1.4)
67
75
  unicode-display_width (2.5.0)
68
76
 
69
77
  PLATFORMS
70
78
  x86_64-linux
71
79
 
72
80
  DEPENDENCIES
81
+ byebug (~> 11.1)
73
82
  faker (~> 3.3)
74
83
  monerorequest!
75
84
  rake (~> 13.0)
@@ -77,6 +86,7 @@ DEPENDENCIES
77
86
  rubocop (~> 1.21)
78
87
  rubocop-rake (~> 0.6)
79
88
  rubocop-rspec (~> 2.29)
89
+ simplecov (~> 0.22)
80
90
 
81
91
  BUNDLED WITH
82
92
  2.4.10
data/README.md CHANGED
@@ -1,24 +1,75 @@
1
1
  # Monerorequest
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
4
-
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/monerorequest`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ ruby gem for encoding and decoding [Monero Payment Request Standard](https://github.com/lukeprofits/Monero_Payment_Request_Standard?tab=readme-ov-file#introduction) requests.
6
4
 
7
5
  ## Installation
8
6
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
-
11
7
  Install the gem and add to the application's Gemfile by executing:
12
8
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
9
+ $ bundle add monerorequest
14
10
 
15
11
  If bundler is not being used to manage dependencies, install the gem by executing:
16
12
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
13
+ $ gem install monerorequest
18
14
 
19
15
  ## Usage
20
16
 
21
- TODO: Write usage instructions here
17
+
18
+ ### V1
19
+ Encode a request:
20
+ ```ruby
21
+ require 'monerorequest'
22
+
23
+ req = {
24
+ "custom_label" => "[some string here]",
25
+ "sellers_wallet" => "[monero main address, starts with 4]",
26
+ "currency" => "USD",
27
+ "amount" => 420.69,
28
+ "payment_id" => "[monero payment ID]",
29
+ "start_date" => "[timestamp in rfc3339 format]",
30
+ "days_per_billing_cycle" => 30,
31
+ "number_of_payments" => 12,
32
+ "change_indicator_url" => "[some url here]"
33
+ }
34
+ enc = Monerorequest::Encoder.new(req, 1)
35
+ ```
36
+
37
+ Decode a request:
38
+ ```ruby
39
+ require 'monerorequest'
40
+
41
+ req = "monero-request:1:H4sIAAAAAAACAy2QS4vUQBSF/0qondA9XanqJJ3sZjEozGYW4kooKpWbTjmVqnQ9pjuKID7HlStxZiEo4pMBQRAcFPwvCq5c2O0fMD24utxzOR/n3FuItyZoj4o0iWm+k8YjJBqu58CkrqTg3lgWrEIFarzvisnEGlOOG5AVaAtSNDuw4m2nYCJsuIkGd7AWtOgHx8GVgwvBedMyxUvYYtafHrz49fT13+8nb+rNnS/nm7ff3v04vb+/fvj8w/WAcQw/3z++dDnai67tbp4cP4ouRP7787M/Z69eJtF+tNycnd/bW9/9+NUN+Ir3jnVgWSmVknrORC8UoCKnI6RDWw4XU7OO9y1o71BB8hH6vzFZDYFIlRFe1yTPeVblpB6YDpQC69iSD3P4DZruUtybtGxXcqZS08yrlMeYmMNF1lF+o1FULvzMxwL3C3DikAM/8jSmSpbG9NbWqll1mQtJ6jgJJqFHIe+obDQsLU22NZzn1rOKe9hGwmQ6xskYp1fjWTElBaFjnBUYo9v/ANjVPGWxAQAA"
42
+ dec = Monerorequest::Decoder.new(req)
43
+ dec.decode
44
+ ```
45
+
46
+ ### V2
47
+ Encode a request:
48
+ ```ruby
49
+ require 'monerorequest'
50
+
51
+ req = {
52
+ "custom_label" => "[some string here]",
53
+ "sellers_wallet" => "[monero main address, starts with 4]",
54
+ "currency" => "USD",
55
+ "amount" => 420.69,
56
+ "payment_id" => "[monero payment ID]",
57
+ "start_date" => "[timestamp in rfc3339 format]",
58
+ "schedule" => '* * 1 * *',
59
+ "number_of_payments" => 12,
60
+ "change_indicator_url" => "[some url here]"
61
+ }
62
+ enc = Monerorequest::Encoder.new(req, 2)
63
+ ```
64
+
65
+ Decode a request:
66
+ ```ruby
67
+ require 'monerorequest'
68
+
69
+ req = "monero-request:2:H4sIAAAAAAACAy2PTW/CMAyG/wrKEVFIE9rS3nbbkcPukZu4JCNNSj6Abtp/X5h2sCw/9vva/iYw++wSGdqm5v2+rXdEanAXFMYpIyH5IHKwZCA6pWU4HIL3Y6XRKHQBjdR7fMK8WDzIkL9IUecQ0Mm1KM7v5z8Qk5+FhRFfNgljKtTlecQg/CQWWGd0KZKB9TvyXwmjyixTHYNpYn0PnerZVHRRalTZYuluN9tNXWL7wmgthigeUHL5hhzfOF19O85Pc7Kt1xfVQk2Zv966hcOnttzc0inVkq43jPIKCPfEa27N6P0awmT1c+libtoILPuG33O/cKMdPgJv4mtlgpCEgvS6hVF2rGhT0fajPg1HNjBe0W6glPz8AihAH5JjAQAA"
70
+ dec = Monerorequest::Decoder.new(req)
71
+ dec.decode
72
+ ```
22
73
 
23
74
  ## Development
24
75
 
@@ -26,9 +77,18 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
26
77
 
27
78
  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).
28
79
 
80
+ ## Adding a new Monerorequest version
81
+
82
+ Adding a new Monerorequest version should be simple.
83
+
84
+ 1. Update SUPPORTED_MR_VERSIONS in ```lib/monerorequest.rb```.
85
+ 2. Add any necessary pipelines and validators in their respective folders.
86
+ 3. Define a file named "vX.rb" where X is your version that pulls in the pipelines and validators it needs.
87
+ 4. Make sure you add spec coverage for any new code you write and run ```bundle exec rubocop``` to ensure it passes the linter.
88
+
29
89
  ## Contributing
30
90
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/monerorequest.
91
+ Bug reports and pull requests are welcome on GitHub at https://github.com/snex/monerorequest.
32
92
 
33
93
  ## License
34
94
 
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Monerorequest
4
+ # Parses and validates cron syntax string.
5
+ class Cron
6
+ MONTH_CODES = %w[jan feb mar apr may jun jul aug sep oct nov dec].freeze
7
+ DOW_CODES = %w[mon tue wed thu fri sat sun].freeze
8
+ DELIMITERS = %r{,|-|/}.freeze
9
+ def self.valid?(schedule)
10
+ parsed_schedule = parse(schedule)
11
+ return false unless parsed_schedule
12
+
13
+ valid_minutes?(parsed_schedule["minutes"]) && valid_hours?(parsed_schedule["hours"]) &&
14
+ valid_days?(parsed_schedule["days"]) && valid_months?(parsed_schedule["months"]) &&
15
+ valid_dow?(parsed_schedule["dow"])
16
+ end
17
+
18
+ def self.valid_minutes?(minutes)
19
+ minutes.all? { |min| ("0".."59").member?(min.to_s) } || minutes == ["*"]
20
+ end
21
+
22
+ def self.valid_hours?(hours)
23
+ hours.all? { |hr| ("0".."23").member?(hr.to_s) } || hours == ["*"]
24
+ end
25
+
26
+ def self.valid_days?(days)
27
+ days.all? { |dy| ("1".."31").member?(dy.to_s) } || days == ["*"]
28
+ end
29
+
30
+ def self.valid_months?(months)
31
+ months.all? do |mth|
32
+ ("1".."12").member?(mth.to_s) || MONTH_CODES.include?(mth.to_s.downcase)
33
+ end || months == ["*"] || months == ["L"]
34
+ end
35
+
36
+ def self.valid_dow?(dow)
37
+ dow.all? { |d| DOW_CODES.include?(d.downcase) } || dow == ["*"]
38
+ end
39
+
40
+ def self.parse(schedule)
41
+ schedule_parts = schedule.split
42
+ return false if schedule_parts.length != 5
43
+
44
+ {
45
+ "minutes" => schedule_parts[0].split(DELIMITERS),
46
+ "hours" => schedule_parts[1].split(DELIMITERS),
47
+ "days" => schedule_parts[2].split(DELIMITERS),
48
+ "months" => schedule_parts[3].split(DELIMITERS),
49
+ "dow" => schedule_parts[4].split(DELIMITERS)
50
+ }
51
+ end
52
+ end
53
+ end
@@ -1,19 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ Monerorequest::SUPPORTED_MR_VERSIONS.each do |mr_v|
4
+ require_relative "v#{mr_v}"
5
+ end
6
+
3
7
  module Monerorequest
4
- # class to Decode an encoded Monerorequest
8
+ # class to choose a proper Decoder Version and then pass the encoded data to it for decoding
5
9
  class Decoder
10
+ attr_reader :errors
11
+
6
12
  def initialize(request)
7
13
  @request = request
14
+ _, @version, @encoded_str = @request.split(":")
15
+ raise RequestVersionError, @version unless Monerorequest::SUPPORTED_MR_VERSIONS.include?(@version.to_i)
16
+
17
+ @decoder = Object.const_get("Monerorequest::V#{@version}")
8
18
  end
9
19
 
10
20
  def decode
11
- _, version, encoded_str = @request.split(":")
12
- raise RequestVersionError, "Only Request Version 1 is supported." unless version == "1"
21
+ @data = @encoded_str
22
+
23
+ @decoder::Decoder::PIPELINES.each do |pipeline|
24
+ @data = pipeline.call(@data)
25
+ end
26
+
27
+ @data["version"] = @version.to_i
28
+ validate!
29
+ @data
30
+ end
31
+
32
+ private
33
+
34
+ def validate!
35
+ @errors = []
36
+ @decoder::VALIDATORS.each do |validator|
37
+ @errors += validator.validate!(@data)
38
+ end
13
39
 
14
- compressed_data = Base64.decode64(encoded_str)
15
- json_str = Zlib::GzipReader.new(StringIO.new(compressed_data)).read
16
- JSON.parse(json_str)
40
+ raise InvalidRequestError, "Invalid request: #{@errors}" unless @errors.empty?
17
41
  end
18
42
  end
19
43
  end
@@ -1,105 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "base64"
4
- require "date"
5
- require "json"
6
- require "stringio"
7
- require "uri"
8
- require "zlib"
3
+ Monerorequest::SUPPORTED_MR_VERSIONS.each do |mr_v|
4
+ require_relative "v#{mr_v}"
5
+ end
9
6
 
10
7
  module Monerorequest
11
8
  # class to Encode a Monerorequest hash
12
9
  class Encoder
13
- class InvalidRequest < StandardError; end
14
-
15
- attr_accessor :request
16
10
  attr_reader :errors
17
11
 
18
- def initialize(request)
19
- @request = request
20
- @errors = []
12
+ def initialize(request, version)
13
+ raise RequestVersionError, version unless Monerorequest::SUPPORTED_MR_VERSIONS.include?(version.to_i)
21
14
 
15
+ @request = request
16
+ @version = version.to_i
17
+ set_encoder_version
22
18
  validate!
23
19
  end
24
20
 
25
- def encode(version)
26
- raise RequestVersionError, "Only Request Version 1 is supported." unless version == 1
27
-
28
- json_str = @request.sort.to_h.to_json.force_encoding("ascii")
29
- compressed_data = StringIO.new
30
- gz = Zlib::GzipWriter.new(compressed_data, 9)
31
- gz.mtime = 0
32
- gz.write(json_str)
33
- gz.close
34
- encoded_str = Base64.encode64(compressed_data.string).gsub("\n", "")
35
- "monero-request:1:#{encoded_str}"
36
- end
37
-
38
- private
39
-
40
- def validate!
41
- validate_custom_label!
42
- validate_sellers_wallet!
43
- validate_currency!
44
- validate_amount!
45
- validate_payment_id!
46
- validate_start_date!
47
- validate_days_per_billing_cycle!
48
- validate_change_indicator_url!
49
- end
50
-
51
- def validate_custom_label!
52
- @errors.push("custom_label must be present.") unless @request.key?("custom_label")
53
- @errors.push("custom_label must be a String.") unless @request["custom_label"].is_a?(String)
54
- end
21
+ def encode
22
+ data = @request
55
23
 
56
- def validate_sellers_wallet!
57
- @errors.push("sellers_wallet must be present.") unless @request.key?("sellers_wallet")
58
- return if MoneroAddress.valid?(@request["sellers_wallet"])
59
-
60
- @errors.push("sellers_wallet must be a main Monero address.")
61
- end
62
-
63
- def validate_currency!
64
- @errors.push("currency must be present.") unless @request.key?("currency")
65
- @errors.push("currency must be a String.") unless @request["currency"].is_a?(String)
66
- end
67
-
68
- def validate_amount!
69
- @errors.push("amount must be present.") unless @request.key?("amount")
70
- @errors.push("amount must be a Numeric.") unless @request["amount"].is_a?(Numeric)
71
- end
24
+ @encoder::Encoder::PIPELINES.each do |pipeline|
25
+ data = pipeline.call(data)
26
+ end
72
27
 
73
- def validate_payment_id!
74
- @errors.push("payment_id must be present.") unless @request.key?("payment_id")
75
- @errors.push("payment_id must be a Monero Payment ID.") unless MoneroPaymentID.valid?(@request["payment_id"])
28
+ "monero-request:#{@version}:#{data}"
76
29
  end
77
30
 
78
- def validate_start_date!
79
- @errors.push("start_date must be present.") unless @request.key?("start_date")
80
- unless @request["start_date"].is_a?(String)
81
- @errors.push("start_date must be a String.") && @request["start_date"] = ""
82
- end
83
- begin
84
- DateTime.rfc3339(@request["start_date"])
85
- rescue Date::Error
86
- @errors.push("start_date must be an RFC3339 timestamp.")
87
- end
88
- end
31
+ private
89
32
 
90
- def validate_days_per_billing_cycle!
91
- @errors.push("days_per_billing_cycle must be present.") unless @request.key?("days_per_billing_cycle")
92
- @errors.push("days_per_billing_cycle must be a Integer.") unless @request["days_per_billing_cycle"].is_a?(Integer)
33
+ def set_encoder_version
34
+ @encoder = Object.const_get("Monerorequest::V#{@version}")
93
35
  end
94
36
 
95
- def validate_change_indicator_url!
96
- @errors.push("change_indicator_url must be present.") unless @request.key?("change_indicator_url")
97
- unless @request["change_indicator_url"].is_a?(String)
98
- @errors.push("change_indicator_url must be a String.") && @request["change_indicator_url"] = ""
37
+ def validate!
38
+ @errors = []
39
+ @encoder::VALIDATORS.each do |validator|
40
+ @errors += validator.validate!(@request)
99
41
  end
100
- return if @request["change_indicator_url"] =~ URI::DEFAULT_PARSER.make_regexp
101
42
 
102
- @errors.push("change_indicator_url must be a URL.")
43
+ raise InvalidRequestError, "Invalid request: #{@errors}" unless @errors.empty?
103
44
  end
104
45
  end
105
46
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module Monerorequest
6
+ module Pipeline
7
+ # pipeline that takes a base64 encoded string and decodes it
8
+ class Base64Decoder
9
+ def self.call(input)
10
+ Base64.strict_decode64(input)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module Monerorequest
6
+ module Pipeline
7
+ # pipeline that takes a string and base64 encodes it
8
+ class Base64Encoder
9
+ def self.call(input)
10
+ Base64.strict_encode64(input)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "zlib"
5
+
6
+ module Monerorequest
7
+ module Pipeline
8
+ # pipeline that takes a gzip blob and unzips it
9
+ class GzipReader
10
+ def self.call(input)
11
+ Zlib::GzipReader.new(StringIO.new(input)).read
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "zlib"
5
+
6
+ module Monerorequest
7
+ module Pipeline
8
+ # pipeline that takes a string and gzips it
9
+ class GzipWriter
10
+ def self.call(input)
11
+ output = StringIO.new
12
+ gz = Zlib::GzipWriter.new(output, 9)
13
+ gz.mtime = 0
14
+ gz.write(input)
15
+ gz.close
16
+ output.string
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Monerorequest
6
+ module Pipeline
7
+ # pipeline that takes a hash, sorts by keys, then converts to valid JSON in ASCII encoding
8
+ class JSONEncoder
9
+ def self.call(input)
10
+ raise InvalidRequestError, "Request must be a Hash." unless input.is_a?(Hash)
11
+
12
+ input.sort.to_h.to_json.force_encoding("ascii")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Monerorequest
6
+ module Pipeline
7
+ # pipeline that takes a JSON string and converts it into a ruby object
8
+ class JSONParser
9
+ def self.call(input)
10
+ JSON.parse(input)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pipeline/base64_decoder"
4
+ require_relative "pipeline/base64_encoder"
5
+ require_relative "pipeline/gzip_reader"
6
+ require_relative "pipeline/gzip_writer"
7
+ require_relative "pipeline/json_parser"
8
+ require_relative "pipeline/json_encoder"
9
+ require_relative "validator/custom_label"
10
+ require_relative "validator/sellers_wallet"
11
+ require_relative "validator/currency"
12
+ require_relative "validator/amount"
13
+ require_relative "validator/payment_id"
14
+ require_relative "validator/start_date"
15
+ require_relative "validator/days_per_billing_cycle"
16
+ require_relative "validator/number_of_payments"
17
+ require_relative "validator/change_indicator_url"
18
+
19
+ module Monerorequest
20
+ module V1
21
+ VALIDATORS = [
22
+ Monerorequest::Validator::CustomLabel,
23
+ Monerorequest::Validator::SellersWallet,
24
+ Monerorequest::Validator::Currency,
25
+ Monerorequest::Validator::Amount,
26
+ Monerorequest::Validator::PaymentID,
27
+ Monerorequest::Validator::StartDate,
28
+ Monerorequest::Validator::DaysPerBillingCycle,
29
+ Monerorequest::Validator::NumberOfPayments,
30
+ Monerorequest::Validator::ChangeIndicatorURL
31
+ ].freeze
32
+
33
+ module Encoder
34
+ PIPELINES = [
35
+ Monerorequest::Pipeline::JSONEncoder,
36
+ Monerorequest::Pipeline::GzipWriter,
37
+ Monerorequest::Pipeline::Base64Encoder
38
+ ].freeze
39
+ end
40
+
41
+ module Decoder
42
+ PIPELINES = [
43
+ Monerorequest::Pipeline::Base64Decoder,
44
+ Monerorequest::Pipeline::GzipReader,
45
+ Monerorequest::Pipeline::JSONParser
46
+ ].freeze
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pipeline/base64_decoder"
4
+ require_relative "pipeline/base64_encoder"
5
+ require_relative "pipeline/gzip_reader"
6
+ require_relative "pipeline/gzip_writer"
7
+ require_relative "pipeline/json_parser"
8
+ require_relative "pipeline/json_encoder"
9
+ require_relative "validator/custom_label"
10
+ require_relative "validator/sellers_wallet"
11
+ require_relative "validator/currency"
12
+ require_relative "validator/amount"
13
+ require_relative "validator/payment_id"
14
+ require_relative "validator/start_date"
15
+ require_relative "validator/schedule"
16
+ require_relative "validator/number_of_payments"
17
+ require_relative "validator/change_indicator_url"
18
+
19
+ module Monerorequest
20
+ module V2
21
+ VALIDATORS = [
22
+ Monerorequest::Validator::CustomLabel,
23
+ Monerorequest::Validator::SellersWallet,
24
+ Monerorequest::Validator::Currency,
25
+ Monerorequest::Validator::Amount,
26
+ Monerorequest::Validator::PaymentID,
27
+ Monerorequest::Validator::StartDate,
28
+ Monerorequest::Validator::Schedule,
29
+ Monerorequest::Validator::NumberOfPayments,
30
+ Monerorequest::Validator::ChangeIndicatorURL
31
+ ].freeze
32
+
33
+ module Encoder
34
+ PIPELINES = [
35
+ Monerorequest::Pipeline::JSONEncoder,
36
+ Monerorequest::Pipeline::GzipWriter,
37
+ Monerorequest::Pipeline::Base64Encoder
38
+ ].freeze
39
+ end
40
+
41
+ module Decoder
42
+ PIPELINES = [
43
+ Monerorequest::Pipeline::Base64Decoder,
44
+ Monerorequest::Pipeline::GzipReader,
45
+ Monerorequest::Pipeline::JSONParser
46
+ ].freeze
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Monerorequest
4
+ module Validator
5
+ # validates the amount field
6
+ class Amount
7
+ def self.validate!(data)
8
+ errors = []
9
+ errors.push("amount must be present.") unless data.key?("amount")
10
+ errors.push("amount must be a Numeric.") unless data["amount"].is_a?(Numeric)
11
+ errors.push("amount must be positive.") unless data["amount"].to_f.positive?
12
+ errors
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Monerorequest
6
+ module Validator
7
+ # validates the change_indicator_url field
8
+ class ChangeIndicatorURL
9
+ def self.validate!(data)
10
+ return [] unless data.key?("change_indicator_url")
11
+
12
+ errors = []
13
+ unless data["change_indicator_url"].is_a?(String)
14
+ errors.push("change_indicator_url must be a String.")
15
+ data["change_indicator_url"] = ""
16
+ end
17
+ return errors if data["change_indicator_url"] =~ URI::DEFAULT_PARSER.make_regexp
18
+
19
+ errors.push("change_indicator_url must be a URL.")
20
+ errors
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Monerorequest
4
+ module Validator
5
+ # validates the currency field
6
+ class Currency
7
+ def self.validate!(data)
8
+ errors = []
9
+ errors.push("currency must be present.") unless data.key?("currency")
10
+ errors.push("currency must be a String.") unless data["currency"].is_a?(String)
11
+ errors
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Monerorequest
4
+ module Validator
5
+ # validates the custom_label field
6
+ class CustomLabel
7
+ def self.validate!(data)
8
+ errors = []
9
+ errors.push("custom_label must be present.") unless data.key?("custom_label")
10
+ errors.push("custom_label must be a String.") unless data["custom_label"].is_a?(String)
11
+ errors
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Monerorequest
4
+ module Validator
5
+ # validates the days_per_billing_cycle field
6
+ class DaysPerBillingCycle
7
+ def self.validate!(data)
8
+ errors = []
9
+ errors.push("days_per_billing_cycle must be present.") unless data.key?("days_per_billing_cycle")
10
+ errors.push("days_per_billing_cycle must be an Integer.") unless data["days_per_billing_cycle"].is_a?(Integer)
11
+ errors.push("days_per_billing_cycle must be positive.") unless data["days_per_billing_cycle"].to_i.positive?
12
+ errors
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Monerorequest
4
+ module Validator
5
+ # validates the number_of_payments field
6
+ class NumberOfPayments
7
+ def self.validate!(data)
8
+ errors = []
9
+ errors.push("number_of_payments must be present.") unless data.key?("number_of_payments")
10
+ errors.push("number_of_payments must be an Integer.") unless data["number_of_payments"].is_a?(Integer)
11
+ errors.push("number_of_payments must be 0 or positive.") if data["number_of_payments"].to_i.negative?
12
+ errors
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Monerorequest
4
+ module Validator
5
+ # validates the payment_id field
6
+ class PaymentID
7
+ def self.validate!(data)
8
+ errors = []
9
+ errors.push("payment_id must be present.") unless data.key?("payment_id")
10
+ errors.push("payment_id must be a Monero Payment ID.") unless MoneroPaymentID.valid?(data["payment_id"])
11
+ errors
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Monerorequest
4
+ module Validator
5
+ # validates the schedule field
6
+ class Schedule
7
+ def self.validate!(data)
8
+ errors = []
9
+ errors.push("schedule must be present.") unless data.key?("schedule")
10
+ errors.push("schedule must be a valid Cron.") unless Cron.valid?(data.fetch("schedule", ""))
11
+ errors
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Monerorequest
4
+ module Validator
5
+ # validates the sellers_wallet field
6
+ class SellersWallet
7
+ def self.validate!(data)
8
+ errors = []
9
+ errors.push("sellers_wallet must be present.") unless data.key?("sellers_wallet")
10
+ return errors if MoneroAddress.valid?(data["sellers_wallet"])
11
+
12
+ errors.push("sellers_wallet must be a main Monero address.")
13
+ errors
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Monerorequest
6
+ module Validator
7
+ # validates the start_date field
8
+ class StartDate
9
+ def self.validate!(data)
10
+ errors = []
11
+ errors.push("start_date must be present.") unless data.key?("start_date")
12
+ errors.push("start_date must be a String.") && data["start_date"] = "" unless data["start_date"].is_a?(String)
13
+ begin
14
+ DateTime.rfc3339(data["start_date"])
15
+ rescue Date::Error
16
+ errors.push("start_date must be an RFC3339 timestamp.")
17
+ end
18
+ errors
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Monerorequest
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/monerorequest.rb CHANGED
@@ -1,17 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "base64"
4
- require "date"
5
- require "json"
6
- require "stringio"
7
- require "uri"
8
- require "zlib"
3
+ module Monerorequest
4
+ SUPPORTED_MR_VERSIONS = [1, 2].freeze
5
+
6
+ # error raised when an unsupported Monerorequest version is supplied
7
+ class RequestVersionError < StandardError
8
+ def initialize(version)
9
+ @version = version
10
+ super
11
+ end
12
+
13
+ def message
14
+ "Unknown version: #{@version}. Allowed versions: #{Monerorequest::SUPPORTED_MR_VERSIONS}"
15
+ end
16
+ end
17
+
18
+ class InvalidRequestError < StandardError; end
19
+ end
20
+
9
21
  require_relative "monerorequest/decoder"
10
22
  require_relative "monerorequest/encoder"
11
23
  require_relative "monerorequest/monero_address"
12
24
  require_relative "monerorequest/monero_payment_id"
13
25
  require_relative "monerorequest/version"
14
-
15
- module Monerorequest
16
- class RequestVersionError < StandardError; end
17
- end
26
+ require_relative "monerorequest/cron"
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/monerorequest/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "monerorequest"
7
+ spec.version = Monerorequest::VERSION
8
+ spec.authors = ["David Havlicek"]
9
+ spec.email = ["snex00@protonmail.com"]
10
+
11
+ spec.summary = "Ruby Implementation of the Monero Payment Request Standard. https://github.com/lukeprofits/Monero_Payment_Request_Standard"
12
+ spec.homepage = "https://github.com/snex/monerorequest-ruby"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.6.0"
15
+
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = spec.homepage
20
+ spec.metadata["changelog_uri"] = "https://github.com/snex/monerorequest-ruby/blob/master/CHANGELOG.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(__dir__) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
27
+ end
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ # Uncomment to register a new dependency of your gem
34
+ # spec.add_dependency "example-gem", "~> 1.0"
35
+
36
+ # For more information and examples about making a new gem, check out our
37
+ # guide at: https://bundler.io/guides/creating_gem.html
38
+ spec.metadata["rubygems_mfa_required"] = "true"
39
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monerorequest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Havlicek
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-07 00:00:00.000000000 Z
11
+ date: 2025-01-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -26,12 +26,31 @@ files:
26
26
  - README.md
27
27
  - Rakefile
28
28
  - lib/monerorequest.rb
29
+ - lib/monerorequest/cron.rb
29
30
  - lib/monerorequest/decoder.rb
30
31
  - lib/monerorequest/encoder.rb
31
32
  - lib/monerorequest/monero_address.rb
32
33
  - lib/monerorequest/monero_payment_id.rb
34
+ - lib/monerorequest/pipeline/base64_decoder.rb
35
+ - lib/monerorequest/pipeline/base64_encoder.rb
36
+ - lib/monerorequest/pipeline/gzip_reader.rb
37
+ - lib/monerorequest/pipeline/gzip_writer.rb
38
+ - lib/monerorequest/pipeline/json_encoder.rb
39
+ - lib/monerorequest/pipeline/json_parser.rb
40
+ - lib/monerorequest/v1.rb
41
+ - lib/monerorequest/v2.rb
42
+ - lib/monerorequest/validator/amount.rb
43
+ - lib/monerorequest/validator/change_indicator_url.rb
44
+ - lib/monerorequest/validator/currency.rb
45
+ - lib/monerorequest/validator/custom_label.rb
46
+ - lib/monerorequest/validator/days_per_billing_cycle.rb
47
+ - lib/monerorequest/validator/number_of_payments.rb
48
+ - lib/monerorequest/validator/payment_id.rb
49
+ - lib/monerorequest/validator/schedule.rb
50
+ - lib/monerorequest/validator/sellers_wallet.rb
51
+ - lib/monerorequest/validator/start_date.rb
33
52
  - lib/monerorequest/version.rb
34
- - sig/monerorequest.rbs
53
+ - monerorequest.gemspec
35
54
  homepage: https://github.com/snex/monerorequest-ruby
36
55
  licenses:
37
56
  - MIT
@@ -1,4 +0,0 @@
1
- module Monerorequest
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end