@@ -15,6 +15,7 @@ import (
1515 cst "github.com/nspcc-dev/neofs-contract/contracts/container/containerconst"
1616 "github.com/nspcc-dev/neofs-contract/contracts/nns/recordtype"
1717 iproto "github.com/nspcc-dev/neofs-contract/internal/proto"
18+ istd "github.com/nspcc-dev/neofs-contract/internal/std"
1819)
1920
2021type (
@@ -138,6 +139,11 @@ const (
138139 nep11TransferAmount = 1 // non-divisible NFT
139140)
140141
142+ // Attributes.
143+ const (
144+ attributeLock = "__NEOFS__LOCK_UNTIL"
145+ )
146+
141147var (
142148 eACLPrefix = []byte ("eACL" )
143149)
@@ -390,9 +396,17 @@ func PutNamedOverloaded(container []byte, signature interop.Signature, publicKey
390396func PutNamed (container []byte , signature interop.Signature ,
391397 publicKey interop.PublicKey , token []byte ,
392398 name , zone string ) {
399+ cnr := fromBytes (container )
400+
401+ for i := range cnr .Attributes {
402+ if cnr .Attributes [i ].Key == attributeLock {
403+ parseLockAttribute (cnr .Attributes [i ].Value )
404+ break
405+ }
406+ }
407+
393408 ctx := storage .GetContext ()
394409
395- ownerID := ownerFromBinaryContainer (container )
396410 containerID := crypto .Sha256 (container )
397411 if storage .Get (ctx , append ([]byte {deletedKeyPrefix }, []byte (containerID )... )) != nil {
398412 panic (cst .ErrorDeleted )
@@ -413,11 +427,10 @@ func PutNamed(container []byte, signature interop.Signature,
413427 }
414428
415429 alphabet := common .AlphabetNodes ()
416- from := common .WalletToScriptHash (ownerID )
417430 netmapContractAddr := storage .Get (ctx , netmapContractKey ).(interop.Hash160 )
418431 balanceContractAddr := storage .Get (ctx , balanceContractKey ).(interop.Hash160 )
419432 containerFee := contract .Call (netmapContractAddr , "config" , contract .ReadOnly , cst .RegistrationFeeKey ).(int )
420- balance := contract .Call (balanceContractAddr , "balanceOf" , contract .ReadOnly , from ).(int )
433+ balance := contract .Call (balanceContractAddr , "balanceOf" , contract .ReadOnly , cnr . Owner ).(int )
421434 if name != "" {
422435 aliasFee := contract .Call (netmapContractAddr , "config" , contract .ReadOnly , cst .AliasFeeKey ).(int )
423436 containerFee += aliasFee
@@ -436,7 +449,7 @@ func PutNamed(container []byte, signature interop.Signature,
436449
437450 if ! contract .Call (balanceContractAddr , "transferX" ,
438451 contract .All ,
439- from ,
452+ cnr . Owner ,
440453 to ,
441454 containerFee ,
442455 details ,
@@ -445,7 +458,7 @@ func PutNamed(container []byte, signature interop.Signature,
445458 }
446459 }
447460
448- addContainer (ctx , containerID , ownerID , container )
461+ addContainer (ctx , containerID , container , cnr )
449462
450463 if name != "" {
451464 if needRegister {
@@ -466,9 +479,9 @@ func PutNamed(container []byte, signature interop.Signature,
466479 runtime .Log ("added new container" )
467480 runtime .Notify ("PutSuccess" , containerID , publicKey )
468481
469- notifyNEP11Transfer (containerID , nil , from ) // 'from' is owner here i.e. 'to' in terms of NEP-11
482+ notifyNEP11Transfer (containerID , nil , cnr . Owner )
470483
471- onNEP11Payment (containerID , nil , from , nil )
484+ onNEP11Payment (containerID , nil , cnr . Owner , nil )
472485}
473486
474487// Create saves container descriptor serialized according to the NeoFS API
@@ -552,6 +565,8 @@ func CreateV2(cnr Info, invocScript, verifScript, sessionToken []byte) interop.H
552565 zone = cnr .Attributes [i ].Value
553566 case "__NEOFS__METAINFO_CONSISTENCY" :
554567 metaOnChain = true
568+ case attributeLock :
569+ parseLockAttribute (cnr .Attributes [i ].Value )
555570 }
556571 }
557572
@@ -688,19 +703,24 @@ func checkNiceNameAvailable(nnsContractAddr interop.Hash160, domain string) bool
688703func Delete (containerID []byte , signature interop.Signature , token []byte ) {
689704 ctx := storage .GetContext ()
690705
691- ownerID := getOwnerByID (ctx , containerID )
692- if ownerID == nil {
706+ cnr , ok := getInfo (ctx , containerID )
707+ if ! ok {
693708 return
694709 }
695710
696711 common .CheckAlphabetWitness ()
697712
713+ checkLock (cnr )
714+
698715 key := append ([]byte (nnsHasAliasKey ), containerID ... )
699716 domain := storage .Get (ctx , key ).(string )
700717 if len (domain ) != 0 {
701718 storage .Delete (ctx , key )
702719 deleteNNSRecords (ctx , domain )
703720 }
721+
722+ ownerID := scriptHashToAddress (cnr .Owner )
723+
704724 removeContainer (ctx , containerID , ownerID )
705725 runtime .Log ("remove container" )
706726 runtime .Notify ("DeleteSuccess" , containerID )
@@ -721,13 +741,15 @@ func Remove(id []byte, invocScript, verifScript, sessionToken []byte) {
721741 }
722742
723743 ctx := storage .GetContext ()
724- cnrItemKey := append ([] byte { containerKeyPrefix }, id ... )
725- cnrItem := storage . Get (ctx , cnrItemKey )
726- if cnrItem == nil {
744+
745+ cnr , ok := getInfo (ctx , id )
746+ if ! ok {
727747 return
728748 }
729749
730- owner := ownerFromBinaryContainer (cnrItem .([]byte ))
750+ checkLock (cnr )
751+
752+ owner := scriptHashToAddress (cnr .Owner )
731753
732754 removeContainer (ctx , id , owner )
733755
@@ -762,14 +784,23 @@ func deleteNNSRecords(ctx storage.Context, domain string) {
762784// GetInfo reads container by ID. If the container is missing, GetInfo throws
763785// [cst.NotFoundError] exception.
764786func GetInfo (id interop.Hash256 ) Info {
765- val := storage .Get (storage .GetReadOnlyContext (), append ([]byte {infoPrefix }, id ... ))
787+ cnr , ok := getInfo (storage .GetReadOnlyContext (), id )
788+ if ! ok {
789+ panic (cst .NotFoundError )
790+ }
791+
792+ return cnr
793+ }
794+
795+ func getInfo (ctx storage.Context , id interop.Hash256 ) (Info , bool ) {
796+ val := storage .Get (ctx , append ([]byte {infoPrefix }, id ... ))
766797 if val == nil {
767- if val = storage .Get (storage . GetReadOnlyContext () , append ([]byte {containerKeyPrefix }, id ... )); val != nil {
768- return fromBytes (val .([]byte ))
798+ if val = storage .Get (ctx , append ([]byte {containerKeyPrefix }, id ... )); val != nil {
799+ return fromBytes (val .([]byte )), true
769800 }
770- panic ( cst . NotFoundError )
801+ return Info {}, false
771802 }
772- return std .Deserialize (val .([]byte )).(Info )
803+ return std .Deserialize (val .([]byte )).(Info ), true
773804}
774805
775806// Get method returns a structure that contains a stable marshaled Container structure,
@@ -1775,6 +1806,46 @@ func Properties(tokenID []byte) map[string]any {
17751806 return props
17761807}
17771808
1809+ // TODO: docs.
1810+ func SetLockUntil (id interop.Hash256 , until int ) {
1811+ if until <= 0 {
1812+ panic ("non-positive until " + std .Itoa10 (until ))
1813+ }
1814+
1815+ ctx := storage .GetContext ()
1816+
1817+ cnr , ok := getInfo (ctx , id )
1818+ if ! ok {
1819+ panic (cst .NotFoundError )
1820+ }
1821+
1822+ if ! runtime .CheckWitness (cnr .Owner ) {
1823+ panic ("missing owner witness" )
1824+ }
1825+
1826+ if now := runtime .GetTime () / 1000 ; now > until {
1827+ panic ("until has already passed " + std .Itoa10 (now ) + " > " + std .Itoa10 (until ))
1828+ }
1829+
1830+ was := false
1831+ for i := range cnr .Attributes {
1832+ if cnr .Attributes [i ].Key == attributeLock {
1833+ cnr .Attributes [i ].Value = std .Itoa10 (until )
1834+ was = true
1835+ break
1836+ }
1837+ }
1838+ if ! was {
1839+ cnr .Attributes = append (cnr .Attributes , Attribute {
1840+ Key : attributeLock ,
1841+ Value : std .Itoa10 (until ),
1842+ })
1843+ }
1844+
1845+ storage .Put (ctx , append ([]byte {infoPrefix }, id ... ), std .Serialize (cnr ))
1846+ storage .Put (ctx , append ([]byte {containerKeyPrefix }, id ... ), toBytes (cnr ))
1847+ }
1848+
17781849func notifyNEP11Transfer (tokenID []byte , from , to interop.Hash160 ) {
17791850 runtime .Notify ("Transfer" , from , to , nep11TransferAmount , tokenID )
17801851}
@@ -1785,15 +1856,15 @@ func onNEP11Payment(tokenID []byte, from, to interop.Hash160, data any) {
17851856 }
17861857}
17871858
1788- func addContainer (ctx storage.Context , id , owner , container []byte ) {
1789- containerListKey := append ([]byte {ownerKeyPrefix }, owner ... )
1859+ func addContainer (ctx storage.Context , id , container []byte , cnr Info ) {
1860+ containerListKey := append ([]byte {ownerKeyPrefix }, scriptHashToAddress ( cnr . Owner ) ... )
17901861 containerListKey = append (containerListKey , id ... )
17911862 storage .Put (ctx , containerListKey , id )
17921863
17931864 idKey := append ([]byte {containerKeyPrefix }, id ... )
17941865 storage .Put (ctx , idKey , container )
17951866
1796- storage .Put (ctx , append ([]byte {infoPrefix }, id ... ), std .Serialize (fromBytes ( container ) ))
1867+ storage .Put (ctx , append ([]byte {infoPrefix }, id ... ), std .Serialize (cnr ))
17971868}
17981869
17991870func removeContainer (ctx storage.Context , id []byte , owner []byte ) {
@@ -1924,6 +1995,27 @@ func scriptHashToAddress(h interop.Hash160) []byte {
19241995 return addr
19251996}
19261997
1998+ func checkLock (cnr Info ) {
1999+ for i := range cnr .Attributes {
2000+ if cnr .Attributes [i ].Key == attributeLock {
2001+ until := parseLockAttribute (cnr .Attributes [i ].Value )
2002+
2003+ if now := runtime .GetTime () / 1000 ; now <= until {
2004+ panic (cst .ErrorLocked + " until " + cnr .Attributes [i ].Value + ", now " + std .Itoa10 (now ))
2005+ }
2006+ }
2007+ }
2008+ }
2009+
2010+ func parseLockAttribute (s string ) int {
2011+ v , e := istd .Atoi (s )
2012+ if e {
2013+ panic ("invalid lock attribute " + s )
2014+ }
2015+
2016+ return v
2017+ }
2018+
19272019const (
19282020 fieldAPIVersion = 1
19292021 fieldOwner = 2
0 commit comments