mobius-client 0.1.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,117 @@
1
+ require "dry-initializer"
2
+ require "stellar-sdk"
3
+ require "faraday"
4
+ require "faraday_middleware"
5
+ require "jwt"
6
+
7
+ require "mobius/client/version"
8
+
9
+ module Mobius
10
+ module Cli
11
+ autoload :Base, "mobius/cli/base"
12
+ autoload :App, "mobius/cli/app"
13
+ autoload :Auth, "mobius/cli/auth"
14
+ autoload :Create, "mobius/cli/create"
15
+ end
16
+
17
+ module Client
18
+ autoload :Error, "mobius/client/error"
19
+ autoload :FriendBot, "mobius/client/friend_bot"
20
+ autoload :App, "mobius/client/app"
21
+
22
+ module Auth
23
+ autoload :Challenge, "mobius/client/auth/challenge"
24
+ autoload :Jwt, "mobius/client/auth/jwt"
25
+ autoload :Sign, "mobius/client/auth/sign"
26
+ autoload :Token, "mobius/client/auth/token"
27
+ end
28
+
29
+ module Blockchain
30
+ autoload :Account, "mobius/client/blockchain/account"
31
+ autoload :AddCosigner, "mobius/client/blockchain/add_cosigner"
32
+ autoload :CreateTrustline, "mobius/client/blockchain/create_trustline"
33
+ autoload :FriendBot, "mobius/client/blockchain/friend_bot"
34
+ autoload :KeyPairFactory, "mobius/client/blockchain/key_pair_factory"
35
+ end
36
+
37
+ class << self
38
+ attr_writer :mobius_host
39
+
40
+ # Mobius API host
41
+ def mobius_host
42
+ @mobius_host ||= "https://mobius.network"
43
+ end
44
+
45
+ def network=(value)
46
+ @network = value
47
+ Stellar.default_network = stellar_network
48
+ end
49
+
50
+ # Stellar network to use (:test || :public). See notes on thread-safety in ruby-stellar-base.
51
+ # Safe to set on startup.
52
+ def network
53
+ @network ||= :test
54
+ end
55
+
56
+ # `Stellar::Client` instance
57
+ attr_writer :horizon_client
58
+
59
+ def horizon_client
60
+ @horizon_client ||= network == :test ? Stellar::Client.default_testnet : Stellar::Client.default
61
+ end
62
+
63
+ # Asset code used for payments (MOBI by default)
64
+ attr_writer :asset_code
65
+
66
+ def asset_code
67
+ @asset_code ||= "MOBI"
68
+ end
69
+
70
+ # Asset issuer account
71
+ attr_writer :asset_issuer
72
+
73
+ def asset_issuer
74
+ return @asset_issuer if @asset_issuer
75
+ return "GA6HCMBLTZS5VYYBCATRBRZ3BZJMAFUDKYYF6AH6MVCMGWMRDNSWJPIH" if network == :public
76
+ "GDRWBLJURXUKM4RWDZDTPJNX6XBYFO3PSE4H4GPUL6H6RCUQVKTSD4AT"
77
+ end
78
+
79
+ # Challenge expires in (seconds, 1d by default)
80
+ attr_writer :challenge_expires_in
81
+
82
+ def challenge_expires_in
83
+ @challenge_expires_in ||= 60 * 60 * 24
84
+ end
85
+
86
+ # Stellar::Asset instance of asset used for payments
87
+ def stellar_asset
88
+ @stellar_asset ||= Stellar::Asset.alphanum4(asset_code, Stellar::KeyPair.from_address(asset_issuer))
89
+ end
90
+
91
+ # In strict mode, session must be not older than seconds from now (10 by default)
92
+ attr_writer :strict_interval
93
+
94
+ def strict_interval
95
+ @strict_interval ||= 10
96
+ end
97
+
98
+ # Runs block on selected Stellar network
99
+ def on_network
100
+ Stellar.on_network(stellar_network) do
101
+ yield if block_given?
102
+ end
103
+ end
104
+
105
+ # Converts given argument to Stellar::KeyPair
106
+ def to_keypair(subject)
107
+ Mobius::Client::Blockchain::KeyPairFactory.produce(subject)
108
+ end
109
+
110
+ private
111
+
112
+ def stellar_network
113
+ Mobius::Client.network == :test ? Stellar::Networks::TESTNET : Stellar::Networks::PUBLIC
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,136 @@
1
+ # Interface to user balance in application.
2
+ class Mobius::Client::App
3
+ extend Dry::Initializer
4
+
5
+ # @!method initialize(seed)
6
+ # @param seed [String] Developers private key.
7
+ # @param address [String] Users public key.
8
+ # @!scope instance
9
+ param :seed
10
+ param :address
11
+
12
+ # Checks if developer is authorized to use an application.
13
+ # @return [Bool] Authorization status.
14
+ def authorized?
15
+ user_account.authorized?(app_keypair)
16
+ end
17
+
18
+ # Returns user balance.
19
+ # @return [Float] User balance.
20
+ def balance
21
+ validate!
22
+ balance_object["balance"].to_f
23
+ end
24
+
25
+ # Returns application balance.
26
+ # @return [Float] Application balance.
27
+ def app_balance
28
+ app_balance_object["balance"].to_f
29
+ end
30
+
31
+ # Makes payment.
32
+ # @param amount [Float] Payment amount.
33
+ # @param target_address [String] Optional: third party receiver address.
34
+ def pay(amount, target_address: nil)
35
+ current_balance = balance
36
+ raise Mobius::Client::Error::InsufficientFunds if current_balance < amount.to_f
37
+ envelope_base64 = payment_tx(amount.to_f, target_address).to_envelope(app_keypair).to_xdr(:base64)
38
+ post_tx(envelope_base64).tap do
39
+ [app_account, user_account].each(&:reload!)
40
+ end
41
+ end
42
+
43
+ # Sends money from application account to third party.
44
+ # @param amount [Float] Payment amount.
45
+ # @param address [String] Target address.
46
+ def transfer(amount, address)
47
+ current_balance = app_balance
48
+ raise Mobius::Client::Error::InsufficientFunds if current_balance < amount.to_f
49
+ envelope_base64 = transfer_tx(amount.to_f, address).to_envelope(app_keypair).to_xdr(:base64)
50
+ post_tx(envelope_base64).tap do
51
+ [app_account, user_account].each(&:reload!)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def post_tx(txe)
58
+ Mobius::Client.horizon_client.horizon.transactions._post(tx: txe)
59
+ end
60
+
61
+ def payment_tx(amount, target_address)
62
+ Stellar::Transaction.for_account(
63
+ account: user_keypair,
64
+ sequence: user_account.next_sequence_value,
65
+ fee: target_address.nil? ? FEE : FEE * 2
66
+ ).tap do |t|
67
+ t.operations << payment_op(amount.to_f)
68
+ t.operations << third_party_payment_op(target_address, amount) if target_address
69
+ end
70
+ end
71
+
72
+ def payment_op(amount)
73
+ Stellar::Operation.payment(
74
+ destination: app_keypair,
75
+ amount: Stellar::Amount.new(amount.to_f, Mobius::Client.stellar_asset).to_payment
76
+ )
77
+ end
78
+
79
+ def third_party_payment_op(target_address, amount)
80
+ Stellar::Operation.payment(
81
+ source_account: app_keypair,
82
+ destination: Mobius::Client.to_keypair(target_address),
83
+ amount: Stellar::Amount.new(amount.to_f, Mobius::Client.stellar_asset).to_payment
84
+ )
85
+ end
86
+
87
+ def transfer_tx(amount, address)
88
+ Stellar::Transaction.payment(
89
+ account: user_keypair,
90
+ sequence: user_account.next_sequence_value,
91
+ destination: Mobius::Client.to_keypair(address),
92
+ amount: Stellar::Amount.new(amount.to_f, Mobius::Client.stellar_asset).to_payment
93
+ )
94
+ end
95
+
96
+ def validate!
97
+ raise Mobius::Client::Error::AuthorisationMissing unless authorized?
98
+ raise Mobius::Client::Error::TrustlineMissing if balance_object.nil?
99
+ end
100
+
101
+ def limit
102
+ balance_object["limit"].to_f
103
+ end
104
+
105
+ def balance_object
106
+ find_balance(user_account.info.balances)
107
+ end
108
+
109
+ def app_balance_object
110
+ find_balance(app_account.info.balances)
111
+ end
112
+
113
+ def find_balance(balances)
114
+ balances.find do |s|
115
+ s["asset_code"] == Mobius::Client.asset_code && s["asset_issuer"] == Mobius::Client.asset_issuer
116
+ end
117
+ end
118
+
119
+ def app_keypair
120
+ @app_keypair ||= Mobius::Client.to_keypair(seed)
121
+ end
122
+
123
+ def user_keypair
124
+ @user_keypair ||= Mobius::Client.to_keypair(address)
125
+ end
126
+
127
+ def app_account
128
+ @app_account ||= Mobius::Client::Blockchain::Account.new(app_keypair)
129
+ end
130
+
131
+ def user_account
132
+ @user_account ||= Mobius::Client::Blockchain::Account.new(user_keypair)
133
+ end
134
+
135
+ FEE = 100
136
+ end
@@ -0,0 +1,72 @@
1
+ # Generates challenge transaction on developer's side.
2
+ class Mobius::Client::Auth::Challenge
3
+ extend Dry::Initializer
4
+
5
+ # @!method initialize(seed)
6
+ # @param seed [String] Developers private key
7
+ # @!scope instance
8
+ param :seed
9
+
10
+ # Generates challenge transaction signed by developers private key. Minimum valid time bound is set to current time.
11
+ # Maximum valid time bound is set to `expire_in` seconds from now.
12
+ #
13
+ # @param expire_in [Integer] Session expiration time (seconds from now). 0 means "never".
14
+ # @return [String] base64-encoded transaction envelope
15
+ def call(expire_in = Mobius::Client.challenge_expires_in)
16
+ payment = Stellar::Transaction.payment(
17
+ source_account: keypair,
18
+ account: Stellar::KeyPair.random,
19
+ destination: keypair,
20
+ sequence: random_sequence,
21
+ amount: micro_xlm,
22
+ memo: memo
23
+ )
24
+
25
+ payment.time_bounds = build_time_bounds(expire_in)
26
+
27
+ payment.to_envelope(keypair).to_xdr(:base64)
28
+ end
29
+
30
+ class << self
31
+ # Shortcut to challenge generation method.
32
+ #
33
+ # @param seed [String] Developers private key
34
+ # @return [String] base64-encoded transaction envelope
35
+ def call(*args)
36
+ new(*args).call
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # @return [Stellar::Keypair] Stellar::Keypair object for given seed.
43
+ def keypair
44
+ @keypair ||= Stellar::KeyPair.from_seed(seed)
45
+ end
46
+
47
+ # @return [Integer] Random sequence number
48
+ def random_sequence
49
+ MAX_SEQ_NUMBER - SecureRandom.random_number(RANDOM_LIMITS)
50
+ end
51
+
52
+ # @return [Stellar::TimeBounds] Current time..expire time
53
+ def build_time_bounds(expire_in)
54
+ Stellar::TimeBounds.new(
55
+ min_time: Time.now.to_i,
56
+ max_time: Time.now.to_i + expire_in.to_i || 0
57
+ )
58
+ end
59
+
60
+ # @return [Stellar::Amount] 1 XLM
61
+ def micro_xlm
62
+ Stellar::Amount.new(1).to_payment
63
+ end
64
+
65
+ # @return [Stellar::Memo] Auth transaction memo
66
+ def memo
67
+ Stellar::Memo.new(:memo_text, "Mobius authentication")
68
+ end
69
+
70
+ MAX_SEQ_NUMBER = (2**128 - 1).freeze # MAX sequence number
71
+ RANDOM_LIMITS = 65535 # Sequence random limits
72
+ end
@@ -0,0 +1,37 @@
1
+ # Generates JWT token based on valid token transaction signed by both parties and decodes JWT token into hash.
2
+ class Mobius::Client::Auth::Jwt
3
+ extend Dry::Initializer
4
+
5
+ # @!method initialize(secret)
6
+ # @param secret [String] JWT secret
7
+ # @!scope instance
8
+ param :secret
9
+
10
+ # Returns JWT token.
11
+ # @param token [Mobius::Client::Auth::Token] Valid auth token
12
+ # @return [String] JWT token
13
+ def encode(token)
14
+ payload = {
15
+ hash: token.hash(:hex),
16
+ public_key: token.address,
17
+ min_time: token.time_bounds.min_time,
18
+ max_time: token.time_bounds.max_time
19
+ }
20
+
21
+ JWT.encode(payload, secret, ALG)
22
+ end
23
+
24
+ # Returns decoded JWT token.
25
+ # @param jwt [String] JWT token
26
+ # @return [Hash] Decoded token params
27
+ def decode!(jwt)
28
+ OpenStruct.new(
29
+ JWT.decode(jwt, secret, true, algorithm: ALG).first
30
+ ).tap do |payload|
31
+ raise TokenExpired unless (payload.min_time..payload.max_time).cover?(Time.now.to_i)
32
+ end
33
+ end
34
+
35
+ # Used JWT algorithm
36
+ ALG = "HS512".freeze
37
+ end
@@ -0,0 +1,48 @@
1
+ # Signs challenge transaction on user's side.
2
+ class Mobius::Client::Auth::Sign
3
+ extend Dry::Initializer
4
+
5
+ # @!method initialize(seed, xdr)
6
+ # @param seed [String] Users private key
7
+ # @param xdr [String] Challenge transaction xdr
8
+ # @param address [String] Developers public key
9
+ # @!scope instance
10
+ param :seed
11
+ param :xdr
12
+ param :address
13
+
14
+ # Adds signature to given transaction.
15
+ #
16
+ # @return [String] base64-encoded transaction envelope
17
+ def call
18
+ validate!
19
+ envelope.dup.tap { |e| e.signatures << e.tx.sign_decorated(keypair) }.to_xdr(:base64)
20
+ end
21
+
22
+ class << self
23
+ def call(*args)
24
+ new(*args).call
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # @return [Stellar::Keypair] Stellar::Keypair object for given seed.
31
+ def keypair
32
+ @keypair ||= Stellar::KeyPair.from_seed(seed)
33
+ end
34
+
35
+ # @return [Stellar::Keypair] Stellar::Keypair object for given address.
36
+ def developer_keypair
37
+ @developer_keypair ||= Stellar::KeyPair.from_address(address)
38
+ end
39
+
40
+ # @return [Stellar::TransactionEnvelope] Stellar::TransactionEnvelope for given challenge.
41
+ def envelope
42
+ @envelope ||= Stellar::TransactionEnvelope.from_xdr(xdr, "base64")
43
+ end
44
+
45
+ def validate!
46
+ raise Mobius::Client::Error::Unauthorized unless envelope.signed_correctly?(developer_keypair)
47
+ end
48
+ end
@@ -0,0 +1,81 @@
1
+ # Checks challenge transaction signed by user on developer's side.
2
+ class Mobius::Client::Auth::Token
3
+ extend Dry::Initializer
4
+
5
+ # @!method initialize(seed, xdr, address)
6
+ # @param seed [String] Developers private key.
7
+ # @param xdr [String] Auth transaction XDR.
8
+ # @param address [String] User public key.
9
+ # @!scope instance
10
+ param :seed
11
+ param :xdr
12
+ param :address
13
+
14
+ # Returns time bounds for given transaction.
15
+ #
16
+ # @return [Stellar::TimeBounds] Time bounds for given transaction (`.min_time` and `.max_time`).
17
+ # @raise [Unauthorized] if one of the signatures is invalid.
18
+ # @raise [Invalid] if transaction is malformed or time bounds are missing.
19
+ def time_bounds
20
+ bounds = envelope.tx.time_bounds
21
+
22
+ raise Mobius::Client::Error::Unauthorized unless signed_correctly?
23
+ raise Mobius::Client::Error::MalformedTransaction if bounds.nil?
24
+
25
+ bounds
26
+ end
27
+
28
+ # Validates transaction signed by developer and user.
29
+ #
30
+ # @param strict [Bool] if true, checks that lower time limit is within Mobius::Client.strict_interval seconds from now
31
+ # @return [Boolean] true if transaction is valid, raises exception otherwise
32
+ # @raise [Unauthorized] if one of the signatures is invalid
33
+ # @raise [Invalid] if transaction is malformed or time bounds are missing
34
+ # @raise [Expired] if transaction is expired (current time outside it's time bounds)
35
+ def validate!(strict = true)
36
+ bounds = time_bounds
37
+ raise Mobius::Client::Error::TokenExpired unless time_now_covers?(bounds)
38
+ raise Mobius::Client::Error::TokenTooOld if strict && too_old?(bounds)
39
+ true
40
+ end
41
+
42
+ # @return [String] transaction hash
43
+ def hash(format = :binary)
44
+ validate! # Guard!
45
+ h = envelope.tx.hash
46
+ return h if format == :binary
47
+ h.unpack("H*").first
48
+ end
49
+
50
+ private
51
+
52
+ # @return [Stellar::KeyPair] Stellar::KeyPair object for given seed
53
+ def keypair
54
+ @keypair ||= Stellar::KeyPair.from_seed(seed)
55
+ end
56
+
57
+ # @return [Stellar::KeyPair] Stellar::KeyPair of user being authorized
58
+ def their_keypair
59
+ @their_keypair ||= Stellar::KeyPair.from_address(address)
60
+ end
61
+
62
+ # @return [Stellar::TrnansactionEnvelope] Stellar::TrnansactionEnvelope of challenge transaction
63
+ def envelope
64
+ @envelope ||= Stellar::TransactionEnvelope.from_xdr(xdr, "base64")
65
+ end
66
+
67
+ # @return [Bool] true if transaction is signed by both parties
68
+ def signed_correctly?
69
+ envelope.signed_correctly?(keypair, their_keypair)
70
+ end
71
+
72
+ # @return [Bool] true if current time is within transaction time bounds
73
+ def time_now_covers?(time_bounds)
74
+ (time_bounds.min_time..time_bounds.max_time).cover?(Time.now.to_i)
75
+ end
76
+
77
+ # @return [Bool] true if transaction is created more than n secods from now
78
+ def too_old?(time_bounds)
79
+ Time.now.to_i > time_bounds.min_time + Mobius::Client.strict_interval
80
+ end
81
+ end