bsv-sdk 0.19.1 → 0.22.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +89 -0
- data/README.md +2 -2
- data/lib/bsv/auth/transport.rb +1 -1
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -3
- data/lib/bsv/network/protocol.rb +4 -5
- data/lib/bsv/network/protocols/arc.rb +4 -30
- data/lib/bsv/network/protocols/arcade.rb +163 -0
- data/lib/bsv/network/protocols/chaintracks.rb +6 -3
- data/lib/bsv/network/protocols/jungle_bus.rb +6 -0
- data/lib/bsv/network/protocols.rb +1 -0
- data/lib/bsv/network/provider.rb +7 -9
- data/lib/bsv/network/providers/gorilla_pool.rb +18 -18
- data/lib/bsv/network/util.rb +44 -0
- data/lib/bsv/network.rb +1 -0
- data/lib/bsv/overlay/lookup_resolver.rb +0 -1
- data/lib/bsv/overlay/topic_broadcaster.rb +0 -1
- data/lib/bsv/primitives/curve.rb +1 -11
- data/lib/bsv/primitives/ecies.rb +1 -8
- data/lib/bsv/primitives/hex.rb +1 -1
- data/lib/bsv/script/script.rb +1 -1
- data/lib/bsv/transaction/beef.rb +0 -2
- data/lib/bsv/transaction/chain_tracker.rb +74 -13
- data/lib/bsv/transaction/chain_trackers/whats_on_chain.rb +3 -3
- data/lib/bsv/transaction/chain_trackers.rb +0 -10
- data/lib/bsv/transaction/fee_models/live_policy.rb +10 -8
- data/lib/bsv/transaction/merkle_path.rb +0 -2
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet/errors.rb +65 -21
- data/lib/bsv/wallet/proto_wallet/validators.rb +7 -49
- data/lib/bsv/wallet/proto_wallet.rb +15 -8
- data/lib/bsv/wallet/serializer/abort_action.rb +38 -0
- data/lib/bsv/wallet/serializer/acquire_certificate.rb +171 -0
- data/lib/bsv/wallet/serializer/certificate.rb +184 -0
- data/lib/bsv/wallet/serializer/common.rb +207 -0
- data/lib/bsv/wallet/serializer/create_action_args.rb +259 -0
- data/lib/bsv/wallet/serializer/create_action_result.rb +85 -0
- data/lib/bsv/wallet/serializer/create_hmac.rb +67 -0
- data/lib/bsv/wallet/serializer/create_signature.rb +90 -0
- data/lib/bsv/wallet/serializer/decrypt.rb +60 -0
- data/lib/bsv/wallet/serializer/discover_by_attributes.rb +61 -0
- data/lib/bsv/wallet/serializer/discover_by_identity_key.rb +49 -0
- data/lib/bsv/wallet/serializer/discover_certificates_result.rb +39 -0
- data/lib/bsv/wallet/serializer/encrypt.rb +60 -0
- data/lib/bsv/wallet/serializer/get_header_for_height.rb +71 -0
- data/lib/bsv/wallet/serializer/get_height.rb +46 -0
- data/lib/bsv/wallet/serializer/get_network.rb +65 -0
- data/lib/bsv/wallet/serializer/get_public_key.rb +86 -0
- data/lib/bsv/wallet/serializer/get_version.rb +44 -0
- data/lib/bsv/wallet/serializer/internalize_action.rb +151 -0
- data/lib/bsv/wallet/serializer/list_actions.rb +348 -0
- data/lib/bsv/wallet/serializer/list_certificates.rb +124 -0
- data/lib/bsv/wallet/serializer/list_outputs.rb +167 -0
- data/lib/bsv/wallet/serializer/prove_certificate.rb +146 -0
- data/lib/bsv/wallet/serializer/relinquish_certificate.rb +56 -0
- data/lib/bsv/wallet/serializer/relinquish_output.rb +44 -0
- data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +108 -0
- data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +116 -0
- data/lib/bsv/wallet/serializer/sign_action_args.rb +94 -0
- data/lib/bsv/wallet/serializer/sign_action_result.rb +49 -0
- data/lib/bsv/wallet/serializer/status.rb +85 -0
- data/lib/bsv/wallet/serializer/verify_hmac.rb +67 -0
- data/lib/bsv/wallet/serializer/verify_signature.rb +101 -0
- data/lib/bsv/wallet/serializer.rb +180 -0
- data/lib/bsv/wallet/substrates/http_wallet_json.rb +129 -0
- data/lib/bsv/wallet/substrates/http_wallet_wire.rb +99 -0
- data/lib/bsv/wallet/wallet_wire.rb +20 -0
- data/lib/bsv/wallet/wallet_wire_processor.rb +61 -0
- data/lib/bsv/wallet/wallet_wire_transceiver.rb +61 -0
- data/lib/bsv/wallet/wire/calls.rb +79 -0
- data/lib/bsv/wallet/wire/frame.rb +181 -0
- data/lib/bsv/wallet/wire/reader_writer.rb +402 -0
- data/lib/bsv/wallet/wire/validation.rb +213 -0
- data/lib/bsv/wallet/wire.rb +13 -0
- data/lib/bsv/wallet.rb +17 -0
- metadata +47 -3
- data/lib/bsv/transaction/chain_trackers/chaintracks.rb +0 -83
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
# Client-side BRC-103 transceiver.
|
|
6
|
+
#
|
|
7
|
+
# Implements every {Interface::BRC100} method by serialising the call
|
|
8
|
+
# arguments to a binary request frame, transmitting it over a {WalletWire},
|
|
9
|
+
# unframing the result, and deserialising the payload back to a Ruby hash.
|
|
10
|
+
#
|
|
11
|
+
# @example In-process loopback (testing / same-process use)
|
|
12
|
+
# proto = BSV::Wallet::ProtoWallet.new(key)
|
|
13
|
+
# processor = BSV::Wallet::WalletWireProcessor.new(proto)
|
|
14
|
+
# client = BSV::Wallet::WalletWireTransceiver.new(processor)
|
|
15
|
+
# client.get_public_key(identity_key: true)
|
|
16
|
+
# #=> { public_key: "02..." }
|
|
17
|
+
#
|
|
18
|
+
# @example Over HTTP
|
|
19
|
+
# wire = BSV::Wallet::Substrates::HTTPWalletWire.new(base_url: 'https://wallet.example')
|
|
20
|
+
# client = BSV::Wallet::WalletWireTransceiver.new(wire)
|
|
21
|
+
#
|
|
22
|
+
# Thread safety: each call is independent. The wire transport is the
|
|
23
|
+
# synchronisation boundary — concurrent calls are serialised only if the
|
|
24
|
+
# underlying wire implementation serialises them.
|
|
25
|
+
class WalletWireTransceiver
|
|
26
|
+
include Interface::BRC100
|
|
27
|
+
|
|
28
|
+
# @param wire [#transmit_to_wallet] any object that includes {WalletWire}
|
|
29
|
+
def initialize(wire)
|
|
30
|
+
@wire = wire
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Generate the 28 BRC-100 methods via metaprogramming.
|
|
34
|
+
#
|
|
35
|
+
# For each call byte → method name mapping in CALL_TO_METHOD, define a
|
|
36
|
+
# method that:
|
|
37
|
+
# 1. Extracts originator from kwargs (default '').
|
|
38
|
+
# 2. Serialises the args via the SERIALIZE_ARGS dispatch table.
|
|
39
|
+
# 3. Frames and transmits the binary request.
|
|
40
|
+
# 4. Unframes the result (raises on error frame).
|
|
41
|
+
# 5. Deserialises the payload via DESERIALIZE_RESULT.
|
|
42
|
+
Wire::Calls::CALL_TO_METHOD.each do |call_byte, method_name|
|
|
43
|
+
define_method(method_name) do |**args|
|
|
44
|
+
_dispatch(call_byte, args)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def _dispatch(call_byte, args)
|
|
51
|
+
originator = args[:originator].to_s
|
|
52
|
+
Wire::Validation.originator_domain!('originator', originator) unless originator.empty?
|
|
53
|
+
params = Serializer::SERIALIZE_ARGS.fetch(call_byte).call(args)
|
|
54
|
+
frame = Wire::Frame.write_request(call: call_byte, originator: originator, params: params)
|
|
55
|
+
reply = @wire.transmit_to_wallet(frame)
|
|
56
|
+
payload = Wire::Frame.read_result(reply)
|
|
57
|
+
Serializer::DESERIALIZE_RESULT.fetch(call_byte).call(payload)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Wire
|
|
6
|
+
# BRC-103 call byte constants and dispatch tables.
|
|
7
|
+
#
|
|
8
|
+
# Each constant maps to the corresponding Go SDK CallXxx constant in
|
|
9
|
+
# go-sdk/wallet/substrates/wallet_wire_calls.go. The CALL_TO_METHOD
|
|
10
|
+
# table maps each byte to the actual Ruby method name on Interface::BRC100.
|
|
11
|
+
#
|
|
12
|
+
# Note: call byte 23 maps to :authenticated? (predicate suffix preserved),
|
|
13
|
+
# not :is_authenticated, to match the existing Interface::BRC100 definition.
|
|
14
|
+
module Calls
|
|
15
|
+
CREATE_ACTION = 1
|
|
16
|
+
SIGN_ACTION = 2
|
|
17
|
+
ABORT_ACTION = 3
|
|
18
|
+
LIST_ACTIONS = 4
|
|
19
|
+
INTERNALIZE_ACTION = 5
|
|
20
|
+
LIST_OUTPUTS = 6
|
|
21
|
+
RELINQUISH_OUTPUT = 7
|
|
22
|
+
GET_PUBLIC_KEY = 8
|
|
23
|
+
REVEAL_COUNTERPARTY_KEY_LINKAGE = 9
|
|
24
|
+
REVEAL_SPECIFIC_KEY_LINKAGE = 10
|
|
25
|
+
ENCRYPT = 11
|
|
26
|
+
DECRYPT = 12
|
|
27
|
+
CREATE_HMAC = 13
|
|
28
|
+
VERIFY_HMAC = 14
|
|
29
|
+
CREATE_SIGNATURE = 15
|
|
30
|
+
VERIFY_SIGNATURE = 16
|
|
31
|
+
ACQUIRE_CERTIFICATE = 17
|
|
32
|
+
LIST_CERTIFICATES = 18
|
|
33
|
+
PROVE_CERTIFICATE = 19
|
|
34
|
+
RELINQUISH_CERTIFICATE = 20
|
|
35
|
+
DISCOVER_BY_IDENTITY_KEY = 21
|
|
36
|
+
DISCOVER_BY_ATTRIBUTES = 22
|
|
37
|
+
IS_AUTHENTICATED = 23
|
|
38
|
+
WAIT_FOR_AUTHENTICATION = 24
|
|
39
|
+
GET_HEIGHT = 25
|
|
40
|
+
GET_HEADER_FOR_HEIGHT = 26
|
|
41
|
+
GET_NETWORK = 27
|
|
42
|
+
GET_VERSION = 28
|
|
43
|
+
|
|
44
|
+
CALL_TO_METHOD = {
|
|
45
|
+
CREATE_ACTION => :create_action,
|
|
46
|
+
SIGN_ACTION => :sign_action,
|
|
47
|
+
ABORT_ACTION => :abort_action,
|
|
48
|
+
LIST_ACTIONS => :list_actions,
|
|
49
|
+
INTERNALIZE_ACTION => :internalize_action,
|
|
50
|
+
LIST_OUTPUTS => :list_outputs,
|
|
51
|
+
RELINQUISH_OUTPUT => :relinquish_output,
|
|
52
|
+
GET_PUBLIC_KEY => :get_public_key,
|
|
53
|
+
REVEAL_COUNTERPARTY_KEY_LINKAGE => :reveal_counterparty_key_linkage,
|
|
54
|
+
REVEAL_SPECIFIC_KEY_LINKAGE => :reveal_specific_key_linkage,
|
|
55
|
+
ENCRYPT => :encrypt,
|
|
56
|
+
DECRYPT => :decrypt,
|
|
57
|
+
CREATE_HMAC => :create_hmac,
|
|
58
|
+
VERIFY_HMAC => :verify_hmac,
|
|
59
|
+
CREATE_SIGNATURE => :create_signature,
|
|
60
|
+
VERIFY_SIGNATURE => :verify_signature,
|
|
61
|
+
ACQUIRE_CERTIFICATE => :acquire_certificate,
|
|
62
|
+
LIST_CERTIFICATES => :list_certificates,
|
|
63
|
+
PROVE_CERTIFICATE => :prove_certificate,
|
|
64
|
+
RELINQUISH_CERTIFICATE => :relinquish_certificate,
|
|
65
|
+
DISCOVER_BY_IDENTITY_KEY => :discover_by_identity_key,
|
|
66
|
+
DISCOVER_BY_ATTRIBUTES => :discover_by_attributes,
|
|
67
|
+
IS_AUTHENTICATED => :authenticated?,
|
|
68
|
+
WAIT_FOR_AUTHENTICATION => :wait_for_authentication,
|
|
69
|
+
GET_HEIGHT => :get_height,
|
|
70
|
+
GET_HEADER_FOR_HEIGHT => :get_header_for_height,
|
|
71
|
+
GET_NETWORK => :get_network,
|
|
72
|
+
GET_VERSION => :get_version
|
|
73
|
+
}.freeze
|
|
74
|
+
|
|
75
|
+
METHOD_TO_CALL = CALL_TO_METHOD.invert.freeze
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Wire
|
|
6
|
+
# BRC-103 request and result frame codec.
|
|
7
|
+
#
|
|
8
|
+
# Port of go-sdk/wallet/serializer/frame.go. Two frame types:
|
|
9
|
+
#
|
|
10
|
+
# Request frame (client → wallet):
|
|
11
|
+
# [1 byte: call][1 byte: originator_len][originator_len bytes: UTF-8][remaining: params]
|
|
12
|
+
#
|
|
13
|
+
# Result frame (wallet → client):
|
|
14
|
+
# [1 byte: error_code]
|
|
15
|
+
# On success (0x00): [remaining bytes: payload]
|
|
16
|
+
# On error (non-zero):
|
|
17
|
+
# [VarInt: message_len][message bytes]
|
|
18
|
+
# [VarInt: stack_len][stack bytes]
|
|
19
|
+
module Frame
|
|
20
|
+
# Maximum originator byte length enforced at write time.
|
|
21
|
+
# Matches the BRC-100 +OriginatorDomainNameStringUnder250Bytes+ branded
|
|
22
|
+
# type used by +Wire::Validation.originator_domain!+.
|
|
23
|
+
MAX_ORIGINATOR_BYTES = 250
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Encode a request frame.
|
|
28
|
+
#
|
|
29
|
+
# @param call [Integer] call byte (1..28)
|
|
30
|
+
# @param originator [String] originator domain (0..250 bytes UTF-8)
|
|
31
|
+
# @param params [String, nil] binary params payload
|
|
32
|
+
# @return [String] binary frame (ASCII-8BIT encoding)
|
|
33
|
+
# @raise [ArgumentError] if originator exceeds 250 bytes
|
|
34
|
+
def write_request(call:, originator:, params: nil)
|
|
35
|
+
originator_bytes = originator.to_s.b
|
|
36
|
+
if originator_bytes.bytesize > MAX_ORIGINATOR_BYTES
|
|
37
|
+
raise ArgumentError,
|
|
38
|
+
"originator must be at most #{MAX_ORIGINATOR_BYTES} bytes, " \
|
|
39
|
+
"got #{originator_bytes.bytesize}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
buf = String.new(encoding: 'BINARY')
|
|
43
|
+
buf << [call, originator_bytes.bytesize].pack('CC')
|
|
44
|
+
buf << originator_bytes
|
|
45
|
+
buf << params.b if params && !params.empty?
|
|
46
|
+
buf
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Decode a request frame.
|
|
50
|
+
#
|
|
51
|
+
# @param bytes [String] binary frame
|
|
52
|
+
# @return [Hash] { call: Integer, originator: String, params: String }
|
|
53
|
+
# @raise [ArgumentError] if the frame is truncated or malformed
|
|
54
|
+
def read_request(bytes)
|
|
55
|
+
data = bytes.b
|
|
56
|
+
raise ArgumentError, 'frame too short: need at least 2 bytes' if data.bytesize < 2
|
|
57
|
+
|
|
58
|
+
call = data.getbyte(0)
|
|
59
|
+
originator_len = data.getbyte(1)
|
|
60
|
+
|
|
61
|
+
if data.bytesize < 2 + originator_len
|
|
62
|
+
raise ArgumentError,
|
|
63
|
+
"frame truncated: need #{2 + originator_len} bytes for originator, " \
|
|
64
|
+
"got #{data.bytesize}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
originator = data.byteslice(2, originator_len).force_encoding('UTF-8')
|
|
68
|
+
raise ArgumentError, 'frame originator is not valid UTF-8' unless originator.valid_encoding?
|
|
69
|
+
|
|
70
|
+
params = data.byteslice(2 + originator_len, data.bytesize - 2 - originator_len) || ''.b
|
|
71
|
+
|
|
72
|
+
{ call: call, originator: originator, params: params }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Encode a success result frame.
|
|
76
|
+
#
|
|
77
|
+
# @param payload [String, nil] binary payload
|
|
78
|
+
# @return [String] binary frame
|
|
79
|
+
def write_result(payload: nil)
|
|
80
|
+
buf = String.new(encoding: 'BINARY')
|
|
81
|
+
buf << "\x00"
|
|
82
|
+
buf << payload.b if payload && !payload.empty?
|
|
83
|
+
buf
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Encode an error result frame.
|
|
87
|
+
#
|
|
88
|
+
# @param error [BSV::Wallet::Error] the error to encode
|
|
89
|
+
# @return [String] binary frame
|
|
90
|
+
def write_error(error:)
|
|
91
|
+
wire = error.to_wire
|
|
92
|
+
msg_bytes = wire[:message].to_s.b
|
|
93
|
+
stack_bytes = wire[:stack].to_s.b
|
|
94
|
+
|
|
95
|
+
buf = String.new(encoding: 'BINARY')
|
|
96
|
+
buf << [wire[:code]].pack('C')
|
|
97
|
+
buf << encode_varint(msg_bytes.bytesize)
|
|
98
|
+
buf << msg_bytes
|
|
99
|
+
buf << encode_varint(stack_bytes.bytesize)
|
|
100
|
+
buf << stack_bytes
|
|
101
|
+
buf
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Decode a result frame.
|
|
105
|
+
#
|
|
106
|
+
# @param bytes [String] binary frame
|
|
107
|
+
# @return [String] binary payload on success
|
|
108
|
+
# @raise [BSV::Wallet::Error] the appropriate subclass on error
|
|
109
|
+
# @raise [ArgumentError] if the frame is truncated or malformed
|
|
110
|
+
def read_result(bytes)
|
|
111
|
+
data = bytes.b
|
|
112
|
+
raise ArgumentError, 'result frame is empty' if data.empty?
|
|
113
|
+
|
|
114
|
+
code = data.getbyte(0)
|
|
115
|
+
|
|
116
|
+
return data.byteslice(1, data.bytesize - 1) || ''.b if code.zero?
|
|
117
|
+
|
|
118
|
+
offset = 1
|
|
119
|
+
msg_len, vi = decode_varint(data, offset)
|
|
120
|
+
offset += vi
|
|
121
|
+
|
|
122
|
+
raise ArgumentError, 'result frame truncated: message' if data.bytesize < offset + msg_len
|
|
123
|
+
|
|
124
|
+
message = data.byteslice(offset, msg_len).force_encoding('UTF-8')
|
|
125
|
+
raise ArgumentError, 'result frame error message is not valid UTF-8' unless message.valid_encoding?
|
|
126
|
+
|
|
127
|
+
offset += msg_len
|
|
128
|
+
|
|
129
|
+
stack_len, vi = decode_varint(data, offset)
|
|
130
|
+
offset += vi
|
|
131
|
+
|
|
132
|
+
raise ArgumentError, 'result frame truncated: stack' if data.bytesize < offset + stack_len
|
|
133
|
+
|
|
134
|
+
stack = data.byteslice(offset, stack_len).force_encoding('UTF-8')
|
|
135
|
+
raise ArgumentError, 'result frame stack is not valid UTF-8' unless stack.valid_encoding?
|
|
136
|
+
|
|
137
|
+
raise BSV::Wallet.error_from_wire(code, message, stack)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @param n [Integer] unsigned integer
|
|
141
|
+
# @return [String] Bitcoin varint encoding
|
|
142
|
+
def encode_varint(n)
|
|
143
|
+
if n < 0xFD
|
|
144
|
+
[n].pack('C')
|
|
145
|
+
elsif n <= 0xFFFF
|
|
146
|
+
[0xFD, n].pack('Cv')
|
|
147
|
+
elsif n <= 0xFFFFFFFF
|
|
148
|
+
[0xFE, n].pack('CV')
|
|
149
|
+
else
|
|
150
|
+
[0xFF, n].pack('CQ<')
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @param data [String] binary data
|
|
155
|
+
# @param offset [Integer] byte offset
|
|
156
|
+
# @return [Array(Integer, Integer)] decoded value, bytes consumed
|
|
157
|
+
def decode_varint(data, offset = 0)
|
|
158
|
+
raise ArgumentError, "varint: need 1 byte at #{offset}" if offset >= data.bytesize
|
|
159
|
+
|
|
160
|
+
first = data.getbyte(offset)
|
|
161
|
+
case first
|
|
162
|
+
when 0..0xFC
|
|
163
|
+
[first, 1]
|
|
164
|
+
when 0xFD
|
|
165
|
+
raise ArgumentError, "varint: need 3 bytes at #{offset}" if data.bytesize < offset + 3
|
|
166
|
+
|
|
167
|
+
[data.byteslice(offset + 1, 2).unpack1('v'), 3]
|
|
168
|
+
when 0xFE
|
|
169
|
+
raise ArgumentError, "varint: need 5 bytes at #{offset}" if data.bytesize < offset + 5
|
|
170
|
+
|
|
171
|
+
[data.byteslice(offset + 1, 4).unpack1('V'), 5]
|
|
172
|
+
when 0xFF
|
|
173
|
+
raise ArgumentError, "varint: need 9 bytes at #{offset}" if data.bytesize < offset + 9
|
|
174
|
+
|
|
175
|
+
[data.byteslice(offset + 1, 8).unpack1('Q<'), 9]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|