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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.md +223 -0
- data/lib/polarloop/abi/polar_loop.json +972 -0
- data/lib/polarloop/abi.rb +91 -0
- data/lib/polarloop/client.rb +159 -0
- data/lib/polarloop/configuration.rb +43 -0
- data/lib/polarloop/contract_caller.rb +203 -0
- data/lib/polarloop/errors.rb +40 -0
- data/lib/polarloop/event_parser.rb +141 -0
- data/lib/polarloop/gas_strategy.rb +40 -0
- data/lib/polarloop/manager.rb +24 -0
- data/lib/polarloop/types/batch_result.rb +32 -0
- data/lib/polarloop/types/charge_ready_result.rb +7 -0
- data/lib/polarloop/types/event.rb +41 -0
- data/lib/polarloop/types/mandate.rb +50 -0
- data/lib/polarloop/types/tx_result.rb +14 -0
- data/lib/polarloop/version.rb +5 -0
- data/lib/polarloop/wallet.rb +41 -0
- data/lib/polarloop.rb +70 -0
- metadata +155 -0
|
@@ -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,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,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: []
|