在本课中,我们将回顾经典的 NFT 项目的代码。此外,我们还将学习 SoulBound (SBT) Item。
" x" k* p e! o1 [# ZNFT Item合约- int min_tons_for_storage() asm "50000000 PUSHINT"; ;; 0.05 TON: z: E' \3 I: P! k
复制代码就像在前面的课程中一样,我们看到有一些常量显示了我们应该在合约上存储的最低代币数量,以便能够支付租金。 6 m# l6 _/ u: l/ S% ~, _
- (int, int, slice, slice, cell) load_data() {
# E: g6 Z$ H D7 K$ ] - slice ds = get_data().begin_parse();- H" k o0 h# X1 ]2 Y' M& D
- var (index, collection_address) = (ds~load_uint(64), ds~load_msg_addr());4 G) f* T- ?2 | v' S3 R# Q
- if (ds.slice_bits() > 0) {
, u. N- T5 F8 P% G - return (-1, index, collection_address, ds~load_msg_addr(), ds~load_ref());6 Z# i0 c: U* D# p+ {- X
- } else {
, G; w- V0 c' B$ b - return (0, index, collection_address, null(), null()); ;; nft not initialized yet" B. k6 }% i: Y4 }- B7 `
- }
2 K) k6 ]. O$ `3 C8 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 {6 F) H# w( F8 t. |+ @" w1 d& k8 y5 f
- set_data(; z# ^% Y# d0 q! J$ C1 a a+ O
- begin_cell()
' y3 B/ a8 W( } - .store_uint(index, 64): t, O/ O2 d8 K A5 F- y7 ]
- .store_slice(collection_address)6 e5 d& B% B4 |' x" Y$ p- w
- .store_slice(owner_address)
: R1 p) q: g F - .store_ref(content)7 n' A. q4 g8 E3 C, g
- .end_cell()8 u G+ b, V7 i- R* I, F
- );9 X7 |4 s7 |/ E! ?6 i4 F
- }
复制代码我们还看到 store_data 就像我们调查的其他合约一样,这只是正常情况。 7 w* i& r" \. u. h$ r7 B) ?
- () send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline {1 ?& `' j4 x/ |# y9 I% k5 J0 Y
- var msg = begin_cell()/ l1 q5 \! O/ C
- .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000
/ M& F2 p8 [& v' Y3 r7 B; K - .store_slice(to_address)
; t: H# c% W! ^$ ?* ?* q - .store_coins(amount)* ^0 y$ X/ b. i9 D; |/ M3 _
- .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
' y: T' t* @1 [3 l3 E7 A- H - .store_uint(op, 32)
# @& o4 Y3 Z6 g5 D - .store_uint(query_id, 64);
: t0 L4 E2 P0 a* s! Q8 F6 E) R$ O
. X0 b- n; d& j4 | E# ]+ U* f- if (~ builder_null?(payload)) {- c, ^4 h( b+ w. S
- msg = msg.store_builder(payload);- U0 { K: Y' H+ S" h: i
- }
复制代码在这里,我们封装了发送消息的逻辑,因此我们可以在合约的其他逻辑中使用这个函数。因此,只要我们想发送信息,就可以使用这个函数。它接受目标地址、我们应附加的金额、操作代码、查询 ID、可能的有效载荷(这次是以生成器格式)以及消息的发送模式。这就是我们发送信息的逻辑。 我们的另一个函数是 transfer_ownership. 让我们稍后再研究,一旦我们在 recv_internal 逻辑,因为它是这里最重要的功能。 - () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {3 `' g# _8 I* @: Q
- if (in_msg_body.slice_empty?()) { ;; ignore empty messages$ t w6 B: }+ v) g3 H2 R) `- T
- return ();
. k7 V# m( x x$ ^: V9 x O# P, f) p - }
# D* P3 v9 n" @ - & H0 }4 y6 O( i
- slice cs = in_msg_full.begin_parse();
( j6 y( z" u! v* U - int flags = cs~load_uint(4);
5 T6 ^& }% O9 T) ^) R6 D) Z - ) ?7 y Y, E0 k& [
- if (flags & 1) { ;; ignore all bounced messages5 [. ?* m2 ]* L& W/ U1 ^: a3 `" u( _
- return ();
: p! S1 s% U6 H - }
# D: B7 \0 l. F- H8 t2 g. \ - slice sender_address = cs~load_msg_addr();# R: W+ v+ G5 X' I, A0 v* q
- 9 `0 `/ g, D( u
- cs~load_msg_addr(); ;; skip dst
( i! `/ P [5 j - cs~load_coins(); ;; skip value
* I- O" X$ S6 U6 b - cs~skip_bits(1); ;; skip extracurrency collection# l0 h; W: E+ p2 D
- cs~load_coins(); ;; skip ihr_fee5 I& x# d- q0 Z0 N% e, Z9 Q x- z
- int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; we use message fwd_fee for estimation of forward_payload costs
8 C0 u/ s) X# e8 { - * R P* v" ?9 L0 j7 j, d
- (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data();
4 b! c- z2 h8 @# }0 m# x, C" b( F - if (~ init?) {
0 S# y' P# u9 L( W - throw_unless(405, equal_slices(collection_address, sender_address));$ e# r0 P8 X/ U; K5 N
- store_data(index, collection_address, in_msg_body~load_msg_addr(), in_msg_body~load_ref());
2 Y7 ]; X$ M1 ~% D - return ();
+ ]/ ~; G5 `. K! |& U - }
复制代码我们来看看它是怎么做的。我们再次忽略空的 in_msg_body 消息,读取标志,忽略退回的信息,并检查发件人是谁。现在我们跳过一些内容,比如信息的目标地址之类的,因为你已经知道这个Cell的内容是什么了。接下来是 fwd_fee 再次根据上一笔交易估算发送下一笔交易的成本。 然后加载数据。正如你所记得的,我们加载数据的方式与这里的实现方式相同。因此,我们要检查它是否未初始化,然后我们期待实际获得这些数据,期待 in_msg_body 以获得所有者的地址和带有元数据的Cell。但这只能通过 collection_address, 因此我们要检查它是否等于发件人地址。然后我们就可以初始化 NFT Item了。 - int op = in_msg_body~load_uint(32);
* H/ u* D8 `5 p1 _( I2 b) Q7 M- Q - int query_id = in_msg_body~load_uint(64);
* A+ _9 X2 p( n, b. W7 \
- G, R6 N7 u4 e! f- if (op == op::transfer()) {
, ?! k! h2 K2 q9 G8 B5 a# Y" } - transfer_ownership(my_balance, index, collection_address, owner_address, content, sender_address, query_id, in_msg_body, fwd_fee);
2 y+ a' ]0 C6 {0 U1 m. L0 n - return ();
# e5 {- [, }8 W& Y' z m8 }# w - }0 R% @( T/ y d! \2 L$ z' d5 z
- if (op == op::get_static_data()) {: u' ~9 W2 |0 N2 w) ~) B" G+ 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
, y: Y9 ^% O: a& r - return ();* ^+ @# I U& [& i; p$ C
- }
复制代码接下来是操作代码,可以是 op::transfer 或 op::get_static_data. 这是我们要处理的两个可能值。当我们说 get_static_data, 我们会立即向发送静态数据请求的用户发送一条信息。我们只是报告数据,所以我们会向他发送 index 和 collection_address. 这是 get_static_data 函数。正如你所看到的,这是我们的第一个 send_ msg 用例 ! m8 U! L# b0 o2 c( X4 j
- () 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 {
' B( G, D- T% {/ ] - throw_unless(401, equal_slices(sender_address, owner_address));3 F) [. J/ L& x+ F
- : u0 Z3 k! h/ s% X% ~
- slice new_owner_address = in_msg_body~load_msg_addr();" v' x( ?1 R' U* g; z' _7 s% _# U2 b
- force_chain(new_owner_address);
. M- C1 @2 z+ _' B1 Q - slice response_destination = in_msg_body~load_msg_addr();: M d; {9 ?+ x' H
- in_msg_body~load_int(1); ;; this nft don't use custom_payload* R4 T J9 Q& k
- int forward_amount = in_msg_body~load_coins();
& ?: o% s* }8 B+ f- {0 k - throw_unless(708, slice_bits(in_msg_body) >= 1);
# U) P4 r# Y. H/ ] s/ E N - & j# d4 r d3 X( G ^: N
- int rest_amount = my_balance - min_tons_for_storage();3 F) E6 ]+ h1 T+ v( n
- if (forward_amount) {
" p4 E/ |5 t- k$ Q, p - rest_amount -= (forward_amount + fwd_fees);7 ?4 v$ q9 X' P E
- }
. _6 U) m% ?% [( Z+ ` - int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00. \" U. o! ^( E& @7 K& ]
- if (need_response) {
3 M7 T9 [, _0 X- U - rest_amount -= fwd_fees;
# |7 M8 x( \; j+ Y - }
2 N/ _. L- ?( R! p$ u2 ? - 2 r- {) h. W0 p* u# j
- throw_unless(402, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response# l0 D1 ~. \! E! t" r/ ]
! i4 g" s$ _: F& a* }- if (forward_amount) {
0 J, }- w; Q) u, F5 Y3 n; B1 H - 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: Y2 x# D) ?/ ^3 R' O) _
- }" [+ {* K4 W2 b _
- if (need_response) {
3 r3 J( M, }8 A) k2 h5 n0 H4 c - force_chain(response_destination);
6 |! P# @5 e8 c% W* K9 [ - send_msg(response_destination, rest_amount, op::excesses(), query_id, null(), 1); ;; paying fees, revert on errors
* G) V: s% C2 R - }
1 y6 J+ {- C7 E: ?; c7 v
( O) M; e" c3 s) B. w2 N- store_data(index, collection_address, new_owner_address, content);9 `6 n4 p4 a) W4 n5 h5 ~
- }
复制代码这里有一个转移所有权的功能。首先,我们要检查物品的所有者地址是否就是发送信息的人,因为只有他才能转移所有权。然后,我们读出新所有者的地址。我们 force_chain 以确保他在同一条链上。然后我们检查是否有 response_destination, 因此,一旦我们转移了这个所有权,我们是否应该告诉某人向另一个人发送信息。我们检查 forward_amount 是多少,如果有人要求的话。然后,我们就可以计算出,在发出信息之后,我们就会有足够的钱 forward_amount 到目的地。然后,我们要弄清楚是否需要回应,如果存在 response_destination 。如果存在,其剩余金额必须大于 0。 下面我就向大家简单介绍一下它的工作原理。如果存在 forward_amount ,我们将发送一条信息,通知用户所有权已由该 forward_amount 分配。如果我们需要回应,我们将 force_chain 以确保响应目的地在同一链上。我们发送的回复通常包括超额费用部分和所有额外资金;它们只是被转发到 response_destination. 然后,我们只需使用 new_owner_address 保存数据。基本上,这是唯一会发生变化的地方、 transfer_ownership 是这里的核心函数之一。您拥有一件物品,有时您会将其所有权转让给其他人或出售,这取决于交易的类型。 ' G2 y% W; S+ Z# r# h7 `& W
SBT item合约GetGems repository 正在主办一些不同的 NFT 合约、藏品、销售和市场以及一些拍卖活动。这里是了解更多与 NFT 藏品、物品及其相关的合约的好地方。但我想向您展示一份确切的合约,我们可以对其进行深入研究。 sbt-item 是一种灵魂绑定代币(SBT)。灵魂绑定与 NFT 非常相似,但不同之处在于灵魂绑定代币不能擅自转让给其他人。我们仔细看看它的代码。 - global int storage::index;
$ v0 g! S7 Y/ ~3 a; O+ k- o4 o - global int init?;. X5 A( v8 D9 V; r) C
- global slice storage::collection_address;
, B5 c Z. _, L$ w8 g4 d - global slice storage::owner_address;/ l9 N& U1 x7 S8 S3 h9 _& d
- global slice storage::authority_address;
O0 U. e) b2 ]3 B# O/ d - global cell storage::content; r4 r7 }. {8 O# L# Z2 ?! a! O+ I3 B. l
- global int storage::revoked_at;' Q6 T# a; {$ }" O/ r6 f
- () load_data() impure {: d% J2 [; o% s. n/ L
- slice ds = get_data().begin_parse();2 o: M3 {# L Z* G
- 0 P3 g5 O b( s9 [4 T2 v. u
- storage::index = ds~load_uint(64);
3 F% r7 { | N3 ?5 S: _ c - storage::collection_address = ds~load_msg_addr();) J' {+ p* w7 H# U6 G
- init? = false;) C/ W; J$ F8 z7 q
- [, F j& n# U& @, F( n$ ]- m! h
- if (ds.slice_bits() > 0) {
: s: z' x" d, A, f" x - init? = true;; b, G3 |: @8 H P
- storage::owner_address = ds~load_msg_addr();5 J% m, f" s2 L7 |+ L* e$ X/ ~
- storage::content = ds~load_ref();
8 \8 [- W$ Z9 C9 M& _/ E - storage::authority_address = ds~load_msg_addr();) z/ \$ k6 Q( a* N1 U! O
- storage::revoked_at = ds~load_uint(64);
" J+ v1 X) }$ T, }4 o( t! u2 o. Q - }
5 |' q/ ~& a0 Q5 k' f2 t - }
复制代码这里有一些变量: storage::index, storage::collection_address. 我们还定义了其他全局变量,这只是 GetGems 实现的一个例子。你不必用同样的方法。我想在这里重点谈谈其他部分:
4 g! |% x }0 |! Q/ }) V- if (op == op::request_owner()) {8 c+ m2 y* B+ \( u3 z
- slice dest = in_msg_body~load_msg_addr();
% k5 ]7 ?) E: O* y - cell body = in_msg_body~load_ref();5 }3 j2 q/ I; w
- int with_content = in_msg_body~load_uint(1);. q. J" e" H8 m. b% C2 X
/ f. C# t1 j# W- var msg = begin_cell()2 M. z! G' C/ r7 p
- .store_uint(storage::index, 256)
* e6 _0 |( F' J - .store_slice(sender_address)
5 l c9 `; b i% l L x# z! } - .store_slice(storage::owner_address)' r9 R! U# E- y1 N
- .store_ref(body)- ]8 `. x5 e( h$ n
- .store_uint(storage::revoked_at, 64)
* m# Y2 _' v( ` - .store_uint(with_content, 1);5 K" X8 M2 Q( y9 k, T
- ( V) B, v, h5 A- Z# e) P+ y# d
- if (with_content != 0) {0 w. P+ l+ E/ Q5 m- y- ~/ F
- msg = msg.store_ref(storage::content);: ^& K6 |' M$ }. I- q
- }( K" ] M+ ?9 l, k3 m, H* ~
- . t( |0 c- H1 @7 b6 w
- ;; mode 64 = carry all the remaining value of the inbound message* X4 h9 K8 l5 z% {+ U0 N/ V- \
- send_msg(flag::regular() | flag::bounce(), dest, 0, op::owner_info(), query_id, msg, 64);$ P% \5 s5 }% n+ @
- return ();/ z: n/ m0 @' [$ f, r
- }
. D# q# [) c) o& L0 O' {3 `" @$ A - if (op == op::prove_ownership()) {
1 x0 f( X7 m, X - throw_unless(401, equal_slices(storage::owner_address, sender_address));
0 U% O6 l$ q6 d - . m8 L3 i- u# t% k
- slice dest = in_msg_body~load_msg_addr();2 `9 ]" s! c* q' A4 @
- cell body = in_msg_body~load_ref(); h/ B9 Z) w1 f
- int with_content = in_msg_body~load_uint(1); S8 v7 s6 A+ Q5 I, C, v
+ O" Z" c3 @7 l# {* ]- var msg = begin_cell()
. v7 O3 g3 L" e' Y. h2 c+ V! J - .store_uint(storage::index, 256)
1 n5 [4 N" q* g% a' @' w8 |5 t* `7 F9 V - .store_slice(storage::owner_address)" y# @6 K2 Q, Y7 t- y9 j
- .store_ref(body)
0 d6 E: R( @: J4 Z' K! r/ Q - .store_uint(storage::revoked_at, 64)/ v% t* n$ B* X/ F4 g
- .store_uint(with_content, 1);4 t3 d9 @/ @9 ^8 {
- 5 v7 N/ b* G# M- T8 b
- if (with_content != 0) {
9 M; R: ~+ l, Y; t+ M% v - msg = msg.store_ref(storage::content);0 A+ T$ v& v4 h# @
- }
4 B3 e) W2 e! W7 Y9 y4 J
V5 y$ ]% q( O9 z- ;; mode 64 = carry all the remaining value of the inbound message& }* c" g" v. u3 S
- send_msg(flag::regular() | flag::bounce(), dest, 0, op::ownership_proof(), query_id, msg, 64);2 F! a2 z8 ^9 ~3 M r0 t4 T
- return ();2 _- z, a" P+ S+ G/ s% F9 ~
- }
- ~; F9 |2 F5 [4 f% j4 l - if (op == op::get_static_data()) {
+ r0 @/ b6 E8 @: s) \# i \0 c* k - var msg = begin_cell().store_uint(storage::index, 256).store_slice(storage::collection_address);+ b5 s i6 d+ ?% v
) h# w+ w* J) X$ d! V- t/ ~# L' C* f H- ;; mode 64 = carry all the remaining value of the inbound message
' C+ P. P$ n: R" p; x - send_msg(flag::regular(), sender_address, 0, op::report_static_data(), query_id, msg, 64);
. a! @6 `7 o$ S+ H- B - return ();
8 y5 B" a* h/ L( n/ O" M5 h - }
& o i# \# [& \ - if (op == op::destroy()) {
) v/ V6 [. Q# s0 E) H }0 d - throw_unless(401, equal_slices(storage::owner_address, sender_address));
$ X& u7 N' x/ ? d0 _3 s# X$ p - 0 k7 w `- r! T! u
- send_msg(flag::regular(), sender_address, 0, op::excesses(), query_id, null(), 128);
7 {9 C9 u, U+ b+ {. \
W9 A$ {! @0 [; ]+ M" G. e- storage::owner_address = null_addr();
0 s- ?9 g* z! p* p0 a - storage::authority_address = null_addr();
4 z" h- ~- `7 ^$ }/ R' R - store_data();
7 C' Z9 D( z1 V$ T( f# {& S - return ();' I/ ], m W4 u7 U& @ T/ S2 j
- }
: ~# `0 L$ R6 o( o5 `7 m/ ]+ n5 ^ - if (op == op::revoke()) {; S$ h9 m0 l" ~7 _* N) q' ]8 J) h
- throw_unless(401, equal_slices(storage::authority_address, sender_address));
" p; j& b+ ?- m+ m7 D7 b! f9 q - throw_unless(403, storage::revoked_at == 0);& H3 Y2 g N! G
- % h1 d" f) N" `3 t# ?' _
- storage::revoked_at = now();, a; C; t1 I# F& j! H
- store_data();
: g/ R% w; Y$ N: c. Z - return ();$ k8 [/ B. V$ s3 K' e5 Q5 k
- }
5 Y+ h- \$ E6 ~9 w - if (op == op::take_excess()) {; p. B! {$ a5 g2 v1 G5 Z* a
- throw_unless(401, equal_slices(storage::owner_address, sender_address));5 p) E* f$ N7 ?& s0 ]! H
0 X$ q0 f8 t7 Q) ^3 Y u3 y. a# {- ;; reserve amount for storage
6 t! b+ p2 K( g8 x! a+ Q+ H - raw_reserve(min_tons_for_storage(), 0);5 C& t5 e- k, }5 v, S# N
/ g6 b# j, A0 v v: z4 m% A- send_msg(flag::regular(), sender_address, 0, op::excesses(), query_id, null(), 128);9 l3 [( u/ g0 L6 k" V4 M* W5 C
- return ();3 D5 ?2 p# R: {( A+ X3 l
- }
, Y$ E5 ?5 Q9 {2 e$ N8 S7 X+ Z - if (op == op::transfer()) { v* ]* d8 ]8 T: D0 c, b) e
- throw(413);' i0 S c2 U# f
- }
复制代码如你所见,这里有许多注释,但我们不会逐行深入研究。你可以看到,这个通证将被用于多种不同的用途,但与普通的 NFT 不同。这里是合约,你可以申请这个合约的所有者。一些操作代码可以请求所有权证明。您可以获取静态数据。你还可以销毁或撤销这个标记。你可以拿走多余的东西,比如,存放在这里的一些钱。如果你是这个合约的所有者,你就可以申请。但你显然不能转让它,就像你在这里看到的一样。这是一种完全不同类型的代币,如果你对 NFT 感兴趣,就应该在 GetGems 存储库里多花点时间。 我想向你展示的是,NFT collections 的所有代码可以保持不变,但collections合约部署的 item 可以不同。例如,你可以用 SBT item 替换 NFT item。这样您就会有一个 SBT item collection,然后您只需更改 code 来初始化你的 collection: - (slice, int, cell, cell, cell) load_data() inline {, P( c! c, U- o0 ~ B# T
- var ds = get_data().begin_parse();( V/ U/ _. @ ]: d: ~
- return
7 o& B8 \( t Q4 ]! r) j - (ds~load_msg_addr(), ;; owner_address# Y- _0 d2 k. @) W- j
- ds~load_uint(64), ;; next_item_index
2 A% g4 E& t5 d/ j - ds~load_ref(), ;; content0 Y* E9 Z* R7 f3 b1 [3 y
- ds~load_ref(), ;; nft_item_code5 h+ d! s' w6 K N
- ds~load_ref() ;; royalty_params
" C7 B8 E- `5 o - );; L6 V3 n0 K: L0 W& N9 Y9 n. J
- }
复制代码这样,您就可以实际部署 SBT item collection。此外,您还可以创建一个独立的没有collection SBT item。我们不会掌握这些item的编程,我只是想向大家展示它在更大范围内的工作原理。 - b8 M5 p! c% S; X) b/ W) c, S
结论
* }- {9 Y+ P# H. D8 ], ]1 M0 l从第 3 章中非常简单的逻辑开始,我们已经了解了 NFT 或 Jettons 等复杂合约中的许多内容。这就是一个很好的例子,你可以一步一步地学习某些语法和概念,然后就可以学习更复杂的逻辑。希望这几节课能帮助你理解 Jettons 和 NFTs 的真正含义,以及如何处理它们的代码库--弄清楚它们是如何工作的,并在使用 TON 构建时学习更多可用的语法和架构。 非常感谢您的关注。对你们来说,这可能是最难的一章,虽然我们没有编写任何代码;我们只是在阅读一些你们从未见过的东西,而且非常复杂。你们能坚持到最后一课,我真的很骄傲,我期待着在接下来的章节中看到你们。希望你们喜欢目前的课程。 ( u! \& H' q1 [8 M- ]
8 @% m+ m" Z& p1 }4 y3 ^
% N* A6 }5 J; R3 r& O8 ?6 W( g- L( r- R0 ^7 y. A
- O; Y$ ^; K7 S" f+ U8 K4 q) L( f7 y
: b; \& e$ ]% f4 Y1 u8 O* y5 F& v. u4 c, i& a9 _
) z% k' P9 y" I, u9 V1 d |