在本课中,我们将回顾经典的 NFT 项目的代码。此外,我们还将学习 SoulBound (SBT) Item。
+ G' n4 J. v# N: I& E* vNFT Item合约- int min_tons_for_storage() asm "50000000 PUSHINT"; ;; 0.05 TON; h* r2 L7 }$ K1 m4 [' n
复制代码就像在前面的课程中一样,我们看到有一些常量显示了我们应该在合约上存储的最低代币数量,以便能够支付租金。
% E/ g: B# C7 F3 H( B; v* w2 F- (int, int, slice, slice, cell) load_data() {4 c7 F0 [2 ]! `/ t
- slice ds = get_data().begin_parse();
: x v) w" X+ @6 P: l - var (index, collection_address) = (ds~load_uint(64), ds~load_msg_addr());
5 f0 w- ^+ u& t: v9 x( X - if (ds.slice_bits() > 0) {
/ n% B& Y; K2 Q, v* { - return (-1, index, collection_address, ds~load_msg_addr(), ds~load_ref());4 q5 y( T& U3 K5 U( e- }
- } else {
5 d, S" u' l& N! q" ]! i- X1 B - return (0, index, collection_address, null(), null()); ;; nft not initialized yet
% l- h& I/ T1 |- H6 Z& W3 h1 h - }2 U+ x8 `) l' a; U0 L! z
- }
复制代码然后我们看到 load_data. 这个 load_data 与我们以前看到的有些不同。通常,我们只是从本地存储区读取数据,但在这里,我们有一些更复杂的逻辑。因此,我们打开本地存储的Cell进行读取,然后读出 index 和 collection_address. 我们知道什么是索引和 Collections 地址。 然后,我们尝试读取 NFT Item的元数据。首先,我们检查它是否存在。如果是,我们返回 –1, 以及Collections地址、所有者地址和元数据。或者我们只返回 0, 索引和Collections地址时,没有所有者和元数据。这意味着 NFT 尚未初始化。基于这一点,我们将在后面的代码中做出一些决定。 - () store_data(int index, slice collection_address, slice owner_address, cell content) impure {! U2 U& ?: ?* q% \
- set_data() b6 B$ g* i" a- r& G/ B: U
- begin_cell()
* k p& ^+ X% l - .store_uint(index, 64)' U* h( h9 [3 U, S' e
- .store_slice(collection_address)3 R/ g# Y' h6 B7 k& Q+ b0 _
- .store_slice(owner_address)
5 F' r! C7 P" ~0 w/ n) n+ n& E, K% E - .store_ref(content)
+ T8 I7 C4 j8 b M* S# ^" J - .end_cell(), l4 m! {- D8 T
- );& I! ?5 W% Y; i" ?- m. n2 a
- }
复制代码我们还看到 store_data 就像我们调查的其他合约一样,这只是正常情况。 , x6 Y7 L6 @8 k% _/ [, o
- () send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline {( i* C7 M: q- v1 w# f5 _! ?
- var msg = begin_cell()# A* y( W( W( i. A
- .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000 Q7 h& w$ |( U4 x, S& n: s
- .store_slice(to_address)0 W& s( C, z+ [; W3 G1 W% A. {6 d) Z' _+ T8 E
- .store_coins(amount)
" x: I& }5 I2 y2 M) L - .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
4 d, g! T1 `* ]* R - .store_uint(op, 32)! `8 b- y' T& B$ ?3 L
- .store_uint(query_id, 64);
) C b, t5 _( [( W/ k/ D$ ^
& o# ^5 [+ _/ V- if (~ builder_null?(payload)) {
6 z" f+ e) K* q( |# i5 H7 O; T+ o4 [ - msg = msg.store_builder(payload);
8 k2 H% {5 _6 t* T0 N - }
复制代码在这里,我们封装了发送消息的逻辑,因此我们可以在合约的其他逻辑中使用这个函数。因此,只要我们想发送信息,就可以使用这个函数。它接受目标地址、我们应附加的金额、操作代码、查询 ID、可能的有效载荷(这次是以生成器格式)以及消息的发送模式。这就是我们发送信息的逻辑。 我们的另一个函数是 transfer_ownership. 让我们稍后再研究,一旦我们在 recv_internal 逻辑,因为它是这里最重要的功能。 - () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {0 H0 D/ c2 H$ @8 D- R
- if (in_msg_body.slice_empty?()) { ;; ignore empty messages
' }6 o% Z; D; t+ I1 Y$ @ - return ();
/ g: A J7 N9 f0 Q - }
% i1 ?! N8 d$ I% Y0 L% E
* B' J. `8 n0 F" p" S- slice cs = in_msg_full.begin_parse();
; d: U) K0 B) i1 n" U, k - int flags = cs~load_uint(4);. C0 d! B; X& F' ^$ k
/ i) r( x6 c H7 m- if (flags & 1) { ;; ignore all bounced messages
9 @' s6 g2 k% `9 O2 V - return ();( S1 H2 _9 j. B+ j! Y) V# ?
- }
: e* C; X6 t5 B# ~8 C& l - slice sender_address = cs~load_msg_addr();' v7 l2 u$ f( X( I( O' q* a% y
- 4 D z+ I0 H/ B* X# V4 F7 w; x
- cs~load_msg_addr(); ;; skip dst5 C0 x, f3 O: c9 U; h+ o8 }0 q
- cs~load_coins(); ;; skip value R& W. ~8 E1 [! x" x$ t
- cs~skip_bits(1); ;; skip extracurrency collection( P8 c7 Q4 ^9 Y
- cs~load_coins(); ;; skip ihr_fee/ ^6 G. S- s- X
- int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; we use message fwd_fee for estimation of forward_payload costs
. @3 U' B" e2 R! l
( b' m u; d: A9 y2 J0 A- d- (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data();
' `$ ]2 V5 C5 y9 @/ {2 ^ - if (~ init?) {
; a4 k! C" {3 Z" B" c; N) O9 K. q: g - throw_unless(405, equal_slices(collection_address, sender_address));
& }# h3 [3 D6 d6 a& s3 r - store_data(index, collection_address, in_msg_body~load_msg_addr(), in_msg_body~load_ref());# U' V2 f9 Z2 o9 K
- return ();
: N7 F5 R+ p$ }4 b6 J; b* T, C; C - }
复制代码我们来看看它是怎么做的。我们再次忽略空的 in_msg_body 消息,读取标志,忽略退回的信息,并检查发件人是谁。现在我们跳过一些内容,比如信息的目标地址之类的,因为你已经知道这个Cell的内容是什么了。接下来是 fwd_fee 再次根据上一笔交易估算发送下一笔交易的成本。 然后加载数据。正如你所记得的,我们加载数据的方式与这里的实现方式相同。因此,我们要检查它是否未初始化,然后我们期待实际获得这些数据,期待 in_msg_body 以获得所有者的地址和带有元数据的Cell。但这只能通过 collection_address, 因此我们要检查它是否等于发件人地址。然后我们就可以初始化 NFT Item了。 - int op = in_msg_body~load_uint(32);
% I, L& o4 Z+ F4 G" }7 G. L - int query_id = in_msg_body~load_uint(64);
& H8 B' M3 P9 p. g8 ^ - / l: x& A; s' s- ~$ F
- if (op == op::transfer()) {, e8 Z; ^8 Q" s
- transfer_ownership(my_balance, index, collection_address, owner_address, content, sender_address, query_id, in_msg_body, fwd_fee);
$ W1 a a5 o" B. A - return ();4 R( ^( U/ S' N7 Y# j6 D
- }
; X$ M0 S5 \! b6 y% C - if (op == op::get_static_data()) {' e* t& t" Y) M/ [# v
- send_msg(sender_address, 0, op::report_static_data(), query_id, begin_cell().store_uint(index, 256).store_slice(collection_address), 64); ;; carry all the remaining value of the inbound message
a' M1 D( }8 s5 K; W2 c6 B3 n - return ();/ {* ^( ~: h" L/ g3 j# d- B
- }
复制代码接下来是操作代码,可以是 op::transfer 或 op::get_static_data. 这是我们要处理的两个可能值。当我们说 get_static_data, 我们会立即向发送静态数据请求的用户发送一条信息。我们只是报告数据,所以我们会向他发送 index 和 collection_address. 这是 get_static_data 函数。正如你所看到的,这是我们的第一个 send_ msg 用例 1 [; K: ?" g x# r3 p
- () transfer_ownership(int my_balance, int index, slice collection_address, slice owner_address, cell content, slice sender_address, int query_id, slice in_msg_body, int fwd_fees) impure inline {
% O! x# Y1 K; c h8 {/ e! c - throw_unless(401, equal_slices(sender_address, owner_address));6 D. q0 K. _) u2 p0 x
. h5 w. e3 p2 W3 l( V) V) x- slice new_owner_address = in_msg_body~load_msg_addr();6 a! X2 o- }3 f
- force_chain(new_owner_address);
: T% _+ v1 O8 Y! |/ U/ Y - slice response_destination = in_msg_body~load_msg_addr();
* U" A$ z% f2 N - in_msg_body~load_int(1); ;; this nft don't use custom_payload
0 a( q$ a: \5 X9 }# m8 [ - int forward_amount = in_msg_body~load_coins();
O0 }$ d3 }8 E& {# B - throw_unless(708, slice_bits(in_msg_body) >= 1);) k& p( n2 j7 ?: f
- 1 F7 I) I. j, b$ q5 K% ~
- int rest_amount = my_balance - min_tons_for_storage();) S/ M- v7 }3 I, s6 W5 u
- if (forward_amount) {
. b# f/ u9 B% O - rest_amount -= (forward_amount + fwd_fees);, h+ d& ~' G: {9 F" n9 `
- }
R# l( f, P+ R" j9 n2 { - int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00
?5 D8 h8 d- }" t# r! Y - if (need_response) {8 E' b% m% S' j: O( R
- rest_amount -= fwd_fees;( s5 a) ]) h' l7 h1 `1 j# e7 [
- }* o. w2 @4 T; O, Z, h* Z2 r
% F; x0 l' M, X7 [! |: ~. Y/ H4 g- throw_unless(402, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response
8 Q) n1 W4 V2 A
( r( U% r- k% ]' m2 ~+ b- if (forward_amount) {+ `8 \ E4 O6 d2 Z
- send_msg(new_owner_address, forward_amount, op::ownership_assigned(), query_id, begin_cell().store_slice(owner_address).store_slice(in_msg_body), 1); ;; paying fees, revert on errors
, J1 A5 v: |! ^- e, Y - }$ z+ }4 V3 u' S
- if (need_response) {
" {+ O ^2 M' B9 _2 `% Y( H# N - force_chain(response_destination);: V) k, h J i+ w: c
- send_msg(response_destination, rest_amount, op::excesses(), query_id, null(), 1); ;; paying fees, revert on errors) ]2 J2 B7 Y- M7 F- I! {$ K
- }( ~. o/ G4 x' Q8 C2 V8 E! @8 `
0 o# K/ x, J* ~& [- store_data(index, collection_address, new_owner_address, content);
% Z8 {" r- f b, m0 q - }
复制代码这里有一个转移所有权的功能。首先,我们要检查物品的所有者地址是否就是发送信息的人,因为只有他才能转移所有权。然后,我们读出新所有者的地址。我们 force_chain 以确保他在同一条链上。然后我们检查是否有 response_destination, 因此,一旦我们转移了这个所有权,我们是否应该告诉某人向另一个人发送信息。我们检查 forward_amount 是多少,如果有人要求的话。然后,我们就可以计算出,在发出信息之后,我们就会有足够的钱 forward_amount 到目的地。然后,我们要弄清楚是否需要回应,如果存在 response_destination 。如果存在,其剩余金额必须大于 0。 下面我就向大家简单介绍一下它的工作原理。如果存在 forward_amount ,我们将发送一条信息,通知用户所有权已由该 forward_amount 分配。如果我们需要回应,我们将 force_chain 以确保响应目的地在同一链上。我们发送的回复通常包括超额费用部分和所有额外资金;它们只是被转发到 response_destination. 然后,我们只需使用 new_owner_address 保存数据。基本上,这是唯一会发生变化的地方、 transfer_ownership 是这里的核心函数之一。您拥有一件物品,有时您会将其所有权转让给其他人或出售,这取决于交易的类型。 H; D5 C" }/ r, a( A* z
SBT item合约GetGems repository 正在主办一些不同的 NFT 合约、藏品、销售和市场以及一些拍卖活动。这里是了解更多与 NFT 藏品、物品及其相关的合约的好地方。但我想向您展示一份确切的合约,我们可以对其进行深入研究。 sbt-item 是一种灵魂绑定代币(SBT)。灵魂绑定与 NFT 非常相似,但不同之处在于灵魂绑定代币不能擅自转让给其他人。我们仔细看看它的代码。 - global int storage::index;
4 I- U4 I4 _$ Z5 X$ A9 r. S - global int init?;
0 I: r1 @' d. a! r3 \4 g1 P$ \ y - global slice storage::collection_address;
4 g( m. d% g! d8 Y - global slice storage::owner_address;! A2 T7 U. m- U7 F0 U7 O* i
- global slice storage::authority_address;9 z! w) w; z% h4 L
- global cell storage::content;
- C- s, ~( w( }3 x$ o4 q" P4 v& t - global int storage::revoked_at;( g, p2 P) s2 Q* Z1 V+ E0 R r
- () load_data() impure {
6 {3 M% D* O0 ^6 P+ |2 K3 p - slice ds = get_data().begin_parse();6 Q |. Z1 Z* W& c& U% R6 |6 x
- : f' K. e! c* z2 y
- storage::index = ds~load_uint(64);2 u6 E1 P* z( R+ y- ~% G3 ^
- storage::collection_address = ds~load_msg_addr();
9 l) l0 N' E( C$ B# \ - init? = false;
. ?* K1 i/ G X* Y1 N% W, Y+ ]; D - * @" ^% l7 b! {+ y0 h7 p
- if (ds.slice_bits() > 0) {
w5 o2 `1 o% T' u1 n0 |) b - init? = true;4 b A* y7 S- Z- v: P: e! \( u! W
- storage::owner_address = ds~load_msg_addr();
/ L7 ]5 l3 q# k - storage::content = ds~load_ref();
+ r) u8 P% f7 e, _. [9 `2 t) j - storage::authority_address = ds~load_msg_addr();& n0 s+ h% U# R9 k$ s6 e
- storage::revoked_at = ds~load_uint(64);
) {! Z* Z" ^6 e9 Y - }
+ c* u2 x. E8 O3 D2 s: P6 U, A - }
复制代码这里有一些变量: storage::index, storage::collection_address. 我们还定义了其他全局变量,这只是 GetGems 实现的一个例子。你不必用同样的方法。我想在这里重点谈谈其他部分:
) M `6 i4 J9 Y6 W' x- l" I. q- if (op == op::request_owner()) {
1 I& V7 ~: ^- Q1 \ - slice dest = in_msg_body~load_msg_addr();
+ C% E$ O3 C2 `5 {, T/ n4 i - cell body = in_msg_body~load_ref();
: F( s) p+ T7 Z( o6 |2 A# x. [) V+ \ - int with_content = in_msg_body~load_uint(1);
- ?( s N! A) M3 F - 6 P6 e% F7 q9 Z2 \
- var msg = begin_cell()7 l. g6 R+ U( _
- .store_uint(storage::index, 256)& F7 P) a C3 T& B4 \
- .store_slice(sender_address)) s( g, H! n! `) G
- .store_slice(storage::owner_address)
( [4 c( u A9 Y+ I {5 | - .store_ref(body)
. z5 L, I- ^. U; u1 b: ~+ O - .store_uint(storage::revoked_at, 64). u% T# ]+ F9 @( R# X
- .store_uint(with_content, 1);. V9 k; A+ N! ^( D' l( `9 V; n8 _+ y8 Z
- ) v; B' d2 G7 \0 N! S
- if (with_content != 0) {" ~+ R/ v- \2 }1 ~* h4 L
- msg = msg.store_ref(storage::content);. x! ]1 Y0 e- P7 t
- }
) M1 p; K- h" \4 E( ^9 X1 h
! ~3 Q& Z0 h, H; a h- ;; mode 64 = carry all the remaining value of the inbound message5 [7 [. c1 [6 t/ L3 ?0 L
- send_msg(flag::regular() | flag::bounce(), dest, 0, op::owner_info(), query_id, msg, 64);0 E9 w/ o' A9 G0 m) H# ^- t/ S+ ]
- return ();, @. M6 `; Y' i& Z: b% z$ N/ V
- }
3 H1 W: `# Z- _( h& @" D9 K+ W - if (op == op::prove_ownership()) {/ X* d) H2 a5 m
- throw_unless(401, equal_slices(storage::owner_address, sender_address));( f* K4 V' z; p) g I4 d& T' ^
- # \. n c' {2 t! w0 {
- slice dest = in_msg_body~load_msg_addr();$ Y! \$ y- t: f) V- D
- cell body = in_msg_body~load_ref();
& t3 s2 V: s& |% ~* r - int with_content = in_msg_body~load_uint(1);. Z- S8 ]8 |0 T2 K" p3 }( @4 w
% q; t) f1 | B9 s- q1 f- var msg = begin_cell()3 z' r$ Y* q, l. f& A
- .store_uint(storage::index, 256)
6 u" }/ P9 h. d; J0 ?5 L - .store_slice(storage::owner_address)
6 R, w6 g$ ^7 u - .store_ref(body), ]) y, a( ]& R/ X* q
- .store_uint(storage::revoked_at, 64)9 B `$ ~' V; m) |5 J* U& G
- .store_uint(with_content, 1);: ]5 ]- C/ g2 F0 \8 Z
3 f$ t3 S Z$ }9 D- if (with_content != 0) {
; l$ f2 ^: I$ y& g; N* g - msg = msg.store_ref(storage::content);6 [' M2 O m& Y) ~+ Y
- }
% d$ |) h6 @6 i, b w4 h& w
$ X4 a$ V) W; U- Y6 t5 j- ;; mode 64 = carry all the remaining value of the inbound message
4 D0 a$ R, V# j1 U - send_msg(flag::regular() | flag::bounce(), dest, 0, op::ownership_proof(), query_id, msg, 64);
# X1 z4 k+ x7 R3 l n( [ - return ();) @7 P) B/ P# a7 Q/ h
- }0 X6 c. Y; G: f! m
- if (op == op::get_static_data()) {
6 U% N) c- i) {( H - var msg = begin_cell().store_uint(storage::index, 256).store_slice(storage::collection_address);3 k0 P! w1 l+ [$ B4 ^# _5 F& g0 r& z
$ `$ o4 v. v) c+ N u- ;; mode 64 = carry all the remaining value of the inbound message v+ T8 K5 i- c* S
- send_msg(flag::regular(), sender_address, 0, op::report_static_data(), query_id, msg, 64);' r6 j7 p: w Q& Z: p @
- return ();* I3 `' o( |& n3 j
- }. K! N; O+ g7 W1 w% g9 B
- if (op == op::destroy()) {) r9 w; ~& q: p2 Z9 j
- throw_unless(401, equal_slices(storage::owner_address, sender_address));
* `/ X% D A7 p4 f6 j( M3 C
- r, r c6 I8 _: Q6 F# R- send_msg(flag::regular(), sender_address, 0, op::excesses(), query_id, null(), 128);
0 e3 K) u* [- e
$ u) {8 {4 d- Y O. |- storage::owner_address = null_addr();
6 c2 C" u5 i8 s- b# m - storage::authority_address = null_addr();+ _1 F/ E# {; o4 Q4 Q
- store_data();
! J, l3 W8 A5 B" Z - return ();
) a4 |4 b4 F% u, }: o - }# ~. }# V- v' L$ ~5 R5 h P; E
- if (op == op::revoke()) {, s: m. i, u$ q' z- U7 l. v
- throw_unless(401, equal_slices(storage::authority_address, sender_address));
$ W$ F8 l2 C) f; i% p9 i* ` - throw_unless(403, storage::revoked_at == 0);0 U6 O' A5 z/ f/ Y
) _9 [. k% Z! K) v/ A/ Z- storage::revoked_at = now();
! ?; e5 U) b: E2 V5 Z! X6 i P! v - store_data();
, S2 n; y: M# O; s; A1 W* a6 y0 o - return ();% d8 M9 o2 M D" `; a
- }
( I2 k, ~9 Z' q1 a' g% \3 _/ z3 e - if (op == op::take_excess()) {- `" ?: z8 O2 B b: B6 m
- throw_unless(401, equal_slices(storage::owner_address, sender_address));
( H3 `6 D& `: b
- v6 b1 t$ f+ S- ;; reserve amount for storage+ k8 W0 v1 v" s5 w/ f% F- Q4 Q0 ?0 s
- raw_reserve(min_tons_for_storage(), 0);
- M6 T6 Q' C+ K
, c( F+ X/ V+ A* q+ W/ T- send_msg(flag::regular(), sender_address, 0, op::excesses(), query_id, null(), 128);. v1 v6 O% \( C" i# q9 D4 E
- return ();; @& k: u, e6 _7 p' D; V+ W
- }4 e6 M) u+ c, L3 w2 a! g
- if (op == op::transfer()) {1 l0 [. a2 Q# c& M; }7 ]5 q) S/ J. }; @
- throw(413);
$ b) \% g2 \8 W; X% b2 X e - }
复制代码如你所见,这里有许多注释,但我们不会逐行深入研究。你可以看到,这个通证将被用于多种不同的用途,但与普通的 NFT 不同。这里是合约,你可以申请这个合约的所有者。一些操作代码可以请求所有权证明。您可以获取静态数据。你还可以销毁或撤销这个标记。你可以拿走多余的东西,比如,存放在这里的一些钱。如果你是这个合约的所有者,你就可以申请。但你显然不能转让它,就像你在这里看到的一样。这是一种完全不同类型的代币,如果你对 NFT 感兴趣,就应该在 GetGems 存储库里多花点时间。 我想向你展示的是,NFT collections 的所有代码可以保持不变,但collections合约部署的 item 可以不同。例如,你可以用 SBT item 替换 NFT item。这样您就会有一个 SBT item collection,然后您只需更改 code 来初始化你的 collection: - (slice, int, cell, cell, cell) load_data() inline {/ m) \5 T& M9 s' o) n$ c# R
- var ds = get_data().begin_parse();1 R' A# x, Q* W8 J
- return
- ]2 c, l# [2 k% @7 f - (ds~load_msg_addr(), ;; owner_address5 d, ]4 o4 C& X$ `+ l" q! `. b
- ds~load_uint(64), ;; next_item_index$ W" _& U3 z" z7 {) h
- ds~load_ref(), ;; content
, T' C1 c4 m# H! F* g - ds~load_ref(), ;; nft_item_code( ?5 g- [3 J6 x3 j! r# ^8 L8 G
- ds~load_ref() ;; royalty_params3 R$ m# D0 L. @3 h+ h) S6 ]# D [
- );: j3 ^; n' z) r% e9 K. V- ^
- }
复制代码这样,您就可以实际部署 SBT item collection。此外,您还可以创建一个独立的没有collection SBT item。我们不会掌握这些item的编程,我只是想向大家展示它在更大范围内的工作原理。 : W. U, b) u$ I0 D* H
结论! D: c' `& h( A8 L! B
从第 3 章中非常简单的逻辑开始,我们已经了解了 NFT 或 Jettons 等复杂合约中的许多内容。这就是一个很好的例子,你可以一步一步地学习某些语法和概念,然后就可以学习更复杂的逻辑。希望这几节课能帮助你理解 Jettons 和 NFTs 的真正含义,以及如何处理它们的代码库--弄清楚它们是如何工作的,并在使用 TON 构建时学习更多可用的语法和架构。 非常感谢您的关注。对你们来说,这可能是最难的一章,虽然我们没有编写任何代码;我们只是在阅读一些你们从未见过的东西,而且非常复杂。你们能坚持到最后一课,我真的很骄傲,我期待着在接下来的章节中看到你们。希望你们喜欢目前的课程。 7 ?2 O% Q& [: e
* V8 |) o6 ^3 n0 ]
' C% E. m, r1 B/ w& C7 k% O$ ^5 \3 \4 x* j& y# @$ m
: a/ b( E0 ]: ?9 V* y: |8 b. f# D' U) |/ A6 @8 B2 ?" v* o
; Y- }! z% _$ X9 }( V9 v
. M1 `1 z* D. J$ `# s |