/* Package policy allows to work with the native PolicyContract contract via RPC. Safe methods are encapsulated into ContractReader structure while Contract provides various methods to perform PolicyContract state-changing calls. */ package policy import ( "encoding/binary" "errors" "fmt" "github.com/google/uuid" "github.com/tutus-one/tutus-chain/pkg/core/native/nativehashes" "github.com/tutus-one/tutus-chain/pkg/core/transaction" "github.com/tutus-one/tutus-chain/pkg/neorpc/result" "github.com/tutus-one/tutus-chain/pkg/rpcclient/unwrap" "github.com/tutus-one/tutus-chain/pkg/smartcontract" "github.com/tutus-one/tutus-chain/pkg/util" "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" ) // Invoker is used by ContractReader to call various methods. type Invoker interface { Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...any) (*result.Invoke, error) TerminateSession(sessionID uuid.UUID) error TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) } // Actor is used by Contract to create and send transactions. type Actor interface { Invoker MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) MakeRun(script []byte) (*transaction.Transaction, error) MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) SendRun(script []byte) (util.Uint256, uint32, error) } // Hash stores the hash of the native PolicyContract contract. var Hash = nativehashes.PolicyContract const ( execFeeSetter = "setExecFeeFactor" feePerByteSetter = "setFeePerByte" storagePriceSetter = "setStoragePrice" attributeFeeSetter = "setAttributeFee" maxValidUntilBlockIncrementSetter = "setMaxValidUntilBlockIncrement" millisecondsPerBlockSetter = "setMillisecondsPerBlock" ) // ContractReader provides an interface to call read-only PolicyContract // contract's methods. type ContractReader struct { invoker Invoker } // Contract represents a PolicyContract contract client that can be used to // invoke all of its methods. type Contract struct { ContractReader actor Actor } // BlockedAccountsIterator is used for iterating over GetBlockedAccounts results. type BlockedAccountsIterator struct { client Invoker session uuid.UUID iterator result.Iterator } // WhitelistedContractIterator is used for iterating over GetWhitelistFeeContracts results. type WhitelistedContractIterator struct { client Invoker session uuid.UUID iterator result.Iterator } // WhitelistedContract represents a whitelisted contract method with the fixed // execution cost. type WhitelistedContract struct { Hash util.Uint160 Offset uint32 } // WhitelistFeeChangedEvent represents a WhitelistFeeChanged Policy event. type WhitelistFeeChangedEvent struct { Hash util.Uint160 Method string ArgCnt int // Fee is the fixed execution cost of the method in Datoshi units (it's not // set if the method is removed from the whitelist). Fee *int64 } // NewReader creates an instance of ContractReader that can be used to read // data from the contract. func NewReader(invoker Invoker) *ContractReader { return &ContractReader{invoker} } // New creates an instance of Contract to perform actions using // the given Actor. Notice that PolicyContract's state can be changed // only by the network's committee, so the Actor provided must be a committee // actor for all methods to work properly. func New(actor Actor) *Contract { return &Contract{*NewReader(actor), actor} } // GetExecFeeFactor returns current execution fee factor used by the network. // This setting affects all executions of all transactions. func (c *ContractReader) GetExecFeeFactor() (int64, error) { return unwrap.Int64(c.invoker.Call(Hash, "getExecFeeFactor")) } // GetExecPicoFeeFactor returns the current execution fee factor used by the // network. It differs from GetExecFeeFactor in that it returns the fee factor // in picoGAS units. This method is available starting from [config.HFFaun] // hardfork. func (c *ContractReader) GetExecPicoFeeFactor() (int64, error) { return unwrap.Int64(c.invoker.Call(Hash, "getExecPicoFeeFactor")) } // GetFeePerByte returns current minimal per-byte network fee value which // affects all transactions on the network. func (c *ContractReader) GetFeePerByte() (int64, error) { return unwrap.Int64(c.invoker.Call(Hash, "getFeePerByte")) } // GetStoragePrice returns current per-byte storage price. Any contract saving // data to the storage pays for it according to this value. func (c *ContractReader) GetStoragePrice() (int64, error) { return unwrap.Int64(c.invoker.Call(Hash, "getStoragePrice")) } // GetAttributeFee returns current fee for the specified attribute usage. Any // contract saving data to the storage pays for it according to this value. func (c *ContractReader) GetAttributeFee(t transaction.AttrType) (int64, error) { return unwrap.Int64(c.invoker.Call(Hash, "getAttributeFee", byte(t))) } // GetMaxValidUntilBlockIncrement returns current MaxValidUntilBlockIncrement // setting. Note that this method is available starting from Echidna hardfork. func (c *ContractReader) GetMaxValidUntilBlockIncrement() (int64, error) { return unwrap.Int64(c.invoker.Call(Hash, "getMaxValidUntilBlockIncrement")) } // GetMillisecondsPerBlock returns current MillisecondsPerBlock setting. Note // that this method is available starting from Echidna hardfork. func (c *ContractReader) GetMillisecondsPerBlock() (int64, error) { return unwrap.Int64(c.invoker.Call(Hash, "getMillisecondsPerBlock")) } // GetBlockedAccounts returns current blocked accounts. Note that this method is // available starting from [config.HFFaun] hardfork. func (c *ContractReader) GetBlockedAccounts() (*BlockedAccountsIterator, error) { sess, iter, err := unwrap.SessionIterator(c.invoker.Call(Hash, "getBlockedAccounts")) if err != nil { return nil, err } return &BlockedAccountsIterator{ client: c.invoker, iterator: iter, session: sess, }, nil } // GetBlockedAccountsExpanded is similar to GetBlockedAccounts (uses the same // contract method), but can be useful if the server used doesn't support // sessions and doesn't expand iterators. It creates a script that will get num // of result items from the iterator right in the VM and return them to you. It's // only limited by VM stack and GAS available for RPC invocations. func (c *ContractReader) GetBlockedAccountsExpanded(num int) ([]util.Uint160, error) { return unwrap.ArrayOfUint160(c.invoker.CallAndExpandIterator(Hash, "getBlockedAccounts", num)) } // Next returns the next set of elements from the iterator (up to num of them). // It can return less than num elements in case iterator doesn't have that many // or zero elements if the iterator has no more elements or the session is // expired. func (b *BlockedAccountsIterator) Next(num int) ([]util.Uint160, error) { items, err := b.client.TraverseIterator(b.session, &b.iterator, num) if err != nil { return nil, err } res := make([]util.Uint160, len(items)) for i, itm := range items { hb, err := itm.TryBytes() if err != nil { return nil, fmt.Errorf("item #%d has wrong hash: %w", i, err) } res[i], err = util.Uint160DecodeBytesBE(hb) if err != nil { return nil, fmt.Errorf("item #%d has wrong hash: %w", i, err) } } return res, nil } // Terminate closes the iterator session used by BlockedAccountsIterator (if it's // session-based). func (b *BlockedAccountsIterator) Terminate() error { if b.iterator.ID == nil { return nil } return b.client.TerminateSession(b.session) } // IsBlocked checks if the given account is blocked in the PolicyContract. func (c *ContractReader) IsBlocked(account util.Uint160) (bool, error) { return unwrap.Bool(c.invoker.Call(Hash, "isBlocked", account)) } // SetExecFeeFactor creates and sends a transaction that sets the new // execution fee factor for the network to use. Note that starting from // [config.HFFaun] hardfork this method accepts the value in picoGAS units // instead of Datoshi units. The action is successful when the transaction ends // in the HALT state. The returned values are transaction hash, its // ValidUntilBlock value and an error if any. func (c *Contract) SetExecFeeFactor(value int64) (util.Uint256, uint32, error) { return c.actor.SendCall(Hash, execFeeSetter, value) } // SetExecFeeFactorTransaction creates a transaction that sets the new execution // fee factor. Note that starting from [config.HFFaun] hardfork this method // accepts the value in picoGAS units instead of Datoshi units. This transaction // is signed but not sent to the network, instead it's returned to the caller. func (c *Contract) SetExecFeeFactorTransaction(value int64) (*transaction.Transaction, error) { return c.actor.MakeCall(Hash, execFeeSetter, value) } // SetExecFeeFactorUnsigned creates a transaction that sets the new execution // fee factor. Note that starting from [config.HFFaun] hardfork this method // accepts the value in picoGAS units instead of Datoshi units. This transaction // is not signed and just returned to the caller. func (c *Contract) SetExecFeeFactorUnsigned(value int64) (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(Hash, execFeeSetter, nil, value) } // SetFeePerByte creates and sends a transaction that sets the new minimal // per-byte network fee value. The action is successful when transaction ends in // HALT state. The returned values are transaction hash, its ValidUntilBlock // value and an error if any. func (c *Contract) SetFeePerByte(value int64) (util.Uint256, uint32, error) { return c.actor.SendCall(Hash, feePerByteSetter, value) } // SetFeePerByteTransaction creates a transaction that sets the new minimal // per-byte network fee value. This transaction is signed, but not sent to the // network, instead it's returned to the caller. func (c *Contract) SetFeePerByteTransaction(value int64) (*transaction.Transaction, error) { return c.actor.MakeCall(Hash, feePerByteSetter, value) } // SetFeePerByteUnsigned creates a transaction that sets the new minimal per-byte // network fee value. This transaction is not signed and just returned to the // caller. func (c *Contract) SetFeePerByteUnsigned(value int64) (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(Hash, feePerByteSetter, nil, value) } // SetStoragePrice creates and sends a transaction that sets the storage price // for contracts. The action is successful when transaction ends in HALT // state. The returned values are transaction hash, its ValidUntilBlock value // and an error if any. func (c *Contract) SetStoragePrice(value int64) (util.Uint256, uint32, error) { return c.actor.SendCall(Hash, storagePriceSetter, value) } // SetStoragePriceTransaction creates a transaction that sets the storage price // for contracts. This transaction is signed, but not sent to the network, // instead it's returned to the caller. func (c *Contract) SetStoragePriceTransaction(value int64) (*transaction.Transaction, error) { return c.actor.MakeCall(Hash, storagePriceSetter, value) } // SetStoragePriceUnsigned creates a transaction that sets the storage price // for contracts. This transaction is not signed and just returned to the // caller. func (c *Contract) SetStoragePriceUnsigned(value int64) (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(Hash, storagePriceSetter, nil, value) } // SetAttributeFee creates and sends a transaction that sets the new attribute // fee value for the specified attribute. The action is successful when // transaction ends in HALT state. The returned values are transaction hash, its // ValidUntilBlock value and an error if any. func (c *Contract) SetAttributeFee(t transaction.AttrType, value int64) (util.Uint256, uint32, error) { return c.actor.SendCall(Hash, attributeFeeSetter, byte(t), value) } // SetAttributeFeeTransaction creates a transaction that sets the new attribute // fee value for the specified attribute. This transaction is signed, but not // sent to the network, instead it's returned to the caller. func (c *Contract) SetAttributeFeeTransaction(t transaction.AttrType, value int64) (*transaction.Transaction, error) { return c.actor.MakeCall(Hash, attributeFeeSetter, byte(t), value) } // SetAttributeFeeUnsigned creates a transaction that sets the new attribute fee // value for the specified attribute. This transaction is not signed and just // returned to the caller. func (c *Contract) SetAttributeFeeUnsigned(t transaction.AttrType, value int64) (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(Hash, attributeFeeSetter, nil, byte(t), value) } // SetMaxValidUntilBlockIncrement creates and sends a transaction that sets the // MaxValidUntilBlockIncrement protocol setting. The action is successful when // transaction ends in HALT state. The returned values are transaction hash, // its ValidUntilBlock value and an error if any. Note that this method is available // starting from Echidna hardfork. func (c *Contract) SetMaxValidUntilBlockIncrement(value int64) (util.Uint256, uint32, error) { return c.actor.SendCall(Hash, maxValidUntilBlockIncrementSetter, value) } // SetMaxValidUntilBlockIncrementTransaction creates a transaction that sets the // MaxValidUntilBlockIncrement value. This transaction is signed, but not sent to // the network, instead it's returned to the caller. Note that this method is available // starting from Echidna hardfork. func (c *Contract) SetMaxValidUntilBlockIncrementTransaction(value int64) (*transaction.Transaction, error) { return c.actor.MakeCall(Hash, maxValidUntilBlockIncrementSetter, value) } // SetMaxValidUntilBlockIncrementUnsigned creates a transaction that sets the // MaxValidUntilBlockIncrement value. This transaction is not signed and just // returned to the caller. Note that this method is available starting from // Echidna hardfork. func (c *Contract) SetMaxValidUntilBlockIncrementUnsigned(value int64) (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(Hash, maxValidUntilBlockIncrementSetter, nil, value) } // SetMillisecondsPerBlock creates and sends a transaction that sets the // block generation time in milliseconds. The action is successful when // transaction ends in HALT state. The returned values are transaction hash, // its ValidUntilBlock value and an error if any. Note that this method is // available starting from Echidna hardfork. func (c *Contract) SetMillisecondsPerBlock(value int64) (util.Uint256, uint32, error) { return c.actor.SendCall(Hash, millisecondsPerBlockSetter, value) } // SetMillisecondsPerBlockTransaction creates a transaction that sets the // block generation time in milliseconds. This transaction is signed, but not // sent to the network, instead it's returned to the caller. Note that this // method is available starting from Echidna hardfork. func (c *Contract) SetMillisecondsPerBlockTransaction(value int64) (*transaction.Transaction, error) { return c.actor.MakeCall(Hash, millisecondsPerBlockSetter, value) } // SetMillisecondsPerBlockUnsigned creates a transaction that sets the // block generation time in milliseconds. This transaction is not signed and // just returned to the caller. Note that this method is available starting // from Echidna hardfork. func (c *Contract) SetMillisecondsPerBlockUnsigned(value int64) (*transaction.Transaction, error) { return c.actor.MakeUnsignedCall(Hash, millisecondsPerBlockSetter, nil, value) } // BlockAccount creates and sends a transaction that blocks an account on the // network (via `blockAccount` method), it fails (with FAULT state) if it's not // successful. The returned values are transaction hash, its // ValidUntilBlock value and an error if any. func (c *Contract) BlockAccount(account util.Uint160) (util.Uint256, uint32, error) { return c.actor.SendRun(blockScript(account)) } // BlockAccountTransaction creates a transaction that blocks an account on the // network and checks for the result of the appropriate call, failing the // transaction if it's not true. This transaction is signed, but not sent to the // network, instead it's returned to the caller. func (c *Contract) BlockAccountTransaction(account util.Uint160) (*transaction.Transaction, error) { return c.actor.MakeRun(blockScript(account)) } // BlockAccountUnsigned creates a transaction that blocks an account on the // network and checks for the result of the appropriate call, failing the // transaction if it's not true. This transaction is not signed and just returned // to the caller. func (c *Contract) BlockAccountUnsigned(account util.Uint160) (*transaction.Transaction, error) { return c.actor.MakeUnsignedRun(blockScript(account), nil) } func blockScript(account util.Uint160) []byte { // We know parameters exactly (unlike with nep17.Transfer), so this can't fail. script, _ := smartcontract.CreateCallWithAssertScript(Hash, "blockAccount", account) return script } // UnblockAccount creates and sends a transaction that removes previously blocked // account from the stop list. It uses `unblockAccount` method and checks for the // result returned, failing the transaction if it's not true. The returned values // are transaction hash, its ValidUntilBlock value and an error if any. func (c *Contract) UnblockAccount(account util.Uint160) (util.Uint256, uint32, error) { return c.actor.SendRun(unblockScript(account)) } // UnblockAccountTransaction creates a transaction that unblocks previously // blocked account via `unblockAccount` method and checks for the result returned, // failing the transaction if it's not true. This transaction is signed, but not // sent to the network, instead it's returned to the caller. func (c *Contract) UnblockAccountTransaction(account util.Uint160) (*transaction.Transaction, error) { return c.actor.MakeRun(unblockScript(account)) } // UnblockAccountUnsigned creates a transaction that unblocks the given account // if it was blocked previously. It uses `unblockAccount` method and checks for // its return value, failing the transaction if it's not true. This transaction // is not signed and just returned to the caller. func (c *Contract) UnblockAccountUnsigned(account util.Uint160) (*transaction.Transaction, error) { return c.actor.MakeUnsignedRun(unblockScript(account), nil) } func unblockScript(account util.Uint160) []byte { // We know parameters exactly (unlike with nep17.Transfer), so this can't fail. script, _ := smartcontract.CreateCallWithAssertScript(Hash, "unblockAccount", account) return script } // SetWhitelistFeeContract creates and sends a transaction that adds a specified // contract method to the native Policy whitelist with the fixed execution fee. // It uses the `setWhitelistFeeContract` method which is active starting from // [config.HFFaun] hardfork. The returned values are transaction hash, its // ValidUntilBlock value and an error if any. func (c *Contract) SetWhitelistFeeContract(h util.Uint160, method string, argCnt int, fee int) (util.Uint256, uint32, error) { return c.actor.SendRun(setWhitelistFeeContractScript(h, method, argCnt, fee)) } // SetWhitelistFeeContractTransaction creates a transaction that adds a // specified contract method to the native Policy whitelist with the fixed // execution fee. It uses the `setWhitelistFeeContract` method which is active // starting from [config.HFFaun] hardfork. This transaction is signed but not // sent to the network, instead it's returned to the caller. func (c *Contract) SetWhitelistFeeContractTransaction(h util.Uint160, method string, argCnt int, fee int) (*transaction.Transaction, error) { return c.actor.MakeRun(setWhitelistFeeContractScript(h, method, argCnt, fee)) } // SetWhitelistFeeContractUnsigned creates a transaction that adds a specified // contract method to the native Policy whitelist with the fixed execution fee. // It uses the `setWhitelistFeeContract` method which is active starting from // [config.HFFaun] hardfork. This transaction is not signed and just returned to // the caller. func (c *Contract) SetWhitelistFeeContractUnsigned(h util.Uint160, method string, argCnt int, fee int) (*transaction.Transaction, error) { return c.actor.MakeUnsignedRun(setWhitelistFeeContractScript(h, method, argCnt, fee), nil) } func setWhitelistFeeContractScript(h util.Uint160, method string, argCnt int, fee int) []byte { // We know parameters exactly (unlike with nep17.Transfer), so this can't fail. script, _ := smartcontract.CreateCallScript(Hash, "setWhitelistFeeContract", h, method, argCnt, fee) return script } // RemoveWhitelistFeeContract creates and sends a transaction that removes a // specified contract method from the native Policy whitelist with the fixed // execution fee. It uses the `removeWhitelistFeeContract` method which is // active starting from [config.HFFaun] hardfork. The returned values are // transaction hash, its ValidUntilBlock value and an error if any. func (c *Contract) RemoveWhitelistFeeContract(h util.Uint160, method string, argCnt int) (util.Uint256, uint32, error) { return c.actor.SendRun(removeWhitelistFeeContractScript(h, method, argCnt)) } // RemoveWhitelistFeeContractTransaction creates a transaction that removes a // specified contract method from the native Policy whitelist with the fixed // execution fee. It uses the `removeWhitelistFeeContract` method which is // active starting from [config.HFFaun] hardfork. This transaction is signed but // not sent to the network, instead it's returned to the caller. func (c *Contract) RemoveWhitelistFeeContractTransaction(h util.Uint160, method string, argCnt int) (*transaction.Transaction, error) { return c.actor.MakeRun(removeWhitelistFeeContractScript(h, method, argCnt)) } // RemoveWhitelistFeeContractUnsigned creates a transaction that removes a // specified contract method from the native Policy whitelist with the fixed // execution fee. It uses the `removeWhitelistFeeContract` method which is // active starting from [config.HFFaun] hardfork. This transaction is not signed // and just returned to the caller. func (c *Contract) RemoveWhitelistFeeContractUnsigned(h util.Uint160, method string, argCnt int) (*transaction.Transaction, error) { return c.actor.MakeUnsignedRun(removeWhitelistFeeContractScript(h, method, argCnt), nil) } func removeWhitelistFeeContractScript(h util.Uint160, method string, argCnt int) []byte { // We know parameters exactly (unlike with nep17.Transfer), so this can't fail. script, _ := smartcontract.CreateCallScript(Hash, "removeWhitelistFeeContract", h, method, argCnt) return script } // GetWhitelistFeeContracts returns an iterator that allows to retrieve all // whitelisted contract methods from it. It depends on the server to provide // a proper session-based iterator but can also work with an expanded one. func (c *ContractReader) GetWhitelistFeeContracts() (*WhitelistedContractIterator, error) { sess, iter, err := unwrap.SessionIterator(c.invoker.Call(Hash, "getWhitelistFeeContracts")) if err != nil { return nil, err } return &WhitelistedContractIterator{ client: c.invoker, iterator: iter, session: sess, }, nil } // GetWhitelistFeeContractsExpanded is similar to GetWhitelistFeeContracts (uses // the same NEO method), but can be useful if the server used doesn't support // sessions and doesn't expand iterators. It creates a script that will get num // of result items from the iterator right in the VM and return them to you. // It's only limited by VM stack and GAS available for RPC invocations. func (c *ContractReader) GetWhitelistFeeContractsExpanded(num int) ([]WhitelistedContract, error) { arr, err := unwrap.Array(c.invoker.CallAndExpandIterator(Hash, "getWhitelistFeeContracts", num)) if err != nil { return nil, err } return itemsToWhitelistedContracts(arr) } // Next returns the next set of elements from the iterator (up to num of them). // It can return less than num elements in case an iterator doesn't have that // many or zero elements if the iterator has no more elements or the session is // expired. func (v *WhitelistedContractIterator) Next(num int) ([]WhitelistedContract, error) { items, err := v.client.TraverseIterator(v.session, &v.iterator, num) if err != nil { return nil, err } return itemsToWhitelistedContracts(items) } // Terminate closes the iterator session used by ValidatorIterator (if it's // session-based). func (v *WhitelistedContractIterator) Terminate() error { if v.iterator.ID == nil { return nil } return v.client.TerminateSession(v.session) } func itemsToWhitelistedContracts(arr []stackitem.Item) ([]WhitelistedContract, error) { res := make([]WhitelistedContract, len(arr)) for i, itm := range arr { str, err := itm.TryBytes() if err != nil { return nil, fmt.Errorf("item #%d is not a ByteString: %w", i, err) } h, err := util.Uint160DecodeBytesBE(str[:util.Uint160Size]) if err != nil { return nil, fmt.Errorf("item #%d: invalid hash: %w", i, err) } res[i].Hash = h res[i].Offset = binary.BigEndian.Uint32(str[util.Uint160Size:]) } return res, nil } // FromStackItem converts provided [stackitem.Array] to WhitelistFeeChangedEvent or returns an // error if it's not possible to do so. func (e *WhitelistFeeChangedEvent) FromStackItem(item *stackitem.Array) error { if item == nil { return errors.New("nil item") } arr, ok := item.Value().([]stackitem.Item) if !ok { return errors.New("not an array") } if len(arr) != 4 { return fmt.Errorf("wrong number of event parameters: %d", len(arr)) } h, err := arr[0].TryBytes() if err != nil { return fmt.Errorf("invalid hash: %w", err) } e.Hash, err = util.Uint160DecodeBytesBE(h) if err != nil { return fmt.Errorf("failed to unwrap hash: %w", err) } m, err := arr[1].TryBytes() if err != nil { return fmt.Errorf("invalid method: %w", err) } e.Method = string(m) argCnt, err := arr[2].TryInteger() if err != nil { return fmt.Errorf("invalid arg count: %w", err) } e.ArgCnt = int(argCnt.Int64()) if !arr[3].Equals(stackitem.Null{}) { fee, err := arr[3].TryInteger() if err != nil { return fmt.Errorf("invalid fee: %w", err) } v := fee.Int64() e.Fee = &v } return nil }