forked from TrueCloudLab/neoneo-go
Merge pull request #1965 from nspcc-dev/native/remove_nns
native: remove NNS contract
This commit is contained in:
commit
e0779f2d6e
37 changed files with 1318 additions and 2214 deletions
|
@ -34,8 +34,8 @@ func TestNEP11Import(t *testing.T) {
|
|||
walletPath := path.Join(tmpDir, "walletForImport.json")
|
||||
defer os.Remove(walletPath)
|
||||
|
||||
nnsContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.NameService)
|
||||
require.NoError(t, err)
|
||||
// deploy NFT NeoNameService contract
|
||||
nnsContractHash := deployNNSContract(t, e)
|
||||
neoContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.Neo)
|
||||
require.NoError(t, err)
|
||||
e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath)
|
||||
|
@ -325,3 +325,7 @@ func TestNEP11_OwnerOf_BalanceOf_Transfer(t *testing.T) {
|
|||
func deployNFTContract(t *testing.T, e *executor) util.Uint160 {
|
||||
return deployContract(t, e, "../examples/nft-nd/nft.go", "../examples/nft-nd/nft.yml", nftOwnerWallet, nftOwnerAddr, nftOwnerPass)
|
||||
}
|
||||
|
||||
func deployNNSContract(t *testing.T, e *executor) util.Uint160 {
|
||||
return deployContract(t, e, "../examples/nft-nd-nns/", "../examples/nft-nd-nns/nns.yml", validatorWallet, validatorAddr, "one")
|
||||
}
|
||||
|
|
|
@ -268,8 +268,7 @@ func TestNEP17ImportToken(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
gasContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.Gas)
|
||||
require.NoError(t, err)
|
||||
nnsContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.NameService)
|
||||
require.NoError(t, err)
|
||||
nnsContractHash := deployNNSContract(t, e)
|
||||
e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath)
|
||||
|
||||
// missing token hash
|
||||
|
|
|
@ -31,7 +31,6 @@ ProtocolConfiguration:
|
|||
PolicyContract: [0]
|
||||
RoleManagement: [0]
|
||||
OracleContract: [0]
|
||||
NameService: [0]
|
||||
|
||||
ApplicationConfiguration:
|
||||
# LogPath could be set up in case you need stdout logs to some proper file.
|
||||
|
|
|
@ -27,7 +27,6 @@ ProtocolConfiguration:
|
|||
PolicyContract: [0]
|
||||
RoleManagement: [0]
|
||||
OracleContract: [0]
|
||||
NameService: [0]
|
||||
|
||||
ApplicationConfiguration:
|
||||
# LogPath could be set up in case you need stdout logs to some proper file.
|
||||
|
|
|
@ -27,7 +27,6 @@ ProtocolConfiguration:
|
|||
PolicyContract: [0]
|
||||
RoleManagement: [0]
|
||||
OracleContract: [0]
|
||||
NameService: [0]
|
||||
|
||||
ApplicationConfiguration:
|
||||
# LogPath could be set up in case you need stdout logs to some proper file.
|
||||
|
|
|
@ -21,7 +21,6 @@ ProtocolConfiguration:
|
|||
PolicyContract: [0]
|
||||
RoleManagement: [0]
|
||||
OracleContract: [0]
|
||||
NameService: [0]
|
||||
|
||||
ApplicationConfiguration:
|
||||
# LogPath could be set up in case you need stdout logs to some proper file.
|
||||
|
|
|
@ -27,7 +27,6 @@ ProtocolConfiguration:
|
|||
PolicyContract: [0]
|
||||
RoleManagement: [0]
|
||||
OracleContract: [0]
|
||||
NameService: [0]
|
||||
|
||||
ApplicationConfiguration:
|
||||
# LogPath could be set up in case you need stdout logs to some proper file.
|
||||
|
|
|
@ -27,7 +27,6 @@ ProtocolConfiguration:
|
|||
PolicyContract: [0]
|
||||
RoleManagement: [0]
|
||||
OracleContract: [0]
|
||||
NameService: [0]
|
||||
|
||||
ApplicationConfiguration:
|
||||
# LogPath could be set up in case you need stdout logs to some proper file.
|
||||
|
|
|
@ -27,7 +27,6 @@ ProtocolConfiguration:
|
|||
PolicyContract: [0]
|
||||
RoleManagement: [0]
|
||||
OracleContract: [0]
|
||||
NameService: [0]
|
||||
|
||||
ApplicationConfiguration:
|
||||
# LogPath could be set up in case you need stdout logs to some proper file.
|
||||
|
|
|
@ -31,7 +31,6 @@ ProtocolConfiguration:
|
|||
PolicyContract: [0]
|
||||
RoleManagement: [0]
|
||||
OracleContract: [0]
|
||||
NameService: [0]
|
||||
|
||||
ApplicationConfiguration:
|
||||
# LogPath could be set up in case you need stdout logs to some proper file.
|
||||
|
|
|
@ -19,7 +19,6 @@ ProtocolConfiguration:
|
|||
PolicyContract: [0]
|
||||
RoleManagement: [0]
|
||||
OracleContract: [0]
|
||||
NameService: [0]
|
||||
Notary: [0]
|
||||
|
||||
ApplicationConfiguration:
|
||||
|
|
|
@ -28,7 +28,6 @@ ProtocolConfiguration:
|
|||
PolicyContract: [0]
|
||||
RoleManagement: [0]
|
||||
OracleContract: [0]
|
||||
NameService: [0]
|
||||
Notary: [0]
|
||||
|
||||
ApplicationConfiguration:
|
||||
|
|
|
@ -26,6 +26,7 @@ See the table below for the detailed examples description.
|
|||
| [events](events) | The contract shows how execution notifications with the different arguments types can be sent with the help of `runtime.Notify` function of the `runtime` interop package. Please, refer to the `runtime.Notify` [function documentation](../pkg/interop/runtime/runtime.go) for details. |
|
||||
| [iterator](iterator) | This example describes a way to work with NEO iterators. Please, refer to the `iterator` [package documentation](../pkg/interop/iterator/iterator.go) for details. |
|
||||
| [nft-nd](nft-nd) | NEP-11 non-divisible NFT. See NEP-11 token standard [specification](https://github.com/neo-project/proposals/pull/130) for details. |
|
||||
| [nft-nd-nns](nft-nd-nns) | Neo Name Service contract which is NEP-11 non-divisible NFT. The contract implements methods for Neo domain name system managing such as domains registration/transferring, records addition and names resolving. |
|
||||
| [oracle](oracle) | Oracle demo contract exposing two methods that you can use to process URLs. It uses oracle native contract, see [interop package documentation](../pkg/interop/native/oracle/oracle.go) also. |
|
||||
| [runtime](runtime) | This contract demonstrates how to use special `_initialize` and `_deploy` methods. See the [compiler documentation](../docs/compiler.md#vm-api-interop-layer ) for methods details. It also shows the pattern for checking owner witness inside the contract with the help of `runtime.CheckWitness` interop [function](../pkg/interop/runtime/runtime.go). |
|
||||
| [storage](storage) | The contract implements API for basic operations with a contract storage. It shows hos to use `storage` interop package. See the `storage` [package documentation](../pkg/interop/storage/storage.go). |
|
||||
|
|
31
examples/nft-nd-nns/namestate.go
Normal file
31
examples/nft-nd-nns/namestate.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package nns
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
)
|
||||
|
||||
// NameState represents domain name state.
|
||||
type NameState struct {
|
||||
Owner interop.Hash160
|
||||
Name string
|
||||
Expiration int
|
||||
Admin interop.Hash160
|
||||
}
|
||||
|
||||
// ensureNotExpired panics if domain name is expired.
|
||||
func (n NameState) ensureNotExpired() {
|
||||
if runtime.GetTime() >= n.Expiration {
|
||||
panic("name has expired")
|
||||
}
|
||||
}
|
||||
|
||||
// checkAdmin panics if script container is not signed by the domain name admin.
|
||||
func (n NameState) checkAdmin() {
|
||||
if runtime.CheckWitness(n.Owner) {
|
||||
return
|
||||
}
|
||||
if n.Admin == nil || !runtime.CheckWitness(n.Admin) {
|
||||
panic("not witnessed by admin")
|
||||
}
|
||||
}
|
650
examples/nft-nd-nns/nns.go
Normal file
650
examples/nft-nd-nns/nns.go
Normal file
|
@ -0,0 +1,650 @@
|
|||
/*
|
||||
Package nns contains non-divisible non-fungible NEP11-compatible token
|
||||
implementation. This token is a compatible analogue of C# Neo Name Service
|
||||
token and is aimed to serve as a domain name service for Neo smart-contracts,
|
||||
thus it's NeoNameService. This token can be minted with new domain name
|
||||
registration, the domain name itself is your NFT. Corresponding domain root
|
||||
must be added by the committee before new domain name can be registered.
|
||||
*/
|
||||
package nns
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/neo"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
||||
)
|
||||
|
||||
// Prefixes used for contract data storage.
|
||||
const (
|
||||
// prefixTotalSupply contains total supply of minted domains.
|
||||
prefixTotalSupply byte = 0x00
|
||||
// prefixBalance contains map from owner to his balance.
|
||||
prefixBalance byte = 0x01
|
||||
// prefixAccountToken contains map from (owner + token key) to token ID,
|
||||
// where token key = hash160(token ID) and token ID = domain name.
|
||||
prefixAccountToken byte = 0x02
|
||||
// prefixRegisterPrice contains price for new domain name registration.
|
||||
prefixRegisterPrice byte = 0x10
|
||||
// prefixRoot contains set of roots (map from root to 0).
|
||||
prefixRoot byte = 0x20
|
||||
// prefixName contains map from token key to token where token is domain
|
||||
// NameState structure.
|
||||
prefixName byte = 0x21
|
||||
// prefixRecord contains map from (token key + hash160(token name) + record type)
|
||||
// to record.
|
||||
prefixRecord byte = 0x22
|
||||
)
|
||||
|
||||
// Values constraints.
|
||||
const (
|
||||
// maxRegisterPrice is the maximum price of register method.
|
||||
maxRegisterPrice = 1_0000_0000_0000
|
||||
// maxRootLength is the maximum domain root length.
|
||||
maxRootLength = 16
|
||||
// maxDomainNameFragmentLength is the maximum length of the domain name fragment.
|
||||
maxDomainNameFragmentLength = 62
|
||||
// minDomainNameLength is minimum domain length.
|
||||
minDomainNameLength = 3
|
||||
// maxDomainNameLength is maximum domain length.
|
||||
maxDomainNameLength = 255
|
||||
// maxTXTRecordLength is the maximum length of the TXT domain record.
|
||||
maxTXTRecordLength = 255
|
||||
)
|
||||
|
||||
// Other constants.
|
||||
const (
|
||||
// defaultRegisterPrice is the default price for new domain registration.
|
||||
defaultRegisterPrice = 10_0000_0000
|
||||
// millisecondsInYear is amount of milliseconds per year.
|
||||
millisecondsInYear = 365 * 24 * 3600 * 1000
|
||||
)
|
||||
|
||||
// Update updates NameService contract.
|
||||
func Update(nef []byte, manifest string) {
|
||||
checkCommittee()
|
||||
management.Update(nef, []byte(manifest))
|
||||
}
|
||||
|
||||
// _deploy initializes defaults (total supply and registration price) on contract deploy.
|
||||
func _deploy(data interface{}, isUpdate bool) {
|
||||
if isUpdate {
|
||||
return
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
storage.Put(ctx, []byte{prefixTotalSupply}, 0)
|
||||
storage.Put(ctx, []byte{prefixRegisterPrice}, defaultRegisterPrice)
|
||||
}
|
||||
|
||||
// Symbol returns NeoNameService symbol.
|
||||
func Symbol() string {
|
||||
return "NNS"
|
||||
}
|
||||
|
||||
// Decimals returns NeoNameService decimals.
|
||||
func Decimals() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// TotalSupply returns overall number of domains minted by the NeoNameService contract.
|
||||
func TotalSupply() int {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return getTotalSupply(ctx)
|
||||
}
|
||||
|
||||
// OwnerOf returns owner of the specified domain.
|
||||
func OwnerOf(tokenID []byte) interop.Hash160 {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
ns := getNameState(ctx, tokenID)
|
||||
return ns.Owner
|
||||
}
|
||||
|
||||
// Properties returns domain name and expiration date of the specified domain.
|
||||
func Properties(tokenID []byte) map[string]interface{} {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
ns := getNameState(ctx, tokenID)
|
||||
return map[string]interface{}{
|
||||
"name": ns.Name,
|
||||
"expiration": ns.Expiration,
|
||||
}
|
||||
}
|
||||
|
||||
// BalanceOf returns overall number of domains owned by the specified owner.
|
||||
func BalanceOf(owner interop.Hash160) int {
|
||||
if !isValid(owner) {
|
||||
panic(`invalid owner`)
|
||||
}
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
balance := storage.Get(ctx, append([]byte{prefixBalance}, owner...))
|
||||
if balance == nil {
|
||||
return 0
|
||||
}
|
||||
return balance.(int)
|
||||
}
|
||||
|
||||
// Tokens returns iterator over a set of all registered domain names.
|
||||
func Tokens() iterator.Iterator {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return storage.Find(ctx, []byte{prefixName}, storage.ValuesOnly|storage.DeserializeValues|storage.PickField1)
|
||||
}
|
||||
|
||||
// TokensOf returns iterator over minted domains owned by the specified owner.
|
||||
func TokensOf(owner interop.Hash160) iterator.Iterator {
|
||||
if !isValid(owner) {
|
||||
panic(`invalid owner`)
|
||||
}
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return storage.Find(ctx, append([]byte{prefixAccountToken}, owner...), storage.ValuesOnly)
|
||||
}
|
||||
|
||||
// Transfer transfers domain with the specified name to new owner.
|
||||
func Transfer(to interop.Hash160, tokenID []byte, data interface{}) bool {
|
||||
if !isValid(to) {
|
||||
panic(`invalid receiver`)
|
||||
}
|
||||
var (
|
||||
tokenKey = getTokenKey(tokenID)
|
||||
ctx = storage.GetContext()
|
||||
)
|
||||
ns := getNameStateWithKey(ctx, tokenKey)
|
||||
from := ns.Owner
|
||||
if !runtime.CheckWitness(from) {
|
||||
return false
|
||||
}
|
||||
if !util.Equals(from, to) {
|
||||
// update token info
|
||||
ns.Owner = to
|
||||
ns.Admin = nil
|
||||
putNameStateWithKey(ctx, tokenKey, ns)
|
||||
|
||||
// update `from` balance
|
||||
updateBalance(ctx, tokenID, from, -1)
|
||||
|
||||
// update `to` balance
|
||||
updateBalance(ctx, tokenID, to, +1)
|
||||
}
|
||||
postTransfer(from, to, tokenID, data)
|
||||
return true
|
||||
}
|
||||
|
||||
// AddRoot registers new root.
|
||||
func AddRoot(root string) {
|
||||
checkCommittee()
|
||||
if !checkFragment(root, true) {
|
||||
panic("invalid root format")
|
||||
}
|
||||
var (
|
||||
ctx = storage.GetContext()
|
||||
rootKey = append([]byte{prefixRoot}, []byte(root)...)
|
||||
)
|
||||
if storage.Get(ctx, rootKey) != nil {
|
||||
panic("root already exists")
|
||||
}
|
||||
storage.Put(ctx, rootKey, 0)
|
||||
}
|
||||
|
||||
// Roots returns iterator over a set of NameService roots.
|
||||
func Roots() iterator.Iterator {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return storage.Find(ctx, []byte{prefixRoot}, storage.KeysOnly|storage.RemovePrefix)
|
||||
}
|
||||
|
||||
// SetPrice sets the domain registration price.
|
||||
func SetPrice(price int) {
|
||||
checkCommittee()
|
||||
if price < 0 || price > maxRegisterPrice {
|
||||
panic("The price is out of range.")
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
storage.Put(ctx, []byte{prefixRegisterPrice}, price)
|
||||
}
|
||||
|
||||
// GetPrice returns the domain registration price.
|
||||
func GetPrice() int {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return storage.Get(ctx, []byte{prefixRegisterPrice}).(int)
|
||||
}
|
||||
|
||||
// IsAvailable checks whether provided domain name is available.
|
||||
func IsAvailable(name string) bool {
|
||||
fragments := splitAndCheck(name, false)
|
||||
if fragments == nil {
|
||||
panic("invalid domain name format")
|
||||
}
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil {
|
||||
panic("root not found")
|
||||
}
|
||||
nsBytes := storage.Get(ctx, append([]byte{prefixName}, getTokenKey([]byte(name))...))
|
||||
if nsBytes == nil {
|
||||
return true
|
||||
}
|
||||
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
|
||||
return runtime.GetTime() >= ns.Expiration
|
||||
}
|
||||
|
||||
// Register registers new domain with the specified owner and name if it's available.
|
||||
func Register(name string, owner interop.Hash160) bool {
|
||||
fragments := splitAndCheck(name, false)
|
||||
if fragments == nil {
|
||||
panic("invalid domain name format")
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
if storage.Get(ctx, append([]byte{prefixRoot}, []byte(fragments[1])...)) == nil {
|
||||
panic("root not found")
|
||||
}
|
||||
|
||||
if !isValid(owner) {
|
||||
panic("invalid owner")
|
||||
}
|
||||
if !runtime.CheckWitness(owner) {
|
||||
panic("not witnessed by owner")
|
||||
}
|
||||
runtime.BurnGas(GetPrice())
|
||||
var (
|
||||
tokenKey = getTokenKey([]byte(name))
|
||||
oldOwner interop.Hash160
|
||||
)
|
||||
nsBytes := storage.Get(ctx, append([]byte{prefixName}, tokenKey...))
|
||||
if nsBytes != nil {
|
||||
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
|
||||
if runtime.GetTime() < ns.Expiration {
|
||||
return false
|
||||
}
|
||||
oldOwner = ns.Owner
|
||||
updateBalance(ctx, []byte(name), oldOwner, -1)
|
||||
} else {
|
||||
updateTotalSupply(ctx, +1)
|
||||
}
|
||||
ns := NameState{
|
||||
Owner: owner,
|
||||
Name: name,
|
||||
Expiration: runtime.GetTime() + millisecondsInYear,
|
||||
}
|
||||
putNameStateWithKey(ctx, tokenKey, ns)
|
||||
updateBalance(ctx, []byte(name), owner, +1)
|
||||
postTransfer(oldOwner, owner, []byte(name), nil)
|
||||
return true
|
||||
}
|
||||
|
||||
// Renew increases domain expiration date.
|
||||
func Renew(name string) int {
|
||||
if len(name) > maxDomainNameLength {
|
||||
panic("invalid domain name format")
|
||||
}
|
||||
runtime.BurnGas(GetPrice())
|
||||
ctx := storage.GetContext()
|
||||
ns := getNameState(ctx, []byte(name))
|
||||
ns.Expiration += millisecondsInYear
|
||||
putNameState(ctx, ns)
|
||||
return ns.Expiration
|
||||
}
|
||||
|
||||
// SetAdmin updates domain admin.
|
||||
func SetAdmin(name string, admin interop.Hash160) {
|
||||
if len(name) > maxDomainNameLength {
|
||||
panic("invalid domain name format")
|
||||
}
|
||||
if admin != nil && !runtime.CheckWitness(admin) {
|
||||
panic("not witnessed by admin")
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
ns := getNameState(ctx, []byte(name))
|
||||
if !runtime.CheckWitness(ns.Owner) {
|
||||
panic("not witnessed by owner")
|
||||
}
|
||||
ns.Admin = admin
|
||||
putNameState(ctx, ns)
|
||||
}
|
||||
|
||||
// SetRecord adds new record of the specified type to the provided domain.
|
||||
func SetRecord(name string, typ RecordType, data string) {
|
||||
tokenID := []byte(tokenIDFromName(name))
|
||||
var ok bool
|
||||
switch typ {
|
||||
case A:
|
||||
ok = checkIPv4(data)
|
||||
case CNAME:
|
||||
ok = splitAndCheck(data, true) != nil
|
||||
case TXT:
|
||||
ok = len(data) <= maxTXTRecordLength
|
||||
case AAAA:
|
||||
ok = checkIPv6(data)
|
||||
default:
|
||||
panic("unsupported record type")
|
||||
}
|
||||
if !ok {
|
||||
panic("invalid record data")
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
ns := getNameState(ctx, tokenID)
|
||||
ns.checkAdmin()
|
||||
putRecord(ctx, tokenID, name, typ, data)
|
||||
}
|
||||
|
||||
// GetRecord returns domain record of the specified type if it exists or an empty
|
||||
// string if not.
|
||||
func GetRecord(name string, typ RecordType) string {
|
||||
tokenID := []byte(tokenIDFromName(name))
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
_ = getNameState(ctx, tokenID) // ensure not expired
|
||||
return getRecord(ctx, tokenID, name, typ)
|
||||
}
|
||||
|
||||
// DeleteRecord removes domain record with the specified type.
|
||||
func DeleteRecord(name string, typ RecordType) {
|
||||
tokenID := []byte(tokenIDFromName(name))
|
||||
ctx := storage.GetContext()
|
||||
ns := getNameState(ctx, tokenID)
|
||||
ns.checkAdmin()
|
||||
recordKey := getRecordKey(tokenID, name, typ)
|
||||
storage.Delete(ctx, recordKey)
|
||||
}
|
||||
|
||||
// Resolve resolves given name (not more then three redirects are allowed).
|
||||
func Resolve(name string, typ RecordType) string {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
return resolve(ctx, name, typ, 2)
|
||||
}
|
||||
|
||||
// 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...)
|
||||
var balance int
|
||||
if b := storage.Get(ctx, balanceKey); b != nil {
|
||||
balance = b.(int)
|
||||
}
|
||||
balance += diff
|
||||
if balance == 0 {
|
||||
storage.Delete(ctx, balanceKey)
|
||||
} else {
|
||||
storage.Put(ctx, balanceKey, balance)
|
||||
}
|
||||
|
||||
tokenKey := getTokenKey(tokenId)
|
||||
accountTokenKey := append(append([]byte{prefixAccountToken}, acc...), tokenKey...)
|
||||
if diff < 0 {
|
||||
storage.Delete(ctx, accountTokenKey)
|
||||
} else {
|
||||
storage.Put(ctx, accountTokenKey, tokenId)
|
||||
}
|
||||
}
|
||||
|
||||
// postTransfer sends Transfer notification to the network and calls onNEP11Payment
|
||||
// method.
|
||||
func postTransfer(from, to interop.Hash160, tokenID []byte, data interface{}) {
|
||||
runtime.Notify("Transfer", from, to, 1, tokenID)
|
||||
if management.GetContract(to) != nil {
|
||||
contract.Call(to, "onNEP11Payment", contract.All, from, 1, tokenID, data)
|
||||
}
|
||||
}
|
||||
|
||||
// getTotalSupply returns total supply from storage.
|
||||
func getTotalSupply(ctx storage.Context) int {
|
||||
val := storage.Get(ctx, []byte{prefixTotalSupply})
|
||||
return val.(int)
|
||||
}
|
||||
|
||||
// updateTotalSupply adds specified diff to the total supply.
|
||||
func updateTotalSupply(ctx storage.Context, diff int) {
|
||||
tsKey := []byte{prefixTotalSupply}
|
||||
ts := getTotalSupply(ctx)
|
||||
storage.Put(ctx, tsKey, ts+diff)
|
||||
}
|
||||
|
||||
// getTokenKey computes hash160 from the given tokenID.
|
||||
func getTokenKey(tokenID []byte) []byte {
|
||||
return crypto.Ripemd160(tokenID)
|
||||
}
|
||||
|
||||
// getNameState returns domain name state by the specified tokenID.
|
||||
func getNameState(ctx storage.Context, tokenID []byte) NameState {
|
||||
tokenKey := getTokenKey(tokenID)
|
||||
return getNameStateWithKey(ctx, tokenKey)
|
||||
}
|
||||
|
||||
// getNameStateWithKey returns domain name state by the specified token key.
|
||||
func getNameStateWithKey(ctx storage.Context, tokenKey []byte) NameState {
|
||||
nameKey := append([]byte{prefixName}, tokenKey...)
|
||||
nsBytes := storage.Get(ctx, nameKey)
|
||||
if nsBytes == nil {
|
||||
panic("token not found")
|
||||
}
|
||||
ns := std.Deserialize(nsBytes.([]byte)).(NameState)
|
||||
ns.ensureNotExpired()
|
||||
return ns
|
||||
}
|
||||
|
||||
// putNameState stores domain name state.
|
||||
func putNameState(ctx storage.Context, ns NameState) {
|
||||
tokenKey := getTokenKey([]byte(ns.Name))
|
||||
putNameStateWithKey(ctx, tokenKey, ns)
|
||||
}
|
||||
|
||||
// putNameStateWithKey stores domain name state with the specified token key.
|
||||
func putNameStateWithKey(ctx storage.Context, tokenKey []byte, ns NameState) {
|
||||
nameKey := append([]byte{prefixName}, tokenKey...)
|
||||
nsBytes := std.Serialize(ns)
|
||||
storage.Put(ctx, nameKey, nsBytes)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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))...)
|
||||
return append(recordKey, []byte{byte(typ)}...)
|
||||
}
|
||||
|
||||
// isValid returns true if the provided address is a valid Uint160.
|
||||
func isValid(address interop.Hash160) bool {
|
||||
return address != nil && len(address) == 20
|
||||
}
|
||||
|
||||
// checkCommittee panics if the script container is not signed by the committee.
|
||||
func checkCommittee() {
|
||||
committee := neo.GetCommittee()
|
||||
if committee == nil {
|
||||
panic("failed to get committee")
|
||||
}
|
||||
l := len(committee)
|
||||
committeeMultisig := contract.CreateMultisigAccount(l-(l-1)/2, committee)
|
||||
if committeeMultisig == nil || !runtime.CheckWitness(committeeMultisig) {
|
||||
panic("not witnessed by committee")
|
||||
}
|
||||
}
|
||||
|
||||
// checkFragment validates root or a part of domain name.
|
||||
func checkFragment(v string, isRoot bool) bool {
|
||||
maxLength := maxDomainNameFragmentLength
|
||||
if isRoot {
|
||||
maxLength = maxRootLength
|
||||
}
|
||||
if len(v) == 0 || len(v) > maxLength {
|
||||
return false
|
||||
}
|
||||
c := v[0]
|
||||
if isRoot {
|
||||
if !(c >= 'a' && c <= 'z') {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if !isAlNum(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for i := 1; i < len(v); i++ {
|
||||
if !isAlNum(v[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isAlNum checks whether provided char is a lowercase letter or a number.
|
||||
func isAlNum(c uint8) bool {
|
||||
return c >= 'a' && c <= 'z' || c >= '0' && c <= '9'
|
||||
}
|
||||
|
||||
// splitAndCheck splits domain name into parts and validates it.
|
||||
func splitAndCheck(name string, allowMultipleFragments bool) []string {
|
||||
l := len(name)
|
||||
if l < minDomainNameLength || maxDomainNameLength < l {
|
||||
return nil
|
||||
}
|
||||
fragments := std.StringSplit(name, ".")
|
||||
l = len(fragments)
|
||||
if l < 2 {
|
||||
return nil
|
||||
}
|
||||
if l > 2 && !allowMultipleFragments {
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < l; i++ {
|
||||
if !checkFragment(fragments[i], i == l-1) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fragments
|
||||
}
|
||||
|
||||
// checkIPv4 checks record on IPv4 compliance.
|
||||
func checkIPv4(data string) bool {
|
||||
l := len(data)
|
||||
if l < 7 || 15 < l {
|
||||
return false
|
||||
}
|
||||
fragments := std.StringSplit(data, ".")
|
||||
if len(fragments) != 4 {
|
||||
return false
|
||||
}
|
||||
numbers := make([]int, 4)
|
||||
for i, f := range fragments {
|
||||
if len(f) == 0 {
|
||||
return false
|
||||
}
|
||||
number := std.Atoi10(f)
|
||||
if number < 0 || 255 < number {
|
||||
panic("not a byte")
|
||||
}
|
||||
if number > 0 && f[0] == '0' {
|
||||
return false
|
||||
}
|
||||
if number == 0 && len(f) > 1 {
|
||||
return false
|
||||
}
|
||||
numbers[i] = number
|
||||
}
|
||||
n0 := numbers[0]
|
||||
n1 := numbers[1]
|
||||
n3 := numbers[3]
|
||||
if n0 == 0 ||
|
||||
n0 == 10 ||
|
||||
n0 == 127 ||
|
||||
n0 >= 224 ||
|
||||
(n0 == 169 && n1 == 254) ||
|
||||
(n0 == 172 && 16 <= n1 && n1 <= 31) ||
|
||||
(n0 == 192 && n1 == 168) ||
|
||||
n3 == 0 ||
|
||||
n3 == 255 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// checkIPv6 checks record on IPv6 compliance.
|
||||
func checkIPv6(data string) bool {
|
||||
l := len(data)
|
||||
if l < 2 || 39 < l {
|
||||
return false
|
||||
}
|
||||
fragments := std.StringSplit(data, ":")
|
||||
l = len(fragments)
|
||||
if l < 3 || 8 < l {
|
||||
return false
|
||||
}
|
||||
if fragments[0] == "2001" { // example addresses prefix
|
||||
return false
|
||||
}
|
||||
var hasEmpty bool
|
||||
for i := 1; i < l; i++ {
|
||||
f := fragments[i]
|
||||
fLen := len(f)
|
||||
if fLen == 0 {
|
||||
if i < l-1 && hasEmpty {
|
||||
return false
|
||||
}
|
||||
hasEmpty = true
|
||||
} else {
|
||||
if fLen > 4 {
|
||||
return false
|
||||
}
|
||||
_ = std.Atoi(f, 16) // check it won't panic
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// tokenIDFromName returns token ID (domain.root) from provided name.
|
||||
func tokenIDFromName(name string) string {
|
||||
fragments := splitAndCheck(name, true)
|
||||
if fragments == nil {
|
||||
panic("invalid domain name format")
|
||||
}
|
||||
l := len(fragments)
|
||||
return name[len(name)-(len(fragments[l-1])+len(fragments[l-2])+1):]
|
||||
}
|
||||
|
||||
// resolve resolves provided name using record with the specified type and given
|
||||
// maximum redirections constraint.
|
||||
func resolve(ctx storage.Context, name string, typ RecordType, redirect int) string {
|
||||
if redirect < 0 {
|
||||
panic("invalid redirect")
|
||||
}
|
||||
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]
|
||||
if rTyp == byte(typ) {
|
||||
return value
|
||||
}
|
||||
if rTyp == byte(CNAME) {
|
||||
cname = value
|
||||
}
|
||||
}
|
||||
if cname == "" {
|
||||
return string([]byte(nil))
|
||||
}
|
||||
return resolve(ctx, cname, typ, redirect-1)
|
||||
}
|
||||
|
||||
// getRecords returns iterator over the set of records corresponded with the
|
||||
// specified name.
|
||||
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)
|
||||
}
|
15
examples/nft-nd-nns/nns.yml
Normal file
15
examples/nft-nd-nns/nns.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
name: "NameService"
|
||||
supportedstandards: ["NEP-11"]
|
||||
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf",
|
||||
"tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord", "resolve"]
|
||||
events:
|
||||
- name: Transfer
|
||||
parameters:
|
||||
- name: from
|
||||
type: Hash160
|
||||
- name: to
|
||||
type: Hash160
|
||||
- name: amount
|
||||
type: Integer
|
||||
- name: tokenId
|
||||
type: ByteArray
|
20
examples/nft-nd-nns/recordtype.go
Normal file
20
examples/nft-nd-nns/recordtype.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package nns
|
||||
|
||||
// RecordType is domain name service record types.
|
||||
type RecordType byte
|
||||
|
||||
// Record types defined in [RFC 1035](https://tools.ietf.org/html/rfc1035)
|
||||
const (
|
||||
// A represents address record type.
|
||||
A RecordType = 1
|
||||
// CNAME represents canonical name record type.
|
||||
CNAME RecordType = 5
|
||||
// TXT represents text record type.
|
||||
TXT RecordType = 16
|
||||
)
|
||||
|
||||
// Record types defined in [RFC 3596](https://tools.ietf.org/html/rfc3596)
|
||||
const (
|
||||
// AAAA represents IPv6 address record type.
|
||||
AAAA RecordType = 28
|
||||
)
|
|
@ -9,14 +9,12 @@ import (
|
|||
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/gas"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/ledger"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/nameservice"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/neo"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/notary"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/native/oracle"
|
||||
|
@ -38,7 +36,6 @@ func TestContractHashes(t *testing.T) {
|
|||
require.Equalf(t, []byte(oracle.Hash), cs.Oracle.Hash.BytesBE(), "%q", string(cs.Oracle.Hash.BytesBE()))
|
||||
require.Equalf(t, []byte(roles.Hash), cs.Designate.Hash.BytesBE(), "%q", string(cs.Designate.Hash.BytesBE()))
|
||||
require.Equalf(t, []byte(policy.Hash), cs.Policy.Hash.BytesBE(), "%q", string(cs.Policy.Hash.BytesBE()))
|
||||
require.Equalf(t, []byte(nameservice.Hash), cs.NameService.Hash.BytesBE(), "%q", string(cs.NameService.Hash.BytesBE()))
|
||||
require.Equalf(t, []byte(ledger.Hash), cs.Ledger.Hash.BytesBE(), "%q", string(cs.Ledger.Hash.BytesBE()))
|
||||
require.Equalf(t, []byte(management.Hash), cs.Management.Hash.BytesBE(), "%q", string(cs.Management.Hash.BytesBE()))
|
||||
require.Equalf(t, []byte(notary.Hash), cs.Notary.Hash.BytesBE(), "%q", string(cs.Notary.Hash.BytesBE()))
|
||||
|
@ -69,13 +66,6 @@ func TestRoleManagementRole(t *testing.T) {
|
|||
require.EqualValues(t, noderoles.P2PNotary, roles.P2PNotary)
|
||||
}
|
||||
|
||||
func TestNameServiceRecordType(t *testing.T) {
|
||||
require.EqualValues(t, nnsrecords.A, nameservice.TypeA)
|
||||
require.EqualValues(t, nnsrecords.CNAME, nameservice.TypeCNAME)
|
||||
require.EqualValues(t, nnsrecords.TXT, nameservice.TypeTXT)
|
||||
require.EqualValues(t, nnsrecords.AAAA, nameservice.TypeAAAA)
|
||||
}
|
||||
|
||||
func TestCryptoLibNamedCurve(t *testing.T) {
|
||||
require.EqualValues(t, native.Secp256k1, crypto.Secp256k1)
|
||||
require.EqualValues(t, native.Secp256r1, crypto.Secp256r1)
|
||||
|
@ -150,31 +140,6 @@ func TestNativeHelpersCompile(t *testing.T) {
|
|||
{"setStoragePrice", []string{"42"}},
|
||||
{"unblockAccount", []string{u160}},
|
||||
})
|
||||
runNativeTestCases(t, cs.NameService.ContractMD, "nameservice", []nativeTestCase{
|
||||
// nonfungible
|
||||
{"symbol", nil},
|
||||
{"decimals", nil},
|
||||
{"totalSupply", nil},
|
||||
{"ownerOf", []string{`"neo.com"`}},
|
||||
{"balanceOf", []string{u160}},
|
||||
{"properties", []string{`"neo.com"`}},
|
||||
{"tokens", nil},
|
||||
{"tokensOf", []string{u160}},
|
||||
{"transfer", []string{u160, `"neo.com"`, "nil"}},
|
||||
|
||||
// name service
|
||||
{"addRoot", []string{`"com"`}},
|
||||
{"deleteRecord", []string{`"neo.com"`, "nameservice.TypeA"}},
|
||||
{"isAvailable", []string{`"neo.com"`}},
|
||||
{"getPrice", nil},
|
||||
{"getRecord", []string{`"neo.com"`, "nameservice.TypeA"}},
|
||||
{"register", []string{`"neo.com"`, u160}},
|
||||
{"renew", []string{`"neo.com"`}},
|
||||
{"resolve", []string{`"neo.com"`, "nameservice.TypeA"}},
|
||||
{"setPrice", []string{"42"}},
|
||||
{"setAdmin", []string{`"neo.com"`, u160}},
|
||||
{"setRecord", []string{`"neo.com"`, "nameservice.TypeA", `"1.1.1.1"`}},
|
||||
})
|
||||
runNativeTestCases(t, cs.Ledger.ContractMD, "ledger", []nativeTestCase{
|
||||
{"currentHash", nil},
|
||||
{"currentIndex", nil},
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
|
||||
"github.com/nspcc-dev/neo-go/internal/testchain"
|
||||
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||
|
@ -22,7 +23,6 @@ import (
|
|||
"github.com/nspcc-dev/neo-go/pkg/core/chaindump"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/fee"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
||||
|
@ -47,6 +47,31 @@ import (
|
|||
// multisig address which possess all NEO.
|
||||
var neoOwner = testchain.MultisigScriptHash()
|
||||
|
||||
// examplesPrefix is a prefix of the example smart-contracts.
|
||||
const examplesPrefix = "../../examples/"
|
||||
|
||||
// newTestChainWithNS should be called before newBlock invocation to properly setup
|
||||
// global state.
|
||||
func newTestChainWithNS(t *testing.T) (*Blockchain, util.Uint160) {
|
||||
bc := newTestChainWithCustomCfg(t, nil)
|
||||
acc := newAccountWithGAS(t, bc)
|
||||
// Push NameService contract into the chain.
|
||||
nsPath := examplesPrefix + "nft-nd-nns/"
|
||||
nsConfigPath := nsPath + "nns.yml"
|
||||
txDeploy4, _ := newDeployTx(t, bc, acc.PrivateKey().GetScriptHash(), nsPath, nsPath, &nsConfigPath)
|
||||
txDeploy4.Nonce = 123
|
||||
txDeploy4.ValidUntilBlock = bc.BlockHeight() + 1
|
||||
require.NoError(t, addNetworkFee(bc, txDeploy4, acc))
|
||||
require.NoError(t, acc.SignTx(testchain.Network(), txDeploy4))
|
||||
b := bc.newBlock(txDeploy4)
|
||||
require.NoError(t, bc.AddBlock(b))
|
||||
checkTxHalt(t, bc, txDeploy4.Hash())
|
||||
|
||||
h, err := bc.GetContractScriptHash(1)
|
||||
require.NoError(t, err)
|
||||
return bc, h
|
||||
}
|
||||
|
||||
// newTestChain should be called before newBlock invocation to properly setup
|
||||
// global state.
|
||||
func newTestChain(t *testing.T) *Blockchain {
|
||||
|
@ -435,21 +460,36 @@ func initBasicChain(t *testing.T, bc *Blockchain) {
|
|||
require.NoError(t, addNetworkFee(bc, txDeploy3, acc0))
|
||||
require.NoError(t, acc0.SignTx(testchain.Network(), txDeploy3))
|
||||
b = bc.newBlock(txDeploy3)
|
||||
require.NoError(t, bc.AddBlock(b))
|
||||
require.NoError(t, bc.AddBlock(b)) // block #10
|
||||
checkTxHalt(t, bc, txDeploy3.Hash())
|
||||
|
||||
// register `neo.com` with A record type and priv0 owner via NNS
|
||||
transferFundsToCommittee(t, bc) // block #11
|
||||
// Push NameService contract into the chain.
|
||||
nsPath := examplesPrefix + "nft-nd-nns/"
|
||||
nsConfigPath := nsPath + "nns.yml"
|
||||
txDeploy4, _ := newDeployTx(t, bc, priv0ScriptHash, nsPath, nsPath, &nsConfigPath)
|
||||
txDeploy4.Nonce = getNextNonce()
|
||||
txDeploy4.ValidUntilBlock = validUntilBlock
|
||||
require.NoError(t, addNetworkFee(bc, txDeploy4, acc0))
|
||||
require.NoError(t, acc0.SignTx(testchain.Network(), txDeploy4))
|
||||
b = bc.newBlock(txDeploy4)
|
||||
require.NoError(t, bc.AddBlock(b)) // block #11
|
||||
checkTxHalt(t, bc, txDeploy4.Hash())
|
||||
nsHash, err := bc.GetContractScriptHash(4)
|
||||
require.NoError(t, err)
|
||||
t.Logf("contract (%s): \n\tHash: %s\n", nsPath, nsHash.StringLE())
|
||||
|
||||
// register `neo.com` with A record type and priv0 owner via NS
|
||||
transferFundsToCommittee(t, bc) // block #12
|
||||
res, err := invokeContractMethodGeneric(bc, defaultNameServiceSysfee,
|
||||
bc.contracts.NameService.Hash, "addRoot", true, "com") // block #12
|
||||
nsHash, "addRoot", true, "com") // block #13
|
||||
require.NoError(t, err)
|
||||
checkResult(t, res, stackitem.Null{})
|
||||
res, err = invokeContractMethodGeneric(bc, native.DefaultDomainPrice+defaultNameServiceSysfee,
|
||||
bc.contracts.NameService.Hash, "register", acc0, "neo.com", priv0ScriptHash) // block #13
|
||||
res, err = invokeContractMethodGeneric(bc, defaultNameServiceDomainPrice+defaultNameServiceSysfee+1_0000_000,
|
||||
nsHash, "register", acc0, "neo.com", priv0ScriptHash) // block #14
|
||||
require.NoError(t, err)
|
||||
checkResult(t, res, stackitem.NewBool(true))
|
||||
res, err = invokeContractMethodGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash,
|
||||
"setRecord", acc0, "neo.com", int64(nnsrecords.A), "1.2.3.4") // block #14
|
||||
res, err = invokeContractMethodGeneric(bc, defaultNameServiceSysfee, nsHash,
|
||||
"setRecord", acc0, "neo.com", int64(nns.A), "1.2.3.4") // block #15
|
||||
require.NoError(t, err)
|
||||
checkResult(t, res, stackitem.Null{})
|
||||
|
||||
|
@ -471,8 +511,16 @@ func newNEP17Transfer(sc, from, to util.Uint160, amount int64, additionalArgs ..
|
|||
|
||||
func newDeployTx(t *testing.T, bc *Blockchain, sender util.Uint160, name, ctrName string, cfgName *string) (*transaction.Transaction, util.Uint160) {
|
||||
c, err := ioutil.ReadFile(name)
|
||||
require.NoError(t, err)
|
||||
tx, h, avm, err := testchain.NewDeployTx(bc, ctrName, sender, bytes.NewReader(c), cfgName)
|
||||
var (
|
||||
tx *transaction.Transaction
|
||||
h util.Uint160
|
||||
avm []byte
|
||||
)
|
||||
if err == nil {
|
||||
tx, h, avm, err = testchain.NewDeployTx(bc, ctrName, sender, bytes.NewReader(c), cfgName)
|
||||
} else {
|
||||
tx, h, avm, err = testchain.NewDeployTx(bc, ctrName, sender, nil, cfgName)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
t.Logf("contract (%s): \n\tHash: %s\n\tAVM: %s", name, h.StringLE(), base64.StdEncoding.EncodeToString(avm))
|
||||
return tx, h
|
||||
|
|
|
@ -32,7 +32,7 @@ func LoadToken(ic *interop.Context) func(id int32) error {
|
|||
}
|
||||
cs, err := ic.GetContract(tok.Hash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("contract not found: %w", err)
|
||||
return fmt.Errorf("token contract %s not found: %w", tok.Hash.StringLE(), err)
|
||||
}
|
||||
return callInternal(ic, cs, tok.Method, tok.CallFlag, tok.HasReturn, args)
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ func Call(ic *interop.Context) error {
|
|||
}
|
||||
cs, err := ic.GetContract(u)
|
||||
if err != nil {
|
||||
return fmt.Errorf("contract not found: %w", err)
|
||||
return fmt.Errorf("called contract %s not found: %w", u.StringLE(), err)
|
||||
}
|
||||
if strings.HasPrefix(method, "_") {
|
||||
return errors.New("invalid method name (starts with '_')")
|
||||
|
|
|
@ -15,18 +15,17 @@ const reservedContractID = -100
|
|||
|
||||
// Contracts is a set of registered native contracts.
|
||||
type Contracts struct {
|
||||
Management *Management
|
||||
Ledger *Ledger
|
||||
NEO *NEO
|
||||
GAS *GAS
|
||||
Policy *Policy
|
||||
Oracle *Oracle
|
||||
Designate *Designate
|
||||
NameService *NameService
|
||||
Notary *Notary
|
||||
Crypto *Crypto
|
||||
Std *Std
|
||||
Contracts []interop.Contract
|
||||
Management *Management
|
||||
Ledger *Ledger
|
||||
NEO *NEO
|
||||
GAS *GAS
|
||||
Policy *Policy
|
||||
Oracle *Oracle
|
||||
Designate *Designate
|
||||
Notary *Notary
|
||||
Crypto *Crypto
|
||||
Std *Std
|
||||
Contracts []interop.Contract
|
||||
// persistScript is vm script which executes "onPersist" method of every native contract.
|
||||
persistScript []byte
|
||||
// postPersistScript is vm script which executes "postPersist" method of every native contract.
|
||||
|
@ -103,11 +102,6 @@ func NewContracts(p2pSigExtensionsEnabled bool, nativeUpdateHistories map[string
|
|||
cs.Oracle = oracle
|
||||
cs.Contracts = append(cs.Contracts, oracle)
|
||||
|
||||
ns := newNameService()
|
||||
ns.NEO = neo
|
||||
cs.NameService = ns
|
||||
cs.Contracts = append(cs.Contracts, ns)
|
||||
|
||||
if p2pSigExtensionsEnabled {
|
||||
notary := newNotary()
|
||||
notary.GAS = gas
|
||||
|
|
|
@ -1,723 +0,0 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
"math/big"
|
||||
"net"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/dao"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
)
|
||||
|
||||
// NameService represents native NameService contract.
|
||||
type NameService struct {
|
||||
nonfungible
|
||||
NEO *NEO
|
||||
}
|
||||
|
||||
type nameState struct {
|
||||
state.NFTTokenState
|
||||
// Expiration is token expiration height.
|
||||
Expiration uint32
|
||||
// HasAdmin is true if token has admin.
|
||||
HasAdmin bool
|
||||
// Admin is token admin.
|
||||
Admin util.Uint160
|
||||
}
|
||||
|
||||
const (
|
||||
nameServiceID = -10
|
||||
|
||||
prefixRoots = 10
|
||||
prefixDomainPrice = 22
|
||||
prefixExpiration = 20
|
||||
prefixRecord = 12
|
||||
|
||||
secondsInYear = 365 * 24 * 3600
|
||||
|
||||
// DefaultDomainPrice is the default price of register method.
|
||||
DefaultDomainPrice = 10_00000000
|
||||
// MinDomainNameLength is minimum domain length.
|
||||
MinDomainNameLength = 3
|
||||
// MaxDomainNameLength is maximum domain length.
|
||||
MaxDomainNameLength = 255
|
||||
)
|
||||
|
||||
var (
|
||||
// Lookahead is not supported by Go, but it is simple `(?=.{3,255}$)`,
|
||||
// so we check name length explicitly.
|
||||
nameRegex = regexp.MustCompile(`^([a-z0-9]{1,62}\.)+[a-z][a-z0-9]{0,15}$`)
|
||||
ipv4Regex = regexp.MustCompile(`^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])$`)
|
||||
ipv6Regex = regexp.MustCompile("(?:^)(([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,7}:|([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:((:[0-9a-f]{1,4}){1,6})|:((:[0-9a-f]{1,4}){1,7}|:))$")
|
||||
rootRegex = regexp.MustCompile("^[a-z][a-z0-9]{0,15}$")
|
||||
)
|
||||
|
||||
// matchName checks if provided name is valid.
|
||||
func matchName(name string) bool {
|
||||
ln := len(name)
|
||||
return MinDomainNameLength <= ln && ln <= MaxDomainNameLength &&
|
||||
nameRegex.Match([]byte(name))
|
||||
}
|
||||
|
||||
func newNameService() *NameService {
|
||||
nf := newNonFungible(nativenames.NameService, nameServiceID, "NNS", 0)
|
||||
nf.getTokenKey = func(tokenID []byte) []byte {
|
||||
return append([]byte{prefixNFTToken}, hash.Hash160(tokenID).BytesBE()...)
|
||||
}
|
||||
nf.newTokenState = func() nftTokenState {
|
||||
return new(nameState)
|
||||
}
|
||||
nf.onTransferred = func(tok nftTokenState) {
|
||||
tok.(*nameState).HasAdmin = false
|
||||
}
|
||||
|
||||
n := &NameService{nonfungible: *nf}
|
||||
defer n.UpdateHash()
|
||||
|
||||
desc := newDescriptor("addRoot", smartcontract.VoidType,
|
||||
manifest.NewParameter("root", smartcontract.StringType))
|
||||
md := newMethodAndPrice(n.addRoot, 1<<15, callflag.States)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("setPrice", smartcontract.VoidType,
|
||||
manifest.NewParameter("price", smartcontract.IntegerType))
|
||||
md = newMethodAndPrice(n.setPrice, 1<<15, callflag.States)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("getPrice", smartcontract.IntegerType)
|
||||
md = newMethodAndPrice(n.getPrice, 1<<15, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("isAvailable", smartcontract.BoolType,
|
||||
manifest.NewParameter("name", smartcontract.StringType))
|
||||
md = newMethodAndPrice(n.isAvailable, 1<<15, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("register", smartcontract.BoolType,
|
||||
manifest.NewParameter("name", smartcontract.StringType),
|
||||
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
||||
md = newMethodAndPrice(n.register, 1<<15, callflag.States)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("renew", smartcontract.IntegerType,
|
||||
manifest.NewParameter("name", smartcontract.StringType))
|
||||
md = newMethodAndPrice(n.renew, 0, callflag.States)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("setAdmin", smartcontract.VoidType,
|
||||
manifest.NewParameter("name", smartcontract.StringType),
|
||||
manifest.NewParameter("admin", smartcontract.Hash160Type))
|
||||
md = newMethodAndPrice(n.setAdmin, 1<<15, callflag.States)
|
||||
md.StorageFee = 20
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("setRecord", smartcontract.VoidType,
|
||||
manifest.NewParameter("name", smartcontract.StringType),
|
||||
manifest.NewParameter("type", smartcontract.IntegerType),
|
||||
manifest.NewParameter("data", smartcontract.StringType))
|
||||
md = newMethodAndPrice(n.setRecord, 1<<15, callflag.States)
|
||||
md.StorageFee = 200
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("getRecord", smartcontract.StringType,
|
||||
manifest.NewParameter("name", smartcontract.StringType),
|
||||
manifest.NewParameter("type", smartcontract.IntegerType))
|
||||
md = newMethodAndPrice(n.getRecord, 1<<15, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("deleteRecord", smartcontract.VoidType,
|
||||
manifest.NewParameter("name", smartcontract.StringType),
|
||||
manifest.NewParameter("type", smartcontract.IntegerType))
|
||||
md = newMethodAndPrice(n.deleteRecord, 1<<15, callflag.States)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("resolve", smartcontract.StringType,
|
||||
manifest.NewParameter("name", smartcontract.StringType),
|
||||
manifest.NewParameter("type", smartcontract.IntegerType))
|
||||
md = newMethodAndPrice(n.resolve, 1<<17, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// Metadata implements interop.Contract interface.
|
||||
func (n *NameService) Metadata() *interop.ContractMD {
|
||||
return &n.ContractMD
|
||||
}
|
||||
|
||||
// Initialize implements interop.Contract interface.
|
||||
func (n *NameService) Initialize(ic *interop.Context) error {
|
||||
if err := n.nonfungible.Initialize(ic); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setIntWithKey(n.ID, ic.DAO, []byte{prefixDomainPrice}, DefaultDomainPrice); err != nil {
|
||||
return err
|
||||
}
|
||||
roots := stringList{}
|
||||
return putSerializableToDAO(n.ID, ic.DAO, []byte{prefixRoots}, &roots)
|
||||
}
|
||||
|
||||
// OnPersist implements interop.Contract interface.
|
||||
func (n *NameService) OnPersist(ic *interop.Context) error {
|
||||
now := uint32(ic.Block.Timestamp/1000 + 1)
|
||||
keys := []string{}
|
||||
ic.DAO.Seek(n.ID, []byte{prefixExpiration}, func(k, v []byte) {
|
||||
if binary.BigEndian.Uint32(k) >= now {
|
||||
return
|
||||
}
|
||||
// Removal is done separately because of `Seek` takes storage mutex.
|
||||
keys = append(keys, string(k))
|
||||
})
|
||||
|
||||
var keysToRemove [][]byte
|
||||
key := []byte{prefixExpiration}
|
||||
keyRecord := []byte{prefixRecord}
|
||||
for i := range keys {
|
||||
key[0] = prefixExpiration
|
||||
key = append(key[:1], []byte(keys[i])...)
|
||||
if err := ic.DAO.DeleteStorageItem(n.ID, key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keysToRemove = keysToRemove[:0]
|
||||
key[0] = prefixRecord
|
||||
key = append(key[:1], keys[i][4:]...)
|
||||
ic.DAO.Seek(n.ID, key, func(k, v []byte) {
|
||||
keysToRemove = append(keysToRemove, k)
|
||||
})
|
||||
for i := range keysToRemove {
|
||||
keyRecord = append(keyRecord[:0], key...)
|
||||
keyRecord = append(keyRecord, keysToRemove[i]...)
|
||||
err := ic.DAO.DeleteStorageItem(n.ID, keyRecord)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
key[0] = prefixNFTToken
|
||||
n.burnByKey(ic, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostPersist implements interop.Contract interface.
|
||||
func (n *NameService) PostPersist(ic *interop.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NameService) addRoot(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
root := toString(args[0])
|
||||
if !rootRegex.Match([]byte(root)) {
|
||||
panic("invalid root")
|
||||
}
|
||||
|
||||
n.checkCommittee(ic)
|
||||
roots, _ := n.getRootsInternal(ic.DAO)
|
||||
if !roots.add(root) {
|
||||
panic("name already exists")
|
||||
}
|
||||
|
||||
err := putSerializableToDAO(n.ID, ic.DAO, []byte{prefixRoots}, &roots)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return stackitem.Null{}
|
||||
}
|
||||
|
||||
var maxPrice = big.NewInt(10000_00000000)
|
||||
|
||||
func (n *NameService) setPrice(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
price := toBigInt(args[0])
|
||||
if price.Sign() <= 0 || price.Cmp(maxPrice) >= 0 {
|
||||
panic("invalid price")
|
||||
}
|
||||
|
||||
n.checkCommittee(ic)
|
||||
err := ic.DAO.PutStorageItem(n.ID, []byte{prefixDomainPrice}, bigint.ToBytes(price))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return stackitem.Null{}
|
||||
}
|
||||
|
||||
func (n *NameService) getPrice(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
||||
return stackitem.NewBigInteger(n.getPriceInternal(ic.DAO))
|
||||
}
|
||||
|
||||
func (n *NameService) getPriceInternal(d dao.DAO) *big.Int {
|
||||
si := d.GetStorageItem(n.ID, []byte{prefixDomainPrice})
|
||||
return bigint.FromBytes(si)
|
||||
}
|
||||
|
||||
func (n *NameService) parseName(item stackitem.Item) (string, []string, []byte) {
|
||||
name := toName(item)
|
||||
names := strings.Split(name, ".")
|
||||
if len(names) != 2 {
|
||||
panic("invalid name")
|
||||
}
|
||||
return name, names, n.getTokenKey([]byte(name))
|
||||
}
|
||||
|
||||
func (n *NameService) isAvailable(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
_, names, key := n.parseName(args[0])
|
||||
if ic.DAO.GetStorageItem(n.ID, key) != nil {
|
||||
return stackitem.NewBool(false)
|
||||
}
|
||||
|
||||
roots, _ := n.getRootsInternal(ic.DAO)
|
||||
_, ok := roots.index(names[1])
|
||||
if !ok {
|
||||
panic("domain is not registered")
|
||||
}
|
||||
return stackitem.NewBool(true)
|
||||
}
|
||||
|
||||
func (n *NameService) getRootsInternal(d dao.DAO) (stringList, bool) {
|
||||
var sl stringList
|
||||
err := getSerializableFromDAO(n.ID, d, []byte{prefixRoots}, &sl)
|
||||
if err != nil {
|
||||
// Roots are being stored in `Initialize()` and thus must always be present.
|
||||
panic(err)
|
||||
}
|
||||
return sl, true
|
||||
}
|
||||
|
||||
func (n *NameService) register(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
name, names, key := n.parseName(args[0])
|
||||
owner := toUint160(args[1])
|
||||
if !n.checkWitness(ic, owner) {
|
||||
panic("owner is not witnessed")
|
||||
}
|
||||
|
||||
if ic.DAO.GetStorageItem(n.ID, key) != nil {
|
||||
return stackitem.NewBool(false)
|
||||
}
|
||||
|
||||
roots, _ := n.getRootsInternal(ic.DAO)
|
||||
if _, ok := roots.index(names[1]); !ok {
|
||||
panic("missing root")
|
||||
}
|
||||
if !ic.VM.AddGas(n.getPriceInternal(ic.DAO).Int64()) {
|
||||
panic("insufficient gas")
|
||||
}
|
||||
token := &nameState{
|
||||
NFTTokenState: state.NFTTokenState{
|
||||
Owner: owner,
|
||||
Name: name,
|
||||
},
|
||||
Expiration: uint32(ic.Block.Timestamp/1000 + secondsInYear),
|
||||
}
|
||||
n.mint(ic, token)
|
||||
err := ic.DAO.PutStorageItem(n.ID,
|
||||
makeExpirationKey(token.Expiration, token.ID()),
|
||||
state.StorageItem{0})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return stackitem.NewBool(true)
|
||||
}
|
||||
|
||||
func (n *NameService) renew(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
_, _, key := n.parseName(args[0])
|
||||
if !ic.VM.AddGas(n.getPriceInternal(ic.DAO).Int64()) {
|
||||
panic("insufficient gas")
|
||||
}
|
||||
token := new(nameState)
|
||||
err := getSerializableFromDAO(n.ID, ic.DAO, key, token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
keyExpiration := makeExpirationKey(token.Expiration, token.ID())
|
||||
if err := ic.DAO.DeleteStorageItem(n.ID, keyExpiration); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
token.Expiration += secondsInYear
|
||||
err = putSerializableToDAO(n.ID, ic.DAO, key, token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(key[1:], token.Expiration)
|
||||
err = ic.DAO.PutStorageItem(n.ID, key, state.StorageItem{0})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bi := new(big.Int).SetUint64(uint64(token.Expiration))
|
||||
return stackitem.NewBigInteger(bi)
|
||||
}
|
||||
|
||||
func (n *NameService) setAdmin(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
_, _, key := n.parseName(args[0])
|
||||
|
||||
var admin util.Uint160
|
||||
_, isNull := args[1].(stackitem.Null)
|
||||
if !isNull {
|
||||
admin = toUint160(args[1])
|
||||
if !n.checkWitness(ic, admin) {
|
||||
panic("not witnessed by admin")
|
||||
}
|
||||
}
|
||||
|
||||
token := new(nameState)
|
||||
err := getSerializableFromDAO(n.ID, ic.DAO, key, token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !n.checkWitness(ic, token.Owner) {
|
||||
panic("only owner can set admin")
|
||||
}
|
||||
token.HasAdmin = !isNull
|
||||
token.Admin = admin
|
||||
err = putSerializableToDAO(n.ID, ic.DAO, key, token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return stackitem.Null{}
|
||||
}
|
||||
|
||||
func (n *NameService) checkWitness(ic *interop.Context, owner util.Uint160) bool {
|
||||
ok, err := runtime.CheckHashedWitness(ic, owner)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func (n *NameService) checkCommittee(ic *interop.Context) {
|
||||
if !n.NEO.checkCommittee(ic) {
|
||||
panic("not witnessed by committee")
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NameService) checkAdmin(ic *interop.Context, token *nameState) bool {
|
||||
if n.checkWitness(ic, token.Owner) {
|
||||
return true
|
||||
}
|
||||
return token.HasAdmin && n.checkWitness(ic, token.Admin)
|
||||
}
|
||||
|
||||
func (n *NameService) setRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
name := toName(args[0])
|
||||
rt := toRecordType(args[1])
|
||||
data := toString(args[2])
|
||||
checkName(rt, data)
|
||||
|
||||
domain := toDomain(name)
|
||||
token, _, err := n.tokenState(ic.DAO, []byte(domain))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !n.checkAdmin(ic, token.(*nameState)) {
|
||||
panic("not witnessed by admin")
|
||||
}
|
||||
key := makeRecordKey(domain, name, rt)
|
||||
si := state.StorageItem(data)
|
||||
if err := ic.DAO.PutStorageItem(n.ID, key, si); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return stackitem.Null{}
|
||||
}
|
||||
|
||||
func checkName(rt nnsrecords.Type, name string) {
|
||||
var valid bool
|
||||
switch rt {
|
||||
case nnsrecords.A:
|
||||
// We can't rely on `len(ip) == net.IPv4len` because
|
||||
// IPv4 can be parsed to mapped representation.
|
||||
valid = ipv4Regex.MatchString(name) &&
|
||||
net.ParseIP(name) != nil
|
||||
case nnsrecords.CNAME:
|
||||
valid = matchName(name)
|
||||
case nnsrecords.TXT:
|
||||
valid = utf8.RuneCountInString(name) <= 255
|
||||
case nnsrecords.AAAA:
|
||||
valid = ipv6Regex.MatchString(name) &&
|
||||
net.ParseIP(name) != nil
|
||||
}
|
||||
if !valid {
|
||||
panic("invalid name")
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NameService) getRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
name := toName(args[0])
|
||||
domain := toDomain(name)
|
||||
rt := toRecordType(args[1])
|
||||
key := makeRecordKey(domain, name, rt)
|
||||
si := ic.DAO.GetStorageItem(n.ID, key)
|
||||
if si == nil {
|
||||
return stackitem.Null{}
|
||||
}
|
||||
return stackitem.NewByteArray(si)
|
||||
}
|
||||
|
||||
func (n *NameService) deleteRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
name := toName(args[0])
|
||||
rt := toRecordType(args[1])
|
||||
domain := toDomain(name)
|
||||
key := n.getTokenKey([]byte(domain))
|
||||
token := new(nameState)
|
||||
err := getSerializableFromDAO(n.ID, ic.DAO, key, token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !n.checkAdmin(ic, token) {
|
||||
panic("not witnessed by admin")
|
||||
}
|
||||
|
||||
key = makeRecordKey(domain, name, rt)
|
||||
if err := ic.DAO.DeleteStorageItem(n.ID, key); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return stackitem.Null{}
|
||||
}
|
||||
|
||||
func (n *NameService) resolve(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
name := toString(args[0])
|
||||
rt := toRecordType(args[1])
|
||||
result, ok := n.resolveInternal(ic, name, rt, 2)
|
||||
if !ok {
|
||||
return stackitem.Null{}
|
||||
}
|
||||
return stackitem.NewByteArray([]byte(result))
|
||||
}
|
||||
|
||||
func (n *NameService) resolveInternal(ic *interop.Context, name string, t nnsrecords.Type, redirect int) (string, bool) {
|
||||
if redirect < 0 {
|
||||
panic("invalid redirect")
|
||||
}
|
||||
records := n.getRecordsInternal(ic.DAO, name)
|
||||
if data, ok := records[t]; ok {
|
||||
return data, true
|
||||
}
|
||||
data, ok := records[nnsrecords.CNAME]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return n.resolveInternal(ic, data, t, redirect-1)
|
||||
}
|
||||
|
||||
func (n *NameService) getRecordsInternal(d dao.DAO, name string) map[nnsrecords.Type]string {
|
||||
domain := toDomain(name)
|
||||
key := makeRecordKey(domain, name, 0)
|
||||
key = key[:len(key)-1]
|
||||
res := make(map[nnsrecords.Type]string)
|
||||
d.Seek(n.ID, key, func(k, v []byte) {
|
||||
rt := nnsrecords.Type(k[len(k)-1])
|
||||
res[rt] = string(v)
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
func makeRecordKey(domain, name string, rt nnsrecords.Type) []byte {
|
||||
key := make([]byte, 1+util.Uint160Size+util.Uint160Size+1)
|
||||
key[0] = prefixRecord
|
||||
i := 1
|
||||
i += copy(key[i:], hash.Hash160([]byte(domain)).BytesBE())
|
||||
i += copy(key[i:], hash.Hash160([]byte(name)).BytesBE())
|
||||
key[i] = byte(rt)
|
||||
return key
|
||||
}
|
||||
|
||||
func makeExpirationKey(expiration uint32, tokenID []byte) []byte {
|
||||
key := make([]byte, 1+4+util.Uint160Size)
|
||||
key[0] = prefixExpiration
|
||||
binary.BigEndian.PutUint32(key[1:], expiration)
|
||||
copy(key[5:], hash.Hash160(tokenID).BytesBE())
|
||||
return key
|
||||
}
|
||||
|
||||
// ToMap implements nftTokenState interface.
|
||||
func (s *nameState) ToMap() *stackitem.Map {
|
||||
m := s.NFTTokenState.ToMap()
|
||||
m.Add(stackitem.NewByteArray([]byte("expiration")),
|
||||
stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(s.Expiration))))
|
||||
return m
|
||||
}
|
||||
|
||||
// EncodeBinary implements io.Serializable.
|
||||
func (s *nameState) EncodeBinary(w *io.BinWriter) {
|
||||
stackitem.EncodeBinaryStackItem(s.ToStackItem(), w)
|
||||
}
|
||||
|
||||
// DecodeBinary implements io.Serializable.
|
||||
func (s *nameState) DecodeBinary(r *io.BinReader) {
|
||||
item := stackitem.DecodeBinaryStackItem(r)
|
||||
if r.Err == nil {
|
||||
r.Err = s.FromStackItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
// ToStackItem implements nftTokenState interface.
|
||||
func (s *nameState) ToStackItem() stackitem.Item {
|
||||
item := s.NFTTokenState.ToStackItem().(*stackitem.Struct)
|
||||
exp := new(big.Int).SetUint64(uint64(s.Expiration))
|
||||
item.Append(stackitem.NewBigInteger(exp))
|
||||
if s.HasAdmin {
|
||||
item.Append(stackitem.NewByteArray(s.Admin.BytesBE()))
|
||||
} else {
|
||||
item.Append(stackitem.Null{})
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
// FromStackItem implements nftTokenState interface.
|
||||
func (s *nameState) FromStackItem(item stackitem.Item) error {
|
||||
err := s.NFTTokenState.FromStackItem(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
elems := item.Value().([]stackitem.Item)
|
||||
if len(elems) < 4 {
|
||||
return errors.New("invalid stack item")
|
||||
}
|
||||
bi, err := elems[2].TryInteger()
|
||||
if err != nil || !bi.IsUint64() {
|
||||
return errors.New("invalid stack item")
|
||||
}
|
||||
|
||||
_, isNull := elems[3].(stackitem.Null)
|
||||
if !isNull {
|
||||
bs, err := elems[3].TryBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u, err := util.Uint160DecodeBytesBE(bs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Admin = u
|
||||
}
|
||||
s.Expiration = uint32(bi.Uint64())
|
||||
s.HasAdmin = !isNull
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
func domainFromString(name string) (string, bool) {
|
||||
i := strings.LastIndexAny(name, ".")
|
||||
if i < 0 {
|
||||
return "", false
|
||||
}
|
||||
i = strings.LastIndexAny(name[:i], ".")
|
||||
if i < 0 {
|
||||
return name, true
|
||||
}
|
||||
return name[i+1:], true
|
||||
}
|
||||
|
||||
func toDomain(name string) string {
|
||||
domain, ok := domainFromString(name)
|
||||
if !ok {
|
||||
panic("invalid record")
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
func toRecordType(item stackitem.Item) nnsrecords.Type {
|
||||
bi, err := item.TryInteger()
|
||||
if err != nil || !bi.IsInt64() {
|
||||
panic("invalid record type")
|
||||
}
|
||||
val := bi.Uint64()
|
||||
if val > math.MaxUint8 {
|
||||
panic("invalid record type")
|
||||
}
|
||||
switch rt := nnsrecords.Type(val); rt {
|
||||
case nnsrecords.A, nnsrecords.CNAME, nnsrecords.TXT, nnsrecords.AAAA:
|
||||
return rt
|
||||
default:
|
||||
panic("invalid record type")
|
||||
}
|
||||
}
|
||||
|
||||
func toName(item stackitem.Item) string {
|
||||
name := toString(item)
|
||||
if !matchName(name) {
|
||||
panic("invalid name")
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
type stringList []string
|
||||
|
||||
// ToStackItem converts sl to stack item.
|
||||
func (sl stringList) ToStackItem() stackitem.Item {
|
||||
arr := make([]stackitem.Item, len(sl))
|
||||
for i := range sl {
|
||||
arr[i] = stackitem.NewByteArray([]byte(sl[i]))
|
||||
}
|
||||
return stackitem.NewArray(arr)
|
||||
}
|
||||
|
||||
// FromStackItem converts stack item to string list.
|
||||
func (sl *stringList) FromStackItem(item stackitem.Item) error {
|
||||
arr, ok := item.Value().([]stackitem.Item)
|
||||
if !ok {
|
||||
return errors.New("invalid stack item")
|
||||
}
|
||||
res := make([]string, len(arr))
|
||||
for i := range res {
|
||||
s, err := stackitem.ToString(arr[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res[i] = s
|
||||
}
|
||||
*sl = res
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeBinary implements io.Serializable.
|
||||
func (sl stringList) EncodeBinary(w *io.BinWriter) {
|
||||
stackitem.EncodeBinaryStackItem(sl.ToStackItem(), w)
|
||||
}
|
||||
|
||||
// DecodeBinary implements io.Serializable.
|
||||
func (sl *stringList) DecodeBinary(r *io.BinReader) {
|
||||
item := stackitem.DecodeBinaryStackItem(r)
|
||||
if r.Err == nil {
|
||||
r.Err = sl.FromStackItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
func (sl stringList) index(s string) (int, bool) {
|
||||
index := sort.Search(len(sl), func(i int) bool {
|
||||
return sl[i] >= s
|
||||
})
|
||||
return index, index < len(sl) && sl[index] == s
|
||||
}
|
||||
|
||||
func (sl *stringList) add(s string) bool {
|
||||
index, has := sl.index(s)
|
||||
if has {
|
||||
return false
|
||||
}
|
||||
|
||||
*sl = append(*sl, "")
|
||||
copy((*sl)[index+1:], (*sl)[index:])
|
||||
(*sl)[index] = s
|
||||
return true
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// The specification is following C# code:
|
||||
// string domain = string.Join('.', name.Split('.')[^2..]);
|
||||
func TestParseDomain(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
domain string
|
||||
}{
|
||||
{"sim.pl.e", "pl.e"},
|
||||
{"some.long.d.o.m.a.i.n", "i.n"},
|
||||
{"t.wo", "t.wo"},
|
||||
{".dot", ".dot"},
|
||||
{".d.ot", "d.ot"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dom, ok := domainFromString(tc.name)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tc.domain, dom)
|
||||
})
|
||||
}
|
||||
|
||||
_, ok := domainFromString("nodots")
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
func TestNameService_CheckName(t *testing.T) {
|
||||
// tests are got from the C# implementation
|
||||
testCases := []struct {
|
||||
Type nnsrecords.Type
|
||||
Name string
|
||||
ShouldFail bool
|
||||
}{
|
||||
{Type: nnsrecords.A, Name: "0.0.0.0"},
|
||||
{Type: nnsrecords.A, Name: "10.10.10.10"},
|
||||
{Type: nnsrecords.A, Name: "255.255.255.255"},
|
||||
{Type: nnsrecords.A, Name: "192.168.1.1"},
|
||||
{Type: nnsrecords.A, Name: "1a", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "256.0.0.0", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "01.01.01.01", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "00.0.0.0", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "0.0.0.-1", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "0.0.0.0.1", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "ff.ff.ff.ff", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "0.0.256", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "0.0.0", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "0.257", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "1.1", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "257", ShouldFail: true},
|
||||
{Type: nnsrecords.A, Name: "1", ShouldFail: true},
|
||||
{Type: nnsrecords.AAAA, Name: "2001:db8::8:800:200c:417a"},
|
||||
{Type: nnsrecords.AAAA, Name: "ff01::101"},
|
||||
{Type: nnsrecords.AAAA, Name: "::1"},
|
||||
{Type: nnsrecords.AAAA, Name: "::"},
|
||||
{Type: nnsrecords.AAAA, Name: "2001:db8:0:0:8:800:200c:417a"},
|
||||
{Type: nnsrecords.AAAA, Name: "ff01:0:0:0:0:0:0:101"},
|
||||
{Type: nnsrecords.AAAA, Name: "0:0:0:0:0:0:0:1"},
|
||||
{Type: nnsrecords.AAAA, Name: "0:0:0:0:0:0:0:0"},
|
||||
{Type: nnsrecords.AAAA, Name: "2001:DB8::8:800:200C:417A", ShouldFail: true},
|
||||
{Type: nnsrecords.AAAA, Name: "FF01::101", ShouldFail: true},
|
||||
{Type: nnsrecords.AAAA, Name: "fF01::101", ShouldFail: true},
|
||||
{Type: nnsrecords.AAAA, Name: "2001:DB8:0:0:8:800:200C:417A", ShouldFail: true},
|
||||
{Type: nnsrecords.AAAA, Name: "FF01:0:0:0:0:0:0:101", ShouldFail: true},
|
||||
{Type: nnsrecords.AAAA, Name: "::ffff:1.01.1.01", ShouldFail: true},
|
||||
{Type: nnsrecords.AAAA, Name: "2001:DB8:0:0:8:800:200C:4Z", ShouldFail: true},
|
||||
{Type: nnsrecords.AAAA, Name: "::13.1.68.3", ShouldFail: true},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
if testCase.ShouldFail {
|
||||
require.Panics(t, func() {
|
||||
checkName(testCase.Type, testCase.Name)
|
||||
})
|
||||
} else {
|
||||
require.NotPanics(t, func() {
|
||||
checkName(testCase.Type, testCase.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNameService_NEP11(t *testing.T) {
|
||||
ns := newNameService()
|
||||
require.NoError(t, standard.Check(&ns.Manifest, manifest.NEP11StandardName))
|
||||
}
|
|
@ -10,7 +10,6 @@ const (
|
|||
Oracle = "OracleContract"
|
||||
Designation = "RoleManagement"
|
||||
Notary = "Notary"
|
||||
NameService = "NameService"
|
||||
CryptoLib = "CryptoLib"
|
||||
StdLib = "StdLib"
|
||||
)
|
||||
|
@ -25,7 +24,6 @@ func IsValid(name string) bool {
|
|||
name == Oracle ||
|
||||
name == Designation ||
|
||||
name == Notary ||
|
||||
name == NameService ||
|
||||
name == CryptoLib ||
|
||||
name == StdLib
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package nnsrecords
|
||||
|
||||
// Type represents name record type.
|
||||
type Type byte
|
||||
|
||||
// Pre-defined record types.
|
||||
const (
|
||||
A Type = 1
|
||||
CNAME Type = 5
|
||||
TXT Type = 16
|
||||
AAAA Type = 28
|
||||
)
|
|
@ -1,410 +0,0 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sort"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/dao"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
|
||||
istorage "github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
||||
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
)
|
||||
|
||||
type nonfungible struct {
|
||||
interop.ContractMD
|
||||
|
||||
tokenSymbol string
|
||||
tokenDecimals byte
|
||||
|
||||
onTransferred func(nftTokenState)
|
||||
getTokenKey func([]byte) []byte
|
||||
newTokenState func() nftTokenState
|
||||
}
|
||||
|
||||
type nftTokenState interface {
|
||||
io.Serializable
|
||||
ToStackItem() stackitem.Item
|
||||
FromStackItem(stackitem.Item) error
|
||||
ToMap() *stackitem.Map
|
||||
ID() []byte
|
||||
Base() *state.NFTTokenState
|
||||
}
|
||||
|
||||
const (
|
||||
prefixNFTTotalSupply = 11
|
||||
prefixNFTAccount = 7
|
||||
prefixNFTToken = 5
|
||||
)
|
||||
|
||||
var (
|
||||
nftTotalSupplyKey = []byte{prefixNFTTotalSupply}
|
||||
)
|
||||
|
||||
func newNonFungible(name string, id int32, symbol string, decimals byte) *nonfungible {
|
||||
n := &nonfungible{
|
||||
ContractMD: *interop.NewContractMD(name, id),
|
||||
|
||||
tokenSymbol: symbol,
|
||||
tokenDecimals: decimals,
|
||||
|
||||
getTokenKey: func(tokenID []byte) []byte {
|
||||
return append([]byte{prefixNFTToken}, tokenID...)
|
||||
},
|
||||
newTokenState: func() nftTokenState {
|
||||
return new(state.NFTTokenState)
|
||||
},
|
||||
}
|
||||
n.Manifest.SupportedStandards = []string{manifest.NEP11StandardName}
|
||||
|
||||
desc := newDescriptor("symbol", smartcontract.StringType)
|
||||
md := newMethodAndPrice(n.symbol, 0, callflag.NoneFlag)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("decimals", smartcontract.IntegerType)
|
||||
md = newMethodAndPrice(n.decimals, 0, callflag.NoneFlag)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("totalSupply", smartcontract.IntegerType)
|
||||
md = newMethodAndPrice(n.totalSupply, 1<<15, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("ownerOf", smartcontract.Hash160Type,
|
||||
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
|
||||
md = newMethodAndPrice(n.OwnerOf, 1<<15, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("balanceOf", smartcontract.IntegerType,
|
||||
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
||||
md = newMethodAndPrice(n.BalanceOf, 1<<15, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("properties", smartcontract.MapType,
|
||||
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
|
||||
md = newMethodAndPrice(n.Properties, 1<<15, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("tokens", smartcontract.InteropInterfaceType)
|
||||
md = newMethodAndPrice(n.tokens, 1<<15, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("tokensOf", smartcontract.InteropInterfaceType,
|
||||
manifest.NewParameter("owner", smartcontract.Hash160Type))
|
||||
md = newMethodAndPrice(n.tokensOf, 1<<15, callflag.ReadStates)
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
desc = newDescriptor("transfer", smartcontract.BoolType,
|
||||
manifest.NewParameter("to", smartcontract.Hash160Type),
|
||||
manifest.NewParameter("tokenId", smartcontract.ByteArrayType),
|
||||
manifest.NewParameter("data", smartcontract.AnyType))
|
||||
md = newMethodAndPrice(n.transfer, 1<<17, callflag.States|callflag.AllowNotify)
|
||||
md.StorageFee = 50
|
||||
n.AddMethod(md, desc)
|
||||
|
||||
n.AddEvent("Transfer",
|
||||
manifest.NewParameter("from", smartcontract.Hash160Type),
|
||||
manifest.NewParameter("to", smartcontract.Hash160Type),
|
||||
manifest.NewParameter("amount", smartcontract.IntegerType),
|
||||
manifest.NewParameter("tokenId", smartcontract.ByteArrayType))
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// Initialize implements interop.Contract interface.
|
||||
func (n nonfungible) Initialize(ic *interop.Context) error {
|
||||
return setIntWithKey(n.ID, ic.DAO, nftTotalSupplyKey, 0)
|
||||
}
|
||||
|
||||
func (n *nonfungible) symbol(_ *interop.Context, _ []stackitem.Item) stackitem.Item {
|
||||
return stackitem.NewByteArray([]byte(n.tokenSymbol))
|
||||
}
|
||||
|
||||
func (n *nonfungible) decimals(_ *interop.Context, _ []stackitem.Item) stackitem.Item {
|
||||
return stackitem.NewBigInteger(big.NewInt(int64(n.tokenDecimals)))
|
||||
}
|
||||
|
||||
func (n *nonfungible) totalSupply(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
||||
return stackitem.NewBigInteger(n.TotalSupply(ic.DAO))
|
||||
}
|
||||
|
||||
func (n *nonfungible) TotalSupply(d dao.DAO) *big.Int {
|
||||
si := d.GetStorageItem(n.ID, nftTotalSupplyKey)
|
||||
if si == nil {
|
||||
panic(errors.New("total supply is not initialized"))
|
||||
}
|
||||
return bigint.FromBytes(si)
|
||||
}
|
||||
|
||||
func (n *nonfungible) setTotalSupply(d dao.DAO, ts *big.Int) {
|
||||
err := d.PutStorageItem(n.ID, nftTotalSupplyKey, bigint.ToBytes(ts))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *nonfungible) tokenState(d dao.DAO, tokenID []byte) (nftTokenState, []byte, error) {
|
||||
key := n.getTokenKey(tokenID)
|
||||
s := n.newTokenState()
|
||||
err := getSerializableFromDAO(n.ID, d, key, s)
|
||||
return s, key, err
|
||||
}
|
||||
|
||||
func (n *nonfungible) accountState(d dao.DAO, owner util.Uint160) (*state.NFTAccountState, []byte, error) {
|
||||
acc := new(state.NFTAccountState)
|
||||
keyAcc := makeNFTAccountKey(owner)
|
||||
err := getSerializableFromDAO(n.ID, d, keyAcc, acc)
|
||||
return acc, keyAcc, err
|
||||
}
|
||||
|
||||
func (n *nonfungible) putAccountState(d dao.DAO, key []byte, acc *state.NFTAccountState) {
|
||||
var err error
|
||||
if acc.Balance.Sign() == 0 {
|
||||
err = d.DeleteStorageItem(n.ID, key)
|
||||
} else {
|
||||
err = putSerializableToDAO(n.ID, d, key, acc)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *nonfungible) OwnerOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
tokenID, err := args[0].TryBytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s, _, err := n.tokenState(ic.DAO, tokenID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return stackitem.NewByteArray(s.Base().Owner.BytesBE())
|
||||
}
|
||||
|
||||
func (n *nonfungible) Properties(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
tokenID, err := args[0].TryBytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s, _, err := n.tokenState(ic.DAO, tokenID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s.ToMap()
|
||||
}
|
||||
|
||||
func (n *nonfungible) BalanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
owner := toUint160(args[0])
|
||||
s, _, err := n.accountState(ic.DAO, owner)
|
||||
if err != nil {
|
||||
if errors.Is(err, storage.ErrKeyNotFound) {
|
||||
return stackitem.NewBigInteger(big.NewInt(0))
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
return stackitem.NewBigInteger(&s.Balance)
|
||||
}
|
||||
|
||||
func (n *nonfungible) tokens(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
prefix := []byte{prefixNFTToken}
|
||||
siMap, err := ic.DAO.GetStorageItemsWithPrefix(n.ID, prefix)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
filteredMap := stackitem.NewMap()
|
||||
for k, v := range siMap {
|
||||
filteredMap.Add(stackitem.NewByteArray(append(prefix, []byte(k)...)), stackitem.NewByteArray(v))
|
||||
}
|
||||
sort.Slice(filteredMap.Value().([]stackitem.MapElement), func(i, j int) bool {
|
||||
return bytes.Compare(filteredMap.Value().([]stackitem.MapElement)[i].Key.Value().([]byte),
|
||||
filteredMap.Value().([]stackitem.MapElement)[j].Key.Value().([]byte)) == -1
|
||||
})
|
||||
iter := istorage.NewIterator(filteredMap, 1, istorage.FindValuesOnly|istorage.FindDeserialize|istorage.FindPick1)
|
||||
return stackitem.NewInterop(iter)
|
||||
}
|
||||
|
||||
func (n *nonfungible) tokensOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
owner := toUint160(args[0])
|
||||
s, _, err := n.accountState(ic.DAO, owner)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
arr := make([]stackitem.Item, len(s.Tokens))
|
||||
for i := range arr {
|
||||
arr[i] = stackitem.NewByteArray(s.Tokens[i])
|
||||
}
|
||||
iter := newArrayIterator(arr)
|
||||
return stackitem.NewInterop(iter)
|
||||
}
|
||||
|
||||
func (n *nonfungible) mint(ic *interop.Context, s nftTokenState) {
|
||||
key := n.getTokenKey(s.ID())
|
||||
if ic.DAO.GetStorageItem(n.ID, key) != nil {
|
||||
panic("token is already minted")
|
||||
}
|
||||
if err := putSerializableToDAO(n.ID, ic.DAO, key, s); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
owner := s.Base().Owner
|
||||
acc, keyAcc, err := n.accountState(ic.DAO, owner)
|
||||
if err != nil && !errors.Is(err, storage.ErrKeyNotFound) {
|
||||
panic(err)
|
||||
}
|
||||
acc.Add(s.ID())
|
||||
n.putAccountState(ic.DAO, keyAcc, acc)
|
||||
|
||||
ts := n.TotalSupply(ic.DAO)
|
||||
ts.Add(ts, intOne)
|
||||
n.setTotalSupply(ic.DAO, ts)
|
||||
n.postTransfer(ic, nil, &owner, s.ID(), stackitem.Null{})
|
||||
}
|
||||
|
||||
func (n *nonfungible) postTransfer(ic *interop.Context, from, to *util.Uint160, tokenID []byte, data stackitem.Item) {
|
||||
ne := state.NotificationEvent{
|
||||
ScriptHash: n.Hash,
|
||||
Name: "Transfer",
|
||||
Item: stackitem.NewArray([]stackitem.Item{
|
||||
addrToStackItem(from),
|
||||
addrToStackItem(to),
|
||||
stackitem.NewBigInteger(intOne),
|
||||
stackitem.NewByteArray(tokenID),
|
||||
}),
|
||||
}
|
||||
ic.Notifications = append(ic.Notifications, ne)
|
||||
if to == nil {
|
||||
return
|
||||
}
|
||||
cs, err := ic.GetContract(*to)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fromArg := stackitem.Item(stackitem.Null{})
|
||||
if from != nil {
|
||||
fromArg = stackitem.NewByteArray((*from).BytesBE())
|
||||
}
|
||||
args := []stackitem.Item{
|
||||
fromArg,
|
||||
stackitem.NewBigInteger(intOne),
|
||||
stackitem.NewByteArray(tokenID),
|
||||
data,
|
||||
}
|
||||
if err := contract.CallFromNative(ic, n.Hash, cs, manifest.MethodOnNEP11Payment, args, false); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
var _ = (*nonfungible).burn // fix unused warning
|
||||
|
||||
func (n *nonfungible) burn(ic *interop.Context, tokenID []byte) {
|
||||
key := n.getTokenKey(tokenID)
|
||||
n.burnByKey(ic, key)
|
||||
}
|
||||
|
||||
func (n *nonfungible) burnByKey(ic *interop.Context, key []byte) {
|
||||
token := n.newTokenState()
|
||||
err := getSerializableFromDAO(n.ID, ic.DAO, key, token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := ic.DAO.DeleteStorageItem(n.ID, key); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
owner := token.Base().Owner
|
||||
acc, keyAcc, err := n.accountState(ic.DAO, owner)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
id := token.ID()
|
||||
acc.Remove(id)
|
||||
n.putAccountState(ic.DAO, keyAcc, acc)
|
||||
|
||||
ts := n.TotalSupply(ic.DAO)
|
||||
ts.Sub(ts, intOne)
|
||||
n.setTotalSupply(ic.DAO, ts)
|
||||
n.postTransfer(ic, &owner, nil, id, stackitem.Null{})
|
||||
}
|
||||
|
||||
func (n *nonfungible) transfer(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
||||
to := toUint160(args[0])
|
||||
tokenID, err := args[1].TryBytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
token, tokenKey, err := n.tokenState(ic.DAO, tokenID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
from := token.Base().Owner
|
||||
ok, err := runtime.CheckHashedWitness(ic, from)
|
||||
if err != nil || !ok {
|
||||
return stackitem.NewBool(false)
|
||||
}
|
||||
if from != to {
|
||||
acc, key, err := n.accountState(ic.DAO, from)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
acc.Remove(tokenID)
|
||||
n.putAccountState(ic.DAO, key, acc)
|
||||
|
||||
token.Base().Owner = to
|
||||
n.onTransferred(token)
|
||||
err = putSerializableToDAO(n.ID, ic.DAO, tokenKey, token)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
acc, key, err = n.accountState(ic.DAO, to)
|
||||
if err != nil && !errors.Is(err, storage.ErrKeyNotFound) {
|
||||
panic(err)
|
||||
}
|
||||
acc.Add(tokenID)
|
||||
n.putAccountState(ic.DAO, key, acc)
|
||||
}
|
||||
n.postTransfer(ic, &from, &to, tokenID, args[2])
|
||||
return stackitem.NewBool(true)
|
||||
}
|
||||
|
||||
func makeNFTAccountKey(owner util.Uint160) []byte {
|
||||
return append([]byte{prefixNFTAccount}, owner.BytesBE()...)
|
||||
}
|
||||
|
||||
type arrayWrapper struct {
|
||||
index int
|
||||
value []stackitem.Item
|
||||
}
|
||||
|
||||
func newArrayIterator(arr []stackitem.Item) *arrayWrapper {
|
||||
return &arrayWrapper{
|
||||
index: -1,
|
||||
value: arr,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *arrayWrapper) Next() bool {
|
||||
if next := a.index + 1; next < len(a.value) {
|
||||
a.index = next
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *arrayWrapper) Value() stackitem.Item {
|
||||
return a.value[a.index]
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest/standard"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNonfungibleNEP11(t *testing.T) {
|
||||
n := newNonFungible("NFToken", -100, "SYM", 1)
|
||||
require.NoError(t, standard.Check(&n.ContractMD.Manifest, manifest.NEP11StandardName))
|
||||
}
|
|
@ -1,404 +0,0 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/internal/testchain"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNameService_Price(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
|
||||
testGetSet(t, bc, bc.contracts.NameService.Hash, "Price",
|
||||
native.DefaultDomainPrice, 1, 10000_00000000)
|
||||
}
|
||||
|
||||
func TestNonfungible(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
|
||||
acc := newAccountWithGAS(t, bc)
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "symbol", "NNS")
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "decimals", 0)
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "totalSupply", 0)
|
||||
}
|
||||
|
||||
func TestAddRoot(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
nsHash := bc.contracts.NameService.Hash
|
||||
|
||||
t.Run("invalid format", func(t *testing.T) {
|
||||
testNameServiceInvoke(t, bc, "addRoot", nil, "")
|
||||
})
|
||||
t.Run("not signed by committee", func(t *testing.T) {
|
||||
aer, err := invokeContractMethod(bc, 1000_0000, nsHash, "addRoot", "some")
|
||||
require.NoError(t, err)
|
||||
checkFAULTState(t, aer)
|
||||
})
|
||||
|
||||
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "some")
|
||||
t.Run("already exists", func(t *testing.T) {
|
||||
testNameServiceInvoke(t, bc, "addRoot", nil, "some")
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpiration(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
acc := newAccountWithGAS(t, bc)
|
||||
|
||||
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc, "register",
|
||||
true, "first.com", acc.Contract.ScriptHash())
|
||||
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc,
|
||||
"setRecord", stackitem.Null{}, "first.com", int64(nnsrecords.TXT), "sometext")
|
||||
b1 := bc.topBlock.Load().(*block.Block)
|
||||
|
||||
tx, err := prepareContractMethodInvokeGeneric(bc, defaultRegisterSysfee, bc.contracts.NameService.Hash,
|
||||
"register", acc, "second.com", acc.Contract.ScriptHash())
|
||||
require.NoError(t, err)
|
||||
b2 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
|
||||
b.Index = b1.Index + 1
|
||||
b.PrevHash = b1.Hash()
|
||||
b.Timestamp = b1.Timestamp + 10000
|
||||
}, tx)
|
||||
require.NoError(t, bc.AddBlock(b2))
|
||||
checkTxHalt(t, bc, tx.Hash())
|
||||
|
||||
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash,
|
||||
"isAvailable", acc, "first.com")
|
||||
require.NoError(t, err)
|
||||
b3 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
|
||||
b.Index = b2.Index + 1
|
||||
b.PrevHash = b2.Hash()
|
||||
b.Timestamp = b1.Timestamp + (secondsInYear+1)*1000
|
||||
}, tx)
|
||||
require.NoError(t, bc.AddBlock(b3))
|
||||
aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
|
||||
require.NoError(t, err)
|
||||
checkResult(t, &aer[0], stackitem.NewBool(true))
|
||||
|
||||
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash,
|
||||
"isAvailable", acc, "second.com")
|
||||
require.NoError(t, err)
|
||||
b4 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
|
||||
b.Index = b3.Index + 1
|
||||
b.PrevHash = b3.Hash()
|
||||
b.Timestamp = b3.Timestamp + 1000
|
||||
}, tx)
|
||||
require.NoError(t, bc.AddBlock(b4))
|
||||
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
|
||||
require.NoError(t, err)
|
||||
checkResult(t, &aer[0], stackitem.NewBool(false))
|
||||
|
||||
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash,
|
||||
"getRecord", acc, "first.com", int64(nnsrecords.TXT))
|
||||
require.NoError(t, err)
|
||||
b5 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
|
||||
b.Index = b4.Index + 1
|
||||
b.PrevHash = b4.Hash()
|
||||
b.Timestamp = b4.Timestamp + 1000
|
||||
}, tx)
|
||||
require.NoError(t, bc.AddBlock(b5))
|
||||
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
|
||||
require.NoError(t, err)
|
||||
checkResult(t, &aer[0], stackitem.Null{})
|
||||
}
|
||||
|
||||
const secondsInYear = 365 * 24 * 3600
|
||||
|
||||
func TestRegisterAndRenew(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
|
||||
testNameServiceInvoke(t, bc, "isAvailable", nil, "neo.com")
|
||||
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "org")
|
||||
testNameServiceInvoke(t, bc, "isAvailable", nil, "neo.com")
|
||||
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
|
||||
testNameServiceInvoke(t, bc, "isAvailable", true, "neo.com")
|
||||
testNameServiceInvoke(t, bc, "register", nil, "neo.org", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, "register", nil, "docs.neo.org", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, "register", nil, "\nneo.com'", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, "register", nil, "neo.com\n", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, "register", nil, "neo.com", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, native.DefaultDomainPrice, true, "register",
|
||||
nil, "neo.com", testchain.CommitteeScriptHash())
|
||||
|
||||
testNameServiceInvoke(t, bc, "isAvailable", true, "neo.com")
|
||||
testNameServiceInvoke(t, bc, "balanceOf", 0, testchain.CommitteeScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "register",
|
||||
true, "neo.com", testchain.CommitteeScriptHash())
|
||||
topBlock := bc.topBlock.Load().(*block.Block)
|
||||
expectedExpiration := topBlock.Timestamp/1000 + secondsInYear
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "register",
|
||||
false, "neo.com", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, "isAvailable", false, "neo.com")
|
||||
|
||||
props := stackitem.NewMap()
|
||||
props.Add(stackitem.Make("name"), stackitem.Make("neo.com"))
|
||||
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
|
||||
testNameServiceInvoke(t, bc, "properties", props, "neo.com")
|
||||
testNameServiceInvoke(t, bc, "balanceOf", 1, testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, "ownerOf", testchain.CommitteeScriptHash().BytesBE(), []byte("neo.com"))
|
||||
|
||||
t.Run("invalid token ID", func(t *testing.T) {
|
||||
testNameServiceInvoke(t, bc, "properties", nil, "not.exists")
|
||||
testNameServiceInvoke(t, bc, "ownerOf", nil, "not.exists")
|
||||
testNameServiceInvoke(t, bc, "properties", nil, []interface{}{})
|
||||
testNameServiceInvoke(t, bc, "ownerOf", nil, []interface{}{})
|
||||
})
|
||||
|
||||
// Renew
|
||||
expectedExpiration += secondsInYear
|
||||
testNameServiceInvokeAux(t, bc, 100_0000_0000, true, "renew", expectedExpiration, "neo.com")
|
||||
|
||||
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
|
||||
testNameServiceInvoke(t, bc, "properties", props, "neo.com")
|
||||
}
|
||||
|
||||
func TestSetGetRecord(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
acc := newAccountWithGAS(t, bc)
|
||||
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
|
||||
|
||||
t.Run("set before register", func(t *testing.T) {
|
||||
testNameServiceInvoke(t, bc, "setRecord", nil, "neo.com", int64(nnsrecords.TXT), "sometext")
|
||||
})
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "register",
|
||||
true, "neo.com", testchain.CommitteeScriptHash())
|
||||
t.Run("invalid parameters", func(t *testing.T) {
|
||||
testNameServiceInvoke(t, bc, "setRecord", nil, "neo.com", int64(0xFF), "1.2.3.4")
|
||||
testNameServiceInvoke(t, bc, "setRecord", nil, "neo.com", int64(nnsrecords.A), "not.an.ip.address")
|
||||
})
|
||||
t.Run("invalid witness", func(t *testing.T) {
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", nil,
|
||||
"neo.com", int64(nnsrecords.A), "1.2.3.4")
|
||||
})
|
||||
testNameServiceInvoke(t, bc, "getRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.A))
|
||||
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.A), "1.2.3.4")
|
||||
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(nnsrecords.A))
|
||||
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.A), "1.2.3.4")
|
||||
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(nnsrecords.A))
|
||||
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.AAAA), "2001:0000:1f1f:0000:0000:0100:11a0:addf")
|
||||
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.CNAME), "nspcc.ru")
|
||||
testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.TXT), "sometext")
|
||||
|
||||
// Delete record.
|
||||
t.Run("invalid witness", func(t *testing.T) {
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", nil,
|
||||
"neo.com", int64(nnsrecords.CNAME))
|
||||
})
|
||||
testNameServiceInvoke(t, bc, "getRecord", "nspcc.ru", "neo.com", int64(nnsrecords.CNAME))
|
||||
testNameServiceInvoke(t, bc, "deleteRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.CNAME))
|
||||
testNameServiceInvoke(t, bc, "getRecord", stackitem.Null{}, "neo.com", int64(nnsrecords.CNAME))
|
||||
testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(nnsrecords.A))
|
||||
}
|
||||
|
||||
func TestSetAdmin(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
owner := newAccountWithGAS(t, bc)
|
||||
admin := newAccountWithGAS(t, bc)
|
||||
guest := newAccountWithGAS(t, bc)
|
||||
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
|
||||
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, owner, "register", true,
|
||||
"neo.com", owner.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, guest, "setAdmin", nil,
|
||||
"neo.com", admin.PrivateKey().GetScriptHash())
|
||||
|
||||
// Must be witnessed by both owner and admin.
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, owner, "setAdmin", nil,
|
||||
"neo.com", admin.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "setAdmin", nil,
|
||||
"neo.com", admin.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, []*wallet.Account{owner, admin},
|
||||
"setAdmin", stackitem.Null{},
|
||||
"neo.com", admin.PrivateKey().GetScriptHash())
|
||||
|
||||
t.Run("set and delete by admin", func(t *testing.T) {
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(nnsrecords.TXT), "sometext")
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, guest, "deleteRecord", nil,
|
||||
"neo.com", int64(nnsrecords.TXT))
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "deleteRecord", stackitem.Null{},
|
||||
"neo.com", int64(nnsrecords.TXT))
|
||||
})
|
||||
|
||||
t.Run("set admin to null", func(t *testing.T) {
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(nnsrecords.TXT), "sometext")
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, owner, "setAdmin", stackitem.Null{},
|
||||
"neo.com", nil)
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "deleteRecord", nil,
|
||||
"neo.com", int64(nnsrecords.TXT))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTransfer(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
from := newAccountWithGAS(t, bc)
|
||||
to := newAccountWithGAS(t, bc)
|
||||
|
||||
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "register",
|
||||
true, "neo.com", from.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(nnsrecords.A), "1.2.3.4")
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "transfer",
|
||||
nil, to.Contract.ScriptHash().BytesBE(), []byte("not.exists"), nil)
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "transfer",
|
||||
false, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil)
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "transfer",
|
||||
true, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil)
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "totalSupply", 1)
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "ownerOf",
|
||||
to.Contract.ScriptHash().BytesBE(), []byte("neo.com"))
|
||||
cs, cs2 := getTestContractState(bc) // cs2 doesn't have OnNEP11Transfer
|
||||
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
||||
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs2))
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, to, "transfer",
|
||||
nil, cs2.Hash.BytesBE(), []byte("neo.com"), nil)
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, to, "transfer",
|
||||
true, cs.Hash.BytesBE(), []byte("neo.com"), nil)
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "totalSupply", 1)
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "ownerOf",
|
||||
cs.Hash.BytesBE(), []byte("neo.com"))
|
||||
}
|
||||
|
||||
func TestTokensOf(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
acc1 := newAccountWithGAS(t, bc)
|
||||
acc2 := newAccountWithGAS(t, bc)
|
||||
|
||||
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc1, "register",
|
||||
true, "neo.com", acc1.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc2, "register",
|
||||
true, "nspcc.com", acc2.PrivateKey().GetScriptHash())
|
||||
|
||||
testTokensOf(t, bc, acc1, [][]byte{[]byte("neo.com")}, acc1.Contract.ScriptHash().BytesBE())
|
||||
testTokensOf(t, bc, acc1, [][]byte{[]byte("nspcc.com")}, acc2.Contract.ScriptHash().BytesBE())
|
||||
testTokensOf(t, bc, acc1, [][]byte{[]byte("neo.com"), []byte("nspcc.com")})
|
||||
testTokensOf(t, bc, acc1, nil, util.Uint160{}.BytesBE())
|
||||
}
|
||||
|
||||
func testTokensOf(t *testing.T, bc *Blockchain, signer *wallet.Account, result [][]byte, args ...interface{}) {
|
||||
method := "tokensOf"
|
||||
if len(args) == 0 {
|
||||
method = "tokens"
|
||||
}
|
||||
w := io.NewBufBinWriter()
|
||||
emit.AppCall(w.BinWriter, bc.contracts.NameService.Hash, method, callflag.All, args...)
|
||||
for range result {
|
||||
emit.Opcodes(w.BinWriter, opcode.DUP)
|
||||
emit.Syscall(w.BinWriter, interopnames.SystemIteratorNext)
|
||||
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
||||
|
||||
emit.Opcodes(w.BinWriter, opcode.DUP)
|
||||
emit.Syscall(w.BinWriter, interopnames.SystemIteratorValue)
|
||||
emit.Opcodes(w.BinWriter, opcode.SWAP)
|
||||
}
|
||||
emit.Opcodes(w.BinWriter, opcode.DROP)
|
||||
emit.Int(w.BinWriter, int64(len(result)))
|
||||
emit.Opcodes(w.BinWriter, opcode.PACK)
|
||||
require.NoError(t, w.Err)
|
||||
script := w.Bytes()
|
||||
tx := transaction.New(script, defaultNameServiceSysfee)
|
||||
tx.ValidUntilBlock = bc.BlockHeight() + 1
|
||||
signTxWithAccounts(bc, tx, signer)
|
||||
aers, err := persistBlock(bc, tx)
|
||||
require.NoError(t, err)
|
||||
if result == nil {
|
||||
checkFAULTState(t, aers[0])
|
||||
return
|
||||
}
|
||||
arr := make([]stackitem.Item, 0, len(result))
|
||||
for i := len(result) - 1; i >= 0; i-- {
|
||||
arr = append(arr, stackitem.Make(result[i]))
|
||||
}
|
||||
checkResult(t, aers[0], stackitem.NewArray(arr))
|
||||
}
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
bc := newTestChain(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
acc := newAccountWithGAS(t, bc)
|
||||
|
||||
testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com")
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc, "register",
|
||||
true, "neo.com", acc.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(nnsrecords.A), "1.2.3.4")
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(nnsrecords.CNAME), "alias.com")
|
||||
|
||||
testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc, "register",
|
||||
true, "alias.com", acc.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
|
||||
"alias.com", int64(nnsrecords.TXT), "sometxt")
|
||||
|
||||
testNameServiceInvoke(t, bc, "resolve", "1.2.3.4",
|
||||
"neo.com", int64(nnsrecords.A))
|
||||
testNameServiceInvoke(t, bc, "resolve", "alias.com",
|
||||
"neo.com", int64(nnsrecords.CNAME))
|
||||
testNameServiceInvoke(t, bc, "resolve", "sometxt",
|
||||
"neo.com", int64(nnsrecords.TXT))
|
||||
testNameServiceInvoke(t, bc, "resolve", stackitem.Null{},
|
||||
"neo.com", int64(nnsrecords.AAAA))
|
||||
}
|
||||
|
||||
const (
|
||||
defaultNameServiceSysfee = 4000_0000
|
||||
defaultRegisterSysfee = 10_0000_0000 + native.DefaultDomainPrice
|
||||
)
|
||||
|
||||
func testNameServiceInvoke(t *testing.T, bc *Blockchain, method string, result interface{}, args ...interface{}) {
|
||||
testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, true, method, result, args...)
|
||||
}
|
||||
|
||||
func testNameServiceInvokeAux(t *testing.T, bc *Blockchain, sysfee int64, signer interface{}, method string, result interface{}, args ...interface{}) {
|
||||
if sysfee < 0 {
|
||||
sysfee = defaultNameServiceSysfee
|
||||
}
|
||||
aer, err := invokeContractMethodGeneric(bc, sysfee, bc.contracts.NameService.Hash, method, signer, args...)
|
||||
require.NoError(t, err)
|
||||
if result == nil {
|
||||
checkFAULTState(t, aer)
|
||||
return
|
||||
}
|
||||
checkResult(t, aer, stackitem.Make(result))
|
||||
}
|
||||
|
||||
func newAccountWithGAS(t *testing.T, bc *Blockchain) *wallet.Account {
|
||||
acc, err := wallet.NewAccount()
|
||||
require.NoError(t, err)
|
||||
transferTokenFromMultisigAccount(t, bc, acc.PrivateKey().GetScriptHash(), bc.contracts.GAS.Hash, 1000_00000000)
|
||||
return acc
|
||||
}
|
468
pkg/core/nonnative_name_service_test.go
Normal file
468
pkg/core/nonnative_name_service_test.go
Normal file
|
@ -0,0 +1,468 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
|
||||
"github.com/nspcc-dev/neo-go/internal/testchain"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNameService_Price(t *testing.T) {
|
||||
bc, nsHash := newTestChainWithNS(t)
|
||||
|
||||
testGetSet(t, bc, nsHash, "Price",
|
||||
defaultNameServiceDomainPrice, 0, 10000_00000000)
|
||||
}
|
||||
|
||||
func TestNonfungible(t *testing.T) {
|
||||
bc, nsHash := newTestChainWithNS(t)
|
||||
|
||||
acc := newAccountWithGAS(t, bc)
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "symbol", "NNS")
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "decimals", 0)
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "totalSupply", 0)
|
||||
}
|
||||
|
||||
func TestAddRoot(t *testing.T) {
|
||||
bc, nsHash := newTestChainWithNS(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
|
||||
t.Run("invalid format", func(t *testing.T) {
|
||||
testNameServiceInvoke(t, bc, nsHash, "addRoot", nil, "")
|
||||
})
|
||||
t.Run("not signed by committee", func(t *testing.T) {
|
||||
aer, err := invokeContractMethod(bc, 1000_0000, nsHash, "addRoot", "some")
|
||||
require.NoError(t, err)
|
||||
checkFAULTState(t, aer)
|
||||
})
|
||||
|
||||
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "some")
|
||||
t.Run("already exists", func(t *testing.T) {
|
||||
testNameServiceInvoke(t, bc, nsHash, "addRoot", nil, "some")
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpiration(t *testing.T) {
|
||||
bc, nsHash := newTestChainWithNS(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
acc := newAccountWithGAS(t, bc)
|
||||
|
||||
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register",
|
||||
true, "first.com", acc.Contract.ScriptHash())
|
||||
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc,
|
||||
"setRecord", stackitem.Null{}, "first.com", int64(nns.TXT), "sometext")
|
||||
b1 := bc.topBlock.Load().(*block.Block)
|
||||
|
||||
tx, err := prepareContractMethodInvokeGeneric(bc, defaultRegisterSysfee, nsHash,
|
||||
"register", acc, "second.com", acc.Contract.ScriptHash())
|
||||
require.NoError(t, err)
|
||||
b2 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
|
||||
b.Index = b1.Index + 1
|
||||
b.PrevHash = b1.Hash()
|
||||
b.Timestamp = b1.Timestamp + 10000
|
||||
}, tx)
|
||||
require.NoError(t, bc.AddBlock(b2))
|
||||
checkTxHalt(t, bc, tx.Hash())
|
||||
|
||||
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash,
|
||||
"isAvailable", acc, "first.com")
|
||||
require.NoError(t, err)
|
||||
b3 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
|
||||
b.Index = b2.Index + 1
|
||||
b.PrevHash = b2.Hash()
|
||||
b.Timestamp = b1.Timestamp + (millisecondsInYear + 1)
|
||||
}, tx)
|
||||
require.NoError(t, bc.AddBlock(b3))
|
||||
aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
|
||||
require.NoError(t, err)
|
||||
checkResult(t, &aer[0], stackitem.NewBool(true))
|
||||
|
||||
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash,
|
||||
"isAvailable", acc, "second.com")
|
||||
require.NoError(t, err)
|
||||
b4 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
|
||||
b.Index = b3.Index + 1
|
||||
b.PrevHash = b3.Hash()
|
||||
b.Timestamp = b3.Timestamp + 1000
|
||||
}, tx)
|
||||
require.NoError(t, bc.AddBlock(b4))
|
||||
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
|
||||
require.NoError(t, err)
|
||||
checkResult(t, &aer[0], stackitem.NewBool(false))
|
||||
|
||||
tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, nsHash,
|
||||
"getRecord", acc, "first.com", int64(nns.TXT))
|
||||
require.NoError(t, err)
|
||||
b5 := newBlockCustom(bc.GetConfig(), func(b *block.Block) {
|
||||
b.Index = b4.Index + 1
|
||||
b.PrevHash = b4.Hash()
|
||||
b.Timestamp = b4.Timestamp + 1000
|
||||
}, tx)
|
||||
require.NoError(t, bc.AddBlock(b5))
|
||||
aer, err = bc.GetAppExecResults(tx.Hash(), trigger.Application)
|
||||
require.NoError(t, err)
|
||||
checkFAULTState(t, &aer[0]) // name has expired (panic)
|
||||
}
|
||||
|
||||
const millisecondsInYear = 365 * 24 * 3600 * 1000
|
||||
|
||||
func TestRegisterAndRenew(t *testing.T) {
|
||||
bc, nsHash := newTestChainWithNS(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
|
||||
testNameServiceInvoke(t, bc, nsHash, "isAvailable", nil, "neo.com")
|
||||
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "org")
|
||||
testNameServiceInvoke(t, bc, nsHash, "isAvailable", nil, "neo.com")
|
||||
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
|
||||
testNameServiceInvoke(t, bc, nsHash, "isAvailable", true, "neo.com")
|
||||
testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.org", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, nsHash, "register", nil, "docs.neo.org", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, nsHash, "register", nil, "\nneo.com'", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.com\n", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, nsHash, "register", nil, "neo.com", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceDomainPrice, true, "register",
|
||||
nil, "neo.com", testchain.CommitteeScriptHash())
|
||||
|
||||
testNameServiceInvoke(t, bc, nsHash, "isAvailable", true, "neo.com")
|
||||
testNameServiceInvoke(t, bc, nsHash, "balanceOf", 0, testchain.CommitteeScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register",
|
||||
true, "neo.com", testchain.CommitteeScriptHash())
|
||||
topBlock := bc.topBlock.Load().(*block.Block)
|
||||
expectedExpiration := topBlock.Timestamp + millisecondsInYear
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register",
|
||||
false, "neo.com", testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, nsHash, "isAvailable", false, "neo.com")
|
||||
|
||||
props := stackitem.NewMap()
|
||||
props.Add(stackitem.Make("name"), stackitem.Make("neo.com"))
|
||||
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
|
||||
testNameServiceInvoke(t, bc, nsHash, "properties", props, "neo.com")
|
||||
testNameServiceInvoke(t, bc, nsHash, "balanceOf", 1, testchain.CommitteeScriptHash())
|
||||
testNameServiceInvoke(t, bc, nsHash, "ownerOf", testchain.CommitteeScriptHash().BytesBE(), []byte("neo.com"))
|
||||
|
||||
t.Run("invalid token ID", func(t *testing.T) {
|
||||
testNameServiceInvoke(t, bc, nsHash, "properties", nil, "not.exists")
|
||||
testNameServiceInvoke(t, bc, nsHash, "ownerOf", nil, "not.exists")
|
||||
testNameServiceInvoke(t, bc, nsHash, "properties", nil, []interface{}{})
|
||||
testNameServiceInvoke(t, bc, nsHash, "ownerOf", nil, []interface{}{})
|
||||
})
|
||||
|
||||
// Renew
|
||||
expectedExpiration += millisecondsInYear
|
||||
testNameServiceInvokeAux(t, bc, nsHash, 100_0000_0000, true, "renew", expectedExpiration, "neo.com")
|
||||
|
||||
props.Add(stackitem.Make("expiration"), stackitem.Make(expectedExpiration))
|
||||
testNameServiceInvoke(t, bc, nsHash, "properties", props, "neo.com")
|
||||
}
|
||||
|
||||
func TestSetGetRecord(t *testing.T) {
|
||||
bc, nsHash := newTestChainWithNS(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
acc := newAccountWithGAS(t, bc)
|
||||
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
|
||||
|
||||
t.Run("set before register", func(t *testing.T) {
|
||||
testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(nns.TXT), "sometext")
|
||||
})
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "register",
|
||||
true, "neo.com", testchain.CommitteeScriptHash())
|
||||
t.Run("invalid parameters", func(t *testing.T) {
|
||||
testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(0xFF), "1.2.3.4")
|
||||
testNameServiceInvoke(t, bc, nsHash, "setRecord", nil, "neo.com", int64(nns.A), "not.an.ip.address")
|
||||
})
|
||||
t.Run("invalid witness", func(t *testing.T) {
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil,
|
||||
"neo.com", int64(nns.A), "1.2.3.4")
|
||||
})
|
||||
testNameServiceInvoke(t, bc, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.A))
|
||||
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4")
|
||||
testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A))
|
||||
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.A), "1.2.3.4")
|
||||
testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A))
|
||||
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.AAAA), "2002:0000:1f1f:0000:0000:0100:11a0:addf")
|
||||
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME), "nspcc.ru")
|
||||
testNameServiceInvoke(t, bc, nsHash, "setRecord", stackitem.Null{}, "neo.com", int64(nns.TXT), "sometext")
|
||||
|
||||
// Delete record.
|
||||
t.Run("invalid witness", func(t *testing.T) {
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", nil,
|
||||
"neo.com", int64(nns.CNAME))
|
||||
})
|
||||
testNameServiceInvoke(t, bc, nsHash, "getRecord", "nspcc.ru", "neo.com", int64(nns.CNAME))
|
||||
testNameServiceInvoke(t, bc, nsHash, "deleteRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME))
|
||||
testNameServiceInvoke(t, bc, nsHash, "getRecord", stackitem.Null{}, "neo.com", int64(nns.CNAME))
|
||||
testNameServiceInvoke(t, bc, nsHash, "getRecord", "1.2.3.4", "neo.com", int64(nns.A))
|
||||
|
||||
t.Run("SetRecord_compatibility", func(t *testing.T) {
|
||||
// tests are got from the NNS C# implementation and changed accordingly to non-native implementation behaviour
|
||||
testCases := []struct {
|
||||
Type nns.RecordType
|
||||
Name string
|
||||
ShouldFail bool
|
||||
}{
|
||||
{Type: nns.A, Name: "0.0.0.0", ShouldFail: true},
|
||||
{Type: nns.A, Name: "1.1.0.1"},
|
||||
{Type: nns.A, Name: "10.10.10.10", ShouldFail: true},
|
||||
{Type: nns.A, Name: "255.255.255.255", ShouldFail: true},
|
||||
{Type: nns.A, Name: "192.168.1.1", ShouldFail: true},
|
||||
{Type: nns.A, Name: "1a", ShouldFail: true},
|
||||
{Type: nns.A, Name: "256.0.0.0", ShouldFail: true},
|
||||
{Type: nns.A, Name: "01.01.01.01", ShouldFail: true},
|
||||
{Type: nns.A, Name: "00.0.0.0", ShouldFail: true},
|
||||
{Type: nns.A, Name: "0.0.0.-1", ShouldFail: true},
|
||||
{Type: nns.A, Name: "0.0.0.0.1", ShouldFail: true},
|
||||
{Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true},
|
||||
{Type: nns.A, Name: "11111111.11111111.11111111.11111111", ShouldFail: true},
|
||||
{Type: nns.A, Name: "ff.ff.ff.ff", ShouldFail: true},
|
||||
{Type: nns.A, Name: "0.0.256", ShouldFail: true},
|
||||
{Type: nns.A, Name: "0.0.0", ShouldFail: true},
|
||||
{Type: nns.A, Name: "0.257", ShouldFail: true},
|
||||
{Type: nns.A, Name: "1.1", ShouldFail: true},
|
||||
{Type: nns.A, Name: "257", ShouldFail: true},
|
||||
{Type: nns.A, Name: "1", ShouldFail: true},
|
||||
{Type: nns.AAAA, Name: "2001:db8::8:800:200c:417a", ShouldFail: true},
|
||||
{Type: nns.AAAA, Name: "ff01:db8::8:800:200c:417a"},
|
||||
{Type: nns.AAAA, Name: "ff01::101"},
|
||||
{Type: nns.AAAA, Name: "::1"},
|
||||
{Type: nns.AAAA, Name: "::"},
|
||||
{Type: nns.AAAA, Name: "2001:db8:0:0:8:800:200c:417a", ShouldFail: true},
|
||||
{Type: nns.AAAA, Name: "ff01:db8:0:0:8:800:200c:417a"},
|
||||
{Type: nns.AAAA, Name: "ff01:0:0:0:0:0:0:101"},
|
||||
{Type: nns.AAAA, Name: "2001:0:0:0:0:0:0:101", ShouldFail: true},
|
||||
{Type: nns.AAAA, Name: "ff01:0:0:0:0:0:0:101"},
|
||||
{Type: nns.AAAA, Name: "0:0:0:0:0:0:0:1"},
|
||||
{Type: nns.AAAA, Name: "2001:0:0:0:0:0:0:1", ShouldFail: true},
|
||||
{Type: nns.AAAA, Name: "0:0:0:0:0:0:0:0"},
|
||||
{Type: nns.AAAA, Name: "2001:0:0:0:0:0:0:0", ShouldFail: true},
|
||||
{Type: nns.AAAA, Name: "2001:DB8::8:800:200C:417A", ShouldFail: true},
|
||||
{Type: nns.AAAA, Name: "FF01:DB8::8:800:200C:417A"},
|
||||
{Type: nns.AAAA, Name: "FF01::101"},
|
||||
{Type: nns.AAAA, Name: "fF01::101"},
|
||||
{Type: nns.AAAA, Name: "2001:DB8:0:0:8:800:200C:417A", ShouldFail: true},
|
||||
{Type: nns.AAAA, Name: "FF01:DB8:0:0:8:800:200C:417A"},
|
||||
{Type: nns.AAAA, Name: "FF01:0:0:0:0:0:0:101"},
|
||||
{Type: nns.AAAA, Name: "::ffff:1.01.1.01", ShouldFail: true},
|
||||
{Type: nns.AAAA, Name: "2001:DB8:0:0:8:800:200C:4Z", ShouldFail: true},
|
||||
{Type: nns.AAAA, Name: "::13.1.68.3", ShouldFail: true},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
var expected interface{}
|
||||
if testCase.ShouldFail {
|
||||
expected = nil
|
||||
} else {
|
||||
expected = stackitem.Null{}
|
||||
}
|
||||
t.Run(testCase.Name, func(t *testing.T) {
|
||||
testNameServiceInvoke(t, bc, nsHash, "setRecord", expected, "neo.com", int64(testCase.Type), testCase.Name)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetAdmin(t *testing.T) {
|
||||
bc, nsHash := newTestChainWithNS(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
owner := newAccountWithGAS(t, bc)
|
||||
admin := newAccountWithGAS(t, bc)
|
||||
guest := newAccountWithGAS(t, bc)
|
||||
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
|
||||
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, owner, "register", true,
|
||||
"neo.com", owner.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, guest, "setAdmin", nil,
|
||||
"neo.com", admin.PrivateKey().GetScriptHash())
|
||||
|
||||
// Must be witnessed by both owner and admin.
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, owner, "setAdmin", nil,
|
||||
"neo.com", admin.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setAdmin", nil,
|
||||
"neo.com", admin.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, []*wallet.Account{owner, admin},
|
||||
"setAdmin", stackitem.Null{},
|
||||
"neo.com", admin.PrivateKey().GetScriptHash())
|
||||
|
||||
t.Run("set and delete by admin", func(t *testing.T) {
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(nns.TXT), "sometext")
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, guest, "deleteRecord", nil,
|
||||
"neo.com", int64(nns.TXT))
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", stackitem.Null{},
|
||||
"neo.com", int64(nns.TXT))
|
||||
})
|
||||
|
||||
t.Run("set admin to null", func(t *testing.T) {
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(nns.TXT), "sometext")
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, owner, "setAdmin", stackitem.Null{},
|
||||
"neo.com", nil)
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, admin, "deleteRecord", nil,
|
||||
"neo.com", int64(nns.TXT))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTransfer(t *testing.T) {
|
||||
bc, nsHash := newTestChainWithNS(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
from := newAccountWithGAS(t, bc)
|
||||
to := newAccountWithGAS(t, bc)
|
||||
|
||||
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "register",
|
||||
true, "neo.com", from.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(nns.A), "1.2.3.4")
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "transfer",
|
||||
nil, to.Contract.ScriptHash().BytesBE(), []byte("not.exists"), nil)
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, true, "transfer",
|
||||
false, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil)
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, from, "transfer",
|
||||
true, to.Contract.ScriptHash().BytesBE(), []byte("neo.com"), nil)
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1)
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "ownerOf",
|
||||
to.Contract.ScriptHash().BytesBE(), []byte("neo.com"))
|
||||
cs, cs2 := getTestContractState(bc) // cs2 doesn't have OnNEP11Transfer
|
||||
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs))
|
||||
require.NoError(t, bc.contracts.Management.PutContractState(bc.dao, cs2))
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, to, "transfer",
|
||||
nil, cs2.Hash.BytesBE(), []byte("neo.com"), nil)
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, to, "transfer",
|
||||
true, cs.Hash.BytesBE(), []byte("neo.com"), nil)
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "totalSupply", 1)
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, from, "ownerOf",
|
||||
cs.Hash.BytesBE(), []byte("neo.com"))
|
||||
}
|
||||
|
||||
func TestTokensOf(t *testing.T) {
|
||||
bc, nsHash := newTestChainWithNS(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
acc1 := newAccountWithGAS(t, bc)
|
||||
acc2 := newAccountWithGAS(t, bc)
|
||||
|
||||
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc1, "register",
|
||||
true, "neo.com", acc1.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc2, "register",
|
||||
true, "nspcc.com", acc2.PrivateKey().GetScriptHash())
|
||||
|
||||
testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("neo.com")}, acc1.Contract.ScriptHash().BytesBE())
|
||||
testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("nspcc.com")}, acc2.Contract.ScriptHash().BytesBE())
|
||||
testTokensOf(t, bc, nsHash, acc1, [][]byte{[]byte("neo.com"), []byte("nspcc.com")})
|
||||
testTokensOf(t, bc, nsHash, acc1, [][]byte{}, util.Uint160{}.BytesBE()) // empty hash is a valid hash still
|
||||
}
|
||||
|
||||
func testTokensOf(t *testing.T, bc *Blockchain, nsHash util.Uint160, signer *wallet.Account, result [][]byte, args ...interface{}) {
|
||||
method := "tokensOf"
|
||||
if len(args) == 0 {
|
||||
method = "tokens"
|
||||
}
|
||||
w := io.NewBufBinWriter()
|
||||
emit.AppCall(w.BinWriter, nsHash, method, callflag.All, args...)
|
||||
for range result {
|
||||
emit.Opcodes(w.BinWriter, opcode.DUP)
|
||||
emit.Syscall(w.BinWriter, interopnames.SystemIteratorNext)
|
||||
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
||||
|
||||
emit.Opcodes(w.BinWriter, opcode.DUP)
|
||||
emit.Syscall(w.BinWriter, interopnames.SystemIteratorValue)
|
||||
emit.Opcodes(w.BinWriter, opcode.SWAP)
|
||||
}
|
||||
emit.Opcodes(w.BinWriter, opcode.DROP)
|
||||
emit.Int(w.BinWriter, int64(len(result)))
|
||||
emit.Opcodes(w.BinWriter, opcode.PACK)
|
||||
require.NoError(t, w.Err)
|
||||
script := w.Bytes()
|
||||
tx := transaction.New(script, defaultNameServiceSysfee)
|
||||
tx.ValidUntilBlock = bc.BlockHeight() + 1
|
||||
signTxWithAccounts(bc, tx, signer)
|
||||
aers, err := persistBlock(bc, tx)
|
||||
require.NoError(t, err)
|
||||
if result == nil {
|
||||
checkFAULTState(t, aers[0])
|
||||
return
|
||||
}
|
||||
arr := make([]stackitem.Item, 0, len(result))
|
||||
for i := len(result) - 1; i >= 0; i-- {
|
||||
arr = append(arr, stackitem.Make(result[i]))
|
||||
}
|
||||
checkResult(t, aers[0], stackitem.NewArray(arr))
|
||||
}
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
bc, nsHash := newTestChainWithNS(t)
|
||||
|
||||
transferFundsToCommittee(t, bc)
|
||||
acc := newAccountWithGAS(t, bc)
|
||||
|
||||
testNameServiceInvoke(t, bc, nsHash, "addRoot", stackitem.Null{}, "com")
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register",
|
||||
true, "neo.com", acc.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(nns.A), "1.2.3.4")
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
|
||||
"neo.com", int64(nns.CNAME), "alias.com")
|
||||
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultRegisterSysfee, acc, "register",
|
||||
true, "alias.com", acc.PrivateKey().GetScriptHash())
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{},
|
||||
"alias.com", int64(nns.TXT), "sometxt")
|
||||
|
||||
testNameServiceInvoke(t, bc, nsHash, "resolve", "1.2.3.4",
|
||||
"neo.com", int64(nns.A))
|
||||
testNameServiceInvoke(t, bc, nsHash, "resolve", "alias.com",
|
||||
"neo.com", int64(nns.CNAME))
|
||||
testNameServiceInvoke(t, bc, nsHash, "resolve", "sometxt",
|
||||
"neo.com", int64(nns.TXT))
|
||||
testNameServiceInvoke(t, bc, nsHash, "resolve", stackitem.Null{},
|
||||
"neo.com", int64(nns.AAAA))
|
||||
}
|
||||
|
||||
const (
|
||||
defaultNameServiceDomainPrice = 10_0000_0000
|
||||
defaultNameServiceSysfee = 4000_0000
|
||||
defaultRegisterSysfee = 10_0000_0000 + defaultNameServiceDomainPrice
|
||||
)
|
||||
|
||||
func testNameServiceInvoke(t *testing.T, bc *Blockchain, nsHash util.Uint160, method string, result interface{}, args ...interface{}) {
|
||||
testNameServiceInvokeAux(t, bc, nsHash, defaultNameServiceSysfee, true, method, result, args...)
|
||||
}
|
||||
|
||||
func testNameServiceInvokeAux(t *testing.T, bc *Blockchain, nsHash util.Uint160, sysfee int64, signer interface{}, method string, result interface{}, args ...interface{}) {
|
||||
if sysfee < 0 {
|
||||
sysfee = defaultNameServiceSysfee
|
||||
}
|
||||
aer, err := invokeContractMethodGeneric(bc, sysfee, nsHash, method, signer, args...)
|
||||
require.NoError(t, err)
|
||||
if result == nil {
|
||||
checkFAULTState(t, aer)
|
||||
return
|
||||
}
|
||||
checkResult(t, aer, stackitem.Make(result))
|
||||
}
|
||||
|
||||
func newAccountWithGAS(t *testing.T, bc *Blockchain) *wallet.Account {
|
||||
acc, err := wallet.NewAccount()
|
||||
require.NoError(t, err)
|
||||
transferTokenFromMultisigAccount(t, bc, acc.PrivateKey().GetScriptHash(), bc.contracts.GAS.Hash, 1000_00000000)
|
||||
return acc
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sort"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
)
|
||||
|
||||
// NFTTokenState represents state of nonfungible token.
|
||||
type NFTTokenState struct {
|
||||
Owner util.Uint160
|
||||
Name string
|
||||
}
|
||||
|
||||
// NFTAccountState represents state of nonfunglible account.
|
||||
type NFTAccountState struct {
|
||||
NEP17BalanceState
|
||||
Tokens [][]byte
|
||||
}
|
||||
|
||||
// Base returns base class.
|
||||
func (s *NFTTokenState) Base() *NFTTokenState {
|
||||
return s
|
||||
}
|
||||
|
||||
// ToStackItem converts NFTTokenState to stackitem.
|
||||
func (s *NFTTokenState) ToStackItem() stackitem.Item {
|
||||
owner := s.Owner
|
||||
return stackitem.NewStruct([]stackitem.Item{
|
||||
stackitem.NewByteArray(owner.BytesBE()),
|
||||
stackitem.NewByteArray([]byte(s.Name)),
|
||||
})
|
||||
}
|
||||
|
||||
// EncodeBinary implements io.Serializable.
|
||||
func (s *NFTTokenState) EncodeBinary(w *io.BinWriter) {
|
||||
stackitem.EncodeBinaryStackItem(s.ToStackItem(), w)
|
||||
}
|
||||
|
||||
// FromStackItem converts stackitem to NFTTokenState.
|
||||
func (s *NFTTokenState) FromStackItem(item stackitem.Item) error {
|
||||
arr, ok := item.Value().([]stackitem.Item)
|
||||
if !ok || len(arr) < 2 {
|
||||
return errors.New("invalid stack item")
|
||||
}
|
||||
|
||||
bs, err := arr[0].TryBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
owner, err := util.Uint160DecodeBytesBE(bs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := stackitem.ToString(arr[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Owner = owner
|
||||
s.Name = name
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeBinary implements io.Serializable.
|
||||
func (s *NFTTokenState) DecodeBinary(r *io.BinReader) {
|
||||
item := stackitem.DecodeBinaryStackItem(r)
|
||||
if r.Err == nil {
|
||||
r.Err = s.FromStackItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
// ToMap converts NFTTokenState to Map stackitem.
|
||||
func (s *NFTTokenState) ToMap() *stackitem.Map {
|
||||
return stackitem.NewMapWithValue([]stackitem.MapElement{
|
||||
{
|
||||
Key: stackitem.NewByteArray([]byte("name")),
|
||||
Value: stackitem.NewByteArray([]byte(s.Name)),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ID returns token id.
|
||||
func (s *NFTTokenState) ID() []byte {
|
||||
return []byte(s.Name)
|
||||
}
|
||||
|
||||
// ToStackItem converts NFTAccountState to stackitem.
|
||||
func (s *NFTAccountState) ToStackItem() stackitem.Item {
|
||||
st := s.NEP17BalanceState.toStackItem().(*stackitem.Struct)
|
||||
arr := make([]stackitem.Item, len(s.Tokens))
|
||||
for i := range arr {
|
||||
arr[i] = stackitem.NewByteArray(s.Tokens[i])
|
||||
}
|
||||
st.Append(stackitem.NewArray(arr))
|
||||
return st
|
||||
}
|
||||
|
||||
// FromStackItem converts stackitem to NFTAccountState.
|
||||
func (s *NFTAccountState) FromStackItem(item stackitem.Item) error {
|
||||
s.NEP17BalanceState.fromStackItem(item)
|
||||
arr := item.Value().([]stackitem.Item)
|
||||
if len(arr) < 2 {
|
||||
return errors.New("invalid stack item")
|
||||
}
|
||||
arr, ok := arr[1].Value().([]stackitem.Item)
|
||||
if !ok {
|
||||
return errors.New("invalid stack item")
|
||||
}
|
||||
s.Tokens = make([][]byte, len(arr))
|
||||
for i := range s.Tokens {
|
||||
bs, err := arr[i].TryBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Tokens[i] = bs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeBinary implements io.Serializable.
|
||||
func (s *NFTAccountState) EncodeBinary(w *io.BinWriter) {
|
||||
stackitem.EncodeBinaryStackItem(s.ToStackItem(), w)
|
||||
}
|
||||
|
||||
// DecodeBinary implements io.Serializable.
|
||||
func (s *NFTAccountState) DecodeBinary(r *io.BinReader) {
|
||||
item := stackitem.DecodeBinaryStackItem(r)
|
||||
if r.Err == nil {
|
||||
r.Err = s.FromStackItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NFTAccountState) index(tokenID []byte) (int, bool) {
|
||||
lt := len(s.Tokens)
|
||||
index := sort.Search(lt, func(i int) bool {
|
||||
return bytes.Compare(s.Tokens[i], tokenID) >= 0
|
||||
})
|
||||
return index, index < lt && bytes.Equal(s.Tokens[index], tokenID)
|
||||
}
|
||||
|
||||
// Add adds token id to the set of account tokens
|
||||
// and returns true on success.
|
||||
func (s *NFTAccountState) Add(tokenID []byte) bool {
|
||||
index, isPresent := s.index(tokenID)
|
||||
if isPresent {
|
||||
return false
|
||||
}
|
||||
|
||||
s.Balance.Add(&s.Balance, big.NewInt(1))
|
||||
s.Tokens = append(s.Tokens, []byte{})
|
||||
copy(s.Tokens[index+1:], s.Tokens[index:])
|
||||
s.Tokens[index] = tokenID
|
||||
return true
|
||||
}
|
||||
|
||||
// Remove removes token id to the set of account tokens
|
||||
// and returns true on success.
|
||||
func (s *NFTAccountState) Remove(tokenID []byte) bool {
|
||||
index, isPresent := s.index(tokenID)
|
||||
if !isPresent {
|
||||
return false
|
||||
}
|
||||
|
||||
s.Balance.Sub(&s.Balance, big.NewInt(1))
|
||||
copy(s.Tokens[index:], s.Tokens[index+1:])
|
||||
s.Tokens = s.Tokens[:len(s.Tokens)-1]
|
||||
return true
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/internal/random"
|
||||
"github.com/nspcc-dev/neo-go/internal/testserdes"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newStruct(args ...interface{}) *stackitem.Struct {
|
||||
arr := make([]stackitem.Item, len(args))
|
||||
for i := range args {
|
||||
arr[i] = stackitem.Make(args[i])
|
||||
}
|
||||
return stackitem.NewStruct(arr)
|
||||
}
|
||||
|
||||
func TestNFTTokenState_Serializable(t *testing.T) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
s := &NFTTokenState{
|
||||
Owner: random.Uint160(),
|
||||
Name: "random name",
|
||||
}
|
||||
id := s.ID()
|
||||
actual := new(NFTTokenState)
|
||||
testserdes.EncodeDecodeBinary(t, s, actual)
|
||||
require.Equal(t, id, actual.ID())
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
errCases := []struct {
|
||||
name string
|
||||
item stackitem.Item
|
||||
}{
|
||||
{"invalid type", stackitem.NewByteArray([]byte{1, 2, 3})},
|
||||
{"invalid owner type",
|
||||
newStruct(stackitem.NewArray(nil), "name", "desc")},
|
||||
{"invalid owner uint160", newStruct("123", "name", "desc")},
|
||||
{"invalid name",
|
||||
newStruct(random.Uint160().BytesBE(), []byte{0x80}, "desc")},
|
||||
}
|
||||
|
||||
for _, tc := range errCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
w := io.NewBufBinWriter()
|
||||
stackitem.EncodeBinaryStackItem(tc.item, w.BinWriter)
|
||||
require.NoError(t, w.Err)
|
||||
require.Error(t, testserdes.DecodeBinary(w.Bytes(), new(NFTTokenState)))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNFTTokenState_ToMap(t *testing.T) {
|
||||
s := &NFTTokenState{
|
||||
Owner: random.Uint160(),
|
||||
Name: "random name",
|
||||
}
|
||||
m := s.ToMap()
|
||||
|
||||
elems := m.Value().([]stackitem.MapElement)
|
||||
i := m.Index(stackitem.Make("name"))
|
||||
require.True(t, i < len(elems))
|
||||
require.Equal(t, []byte("random name"), elems[i].Value.Value())
|
||||
}
|
||||
|
||||
func TestNFTAccountState_Serializable(t *testing.T) {
|
||||
t.Run("good", func(t *testing.T) {
|
||||
s := &NFTAccountState{
|
||||
NEP17BalanceState: NEP17BalanceState{
|
||||
Balance: *big.NewInt(10),
|
||||
},
|
||||
Tokens: [][]byte{
|
||||
{1, 2, 3},
|
||||
{3},
|
||||
},
|
||||
}
|
||||
testserdes.EncodeDecodeBinary(t, s, new(NFTAccountState))
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
errCases := []struct {
|
||||
name string
|
||||
item stackitem.Item
|
||||
}{
|
||||
{"small size", newStruct(42)},
|
||||
{"not an array", newStruct(11, stackitem.NewByteArray([]byte{}))},
|
||||
{"not an array",
|
||||
newStruct(11, stackitem.NewArray([]stackitem.Item{
|
||||
stackitem.NewArray(nil),
|
||||
}))},
|
||||
}
|
||||
|
||||
for _, tc := range errCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
w := io.NewBufBinWriter()
|
||||
stackitem.EncodeBinaryStackItem(tc.item, w.BinWriter)
|
||||
require.NoError(t, w.Err)
|
||||
require.Error(t, testserdes.DecodeBinary(w.Bytes(), new(NFTAccountState)))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNFTAccountState_AddRemove(t *testing.T) {
|
||||
var s NFTAccountState
|
||||
require.True(t, s.Add([]byte{1, 2, 3}))
|
||||
require.EqualValues(t, 1, s.Balance.Int64())
|
||||
require.True(t, s.Add([]byte{1}))
|
||||
require.EqualValues(t, 2, s.Balance.Int64())
|
||||
|
||||
require.False(t, s.Add([]byte{1, 2, 3}))
|
||||
require.EqualValues(t, 2, s.Balance.Int64())
|
||||
|
||||
require.True(t, s.Remove([]byte{1}))
|
||||
require.EqualValues(t, 1, s.Balance.Int64())
|
||||
require.False(t, s.Remove([]byte{1}))
|
||||
require.EqualValues(t, 1, s.Balance.Int64())
|
||||
require.True(t, s.Remove([]byte{1, 2, 3}))
|
||||
require.EqualValues(t, 0, s.Balance.Int64())
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
/*
|
||||
Package nameservice provides interface to NameService native contract.
|
||||
It's a NEP-11 contract implementing a domain name service.
|
||||
*/
|
||||
package nameservice
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
|
||||
)
|
||||
|
||||
// RecordType represents NameService record type.
|
||||
type RecordType byte
|
||||
|
||||
// Various record type.
|
||||
const (
|
||||
TypeA RecordType = 1
|
||||
TypeCNAME RecordType = 5
|
||||
TypeTXT RecordType = 16
|
||||
TypeAAAA RecordType = 28
|
||||
)
|
||||
|
||||
// Hash represents NameService contract hash.
|
||||
const Hash = "\x6b\x59\x2b\x87\x66\xcc\x45\x8e\xfa\x7a\x90\x47\x56\x62\xcd\x92\x03\xcf\x8f\x7a"
|
||||
|
||||
// Symbol represents `symbol` method of NameService native contract.
|
||||
func Symbol() string {
|
||||
return contract.Call(interop.Hash160(Hash), "symbol", contract.NoneFlag).(string)
|
||||
}
|
||||
|
||||
// Decimals represents `decimals` method of NameService native contract.
|
||||
func Decimals() int {
|
||||
return contract.Call(interop.Hash160(Hash), "decimals", contract.NoneFlag).(int)
|
||||
}
|
||||
|
||||
// TotalSupply represents `totalSupply` method of NameService native contract.
|
||||
func TotalSupply() int {
|
||||
return contract.Call(interop.Hash160(Hash), "totalSupply", contract.ReadStates).(int)
|
||||
}
|
||||
|
||||
// OwnerOf represents `ownerOf` method of NameService native contract.
|
||||
func OwnerOf(tokenID string) interop.Hash160 {
|
||||
return contract.Call(interop.Hash160(Hash), "ownerOf", contract.ReadStates, tokenID).(interop.Hash160)
|
||||
}
|
||||
|
||||
// BalanceOf represents `balanceOf` method of NameService native contract.
|
||||
func BalanceOf(owner interop.Hash160) int {
|
||||
return contract.Call(interop.Hash160(Hash), "balanceOf", contract.ReadStates, owner).(int)
|
||||
}
|
||||
|
||||
// Properties represents `properties` method of NameService native contract.
|
||||
func Properties(tokenID string) map[string]interface{} {
|
||||
return contract.Call(interop.Hash160(Hash), "properties", contract.ReadStates, tokenID).(map[string]interface{})
|
||||
}
|
||||
|
||||
// Tokens represents `tokens` method of NameService native contract.
|
||||
func Tokens() iterator.Iterator {
|
||||
return contract.Call(interop.Hash160(Hash), "tokens",
|
||||
contract.ReadStates).(iterator.Iterator)
|
||||
}
|
||||
|
||||
// TokensOf represents `tokensOf` method of NameService native contract.
|
||||
func TokensOf(addr interop.Hash160) iterator.Iterator {
|
||||
return contract.Call(interop.Hash160(Hash), "tokensOf",
|
||||
contract.ReadStates, addr).(iterator.Iterator)
|
||||
}
|
||||
|
||||
// Transfer represents `transfer` method of NameService native contract.
|
||||
func Transfer(to interop.Hash160, tokenID string, data interface{}) bool {
|
||||
return contract.Call(interop.Hash160(Hash), "transfer",
|
||||
contract.ReadStates|contract.States|contract.AllowNotify, to, tokenID, data).(bool)
|
||||
}
|
||||
|
||||
// AddRoot represents `addRoot` method of NameService native contract.
|
||||
func AddRoot(root string) {
|
||||
contract.Call(interop.Hash160(Hash), "addRoot", contract.States, root)
|
||||
}
|
||||
|
||||
// SetPrice represents `setPrice` method of NameService native contract.
|
||||
func SetPrice(price int) {
|
||||
contract.Call(interop.Hash160(Hash), "setPrice", contract.States, price)
|
||||
}
|
||||
|
||||
// GetPrice represents `getPrice` method of NameService native contract.
|
||||
func GetPrice() int {
|
||||
return contract.Call(interop.Hash160(Hash), "getPrice", contract.ReadStates).(int)
|
||||
}
|
||||
|
||||
// IsAvailable represents `isAvailable` method of NameService native contract.
|
||||
func IsAvailable(name string) bool {
|
||||
return contract.Call(interop.Hash160(Hash), "isAvailable", contract.ReadStates, name).(bool)
|
||||
}
|
||||
|
||||
// Register represents `register` method of NameService native contract.
|
||||
func Register(name string, owner interop.Hash160) bool {
|
||||
return contract.Call(interop.Hash160(Hash), "register", contract.States, name, owner).(bool)
|
||||
}
|
||||
|
||||
// Renew represents `renew` method of NameService native contract.
|
||||
func Renew(name string) int {
|
||||
return contract.Call(interop.Hash160(Hash), "renew", contract.States, name).(int)
|
||||
}
|
||||
|
||||
// SetAdmin represents `setAdmin` method of NameService native contract.
|
||||
func SetAdmin(name string, admin interop.Hash160) {
|
||||
contract.Call(interop.Hash160(Hash), "setAdmin", contract.States, name, admin)
|
||||
}
|
||||
|
||||
// SetRecord represents `setRecord` method of NameService native contract.
|
||||
func SetRecord(name string, recType RecordType, data string) {
|
||||
contract.Call(interop.Hash160(Hash), "setRecord", contract.States, name, recType, data)
|
||||
}
|
||||
|
||||
// GetRecord represents `getRecord` method of NameService native contract.
|
||||
// It returns `nil` if record is missing.
|
||||
func GetRecord(name string, recType RecordType) []byte {
|
||||
return contract.Call(interop.Hash160(Hash), "getRecord", contract.ReadStates, name, recType).([]byte)
|
||||
}
|
||||
|
||||
// DeleteRecord represents `deleteRecord` method of NameService native contract.
|
||||
func DeleteRecord(name string, recType RecordType) {
|
||||
contract.Call(interop.Hash160(Hash), "deleteRecord", contract.States, name, recType)
|
||||
}
|
||||
|
||||
// Resolve represents `resolve` method of NameService native contract.
|
||||
func Resolve(name string, recType RecordType) []byte {
|
||||
return contract.Call(interop.Hash160(Hash), "resolve", contract.ReadStates, name, recType).([]byte)
|
||||
}
|
|
@ -6,11 +6,12 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
)
|
||||
|
||||
// GetOraclePrice invokes `getPrice` method on a native Oracle contract.
|
||||
|
@ -22,12 +23,8 @@ func (c *Client) GetOraclePrice() (int64, error) {
|
|||
return c.invokeNativeGetMethod(oracleHash, "getPrice")
|
||||
}
|
||||
|
||||
// GetNNSPrice invokes `getPrice` method on a native NameService contract.
|
||||
func (c *Client) GetNNSPrice() (int64, error) {
|
||||
nnsHash, err := c.GetNativeContractHash(nativenames.NameService)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get native NameService hash: %w", err)
|
||||
}
|
||||
// GetNNSPrice invokes `getPrice` method on a NeoNameService contract with the specified hash.
|
||||
func (c *Client) GetNNSPrice(nnsHash util.Uint160) (int64, error) {
|
||||
return c.invokeNativeGetMethod(nnsHash, "getPrice")
|
||||
}
|
||||
|
||||
|
@ -66,16 +63,12 @@ func (c *Client) GetDesignatedByRole(role noderoles.Role, index uint32) (keys.Pu
|
|||
return topPublicKeysFromStack(result.Stack)
|
||||
}
|
||||
|
||||
// NNSResolve invokes `resolve` method on a native NameService contract.
|
||||
func (c *Client) NNSResolve(name string, typ nnsrecords.Type) (string, error) {
|
||||
if typ == nnsrecords.CNAME {
|
||||
// NNSResolve invokes `resolve` method on a NameService contract with the specified hash.
|
||||
func (c *Client) NNSResolve(nnsHash util.Uint160, name string, typ nns.RecordType) (string, error) {
|
||||
if typ == nns.CNAME {
|
||||
return "", errors.New("can't resolve CNAME record type")
|
||||
}
|
||||
rmHash, err := c.GetNativeContractHash(nativenames.NameService)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get native NameService hash: %w", err)
|
||||
}
|
||||
result, err := c.InvokeFunction(rmHash, "resolve", []smartcontract.Parameter{
|
||||
result, err := c.InvokeFunction(nnsHash, "resolve", []smartcontract.Parameter{
|
||||
{
|
||||
Type: smartcontract.StringType,
|
||||
Value: name,
|
||||
|
@ -95,13 +88,9 @@ func (c *Client) NNSResolve(name string, typ nnsrecords.Type) (string, error) {
|
|||
return topStringFromStack(result.Stack)
|
||||
}
|
||||
|
||||
// NNSIsAvailable invokes `isAvailable` method on a native NameService contract.
|
||||
func (c *Client) NNSIsAvailable(name string) (bool, error) {
|
||||
rmHash, err := c.GetNativeContractHash(nativenames.NameService)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get native NameService hash: %w", err)
|
||||
}
|
||||
result, err := c.InvokeFunction(rmHash, "isAvailable", []smartcontract.Parameter{
|
||||
// NNSIsAvailable invokes `isAvailable` method on a NeoNameService contract with the specified hash.
|
||||
func (c *Client) NNSIsAvailable(nnsHash util.Uint160, name string) (bool, error) {
|
||||
result, err := c.InvokeFunction(nnsHash, "isAvailable", []smartcontract.Parameter{
|
||||
{
|
||||
Type: smartcontract.StringType,
|
||||
Value: name,
|
||||
|
|
|
@ -458,7 +458,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
|
|||
{
|
||||
name: "positive",
|
||||
invoke: func(c *Client) (interface{}, error) {
|
||||
return c.GetNNSPrice()
|
||||
return c.GetNNSPrice(util.Uint160{1, 2, 3})
|
||||
},
|
||||
serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"state":"HALT","gasconsumed":"2007390","script":"EMAMDWdldEZlZVBlckJ5dGUMFJphpG7sl7iTBtfOgfFbRiCR0AkyQWJ9W1I=","stack":[{"type":"Integer","value":"1000000"}],"tx":null}}`,
|
||||
result: func(c *Client) interface{} {
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
nns "github.com/nspcc-dev/neo-go/examples/nft-nd-nns"
|
||||
"github.com/nspcc-dev/neo-go/internal/testchain"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/fee"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nnsrecords"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
|
@ -784,7 +784,7 @@ func TestClient_NEP11(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.NoError(t, c.Init())
|
||||
|
||||
h, err := chain.GetNativeContractScriptHash(nativenames.NameService)
|
||||
h, err := util.Uint160DecodeStringLE(nameServiceContractHash)
|
||||
require.NoError(t, err)
|
||||
acc := testchain.PrivateKeyByID(0).GetScriptHash()
|
||||
|
||||
|
@ -807,7 +807,7 @@ func TestClient_NEP11(t *testing.T) {
|
|||
tok, err := c.NEP11TokenInfo(h)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &wallet.Token{
|
||||
Name: nativenames.NameService,
|
||||
Name: "NameService",
|
||||
Hash: h,
|
||||
Decimals: 0,
|
||||
Symbol: "NNS",
|
||||
|
@ -827,12 +827,12 @@ func TestClient_NEP11(t *testing.T) {
|
|||
t.Run("Properties", func(t *testing.T) {
|
||||
p, err := c.NEP11Properties(h, "neo.com")
|
||||
require.NoError(t, err)
|
||||
blockRegisterDomain, err := chain.GetBlock(chain.GetHeaderHash(13)) // `neo.com` domain was registered in 13th block
|
||||
blockRegisterDomain, err := chain.GetBlock(chain.GetHeaderHash(14)) // `neo.com` domain was registered in 14th block
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(blockRegisterDomain.Transactions))
|
||||
expected := stackitem.NewMap()
|
||||
expected.Add(stackitem.Make([]byte("name")), stackitem.Make([]byte("neo.com")))
|
||||
expected.Add(stackitem.Make([]byte("expiration")), stackitem.Make(blockRegisterDomain.Timestamp/1000+365*24*3600)) // expiration formula
|
||||
expected.Add(stackitem.Make([]byte("expiration")), stackitem.Make(blockRegisterDomain.Timestamp+365*24*3600*1000)) // expiration formula
|
||||
require.EqualValues(t, expected, p)
|
||||
})
|
||||
t.Run("Transfer", func(t *testing.T) {
|
||||
|
@ -850,27 +850,30 @@ func TestClient_NNS(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.NoError(t, c.Init())
|
||||
|
||||
nsHash, err := util.Uint160DecodeStringLE(nameServiceContractHash)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("NNSIsAvailable, false", func(t *testing.T) {
|
||||
b, err := c.NNSIsAvailable("neo.com")
|
||||
b, err := c.NNSIsAvailable(nsHash, "neo.com")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, b)
|
||||
})
|
||||
t.Run("NNSIsAvailable, true", func(t *testing.T) {
|
||||
b, err := c.NNSIsAvailable("neogo.com")
|
||||
b, err := c.NNSIsAvailable(nsHash, "neogo.com")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, b)
|
||||
})
|
||||
t.Run("NNSResolve, good", func(t *testing.T) {
|
||||
b, err := c.NNSResolve("neo.com", nnsrecords.A)
|
||||
b, err := c.NNSResolve(nsHash, "neo.com", nns.A)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "1.2.3.4", b)
|
||||
})
|
||||
t.Run("NNSResolve, bad", func(t *testing.T) {
|
||||
_, err := c.NNSResolve("neogo.com", nnsrecords.A)
|
||||
_, err := c.NNSResolve(nsHash, "neogo.com", nns.A)
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("NNSResolve, forbidden", func(t *testing.T) {
|
||||
_, err := c.NNSResolve("neogo.com", nnsrecords.CNAME)
|
||||
_, err := c.NNSResolve(nsHash, "neogo.com", nns.CNAME)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -55,13 +55,14 @@ type rpcTestCase struct {
|
|||
}
|
||||
|
||||
const testContractHash = "63cc6571e990dd3f345f699fc9c2a6e49edb89af"
|
||||
const deploymentTxHash = "4450d0047d4b6a20e85176c709df44fae4c63cfa9a698acb11871554b93016df"
|
||||
const deploymentTxHash = "9b0c586eb07f8c9b6fc46b05c78d87651d50af8e1f44478827848d826f8cd174"
|
||||
const genesisBlockHash = "73fe50b5564d57118296cbab0a78fe7cb11c97b7699d07a9a21fab60e79bb8fc"
|
||||
|
||||
const verifyContractHash = "c50082e0d8364d61ce6933bd24027a3363474dce"
|
||||
const verifyContractAVM = "VwMAQS1RCDAhcAwU7p6iLCfjS9AUj8QQjgj3To9QSLLbMHFoE87bKGnbKJdA"
|
||||
const verifyWithArgsContractHash = "8744ffdd07af8e9f18ab90685c8c2ebfd37c6415"
|
||||
const invokescriptContractAVM = "VwcADBQBDAMOBQYMDQIODw0DDgcJAAAAANswcGhB+CfsjCGqJgQRQAwUDQ8DAgkAAgEDBwMEBQIBAA4GDAnbMHFpQfgn7IwhqiYEEkATQA=="
|
||||
const nameServiceContractHash = "60d78a0fc048399438c3764f8a67d0fc86d6e0e6"
|
||||
|
||||
var rpcTestCases = map[string][]rpcTestCase{
|
||||
"getapplicationlog": {
|
||||
|
@ -648,7 +649,7 @@ var rpcTestCases = map[string][]rpcTestCase{
|
|||
require.True(t, ok)
|
||||
expected := result.UnclaimedGas{
|
||||
Address: testchain.MultisigScriptHash(),
|
||||
Unclaimed: *big.NewInt(7000),
|
||||
Unclaimed: *big.NewInt(7500),
|
||||
}
|
||||
assert.Equal(t, expected, *actual)
|
||||
},
|
||||
|
@ -1407,7 +1408,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
|
|||
require.NoErrorf(t, err, "could not parse response: %s", txOut)
|
||||
|
||||
assert.Equal(t, *block.Transactions[0], actual.Transaction)
|
||||
assert.Equal(t, 15, actual.Confirmations)
|
||||
assert.Equal(t, 16, actual.Confirmations)
|
||||
assert.Equal(t, TXHash, actual.Transaction.Hash())
|
||||
})
|
||||
|
||||
|
@ -1525,12 +1526,12 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
|
|||
require.NoError(t, json.Unmarshal(res, actual))
|
||||
checkNep17TransfersAux(t, e, actual, sent, rcvd)
|
||||
}
|
||||
t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{8, 9, 10, 11}, []int{2, 3}) })
|
||||
t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{9, 10, 11, 12}, []int{2, 3}) })
|
||||
t.Run("no res", func(t *testing.T) { testNEP17T(t, 100, 100, 0, 0, []int{}, []int{}) })
|
||||
t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{5, 6}, []int{1}) })
|
||||
t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{8}, []int{2}) })
|
||||
t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{7, 8}, []int{2}) })
|
||||
t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{9, 10}, []int{3}) })
|
||||
t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{6, 7}, []int{1}) })
|
||||
t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{9}, []int{2}) })
|
||||
t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{8, 9}, []int{2}) })
|
||||
t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{10, 11}, []int{3}) })
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1633,8 +1634,8 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) {
|
|||
},
|
||||
{
|
||||
Asset: e.chain.UtilityTokenHash(),
|
||||
Amount: "67960000780",
|
||||
LastUpdated: 14,
|
||||
Amount: "57941360260",
|
||||
LastUpdated: 15,
|
||||
}},
|
||||
Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(),
|
||||
}
|
||||
|
@ -1643,7 +1644,7 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) {
|
|||
}
|
||||
|
||||
func checkNep17Transfers(t *testing.T, e *executor, acc interface{}) {
|
||||
checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}, []int{0, 1, 2, 3, 4, 5, 6, 7})
|
||||
checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, []int{0, 1, 2, 3, 4, 5, 6, 7})
|
||||
}
|
||||
|
||||
func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) {
|
||||
|
@ -1652,12 +1653,12 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
|
|||
rublesHash, err := util.Uint160DecodeStringLE(testContractHash)
|
||||
require.NoError(t, err)
|
||||
|
||||
blockSetRecord, err := e.chain.GetBlock(e.chain.GetHeaderHash(14)) // add type A record to `neo.com` domain via NNS
|
||||
blockSetRecord, err := e.chain.GetBlock(e.chain.GetHeaderHash(15)) // add type A record to `neo.com` domain via NNS
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(blockSetRecord.Transactions))
|
||||
txSetRecord := blockSetRecord.Transactions[0]
|
||||
|
||||
blockRegisterDomain, err := e.chain.GetBlock(e.chain.GetHeaderHash(13)) // register `neo.com` domain via NNS
|
||||
blockRegisterDomain, err := e.chain.GetBlock(e.chain.GetHeaderHash(14)) // register `neo.com` domain via NNS
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(blockRegisterDomain.Transactions))
|
||||
txRegisterDomain := blockRegisterDomain.Transactions[0]
|
||||
|
@ -1665,6 +1666,11 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
|
|||
blockGASBounty2, err := e.chain.GetBlock(e.chain.GetHeaderHash(12)) // size of committee = 6
|
||||
require.NoError(t, err)
|
||||
|
||||
blockDeploy4, err := e.chain.GetBlock(e.chain.GetHeaderHash(11)) // deploy ns.go (non-native neo name service contract)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(blockDeploy4.Transactions))
|
||||
txDeploy4 := blockDeploy4.Transactions[0]
|
||||
|
||||
blockDeploy3, err := e.chain.GetBlock(e.chain.GetHeaderHash(10)) // deploy verification_with_args_contract.go
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(blockDeploy3.Transactions))
|
||||
|
@ -1728,7 +1734,7 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
|
|||
Asset: e.chain.UtilityTokenHash(),
|
||||
Address: "", // burn
|
||||
Amount: big.NewInt(txSetRecord.SystemFee + txSetRecord.NetworkFee).String(),
|
||||
Index: 14,
|
||||
Index: 15,
|
||||
TxHash: blockSetRecord.Hash(),
|
||||
},
|
||||
{
|
||||
|
@ -1736,9 +1742,17 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
|
|||
Asset: e.chain.UtilityTokenHash(),
|
||||
Address: "", // burn
|
||||
Amount: big.NewInt(txRegisterDomain.SystemFee + txRegisterDomain.NetworkFee).String(),
|
||||
Index: 13,
|
||||
Index: 14,
|
||||
TxHash: blockRegisterDomain.Hash(),
|
||||
},
|
||||
{
|
||||
Timestamp: blockDeploy4.Timestamp,
|
||||
Asset: e.chain.UtilityTokenHash(),
|
||||
Address: "", // burn
|
||||
Amount: big.NewInt(txDeploy4.SystemFee + txDeploy4.NetworkFee).String(),
|
||||
Index: 11,
|
||||
TxHash: blockDeploy4.Hash(),
|
||||
},
|
||||
{
|
||||
Timestamp: blockDeploy3.Timestamp,
|
||||
Asset: e.chain.UtilityTokenHash(),
|
||||
|
|
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
Binary file not shown.
Loading…
Reference in a new issue