polarloop 1.0.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.
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PolarLoop
4
+ class GasStrategy
5
+ @cache = {}
6
+
7
+ class << self
8
+ def for_chain(chain_id, eth_client)
9
+ @cache[chain_id] ||= detect(chain_id, eth_client)
10
+ end
11
+
12
+ def reset!
13
+ @cache = {}
14
+ end
15
+
16
+ private
17
+
18
+ def detect(chain_id, eth_client)
19
+ response = eth_client.send(:do_rpc, "eth_feeHistory", params: [1, "latest", []])
20
+ if response && response["baseFeePerGas"]
21
+ :eip1559
22
+ else
23
+ :legacy
24
+ end
25
+ rescue
26
+ :legacy
27
+ end
28
+ end
29
+
30
+ attr_reader :strategy
31
+
32
+ def initialize(chain_id, eth_client)
33
+ @strategy = self.class.for_chain(chain_id, eth_client)
34
+ end
35
+
36
+ def legacy?
37
+ @strategy == :legacy
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PolarLoop
4
+ class Manager
5
+ def initialize(configuration)
6
+ @config = configuration
7
+ @config.validate!
8
+ @wallet = Wallet.new(@config.mnemonic, index: @config.index)
9
+ @clients = {}
10
+ end
11
+
12
+ def client(chain_name)
13
+ chain_name = chain_name.to_sym
14
+ @clients[chain_name] ||= begin
15
+ chain_config = @config.chain(chain_name)
16
+ Client.new(chain_config: chain_config, key: @wallet.key)
17
+ end
18
+ end
19
+
20
+ def address
21
+ @wallet.address
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PolarLoop
4
+ module Types
5
+ BatchResult = Struct.new(
6
+ :mandate_id,
7
+ :success,
8
+ :reason,
9
+ keyword_init: true
10
+ ) do
11
+ def self.from_abi_array(tuples)
12
+ tuples.map do |tuple|
13
+ new(
14
+ mandate_id: to_hex_bytes32(tuple[0]),
15
+ success: tuple[1],
16
+ reason: tuple[2]
17
+ )
18
+ end
19
+ end
20
+
21
+ def self.to_hex_bytes32(binary)
22
+ if binary.is_a?(String) && binary.start_with?("0x")
23
+ binary.downcase
24
+ elsif binary.is_a?(String)
25
+ "0x" + binary.unpack1("H*")
26
+ else
27
+ "0x" + binary.to_s(16).rjust(64, "0")
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PolarLoop
4
+ module Types
5
+ ChargeReadyResult = Struct.new(:ready, :reason, keyword_init: true)
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PolarLoop
4
+ module Types
5
+ MandateCreatedEvent = Struct.new(
6
+ :mandate_id,
7
+ :payer,
8
+ :merchant,
9
+ :token,
10
+ :amount,
11
+ :interval,
12
+ :max_total_amount,
13
+ :reference_id,
14
+ :block_number,
15
+ :tx_hash,
16
+ :log_index,
17
+ keyword_init: true
18
+ )
19
+
20
+ MandateChargedEvent = Struct.new(
21
+ :mandate_id,
22
+ :payer,
23
+ :merchant,
24
+ :amount,
25
+ :timestamp,
26
+ :block_number,
27
+ :tx_hash,
28
+ :log_index,
29
+ keyword_init: true
30
+ )
31
+
32
+ MandateRevokedEvent = Struct.new(
33
+ :mandate_id,
34
+ :payer,
35
+ :block_number,
36
+ :tx_hash,
37
+ :log_index,
38
+ keyword_init: true
39
+ )
40
+ end
41
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PolarLoop
4
+ module Types
5
+ Mandate = Struct.new(
6
+ :payer,
7
+ :created_at,
8
+ :last_charged_at,
9
+ :merchant,
10
+ :interval,
11
+ :active,
12
+ :token,
13
+ :amount,
14
+ :max_total_amount,
15
+ :total_charged,
16
+ :reference_id,
17
+ keyword_init: true
18
+ ) do
19
+ # Converts positional array from ABI decoder to Mandate struct.
20
+ # ABI order matches Solidity struct field order.
21
+ def self.from_abi(values)
22
+ new(
23
+ payer: values[0],
24
+ created_at: values[1].to_i,
25
+ last_charged_at: values[2].to_i,
26
+ merchant: values[3],
27
+ interval: values[4].to_i,
28
+ active: values[5],
29
+ token: values[6],
30
+ amount: values[7].to_i,
31
+ max_total_amount: values[8].to_i,
32
+ total_charged: values[9].to_i,
33
+ reference_id: to_hex_bytes32(values[10])
34
+ )
35
+ end
36
+
37
+ def self.to_hex_bytes32(binary)
38
+ return "0x" + "00" * 32 if binary.nil?
39
+
40
+ if binary.is_a?(String) && binary.start_with?("0x")
41
+ binary.downcase
42
+ elsif binary.is_a?(String)
43
+ "0x" + binary.unpack1("H*")
44
+ else
45
+ "0x" + binary.to_s(16).rjust(64, "0")
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PolarLoop
4
+ module Types
5
+ TxResult = Struct.new(
6
+ :tx_hash,
7
+ :block_number,
8
+ :gas_used,
9
+ :status,
10
+ :batch_results,
11
+ keyword_init: true
12
+ )
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PolarLoop
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bip_mnemonic"
4
+ require "money-tree"
5
+
6
+ module PolarLoop
7
+ class Wallet
8
+ BIP44_BASE_PATH = "m/44'/60'/0'/0"
9
+
10
+ attr_reader :key
11
+
12
+ def initialize(mnemonic, index: 0)
13
+ validate_mnemonic!(mnemonic)
14
+ seed_hex = BipMnemonic.to_seed(mnemonic: mnemonic)
15
+ master = MoneyTree::Master.new(seed_hex: seed_hex)
16
+ node = master.node_for_path("#{BIP44_BASE_PATH}/#{index}")
17
+ @key = Eth::Key.new(priv: node.private_key.to_hex)
18
+ rescue WalletError
19
+ raise
20
+ rescue => e
21
+ raise WalletError, "Failed to derive wallet: #{e.message}"
22
+ end
23
+
24
+ def address
25
+ @key.address.to_s
26
+ end
27
+
28
+ private
29
+
30
+ def validate_mnemonic!(mnemonic)
31
+ words = mnemonic.to_s.strip.split(/\s+/)
32
+ raise WalletError, "Invalid mnemonic: must be 12, 15, 18, 21 or 24 words" unless [12, 15, 18, 21, 24].include?(words.length)
33
+
34
+ BipMnemonic.to_entropy(mnemonic: mnemonic)
35
+ rescue WalletError
36
+ raise
37
+ rescue => e
38
+ raise WalletError, "Invalid mnemonic: #{e.message}"
39
+ end
40
+ end
41
+ end
data/lib/polarloop.rb ADDED
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eth"
4
+
5
+ # Patch eth.rb HTTP client — HTTPX hangs on some RPC endpoints (HTTP/2 + persistent).
6
+ # Replace with Net::HTTP which works reliably everywhere.
7
+ require "net/http"
8
+
9
+ module Eth
10
+ class Client::Http < Client
11
+ def send_request(payload)
12
+ http = Net::HTTP.new(@uri.host, @uri.port)
13
+ http.use_ssl = @ssl
14
+ http.open_timeout = 15
15
+ http.read_timeout = 30
16
+ request = Net::HTTP::Post.new(@uri.request_uri)
17
+ request["Content-Type"] = "application/json"
18
+ request.body = payload
19
+ response = http.request(request)
20
+ response.body
21
+ end
22
+ end
23
+ end
24
+
25
+ require_relative "polarloop/version"
26
+ require_relative "polarloop/errors"
27
+ require_relative "polarloop/types/mandate"
28
+ require_relative "polarloop/types/batch_result"
29
+ require_relative "polarloop/types/charge_ready_result"
30
+ require_relative "polarloop/types/tx_result"
31
+ require_relative "polarloop/types/event"
32
+ require_relative "polarloop/abi"
33
+ require_relative "polarloop/configuration"
34
+ require_relative "polarloop/wallet"
35
+ require_relative "polarloop/gas_strategy"
36
+ require_relative "polarloop/contract_caller"
37
+ require_relative "polarloop/event_parser"
38
+ require_relative "polarloop/client"
39
+ require_relative "polarloop/manager"
40
+
41
+ module PolarLoop
42
+ class << self
43
+ def configure
44
+ @configuration = Configuration.new
45
+ yield(@configuration)
46
+ @manager = Manager.new(@configuration)
47
+ @configuration
48
+ end
49
+
50
+ def client(chain_name)
51
+ raise ConfigurationError, "PolarLoop not configured. Call PolarLoop.configure first." unless @manager
52
+
53
+ @manager.client(chain_name)
54
+ end
55
+
56
+ def address
57
+ @manager&.address
58
+ end
59
+
60
+ def configuration
61
+ @configuration
62
+ end
63
+
64
+ def reset!
65
+ @configuration = nil
66
+ @manager = nil
67
+ GasStrategy.reset!
68
+ end
69
+ end
70
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: polarloop
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Pontifi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: eth
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.5'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.5.17
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.5'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.5.17
33
+ - !ruby/object:Gem::Dependency
34
+ name: bip_mnemonic
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: money-tree
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.11'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.11'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rspec
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.12'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.12'
75
+ - !ruby/object:Gem::Dependency
76
+ name: webmock
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.18'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.18'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '13.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '13.0'
103
+ description: Ruby gem for interacting with PolarLoop EVM smart contract — mandate
104
+ management, charging, events
105
+ email:
106
+ executables: []
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - CHANGELOG.md
111
+ - LICENSE
112
+ - README.md
113
+ - lib/polarloop.rb
114
+ - lib/polarloop/abi.rb
115
+ - lib/polarloop/abi/polar_loop.json
116
+ - lib/polarloop/client.rb
117
+ - lib/polarloop/configuration.rb
118
+ - lib/polarloop/contract_caller.rb
119
+ - lib/polarloop/errors.rb
120
+ - lib/polarloop/event_parser.rb
121
+ - lib/polarloop/gas_strategy.rb
122
+ - lib/polarloop/manager.rb
123
+ - lib/polarloop/types/batch_result.rb
124
+ - lib/polarloop/types/charge_ready_result.rb
125
+ - lib/polarloop/types/event.rb
126
+ - lib/polarloop/types/mandate.rb
127
+ - lib/polarloop/types/tx_result.rb
128
+ - lib/polarloop/version.rb
129
+ - lib/polarloop/wallet.rb
130
+ homepage: https://rubygems.org/gems/polarloop
131
+ licenses:
132
+ - MIT
133
+ metadata:
134
+ allowed_push_host: https://rubygems.org
135
+ homepage_uri: https://rubygems.org/gems/polarloop
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '3.0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubygems_version: 3.3.7
152
+ signing_key:
153
+ specification_version: 4
154
+ summary: Ruby client for PolarLoop smart contract
155
+ test_files: []