mobius-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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