From aaccf748ac7fc4dfaf22ba91d22986c2668ee324 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 9 Sep 2021 22:52:27 +0300 Subject: [PATCH] nft-nd-nns: add getAllRecords method See neo-project/non-native-contracts#5. --- examples/nft-nd-nns/nns.go | 56 +++++++++++++++++++----- examples/nft-nd-nns/nns.yml | 3 +- pkg/core/nonnative_name_service_test.go | 2 +- pkg/rpc/client/helper.go | 30 +++++++++++++ pkg/rpc/client/native.go | 27 ++++++++++++ pkg/rpc/server/client_test.go | 15 +++++++ pkg/rpc/server/server_test.go | 7 +-- pkg/rpc/server/testdata/testblocks.acc | Bin 22845 -> 23193 bytes 8 files changed, 123 insertions(+), 17 deletions(-) diff --git a/examples/nft-nd-nns/nns.go b/examples/nft-nd-nns/nns.go index 4b8d7bb96..e45835cf2 100644 --- a/examples/nft-nd-nns/nns.go +++ b/examples/nft-nd-nns/nns.go @@ -66,6 +66,13 @@ const ( millisecondsInYear = 365 * 24 * 3600 * 1000 ) +// RecordState is a type that registered entities are saved to. +type RecordState struct { + Name string + Type RecordType + Data string +} + // Update updates NameService contract. func Update(nef []byte, manifest string) { checkCommittee() @@ -353,6 +360,15 @@ func Resolve(name string, typ RecordType) string { return resolve(ctx, name, typ, 2) } +// GetAllRecords returns an Iterator with RecordState items for given name. +func GetAllRecords(name string) iterator.Iterator { + tokenID := []byte(tokenIDFromName(name)) + ctx := storage.GetReadOnlyContext() + _ = getNameState(ctx, tokenID) // ensure not expired + recordsKey := getRecordsKey(tokenID, name) + return storage.Find(ctx, recordsKey, storage.ValuesOnly|storage.DeserializeValues) +} + // updateBalance updates account's balance and account's tokens. func updateBalance(ctx storage.Context, tokenId []byte, acc interop.Hash160, diff int) { balanceKey := append([]byte{prefixBalance}, acc...) @@ -437,20 +453,35 @@ func putNameStateWithKey(ctx storage.Context, tokenKey []byte, ns NameState) { // getRecord returns domain record. func getRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType) string { recordKey := getRecordKey(tokenId, name, typ) - record := storage.Get(ctx, recordKey) - return record.(string) + recBytes := storage.Get(ctx, recordKey) + if recBytes == nil { + return recBytes.(string) // A hack to actually return NULL. + } + record := std.Deserialize(recBytes.([]byte)).(RecordState) + return record.Data } // putRecord stores domain record. func putRecord(ctx storage.Context, tokenId []byte, name string, typ RecordType, record string) { recordKey := getRecordKey(tokenId, name, typ) - storage.Put(ctx, recordKey, record) + rs := RecordState{ + Name: name, + Type: typ, + Data: record, + } + recBytes := std.Serialize(rs) + storage.Put(ctx, recordKey, recBytes) +} + +// getRecordsKey returns prefix used to store domain records of different types. +func getRecordsKey(tokenId []byte, name string) []byte { + recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...) + return append(recordKey, getTokenKey([]byte(name))...) } // getRecordKey returns key used to store domain records. func getRecordKey(tokenId []byte, name string, typ RecordType) []byte { - recordKey := append([]byte{prefixRecord}, getTokenKey(tokenId)...) - recordKey = append(recordKey, getTokenKey([]byte(name))...) + recordKey := getRecordsKey(tokenId, name) return append(recordKey, []byte{byte(typ)}...) } @@ -649,10 +680,12 @@ func resolve(ctx storage.Context, name string, typ RecordType, redirect int) str records := getRecords(ctx, name) cname := "" for iterator.Next(records) { - r := iterator.Value(records).([]string) - key := []byte(r[0]) - value := r[1] - rTyp := key[len(key)-1] + r := iterator.Value(records).(struct { + key string + rs RecordState + }) + value := r.rs.Data + rTyp := r.key[len(r.key)-1] if rTyp == byte(typ) { return value } @@ -671,7 +704,6 @@ func resolve(ctx storage.Context, name string, typ RecordType, redirect int) str func getRecords(ctx storage.Context, name string) iterator.Iterator { tokenID := []byte(tokenIDFromName(name)) _ = getNameState(ctx, tokenID) - recordsKey := append([]byte{prefixRecord}, getTokenKey(tokenID)...) - recordsKey = append(recordsKey, getTokenKey([]byte(name))...) - return storage.Find(ctx, recordsKey, storage.None) + recordsKey := getRecordsKey(tokenID, name) + return storage.Find(ctx, recordsKey, storage.DeserializeValues) } diff --git a/examples/nft-nd-nns/nns.yml b/examples/nft-nd-nns/nns.yml index 01e89b72b..289793c9d 100644 --- a/examples/nft-nd-nns/nns.yml +++ b/examples/nft-nd-nns/nns.yml @@ -1,7 +1,8 @@ name: "NameService" supportedstandards: ["NEP-11"] safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", - "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord", "resolve"] + "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord", + "resolve", "getAllRecords"] events: - name: Transfer parameters: diff --git a/pkg/core/nonnative_name_service_test.go b/pkg/core/nonnative_name_service_test.go index 43486b2fb..0a071001c 100644 --- a/pkg/core/nonnative_name_service_test.go +++ b/pkg/core/nonnative_name_service_test.go @@ -441,7 +441,7 @@ func TestResolve(t *testing.T) { const ( defaultNameServiceDomainPrice = 10_0000_0000 - defaultNameServiceSysfee = 4000_0000 + defaultNameServiceSysfee = 6000_0000 defaultRegisterSysfee = 10_0000_0000 + defaultNameServiceDomainPrice ) diff --git a/pkg/rpc/client/helper.go b/pkg/rpc/client/helper.go index 2744deb16..0a7b0c0fa 100644 --- a/pkg/rpc/client/helper.go +++ b/pkg/rpc/client/helper.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/util" @@ -124,6 +125,35 @@ func topIterableFromStack(st []stackitem.Item, resultItemType interface{}) ([]in if err != nil { return nil, fmt.Errorf("failed to decode uint160 from stackitem #%d: %w", i, err) } + case nns.RecordState: + rs, ok := iter.Values[i].Value().([]stackitem.Item) + if !ok { + return nil, fmt.Errorf("failed to decode RecordState from stackitem #%d: not a struct", i) + } + if len(rs) != 3 { + return nil, fmt.Errorf("failed to decode RecordState from stackitem #%d: wrong number of elements", i) + } + name, err := rs[0].TryBytes() + if err != nil { + return nil, fmt.Errorf("failed to deocde RecordState from stackitem #%d: %w", i, err) + } + typ, err := rs[1].TryInteger() + if err != nil { + return nil, fmt.Errorf("failed to deocde RecordState from stackitem #%d: %w", i, err) + } + data, err := rs[2].TryBytes() + if err != nil { + return nil, fmt.Errorf("failed to deocde RecordState from stackitem #%d: %w", i, err) + } + u64Typ := typ.Uint64() + if !typ.IsUint64() || u64Typ > 255 { + return nil, fmt.Errorf("failed to deocde RecordState from stackitem #%d: bad type", i) + } + result[i] = nns.RecordState{ + Name: string(name), + Type: nns.RecordType(u64Typ), + Data: string(data), + } default: return nil, errors.New("unsupported iterable type") } diff --git a/pkg/rpc/client/native.go b/pkg/rpc/client/native.go index bfbed495e..ab36030c0 100644 --- a/pkg/rpc/client/native.go +++ b/pkg/rpc/client/native.go @@ -114,3 +114,30 @@ func (c *Client) NNSIsAvailable(nnsHash util.Uint160, name string) (bool, error) } return topBoolFromStack(result.Stack) } + +// NNSGetAllRecords returns all records for a given name from NNS service. +func (c *Client) NNSGetAllRecords(nnsHash util.Uint160, name string) ([]nns.RecordState, error) { + result, err := c.InvokeFunction(nnsHash, "getAllRecords", []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: name, + }, + }, nil) + if err != nil { + return nil, err + } + err = getInvocationError(result) + if err != nil { + return nil, err + } + + arr, err := topIterableFromStack(result.Stack, nns.RecordState{}) + if err != nil { + return nil, fmt.Errorf("failed to get token IDs from stack: %w", err) + } + rss := make([]nns.RecordState, len(arr)) + for i := range rss { + rss[i] = arr[i].(nns.RecordState) + } + return rss, nil +} diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index 606750ef0..2b9b2210c 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -876,4 +876,19 @@ func TestClient_NNS(t *testing.T) { _, err := c.NNSResolve(nsHash, "neogo.com", nns.CNAME) require.Error(t, err) }) + t.Run("NNSGetAllRecords, good", func(t *testing.T) { + rss, err := c.NNSGetAllRecords(nsHash, "neo.com") + require.NoError(t, err) + require.Equal(t, []nns.RecordState{ + nns.RecordState{ + Name: "neo.com", + Type: nns.A, + Data: "1.2.3.4", + }, + }, rss) + }) + t.Run("NNSGetAllRecords, bad", func(t *testing.T) { + _, err := c.NNSGetAllRecords(nsHash, "neopython.com") + require.Error(t, err) + }) } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index c7f11b944..061398043 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -55,14 +55,15 @@ type rpcTestCase struct { } const testContractHash = "bb6a679438ce0fc6cb0ed1aa85ce83cf96cd3aeb" -const deploymentTxHash = "1f8792e07f223e5e83f86cda3327cbe78c15ea382a1c350101c9119747682ce2" +const deploymentTxHash = "4c631654b04f6a3b25af45082d260b555de4d0eeba6b7697e3a0f18b3f96434f" const genesisBlockHash = "0f8fb4e17d2ab9f3097af75ca7fd16064160fb8043db94909e00dd4e257b9dc4" const verifyContractHash = "f68822e4ecd93de334bdf1f7c409eda3431bcbd0" const verifyContractAVM = "VwIAQS1RCDAhcAwU7p6iLCfjS9AUj8QQjgj3To9QSLLbMHFoE87bKGnbKJdA" const verifyWithArgsContractHash = "947c780f45b2a3d32e946355ee5cb57faf4decb7" const invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAANswcGhB+CfsjCGqJgQRQAwUDQ8DAgkAAgEDBwMEBQIBAA4GDAnbMHFpQfgn7IwhqiYEEkATQA==" -const nameServiceContractHash = "66206eb850818ec862a9332e0da10b9b7826cb0b" + +const nameServiceContractHash = "3a602b3e7cfd760850bfac44f4a9bb0ebad3e2dc" var rpcTestCases = map[string][]rpcTestCase{ "getapplicationlog": { @@ -1643,7 +1644,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "57941227260", + Amount: "57900879260", LastUpdated: 15, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index cc610cb70603d2029802fd994cafede7a9628f29..155622bacf2762b6cc0afb04d147b861069ed32d 100644 GIT binary patch delta 6608 zcmc(DcQl;Q*0(X*Fv{pe8Ep)r8=^nT=%UwXF^JJilpzRdB9h=CB6^MJJw%BRB}5I0 z7D0p%5nYP#4)?zIyX&s+`~G^@de53QbDlH%?DpGdpS_>{0^q=X0ggV7y|%<(T2!xs!?teu4J39ZY*NuUxmZ6E{JL4ygN4lh z7_1>xZ~5Yd@R4s}+?Q!h8Ti51{hg9Eocb7Q#KLBD+gnv-(Zpx+8+?OfDN zaA|5MD}D3K>>*cKQTNPSj*o*In_vp!MmbXR4BSx`I3rEk{+#L~-CeMj`n~PE&*`DwFE&S)9j$0k z;#AmbA!RR<8cSB35%sBGASC}9q~I7cS4_Ta%29lg@sb}2>;Ac|yWpC!Ve7-hR@86;oJVAo5NBx{}9az8DGa^=jUms&I@VR!oOfW?oF%{xckiHJagJ9nAfh>>u?bw7Fovp%qvPd_TcO9`FF-sA+~ zz~_Rm%+~{p1g}(XXtAx`3-B@LDG|1`5*q8)*6;bbstxNv#X=ocMIoe&LPPGP68pM} z4SeYCr0GiW2v3U*T~q)=0X8Za~P9)=0@q1FH=D4&4T)Fz;} z3^P~)a|0*kn7~-7CQwooUQa=j2?fRiZd^|=oTpul!lZKamQUMU!R%YLB+!haZBHPwC~3>e$1Tc zra1)&w4q1GL|!Y=$^VgJsg@&U=XZBaHvJM?G_Ov3_v3=kytaY&2&F3}Z`9sjAA658 zf@5r+%1i1Vl!%d$nZ0tO$Ep{8Er$bJHb_d&ZY<+#k5zTT3pVtAu0;#UCgBtZ zi|AK`P7j@bKS8*o74n{|ffxABbB+e17S z$)UfU#6u?3RkiHfOyu2UKRgdFQK$-oE&J3Y32lo@-%~3pMeE$L^68@aB7aj{jNl5K zfilgoh!mfRg45=zEYO9gl%ra>Xx{}`tTOA3hzGZ|rbs-Zp8{tPNOJg@@h1p$jNWyj zXF;@*e6DqOlv+a`kA@)}gac~*m`r}s8C-R`?je6$+ec)rbg*&*U@iQRzr>1Y*y$#7 z(@5iBJ1i>Lje-M8$s*kqg+0mN2mH2vv@=6{wy$4*8EBRyXCpJvKY7qsWFO^JM0G8F z9K)MUSrk(IAX^EM8Sz8t-hTQ`dTfapBODk@ER08&xQ|GiA32Jejd0$zzlN%Krmq)r z8QN|WZf&P^ICR;Tk>M9(EX4Rv#Nc^t$pQy@M!fP|W5lP%@paxa<71glknRcFlxteo z!Vn|iL-i)_9BxjGSD6o+8)x35=90H+V_+ULlAQL;_#S3-Ot0Ux3AcnT0e@+fg<+4C zk~{%Ab;o7y+Ko6LrNH*OC`~ILk&b;Rf1SIWZJ#`~uWo?He0f&{X}JMFr(nK#1P3yL z3Jak+$|}6!o%H4#1Jspt)3UnfKS$G>%86xjh{yLhxf=z{KL=-jCqRi&sBgXKX1p z8n8Y{jf)Q_b3VY3Ov9aJ4z#;j*Yw8nmFjC*Y)FyXF_f8|r58urRucwV%==t}Q!Jk; zYX3}>FNf$T^*ic-Xm%ty-I?Xa?C2PYr2Sg6pHnWIKh^^|Uzv)8y%6qTXg!wRmC8GC z7Hp;ELH*D*-aKB7C@@|6lGl289jY&TeI+HUhfCb6w8U*7}|Gq)eq>GDNd@Zr-*^pWl9#Y7Cw7GR96(rs5?aqs<`DS_)&A~Yoz5{^+*>->KG$vomHA%Gp?UoR1LZ8*<^x%WP zaAG@08BE9C4z5FE6&p1}dwjb+r0CZi9rR_buadZFyy+G8lTGZ}ek2^Yn?kN4TpzN= z%Gi&u9#Bb0a^(ehV`QH0C%DI!FQ1cj3f&{St-L1*QJ!I*l_+e_maCQh&Ym&UfzuS` z9GhD>bwqtC*LhMTEPJndZQRj&fskOs#h&)d_PC0qgYT`iC{rJ)Tys_|xWI!%F#Lxn zsLni*?WSLh*4HrNw_?9TjU*2mHh8^=Rk6yhM>2F|m8|*JMXY~)xyiAKsUUl$qHbqB z?!2J<^D4?d>8XS3VVgW997vpZk=5(iP<(w~S2{YUAlH3#;s)ZUm-Vx7s82k9(%+V+ zVwUW2O?kfc5BBQXa@n1}FLNE@zCtcK)qn26dMUqz138;=Jn+gwDTEUV6wkW$vX}MU zyDRfE5tZDikI&hk&({nwlIChscK@Q}SElP5rjq)~T>*rIkw@<&^m6!&*4J6nCihl*#MCs{1$u@gtO{r+xeJz|%4RM_qS3}zk)tWundJ=!l=2vd1wR80Q zD{wtcbT#(!D2nhMadu8t_|P#Fea9rj54db|U$r1@Mj3vGNVeA>ZhXsR)*;d9g}~f~ zSS?H5e~QW9Cm0rmAYB+eE+j21Bnqfk;K*L~_=~#`gWRpicCPN)a-CY8Um1P-?V(Hf zOH-4gzPA_;1t;ODFf9sQ@tDi!O?z~#{>Zl;B`?$EDTZQ9ho-f@r%q#ki(=ux?D*6A zXXi?__n%F2`P;IPZIoLo6u+g;SX$xvOD|5o`#H*OWxPRjSKLljSxXl1ed$=xbf+9S z6wRhlPJ7N;;WRwRWc_oEK_7vv7$2jwd&9l-47A0cmLS}^ZgtD==hR4Km}<3dvSkx;yP*bjnRdGT?v)F3$a3B42<)7W=Hb!4*R} zTwlP}VC5FG$Rl$>s$*QTc$|1}ls9~xbD;v-dAoz!8})0?wjgrcnfgW)*2eJz9B_(v zc*rHNAG)`3-ueyhE;oIVj$oJfJxJ&9#0$b<$HfYdY~L8ix%M(aSiy(~&$p^H{D6yn zTq08xZdSUe(U4fErGhR(N zj|dF651aBv@Lz>-$JkiEA8>%`a!Hq#s7=F`9nXxUvxdK{MW+)FZ`?!rq_BvRu6zJJu;ZcF7*s(uLd zCk5ueKA~3U`&k!_D71d$bA<7Xh*6F_AN8e^5md|3oo%*-aX3ys%UJ)Jk4=d(5PGxz zuop2Y!gQ7ow#Cpfix2<&O%mVO$P1{-)JgD)oa4`GxQDL)zNN6#5`Vtk`JuqBDgSe}PdELXT>90js8;D8L{iv?nktP+c*onPub!y=r! zmHS%_?}91Clv~*=FWK*0Y;#=Q=$`W?9{9~e(Va{5LOmDhu%nvp#4d+$+xC-&TlSh{O8*EH9_J@Lk%H%Yx1KV&U?k3pzO*tj1{)=fx`ye~1 zv1iN45SCUq|pa8~7NPtI@3}7dQ49rpx zB=!ZCpoz2@_z4pMGRoM2Rr0CC-XN6N7c_yL3ZdX%iF8m|_7RvT9RM!M*n^c93{G1L z`3qs-m~09tB85L~9Y6}28}+vp;Ska{2rx%fANrSo02h>!;HA_gB!pS1fN)P!Mo>+Q z158s!F~f+q05+r$je`d7CB6X9a8ryOIHJr7(F9kNg-N$zps6|wD5#=K3jSjnry?kB z3nL*ZL$Saa@P46}eeiC`Aa|TQKGZ!(iGY!&Bq0gV$UL|*%om9FG{v}r?^L7|UJzID z1aCy9C8n4BbQ+GZ#IzA--rqew4Pfi0?@ zq@plzTTKYmQ`3RCgAdhQNFAt%3y!2U6hsyEA@<-C^)Q-lB1-(!c!VjY1LOi^IQ+?o z!op{z2t;ua#Q%H=#0MHn%sO}lFogtw%>WN%2>bweK~_Lb4FzdY=&3u%py8MyqK!mn zG0Y+Z2?mII6ioz1lc;4NzpVjFJqi(VrePsDSf+s@O``h4O&NHwf>=Wufs&s`&|DeSzix z1acMh)%Fu4BO^paT3|+rQGFT_8YzxnrHP15p(yaPXaGMeh3E_h^Yz)lX?0rASsTvB zI84-#{B)CCcmN+$uK`2Er~x8!)PV8YXdwQ-sav#ZbiFM#T%#_T^UX>Hg?oBgXsoES z-{b>qAf#{z|E!e36p9=RCke-h9-o}t3-J8sGchtT?58V4LTEU_RBo(Z61xkv=ONl+ z5W;9|I2`I{FeOMnz)yx{U=WKY>g~%Q%8N$~|DRPf42#7Q5d#=}e9cDrt$aNZxuSe{ zUqtTzjR-WZ zG=jVk!;HhhUJS#;&%Y!S1zh!s5GC^ld{~IzAh+Rp?$tvgXHSADCV&_Zf#G=ee}VwW zI!v7d5n`f%U}rx6<@p-MdP83ge=CDO-KCqC23PW@EvnB;w;lvFPd7V=O67-s>nOH+ zdl0}Y>KyXO>8 z86a65S-S4*#@ko{dEcVLw?C8ZP&s*7x)dW8x`L_SC}M0pBI;WHsHBlzpz^%`mb_Ar3d!jIwAx z6BBG=;wD|WYb`e%uh9gDE z@-q74r*MiNCuL-#9jql!Y?!&``3Sw;RT(cKBj?}ezw2+9OQVEYo|bjSw8Z3gI_M?t zQnn}~VCpH;VsR}bA<@xV>FeB@9Ko9UQhA4ACsS49)kFPz9?UwW&?*Pu+QV?B(QfHR zGnM;s2qRW&KbEswo5vI#lTLP9n}h~5j|*bm{-W~9YR8fp{Y+3cj}B8WsKqX`Z5A(- z^M2G%exj^V$X?N0(ZO!bs_{lz2@?O2cm2KJ931d4PAZHqTHm?bj8WB?H%JoAKgNAY z`=+}awki*c%}WmtxuaxU7_db{C|F%fl-@OdP~x?hH5Qu5mfIJFwaVCr1EvR$z2<|v zCRb=x$@*T1SGWcz^*R}Q))zSXkMunz4E;u1h1}VM#JN`G8uX27IT#JJvxxDp-DtTh z?>4HaT5$?D6}>oNm*frz1xwA8W9F9Ly`XD-wx?b3(v&Ln?=>|s7p1cl1^G)wY)qLP zn?Bw)e(Nn5;OYK#zm7BR+~b|DiCF>R(yh8z3G-Z@*GT5f^(Bp&+*G`e$}F2*U=64V z`)97yTOB1#as9$@K=dl$+R$}XxshxgfmaR9xBV>2c7Oet2j$ViH>Vz>FQ%q6vfi|F zV8FvYMk@a2v5w>Y^q(&x6LH?#@XEBx_?vLx#hdtMzeK+r4QvSSIfE2+m+>o>BDVr1 zFDm4iTkXt|d#u?bxd-02D<@Prt|WJbHZ9M%$G#(l^i>a!>}Ah6!-02GKW*Ps)Na)b zyNlrGq-nSV%BE{3*9dz4sR7tUfm3oS`Ybj+-w{0|~Rett!lVQeyD@>1t&mB@# z1<@09l44l8Y=UC-93p_6^IA!i*KePN7Okf2^Xsd<>R^kNwd4Ogv^3#>kSve*K$?kH z{$i0AS~^9fPu}u(R`MEWow=c{*s!c91^ThJ*XwFq_m!2=;_Um2nfgf0!j($}qdshk zW77kUr|)mgmU7a4_bUb|>lbW)$f8io(Yk7TFWD-b*gS69m~m>p|DNtpM^3d9Hc_Rx zBmO38|A`x~K4|%Yp1@cOTHu%=Z2yti>CY1T8ur$Z?#9cvt_@#%*KU|rPb`(ul1RwK zJd0aSWm=LRKN@I^nZwVOF4^gxQO}faGeZM4g>^(1TD+%`p1KNUGB(&ZnLU z*x5KfIKaGI{WFj}Ul=w`A^VA(b@W1AI2hxR=yG+8oP)K z9ZCAdXL~Tcd~CF}K5zHoDTU9#aqz{=?+u+1k(E62u6c3yo2`vJW|$pRB~I_#{{ru> BlBECu delta 6353 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