webull 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7d1daa5fda4d5eccdcc7740e87cbef4ce22d7a37a1aca6fcfc12a68f046576fc
4
+ data.tar.gz: 3b13d4f635149fc836a26817f2afb9dcc0fbbd5fde340ccf9b52786f261a271d
5
+ SHA512:
6
+ metadata.gz: 67087b23ccd5deedeb63af2566201f3141ca27741be7351bd1f8f6ca886a712a5654f3914d671ea39565c1950e4a4dbec876b284fe7709a47f1516ed46652010
7
+ data.tar.gz: d842d7f77917590e909f293b9302d0b0b4d022400620879c0c67958287c9afec53044c35bcc5538e4cb9b93e0a64b5029a70270627d0090aa3e37d56943461e7
@@ -0,0 +1,2 @@
1
+ 2.7.2
2
+ 2.6.6
@@ -0,0 +1,44 @@
1
+ name: Build
2
+ on: [pull_request, push]
3
+ jobs:
4
+ build:
5
+ name: Build
6
+ runs-on: ubuntu-latest
7
+ steps:
8
+ - name: Checkout
9
+ uses: akerl/action-checkout@v2.0.0
10
+ - name: Install Ruby
11
+ uses: akerl/setup-ruby@v1
12
+ with:
13
+ ruby-version: '2.7'
14
+ - name: Install deps
15
+ run: bundle install
16
+ - name: Rake
17
+ run: bundle exec rake
18
+ - name: Log in to Rubygems
19
+ run: 'mkdir -p ~/.gem && echo ":rubygems_api_key: ${RUBY_TOKEN}" > ~/.gem/credentials && chmod 600 ~/.gem/credentials'
20
+ if: startsWith(github.ref, 'refs/tags/')
21
+ env:
22
+ RUBY_TOKEN: ${{ secrets.RUBY_TOKEN }}
23
+ - name: Release
24
+ run: bundle exec rake release
25
+ if: startsWith(github.ref, 'refs/tags/')
26
+ - name: Notify on success
27
+ if: success()
28
+ env:
29
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
30
+ uses: akerl/github-action-slack-notify-build@v1.1.1
31
+ with:
32
+ channel_id: ${{ secrets.SLACK_BOT_CHANNEL }}
33
+ status: success
34
+ color: good
35
+ - name: Notify on failure
36
+ if: failure()
37
+ env:
38
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
39
+ uses: akerl/github-action-slack-notify-build@v1.1.1
40
+ with:
41
+ channel_id: ${{ secrets.SLACK_BOT_CHANNEL }}
42
+ status: failed
43
+ color: danger
44
+
@@ -0,0 +1,6 @@
1
+ pkg/*.gem
2
+ coverage/
3
+ .coveralls.yml
4
+ .bundle
5
+ Gemfile.lock
6
+ spec/.creds
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format Fuubar
2
+ --color
@@ -0,0 +1,2 @@
1
+ inherit_gem:
2
+ goodcop: .rubocop.yml
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Les Aker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
@@ -0,0 +1,20 @@
1
+ webull
2
+ =========
3
+
4
+ [![Gem Version](https://img.shields.io/gem/v/webull.svg)](https://rubygems.org/gems/webull)
5
+ [![MIT Licensed](https://img.shields.io/badge/license-MIT-green.svg)](https://tldrlegal.com/license/mit-license)
6
+
7
+ Access the unofficial [Webull](https://webull.com) API to view data about your account.
8
+
9
+ The API information is derived from [@tedchou12's Python Webull module](https://github.com/tedchou12/webull/).
10
+
11
+ ## Usage
12
+
13
+ ## Installation
14
+
15
+ gem install webull
16
+
17
+ ## License
18
+
19
+ webull is released under the MIT License. See the bundled LICENSE file for details.
20
+
@@ -0,0 +1,13 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+
5
+ desc 'Run tests'
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ desc 'Run Rubocop on the gem'
9
+ RuboCop::RakeTask.new(:rubocop) do |task|
10
+ task.fail_on_error = true
11
+ end
12
+
13
+ task default: %i[spec rubocop build install]
@@ -0,0 +1,20 @@
1
+ ##
2
+ # This module provides an interface to Webull's API
3
+ module Webull
4
+ class << self
5
+ ##
6
+ # Insert a helper .new() method for creating a new Account
7
+
8
+ def new(*args)
9
+ self::Account.new(*args)
10
+ end
11
+
12
+ def generate_tokens(*args)
13
+ self::Authenticator.new(*args).generate_tokens
14
+ end
15
+ end
16
+ end
17
+
18
+ require 'webull/version'
19
+ require 'webull/account'
20
+ require 'webull/authenticator'
@@ -0,0 +1,53 @@
1
+ require 'httparty'
2
+
3
+ module Webull
4
+ ##
5
+ # Account defines a user account object for the Webull API
6
+ class Account
7
+ def initialize(params = {})
8
+ @tokens = params[:tokens] || raise('API tokens not provided')
9
+ @udid = params[:udid] || raise('Device ID (udid) not provided')
10
+ end
11
+
12
+ def refresh
13
+ resp = HTTParty.post(
14
+ 'https://userapi.webull.com/api/passport/refreshToken',
15
+ query: { refreshToken: @tokens.refresh },
16
+ headers: headers
17
+ )
18
+ raise("Refresh request failed: #{resp.code}") unless resp.success?
19
+ @tokens = Tokens.from_resp(resp)
20
+ end
21
+
22
+ def account_id
23
+ return @account_id if @account_id
24
+ resp = HTTParty.get(
25
+ 'https://tradeapi.webullbroker.com/api/trade/account/getSecAccountList/v4',
26
+ headers: headers
27
+ )
28
+ raise("Account ID request failed: #{resp.code}") unless resp.success?
29
+ raise("Account ID error received: #{resp['msg']}") unless resp['success']
30
+ @account_id = resp['data'].first['secAccountId']
31
+ end
32
+
33
+ def orders(params = {})
34
+ HTTParty.get(
35
+ 'https://tradeapi.webullbroker.com/api/trade/v2/option/list',
36
+ query: { secAccountId: account_id }.merge(params),
37
+ headers: headers
38
+ )
39
+ end
40
+
41
+ private
42
+
43
+ def headers
44
+ {
45
+ 'did' => @udid,
46
+ 'access_token' => @tokens.access,
47
+ 'Accept' => '*/*',
48
+ 'Accept-Encoding' => 'gzip, deflate',
49
+ 'Content-Type' => 'application/json'
50
+ }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,95 @@
1
+ require 'userinput'
2
+ require 'httparty'
3
+ require 'digest'
4
+
5
+ module Webull
6
+ ##
7
+ # Tokens object describes a full set of API tokens
8
+ class Tokens
9
+ attr_reader :access, :refresh
10
+
11
+ def initialize(params = {})
12
+ @access = params[:access]
13
+ @refresh = params[:refresh]
14
+ end
15
+
16
+ def self.from_resp(resp)
17
+ Tokens.new(
18
+ access: resp['accessToken'],
19
+ refresh: resp['refreshToken']
20
+ )
21
+ end
22
+ end
23
+
24
+ ##
25
+ # Authenticator generates access/refresh tokens from a username/password
26
+ class Authenticator
27
+ attr_reader :udid
28
+
29
+ def initialize(udid)
30
+ @udid = udid
31
+ end
32
+
33
+ def generate_tokens # rubocop:disable Metrics/MethodLength
34
+ params = {
35
+ account: username,
36
+ accountType: 2,
37
+ deviceId: udid,
38
+ regionId: 6,
39
+ grade: 1,
40
+ pwd: hashed_password,
41
+ verificationCode: mfa
42
+ }
43
+ resp = HTTParty.post(
44
+ 'https://userapi.webull.com/api/passport/login/v3/account',
45
+ body: params.to_json,
46
+ headers: { 'Content-Type' => 'application/json' }
47
+ )
48
+ raise("Login request failed: #{resp.code}") unless resp.success?
49
+ Tokens.from_resp(resp)
50
+ end
51
+
52
+ private
53
+
54
+ def mfa
55
+ return @mfa if @mfa
56
+ generate_mfa!
57
+ @mfa = UserInput.new(
58
+ message: 'Webull MFA code',
59
+ validation: /^\d{6}$/
60
+ ).ask
61
+ end
62
+
63
+ def generate_mfa!
64
+ HTTParty.get(
65
+ 'https://userapi.webull.com/api/passport/verificationCode/sendCode',
66
+ query: {
67
+ account: username,
68
+ accountType: 2,
69
+ deviceId: udid,
70
+ codeType: 5,
71
+ regionCode: 1
72
+ }
73
+ )
74
+ end
75
+
76
+ def username
77
+ @username ||= UserInput.new(
78
+ message: 'Webull username',
79
+ validation: /^.*@.*$/
80
+ ).ask
81
+ end
82
+
83
+ def password
84
+ @password ||= UserInput.new(
85
+ message: 'Webull password',
86
+ secret: true,
87
+ validation: /.*/
88
+ ).ask
89
+ end
90
+
91
+ def hashed_password
92
+ @hashed_password ||= Digest::MD5.hexdigest("wl_app-a&b@!423^#{password}")
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Set the version
5
+ module Webull
6
+ VERSION = '0.0.1'
7
+ end
@@ -0,0 +1,2 @@
1
+ require 'rspec'
2
+ require 'webull'
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe Webull do
4
+ describe '#new' do
5
+ it 'creates Account objects' do
6
+ expect(
7
+ Webull.new(tokens: 1, udid: 1)
8
+ ).to be_an_instance_of Webull::Account
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ require 'English'
2
+ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
3
+ require 'webull/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'webull'
7
+ s.version = Webull::VERSION
8
+ s.date = Time.now.strftime('%Y-%m-%d')
9
+
10
+ s.summary = 'Access the Webull API'
11
+ s.description = 'Access the Webull API'
12
+ s.authors = ['Les Aker']
13
+ s.email = 'me@lesaker.org'
14
+ s.homepage = 'https://github.com/akerl/webull'
15
+ s.license = 'MIT'
16
+ s.required_ruby_version = '>= 2.6.0'
17
+
18
+ s.files = `git ls-files`.split
19
+ s.test_files = `git ls-files spec/*`.split
20
+
21
+ s.add_dependency 'httparty', '~> 0.18.1'
22
+ s.add_dependency 'userinput', '~> 1.0.2'
23
+
24
+ s.add_development_dependency 'codecov', '~> 0.2.12'
25
+ s.add_development_dependency 'goodcop', '~> 0.9.2'
26
+ s.add_development_dependency 'rake', '~> 13.0.0'
27
+ s.add_development_dependency 'rspec', '~> 3.10.0'
28
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: webull
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Les Aker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-11-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httparty
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.18.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.18.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: userinput
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: codecov
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.2.12
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.2.12
55
+ - !ruby/object:Gem::Dependency
56
+ name: goodcop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.9.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.9.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 13.0.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 13.0.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 3.10.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 3.10.0
97
+ description: Access the Webull API
98
+ email: me@lesaker.org
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - ".circle-ruby"
104
+ - ".github/workflows/build.yml"
105
+ - ".gitignore"
106
+ - ".rspec"
107
+ - ".rubocop.yml"
108
+ - Gemfile
109
+ - LICENSE
110
+ - README.md
111
+ - Rakefile
112
+ - lib/webull.rb
113
+ - lib/webull/account.rb
114
+ - lib/webull/authenticator.rb
115
+ - lib/webull/version.rb
116
+ - spec/spec_helper.rb
117
+ - spec/webull_spec.rb
118
+ - webull.gemspec
119
+ homepage: https://github.com/akerl/webull
120
+ licenses:
121
+ - MIT
122
+ metadata: {}
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: 2.6.0
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubygems_version: 3.1.4
139
+ signing_key:
140
+ specification_version: 4
141
+ summary: Access the Webull API
142
+ test_files:
143
+ - spec/spec_helper.rb
144
+ - spec/webull_spec.rb