bullion_vault 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.
- data/Gemfile +3 -0
- data/Gemfile.lock +45 -0
- data/MIT-LICENSE +21 -0
- data/README.mkd +51 -0
- data/Rakefile +8 -0
- data/bullion_vault.gemspec +27 -0
- data/lib/bullion_vault.rb +23 -0
- data/lib/bullion_vault/api.rb +21 -0
- data/lib/bullion_vault/authentication.rb +9 -0
- data/lib/bullion_vault/client.rb +11 -0
- data/lib/bullion_vault/client/login.rb +26 -0
- data/lib/bullion_vault/client/view_balance.rb +11 -0
- data/lib/bullion_vault/client/view_market.rb +11 -0
- data/lib/bullion_vault/configuration.rb +66 -0
- data/lib/bullion_vault/connection.rb +33 -0
- data/lib/bullion_vault/error.rb +29 -0
- data/lib/bullion_vault/request.rb +34 -0
- data/lib/bullion_vault/version.rb +3 -0
- data/lib/faraday/cookie_auth.rb +14 -0
- data/lib/faraday/raise_http_4xx.rb +48 -0
- data/lib/faraday/raise_http_5xx.rb +29 -0
- data/lib/faraday/raise_invalid_cookie.rb +34 -0
- data/spec/bullion_vault/api_spec.rb +67 -0
- data/spec/bullion_vault/client/login_spec.rb +51 -0
- data/spec/bullion_vault/client/view_balance_spec.rb +17 -0
- data/spec/bullion_vault/client/view_market_spec.rb +16 -0
- data/spec/bullion_vault/client_spec.rb +13 -0
- data/spec/bullion_vault_spec.rb +65 -0
- data/spec/faraday/cookie_auth_spec.rb +14 -0
- data/spec/faraday/response_spec.rb +40 -0
- data/spec/fixtures/view_balance.xml +41 -0
- data/spec/fixtures/view_balance.yaml +32 -0
- data/spec/fixtures/view_market.xml +261 -0
- data/spec/fixtures/view_market.yaml +164 -0
- data/spec/spec_helper.rb +37 -0
- metadata +148 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
|
|
3
|
+
module Faraday
|
|
4
|
+
class Request::CookieAuth < Faraday::Middleware
|
|
5
|
+
def call(env)
|
|
6
|
+
env[:request_headers]['Cookie'] = @cookie
|
|
7
|
+
@app.call(env)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(app, cookie)
|
|
11
|
+
@app, @cookie = app, cookie
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
|
|
3
|
+
module Faraday
|
|
4
|
+
class Response::RaiseHttp4xx < Response::Middleware
|
|
5
|
+
def self.register_on_complete(env)
|
|
6
|
+
env[:response].on_complete do |response|
|
|
7
|
+
case response[:status].to_i
|
|
8
|
+
when 400
|
|
9
|
+
raise BullionVault::BadRequest, error_message(response)
|
|
10
|
+
when 401
|
|
11
|
+
raise BullionVault::Unauthorized, error_message(response)
|
|
12
|
+
when 403
|
|
13
|
+
raise BullionVault::Forbidden, error_message(response)
|
|
14
|
+
when 404
|
|
15
|
+
raise BullionVault::NotFound, error_message(response)
|
|
16
|
+
when 406
|
|
17
|
+
raise BullionVault::NotAcceptable, error_message(response)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(app)
|
|
23
|
+
super
|
|
24
|
+
@parser = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def self.error_message(response)
|
|
30
|
+
"#{response[:method].to_s.upcase} #{response[:url].to_s}: #{response[:status]}#{error_body(response[:body])}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.error_body(body)
|
|
34
|
+
if body.nil?
|
|
35
|
+
nil
|
|
36
|
+
elsif body['error']
|
|
37
|
+
": #{body['error']}"
|
|
38
|
+
elsif body['errors']
|
|
39
|
+
first = body['errors'].to_a.first
|
|
40
|
+
if first.kind_of? Hash
|
|
41
|
+
": #{first['message'].chomp}"
|
|
42
|
+
else
|
|
43
|
+
": #{first.chomp}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
|
|
3
|
+
module Faraday
|
|
4
|
+
class Response::RaiseHttp5xx < Response::Middleware
|
|
5
|
+
def self.register_on_complete(env)
|
|
6
|
+
env[:response].on_complete do |response|
|
|
7
|
+
case response[:status].to_i
|
|
8
|
+
when 500
|
|
9
|
+
raise BullionVault::InternalServerError, error_message(response, 'Something is technically wrong.')
|
|
10
|
+
when 502
|
|
11
|
+
raise BullionVault::BadGateway, error_message(response, 'BullionVault is down or being upgraded.')
|
|
12
|
+
when 503
|
|
13
|
+
raise BullionVault::ServiceUnavailable, error_message(response, '(__-){ BullionVault is over capacity.')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(app)
|
|
19
|
+
super
|
|
20
|
+
@parser = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def self.error_message(response, body=nil)
|
|
26
|
+
"#{response[:method].to_s.upcase} #{response[:url].to_s}: #{[response[:status].to_s + ':', body].compact.join(' ')} Check http://goldnews.bullionvault.com/ for updates on the status of the BullionVault service."
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
|
|
3
|
+
module Faraday
|
|
4
|
+
class Response::RaiseInvalidCookie < Response::Middleware
|
|
5
|
+
def self.register_on_complete(env)
|
|
6
|
+
env[:response].on_complete do |response|
|
|
7
|
+
if response[:response_headers]['set-cookie']
|
|
8
|
+
raise BullionVault::InvalidCookie, error_message(response)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def self.error_message(response)
|
|
16
|
+
"#{response[:method].to_s.upcase} #{response[:url].to_s}: #{response[:status]}#{error_body(response[:body])}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.error_body(body)
|
|
20
|
+
if body.nil?
|
|
21
|
+
nil
|
|
22
|
+
elsif body['error']
|
|
23
|
+
": #{body['error']}"
|
|
24
|
+
elsif body['errors']
|
|
25
|
+
first = body['errors'].to_a.first
|
|
26
|
+
if first.kind_of? Hash
|
|
27
|
+
": #{first['message'].chomp}"
|
|
28
|
+
else
|
|
29
|
+
": #{first.chomp}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
|
2
|
+
|
|
3
|
+
describe BullionVault::API do
|
|
4
|
+
before(:each) do
|
|
5
|
+
@keys = BullionVault::Configuration::VALID_OPTIONS_KEYS
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
context 'with module configuration' do
|
|
9
|
+
|
|
10
|
+
before do
|
|
11
|
+
BullionVault.configure do |config|
|
|
12
|
+
@keys.each do |key|
|
|
13
|
+
config.public_send("#{key}=", key)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
after do
|
|
19
|
+
BullionVault.reset
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'inherits module configuration' do
|
|
23
|
+
api = BullionVault::API.new
|
|
24
|
+
@keys.each do |key|
|
|
25
|
+
api.public_send(key).should == key
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
context 'with class configuration' do
|
|
30
|
+
|
|
31
|
+
before do
|
|
32
|
+
@configuration = {
|
|
33
|
+
:user_login => 'login',
|
|
34
|
+
:user_password => 'secret',
|
|
35
|
+
:adapter => :typhoeus,
|
|
36
|
+
:endpoint => 'http://example.com/',
|
|
37
|
+
:format => :xml,
|
|
38
|
+
:proxy => 'http://user:passwd@proxy.example.com:8080',
|
|
39
|
+
:cookie => 'COOKIE_DATA',
|
|
40
|
+
:user_agent => 'Custom User Agent',
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
context 'during initialization'
|
|
45
|
+
|
|
46
|
+
it 'overrides module configuration' do
|
|
47
|
+
api = BullionVault::API.new(@configuration)
|
|
48
|
+
@keys.each do |key|
|
|
49
|
+
api.public_send(key).should == @configuration[key]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context 'after initilization' do
|
|
54
|
+
|
|
55
|
+
it 'overrides module configuration after initialization' do
|
|
56
|
+
api = BullionVault::API.new
|
|
57
|
+
@configuration.each do |key, value|
|
|
58
|
+
api.public_send("#{key}=", value)
|
|
59
|
+
end
|
|
60
|
+
@keys.each do |key|
|
|
61
|
+
api.public_send(key).should == @configuration[key]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe BullionVault::Client::Login do
|
|
4
|
+
describe '#reset_cookie' do
|
|
5
|
+
before(:each) do
|
|
6
|
+
@client = BullionVault::Client.new(:user_login => 'user', :user_password => 'pass')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'resets the cookie with a new value from the server' do
|
|
10
|
+
stub_request(:get, 'https://live.bullionvault.com/secure/login.do')
|
|
11
|
+
.to_return(:status => 200, :headers => {'set-cookie' => 'monster'})
|
|
12
|
+
|
|
13
|
+
@client.send(:reset_cookie).should eq 'monster'
|
|
14
|
+
@client.cookie.should eq 'monster'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '#login' do
|
|
19
|
+
before(:each) do
|
|
20
|
+
@client = BullionVault::Client.new(
|
|
21
|
+
:user_login => 'user',
|
|
22
|
+
:user_password => 'pass',
|
|
23
|
+
:cookie => 'monster',
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'posts the login credentials' do
|
|
28
|
+
stub_request(:post, 'https://live.bullionvault.com/secure/j_security_check')
|
|
29
|
+
.with(:body => 'j_username=user&j_password=pass', :headers => {'Cookie' => 'monster'})
|
|
30
|
+
.to_return(:status => 302, :headers => {'location' => 'https://live.bullionvault.com/secure/main_frame.do'})
|
|
31
|
+
|
|
32
|
+
@client.send(:login).should be_true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'fails when the server redirects to an unexpected URL' do
|
|
36
|
+
stub_request(:post, 'https://live.bullionvault.com/secure/j_security_check')
|
|
37
|
+
.with(:body => 'j_username=user&j_password=pass', :headers => {'Cookie' => 'monster'})
|
|
38
|
+
.to_return(:status => 302, :headers => {'location' => 'https://live.bullionvault.com/secure/surprise.do'})
|
|
39
|
+
|
|
40
|
+
@client.send(:login).should be_false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'fails when the server returns a response other than 302' do
|
|
44
|
+
stub_request(:post, 'https://live.bullionvault.com/secure/j_security_check')
|
|
45
|
+
.with(:body => 'j_username=user&j_password=pass', :headers => {'Cookie' => 'monster'})
|
|
46
|
+
.to_return(:status => 200, :headers => {'location' => 'https://live.bullionvault.com/secure/surprise.do'})
|
|
47
|
+
|
|
48
|
+
@client.send(:login).should be_false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe BullionVault::Client::ViewBalance do
|
|
4
|
+
before(:each) do
|
|
5
|
+
@client = BullionVault::Client.new(:cookie => 'monster')
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe '#view_market' do
|
|
9
|
+
it 'gets market offers' do
|
|
10
|
+
stub_request(:get, 'https://live.bullionvault.com/view_market_xml.do')
|
|
11
|
+
.with(:headers => {'Cookie' => 'monster'})
|
|
12
|
+
.to_return(:status => 200, :body => fixture('view_balance.xml'))
|
|
13
|
+
|
|
14
|
+
@client.view_market.should eq yaml_fixture('view_balance.yaml')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe BullionVault::Client::ViewMarket do
|
|
4
|
+
before(:each) do
|
|
5
|
+
@client = BullionVault::Client.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe '#view_market' do
|
|
9
|
+
it 'gets market offers' do
|
|
10
|
+
stub_request(:get, 'https://live.bullionvault.com/view_market_xml.do')
|
|
11
|
+
.to_return(:status => 200, :body => fixture('view_market.xml'))
|
|
12
|
+
|
|
13
|
+
@client.view_market.should eq yaml_fixture('view_market.yaml')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
|
2
|
+
|
|
3
|
+
describe BullionVault::Client do
|
|
4
|
+
before(:each) do
|
|
5
|
+
@client = BullionVault::Client.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it 'connects to the endpoint configuration' do
|
|
9
|
+
endpoint = URI.parse(@client.api_endpoint).to_s
|
|
10
|
+
connection = @client.send(:connection).build_url(nil).to_s
|
|
11
|
+
connection.should == endpoint
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
|
2
|
+
require 'spec_helper'
|
|
3
|
+
|
|
4
|
+
describe BullionVault do
|
|
5
|
+
after do
|
|
6
|
+
BullionVault.reset
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
context 'when delegating to a client' do
|
|
10
|
+
before do
|
|
11
|
+
stub_request(:get, 'https://live.bullionvault.com/view_market_xml.do')
|
|
12
|
+
.to_return(:body => fixture('view_market.xml'))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'gets the correct resource' do
|
|
16
|
+
BullionVault.view_market
|
|
17
|
+
a_request(:get, 'https://live.bullionvault.com/view_market_xml.do')
|
|
18
|
+
.should have_been_made
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns the same results as a client' do
|
|
22
|
+
BullionVault.view_market.should == BullionVault::Client.new.view_market
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '.client' do
|
|
27
|
+
it 'is a BullionVault::Client' do
|
|
28
|
+
BullionVault.client.should be_a BullionVault::Client
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
OPTIONS_KEYS = %w{adapter user_login user_password endpoint format proxy cookie user_agent}
|
|
33
|
+
|
|
34
|
+
describe 'VALID_OPTIONS_KEYS' do
|
|
35
|
+
it 'matches the list in the spec' do
|
|
36
|
+
OPTIONS_KEYS.map(&:to_sym).should eq BullionVault::Configuration::VALID_OPTIONS_KEYS
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
OPTIONS_KEYS.each do |key|
|
|
41
|
+
describe ".#{key}" do
|
|
42
|
+
it "returns the default #{key}" do
|
|
43
|
+
BullionVault.public_send(key).should eq BullionVault::Configuration.const_get("default_#{key}".upcase)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe ".#{key}=" do
|
|
48
|
+
it "sets the #{key}" do
|
|
49
|
+
BullionVault.public_send("#{key}=", 'test_value')
|
|
50
|
+
BullionVault.public_send(key).should eq 'test_value'
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe '.configure' do
|
|
56
|
+
BullionVault::Configuration::VALID_OPTIONS_KEYS.each do |key|
|
|
57
|
+
it "sets the #{key}" do
|
|
58
|
+
BullionVault.configure do |config|
|
|
59
|
+
config.public_send("#{key}=", key)
|
|
60
|
+
BullionVault.public_send(key).should == key
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
|
2
|
+
|
|
3
|
+
describe Faraday::Request::CookieAuth do
|
|
4
|
+
before(:each) do
|
|
5
|
+
@client = BullionVault::Client.new(:cookie => 'monster')
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it 'sets the cookie in requests' do
|
|
9
|
+
stub_request(:get, 'https://live.bullionvault.com/secure/login.do')
|
|
10
|
+
.with(:headers => {'Cookie' => 'monster'})
|
|
11
|
+
.to_return(:status => 200)
|
|
12
|
+
@client.get('secure/login.do', {}, true).should be_success
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
|
2
|
+
|
|
3
|
+
describe Faraday::Response do
|
|
4
|
+
before do
|
|
5
|
+
@client = BullionVault::Client.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
context 'when the cookie is set' do
|
|
9
|
+
it 'raises InvalidCookieError' do
|
|
10
|
+
stub_request(:get, 'https://live.bullionvault.com/action.do')
|
|
11
|
+
.to_return(:status => 200, :headers => {'set-cookie' => 'monster'})
|
|
12
|
+
|
|
13
|
+
@client.cookie = 'illegitimate_value'
|
|
14
|
+
proc { @client.get('action.do') }.should raise_error(BullionVault::InvalidCookie)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
400 => BullionVault::BadRequest,
|
|
20
|
+
401 => BullionVault::Unauthorized,
|
|
21
|
+
403 => BullionVault::Forbidden,
|
|
22
|
+
404 => BullionVault::NotFound,
|
|
23
|
+
406 => BullionVault::NotAcceptable,
|
|
24
|
+
500 => BullionVault::InternalServerError,
|
|
25
|
+
502 => BullionVault::BadGateway,
|
|
26
|
+
503 => BullionVault::ServiceUnavailable,
|
|
27
|
+
}.each do |status, exception|
|
|
28
|
+
context "when HTTP status is #{status}" do
|
|
29
|
+
|
|
30
|
+
before do
|
|
31
|
+
stub_request(:get, 'https://live.bullionvault.com/error_inducing_action.do')
|
|
32
|
+
.to_return(:status => status)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "raises #{exception.name} error" do
|
|
36
|
+
proc { @client.get('error_inducing_action.do') }.should raise_error(exception)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<?xml version="1.0"?>
|
|
2
|
+
<envelope>
|
|
3
|
+
<message type="CLIENT_BALANCE_A" version="0.2">
|
|
4
|
+
<clientBalance>
|
|
5
|
+
<clientPositions>
|
|
6
|
+
<clientPosition
|
|
7
|
+
securityId="USD"
|
|
8
|
+
available="101"
|
|
9
|
+
total="101"
|
|
10
|
+
classNarrative="CURRENCY"
|
|
11
|
+
totalValuation="101"
|
|
12
|
+
valuationCurrency="USD"
|
|
13
|
+
/>
|
|
14
|
+
<clientPosition
|
|
15
|
+
securityId="GBP"
|
|
16
|
+
available="1"
|
|
17
|
+
total="1"
|
|
18
|
+
classNarrative="CURRENCY"
|
|
19
|
+
totalValuation="1.61"
|
|
20
|
+
valuationCurrency="USD"
|
|
21
|
+
/>
|
|
22
|
+
<clientPosition
|
|
23
|
+
securityId="EUR"
|
|
24
|
+
available="1"
|
|
25
|
+
total="1"
|
|
26
|
+
classNarrative="CURRENCY"
|
|
27
|
+
totalValuation="1.43"
|
|
28
|
+
valuationCurrency="USD"
|
|
29
|
+
/>
|
|
30
|
+
<clientPosition
|
|
31
|
+
securityId="AUXZU"
|
|
32
|
+
available="0.001"
|
|
33
|
+
total="0.001"
|
|
34
|
+
classNarrative="GOLD"
|
|
35
|
+
totalValuation="45.95"
|
|
36
|
+
valuationCurrency="USD"
|
|
37
|
+
/>
|
|
38
|
+
</clientPositions>
|
|
39
|
+
</clientBalance>
|
|
40
|
+
</message>
|
|
41
|
+
</envelope>
|