chain-sdk 1.0.0.pre
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/README.md +33 -0
- data/lib/chain/access_token.rb +66 -0
- data/lib/chain/account.rb +101 -0
- data/lib/chain/asset.rb +103 -0
- data/lib/chain/balance.rb +36 -0
- data/lib/chain/batch_response.rb +21 -0
- data/lib/chain/client.rb +75 -0
- data/lib/chain/client_module.rb +11 -0
- data/lib/chain/config.rb +121 -0
- data/lib/chain/connection.rb +187 -0
- data/lib/chain/constants.rb +4 -0
- data/lib/chain/control_program.rb +10 -0
- data/lib/chain/errors.rb +80 -0
- data/lib/chain/hsm_signer.rb +91 -0
- data/lib/chain/mock_hsm.rb +65 -0
- data/lib/chain/query.rb +50 -0
- data/lib/chain/response_object.rb +81 -0
- data/lib/chain/transaction.rb +472 -0
- data/lib/chain/transaction_feed.rb +100 -0
- data/lib/chain/unspent_output.rb +90 -0
- data/lib/chain/version.rb +3 -0
- data/lib/chain.rb +3 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f0aa5bb32b142eae9a8ccad181ddfa907e75215b
|
4
|
+
data.tar.gz: dd2f336ad6c531de0cee5bf0ceb525b4b4c3f750
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7a888f2ff7604a2edb0d5ec4713e07e6086a774cad048e529073be20630e0339450b73d3fb238cea744b8cd3c2540282cc8d10e4851880c05668009de3108527
|
7
|
+
data.tar.gz: 7f8c2c45ae8ed6fc39914df1c34f803ab128eea95b36d1c4008497aa8eb43341171a3d28e07d5cc120424248291402a81e523c92850e569fb2e2180caa06620c
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Chain Ruby SDK
|
2
|
+
|
3
|
+
## Usage
|
4
|
+
|
5
|
+
### Installing the library
|
6
|
+
|
7
|
+
#### Via Rubygems
|
8
|
+
|
9
|
+
TBA
|
10
|
+
|
11
|
+
#### Via downloaded .gem
|
12
|
+
|
13
|
+
Install the gem into your gem library:
|
14
|
+
|
15
|
+
```
|
16
|
+
gem install --local chain-sdk-<VERSION>.gem
|
17
|
+
```
|
18
|
+
|
19
|
+
### In your code
|
20
|
+
|
21
|
+
```
|
22
|
+
require 'chain'
|
23
|
+
|
24
|
+
chain = Chain::Client.new
|
25
|
+
```
|
26
|
+
|
27
|
+
## Testing
|
28
|
+
|
29
|
+
To run integration tests, run a configured, empty Chain Core on http://localhost:1999. Then run:
|
30
|
+
|
31
|
+
```
|
32
|
+
bundle exec rspec
|
33
|
+
```
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative './client_module'
|
2
|
+
require_relative './query'
|
3
|
+
require_relative './response_object'
|
4
|
+
|
5
|
+
module Chain
|
6
|
+
class AccessToken < ResponseObject
|
7
|
+
|
8
|
+
# @!attribute [r] id
|
9
|
+
# User specified, unique identifier.
|
10
|
+
# @return [String]
|
11
|
+
attrib :id
|
12
|
+
|
13
|
+
# @!attribute [r] token
|
14
|
+
# Only returned in the response from {ClientModule.create}.
|
15
|
+
# @return [String]
|
16
|
+
attrib :token
|
17
|
+
|
18
|
+
# @!attribute [r] type
|
19
|
+
# Either 'client' or 'network'.
|
20
|
+
# @return [String]
|
21
|
+
attrib :type
|
22
|
+
|
23
|
+
# @!attribute [r] created_at
|
24
|
+
# Timestamp of token creation.
|
25
|
+
# @return [Time]
|
26
|
+
attrib(:created_at) { |raw| Time.parse(raw) }
|
27
|
+
|
28
|
+
class ClientModule < Chain::ClientModule
|
29
|
+
|
30
|
+
# @return [AccessToken]
|
31
|
+
def create(type:, id:)
|
32
|
+
AccessToken.new(client.conn.request(
|
33
|
+
'create-access-token',
|
34
|
+
{type: type, id: id}
|
35
|
+
))
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param [Hash] opts
|
39
|
+
# @return [Query]
|
40
|
+
def query(opts = {})
|
41
|
+
Query.new(client, opts)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Delete the access token specified.
|
45
|
+
# @param [String] id access token ID
|
46
|
+
# @raise [APIError]
|
47
|
+
# @return [void]
|
48
|
+
def delete(id)
|
49
|
+
client.conn.request('delete-access-token', {id: id})
|
50
|
+
return
|
51
|
+
end
|
52
|
+
|
53
|
+
class Query < Chain::Query
|
54
|
+
def fetch(query)
|
55
|
+
client.conn.request('list-access-tokens', query)
|
56
|
+
end
|
57
|
+
|
58
|
+
def translate(raw)
|
59
|
+
AccessToken.new(raw)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require_relative './client_module'
|
2
|
+
require_relative './control_program'
|
3
|
+
require_relative './errors'
|
4
|
+
require_relative './query'
|
5
|
+
require_relative './response_object'
|
6
|
+
|
7
|
+
module Chain
|
8
|
+
class Account < ResponseObject
|
9
|
+
|
10
|
+
# @!attribute [r] id
|
11
|
+
# Unique account identifier.
|
12
|
+
# @return [String]
|
13
|
+
attrib :id
|
14
|
+
|
15
|
+
# @!attribute [r] alias
|
16
|
+
# User specified, unique identifier.
|
17
|
+
# @return [String]
|
18
|
+
attrib :alias
|
19
|
+
|
20
|
+
# @!attribute [r] keys
|
21
|
+
# The list of keys used to create control programs under the account.
|
22
|
+
# Signatures from these keys are required for spending funds held in the account.
|
23
|
+
# @return [Array<Key>]
|
24
|
+
attrib(:keys) { |raw| raw.map { |v| Key.new(v) } }
|
25
|
+
|
26
|
+
# @!attribute [r] quorum
|
27
|
+
# The number of keys required to sign transactions for the account.
|
28
|
+
# @return [Integer]
|
29
|
+
attrib :quorum
|
30
|
+
|
31
|
+
# @!attribute [r] tags
|
32
|
+
# User-specified tag structure for the account.
|
33
|
+
# @return [Hash]
|
34
|
+
attrib :tags
|
35
|
+
|
36
|
+
class ClientModule < Chain::ClientModule
|
37
|
+
# @param [Hash] opts
|
38
|
+
# @return [Account]
|
39
|
+
def create(opts)
|
40
|
+
opts = {client_token: SecureRandom.uuid}.merge(opts)
|
41
|
+
client.conn.singleton_batch_request('create-account', [opts]) { |item| Account.new(item) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param [Array<Hash>] opts
|
45
|
+
# @return [Array<Account>]
|
46
|
+
def create_batch(opts)
|
47
|
+
opts = opts.map { |i| {client_token: SecureRandom.uuid}.merge(i) }
|
48
|
+
client.conn.batch_request('create-account', opts) { |item| Account.new(item) }
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param [Hash] opts
|
52
|
+
# @return [ControlProgram]
|
53
|
+
def create_control_program(opts = {})
|
54
|
+
# We don't use keyword params here because 'alias' is a Ruby reserverd
|
55
|
+
# word.
|
56
|
+
params = {}
|
57
|
+
params[:account_alias] = opts[:alias] if opts.key?(:alias)
|
58
|
+
params[:account_id] = opts[:id] if opts.key?(:id)
|
59
|
+
|
60
|
+
client.conn.singleton_batch_request(
|
61
|
+
'create-control-program',
|
62
|
+
[{type: :account, params: params}]
|
63
|
+
) { |item| ControlProgram.new(item) }
|
64
|
+
end
|
65
|
+
|
66
|
+
# @param [Hash] query
|
67
|
+
# @return [Query]
|
68
|
+
def query(query = {})
|
69
|
+
Query.new(client, query)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Query < Chain::Query
|
74
|
+
def fetch(query)
|
75
|
+
client.conn.request('list-accounts', query)
|
76
|
+
end
|
77
|
+
|
78
|
+
def translate(raw)
|
79
|
+
Account.new(raw)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class Key < ResponseObject
|
84
|
+
# @!attribute [r] root_xpub
|
85
|
+
# Hex-encoded representation of the root extended public key.
|
86
|
+
# @return [String]
|
87
|
+
attrib :root_xpub
|
88
|
+
|
89
|
+
# @!attribute [r] account_xpub
|
90
|
+
# The extended public key used to create control programs for the account.
|
91
|
+
# @return [String]
|
92
|
+
attrib :account_xpub
|
93
|
+
|
94
|
+
# @!attribute [r] account_derivation_path
|
95
|
+
# The derivation path of the extended key.
|
96
|
+
# @return [String]
|
97
|
+
attrib :account_derivation_path
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
data/lib/chain/asset.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
require_relative './client_module'
|
4
|
+
require_relative './errors'
|
5
|
+
require_relative './query'
|
6
|
+
require_relative './response_object'
|
7
|
+
|
8
|
+
module Chain
|
9
|
+
class Asset < ResponseObject
|
10
|
+
|
11
|
+
# @!attribute [r] id
|
12
|
+
# Globally unique identifier of the asset.
|
13
|
+
# Asset version 1 specifies the asset id as the hash of:
|
14
|
+
# - the asset version
|
15
|
+
# - the asset's issuance program
|
16
|
+
# - the core's VM version
|
17
|
+
# - the hash of the network's initial block
|
18
|
+
# @return [String]
|
19
|
+
attrib :id
|
20
|
+
|
21
|
+
# @!attribute [r] alias
|
22
|
+
# User specified, unique identifier.
|
23
|
+
# @return [String]
|
24
|
+
attrib :alias
|
25
|
+
|
26
|
+
# @!attribute [r] issuance_program
|
27
|
+
# @return [String]
|
28
|
+
attrib :issuance_program
|
29
|
+
|
30
|
+
# @!attribute [r] keys
|
31
|
+
# @return [Array<Key>]
|
32
|
+
attrib(:keys) { |raw| raw.map { |v| Key.new(v) } }
|
33
|
+
|
34
|
+
# @!attribute [r] quorum
|
35
|
+
# @return [Integer]
|
36
|
+
attrib :quorum
|
37
|
+
|
38
|
+
# @!attribute [r] definition
|
39
|
+
# User-specified, arbitrary/unstructured data visible across
|
40
|
+
# blockchain networks. Version 1 assets specify the definition in their
|
41
|
+
# issuance programs, rendering the definition immutable.
|
42
|
+
# @return [Hash]
|
43
|
+
attrib :definition
|
44
|
+
|
45
|
+
# @!attribute [r] tags
|
46
|
+
# @return [Hash]
|
47
|
+
attrib :tags
|
48
|
+
|
49
|
+
# @!attribute [r] is_local
|
50
|
+
# @return [Boolean]
|
51
|
+
attrib :is_local
|
52
|
+
|
53
|
+
class ClientModule < Chain::ClientModule
|
54
|
+
# @param [Hash] opts
|
55
|
+
# @return [Asset]
|
56
|
+
def create(opts)
|
57
|
+
opts = {client_token: SecureRandom.uuid}.merge(opts)
|
58
|
+
client.conn.singleton_batch_request('create-asset', [opts]) { |item| Asset.new(item) }
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param [Hash] opts
|
62
|
+
# @return [Array<Asset>]
|
63
|
+
def create_batch(opts)
|
64
|
+
opts = opts.map { |i| {client_token: SecureRandom.uuid}.merge(i) }
|
65
|
+
client.conn.batch_request('create-asset', opts) { |item| Asset.new(item) }
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param [Hash] query
|
69
|
+
# @return [Query]
|
70
|
+
def query(query = {})
|
71
|
+
Query.new(client, query)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Query < Chain::Query
|
76
|
+
def fetch(query)
|
77
|
+
client.conn.request('list-assets', query)
|
78
|
+
end
|
79
|
+
|
80
|
+
def translate(raw)
|
81
|
+
Asset.new(raw)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Key < ResponseObject
|
86
|
+
# @!attribute [r] root_xpub
|
87
|
+
# Hex-encoded representation of the root extended public key.
|
88
|
+
# @return [String]
|
89
|
+
attrib :root_xpub
|
90
|
+
|
91
|
+
# @!attribute [r] asset_pubkey
|
92
|
+
# The derived public key, used in the asset's issuance program.
|
93
|
+
# @return [String]
|
94
|
+
attrib :asset_pubkey
|
95
|
+
|
96
|
+
# @!attribute [r] asset_derivation_path
|
97
|
+
# The derivation path of the extended key.
|
98
|
+
# @return [String]
|
99
|
+
attrib :asset_derivation_path
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative './client_module'
|
2
|
+
require_relative './response_object'
|
3
|
+
require_relative './query'
|
4
|
+
|
5
|
+
module Chain
|
6
|
+
class Balance < ResponseObject
|
7
|
+
|
8
|
+
# @!attribute [r] amount
|
9
|
+
# Sum of the unspent outputs.
|
10
|
+
# @return [Integer]
|
11
|
+
attrib :amount
|
12
|
+
|
13
|
+
# @!attribute [r] sum_by
|
14
|
+
# List of parameters on which to sum unspent outputs.
|
15
|
+
# @return [Hash<String => String>]
|
16
|
+
attrib :sum_by
|
17
|
+
|
18
|
+
class ClientModule < Chain::ClientModule
|
19
|
+
# @param [Hash] query
|
20
|
+
# @return [Query]
|
21
|
+
def query(query = {})
|
22
|
+
Query.new(client, query)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Query < Chain::Query
|
27
|
+
def fetch(query)
|
28
|
+
client.conn.request('list-balances', query)
|
29
|
+
end
|
30
|
+
|
31
|
+
def translate(raw)
|
32
|
+
Balance.new(raw)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
def ensure_key_sorting(h)
|
2
|
+
sorted = h.keys.sort
|
3
|
+
return h if sorted == h.keys
|
4
|
+
sorted.reduce({}) { |memo, k| memo[k] = h[k]; memo }
|
5
|
+
end
|
6
|
+
|
7
|
+
module Chain
|
8
|
+
class BatchResponse
|
9
|
+
def initialize(successes: {}, errors: {}, response: nil)
|
10
|
+
@successes = ensure_key_sorting(successes)
|
11
|
+
@errors = ensure_key_sorting(errors)
|
12
|
+
@response = response
|
13
|
+
end
|
14
|
+
|
15
|
+
def size
|
16
|
+
successes.size + errors.size
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :successes, :errors, :response
|
20
|
+
end
|
21
|
+
end
|
data/lib/chain/client.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require_relative './access_token'
|
2
|
+
require_relative './account'
|
3
|
+
require_relative './asset'
|
4
|
+
require_relative './balance'
|
5
|
+
require_relative './config'
|
6
|
+
require_relative './constants'
|
7
|
+
require_relative './hsm_signer'
|
8
|
+
require_relative './mock_hsm'
|
9
|
+
require_relative './transaction'
|
10
|
+
require_relative './transaction_feed'
|
11
|
+
require_relative './unspent_output'
|
12
|
+
|
13
|
+
module Chain
|
14
|
+
class Client
|
15
|
+
|
16
|
+
def initialize(opts = {})
|
17
|
+
@opts = {url: DEFAULT_API_HOST}.merge(opts)
|
18
|
+
end
|
19
|
+
|
20
|
+
def opts
|
21
|
+
@opts.dup
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Connection]
|
25
|
+
def conn
|
26
|
+
@conn ||= Connection.new(@opts)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [AccessToken::ClientModule]
|
30
|
+
def access_tokens
|
31
|
+
@access_tokens ||= AccessToken::ClientModule.new(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Account::ClientModule]
|
35
|
+
def accounts
|
36
|
+
@accounts ||= Account::ClientModule.new(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Asset::ClientModule]
|
40
|
+
def assets
|
41
|
+
@assets ||= Asset::ClientModule.new(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Balance::ClientModule]
|
45
|
+
def balances
|
46
|
+
@balances ||= Balance::ClientModule.new(self)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Config::ClientModule]
|
50
|
+
def config
|
51
|
+
@config ||= Config::ClientModule.new(self)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [MockHSM::ClientModule]
|
55
|
+
def mock_hsm
|
56
|
+
@mock_hsm ||= MockHSM::ClientModule.new(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [Transaction::ClientModule]
|
60
|
+
def transactions
|
61
|
+
@transactions ||= Transaction::ClientModule.new(self)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [TransactionFeed::ClientModule]
|
65
|
+
def transaction_feeds
|
66
|
+
@transaction_feeds ||= TransactionFeed::ClientModule.new(self)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [UnspentOutput::ClientModule]
|
70
|
+
def unspent_outputs
|
71
|
+
@unspent_outputs ||= UnspentOutput::ClientModule.new(self)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
data/lib/chain/config.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require_relative './client_module'
|
2
|
+
require_relative './response_object'
|
3
|
+
|
4
|
+
module Chain
|
5
|
+
module Config
|
6
|
+
|
7
|
+
class Info < ResponseObject
|
8
|
+
class Snapshot < ResponseObject
|
9
|
+
# @!attribute [r] attempt
|
10
|
+
# @return [Integer]
|
11
|
+
attrib :attempt
|
12
|
+
|
13
|
+
# @!attribute [r] height
|
14
|
+
# @return [Integer]
|
15
|
+
attrib :height
|
16
|
+
|
17
|
+
# @!attribute [r] size
|
18
|
+
# @return [Integer]
|
19
|
+
attrib :size
|
20
|
+
|
21
|
+
# @!attribute [r] downloaded
|
22
|
+
# @return [Integer]
|
23
|
+
attrib :downloaded
|
24
|
+
|
25
|
+
# @!attribute [r] in_progress
|
26
|
+
# @return [Boolean]
|
27
|
+
attrib :in_progress
|
28
|
+
end
|
29
|
+
|
30
|
+
# @!attribute [r] is_configured
|
31
|
+
# @return [Boolean]
|
32
|
+
attrib :is_configured
|
33
|
+
|
34
|
+
# @!attribute [r] configured_at
|
35
|
+
# @return [Time]
|
36
|
+
attrib(:configured_at) { |raw| Time.parse(raw) }
|
37
|
+
|
38
|
+
# @!attribute [r] is_signer
|
39
|
+
# @return [Boolean]
|
40
|
+
attrib :is_signer
|
41
|
+
|
42
|
+
# @!attribute [r] is_generator
|
43
|
+
# @return [Boolean]
|
44
|
+
attrib :is_generator
|
45
|
+
|
46
|
+
# @!attribute [r] is_generator
|
47
|
+
# @return [String]
|
48
|
+
attrib :generator_url
|
49
|
+
|
50
|
+
# @!attribute [r] generator_access_token
|
51
|
+
# @return [String]
|
52
|
+
attrib :generator_access_token
|
53
|
+
|
54
|
+
# @!attribute [r] blockchain_id
|
55
|
+
# @return [String]
|
56
|
+
attrib :blockchain_id
|
57
|
+
|
58
|
+
# @!attribute [r] block_height
|
59
|
+
# @return [Integer]
|
60
|
+
attrib :block_height
|
61
|
+
|
62
|
+
# @!attribute [r] generator_block_height
|
63
|
+
# @return [Integer]
|
64
|
+
attrib :generator_block_height
|
65
|
+
|
66
|
+
# @!attribute [r] generator_block_height_fetched_at
|
67
|
+
# @return [Time]
|
68
|
+
attrib(:generator_block_height_fetched_at) { |raw| Time.parse(raw) }
|
69
|
+
|
70
|
+
# @!attribute [r] is_production
|
71
|
+
# @return [Boolean]
|
72
|
+
attrib :is_production
|
73
|
+
|
74
|
+
# @!attribute [r] network_rpc_version
|
75
|
+
# @return [Integer]
|
76
|
+
attrib :network_rpc_version
|
77
|
+
|
78
|
+
# @!attribute [r] core_id
|
79
|
+
# @return [String]
|
80
|
+
attrib :core_id
|
81
|
+
|
82
|
+
# @!attribute [r] build_commit
|
83
|
+
# @return [String]
|
84
|
+
attrib :build_commit
|
85
|
+
|
86
|
+
# @!attribute [r] build_date
|
87
|
+
# Date when the core binary was compiled.
|
88
|
+
#
|
89
|
+
# The API may not return this field as an RFC3399 timestamp,
|
90
|
+
# so it is not converted into a Time object.
|
91
|
+
# @return [String]
|
92
|
+
attrib :build_date
|
93
|
+
|
94
|
+
# @!attribute [r] health
|
95
|
+
# @return [Hash]
|
96
|
+
attrib :health
|
97
|
+
|
98
|
+
# @!attribute [r] snapshot
|
99
|
+
# @return [Snapshot]
|
100
|
+
attrib(:snapshot) { |raw| Snapshot.new(raw) }
|
101
|
+
end
|
102
|
+
|
103
|
+
class ClientModule < Chain::ClientModule
|
104
|
+
# @return [void]
|
105
|
+
def reset(everything: false)
|
106
|
+
client.conn.request('reset', {everything: everything})
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [void]
|
110
|
+
def configure(opts)
|
111
|
+
client.conn.request('configure', opts)
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Info]
|
115
|
+
def info
|
116
|
+
Info.new(client.conn.request('info'))
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|