diff --git a/pkg/compiler/native_test.go b/pkg/compiler/native_test.go index 457c2b8dfd..a8ac1361ee 100644 --- a/pkg/compiler/native_test.go +++ b/pkg/compiler/native_test.go @@ -9,6 +9,7 @@ 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/interop/native/crypto" "github.com/nspcc-dev/neo-go/pkg/interop/native/gas" @@ -77,10 +78,10 @@ func TestRoleManagementRole(t *testing.T) { } func TestNameServiceRecordType(t *testing.T) { - require.EqualValues(t, native.RecordTypeA, nameservice.TypeA) - require.EqualValues(t, native.RecordTypeCNAME, nameservice.TypeCNAME) - require.EqualValues(t, native.RecordTypeTXT, nameservice.TypeTXT) - require.EqualValues(t, native.RecordTypeAAAA, nameservice.TypeAAAA) + 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) { diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 4479758176..5d8d8c47d1 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -23,6 +23,7 @@ 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" @@ -484,6 +485,21 @@ func initBasicChain(t *testing.T, bc *Blockchain) { require.NoError(t, bc.AddBlock(b)) checkTxHalt(t, bc, txDeploy3.Hash()) + // register `neo.com` with A record type and priv0 owner via NNS + transferFundsToCommittee(t, bc) // block #11 + res, err := invokeContractMethodGeneric(bc, defaultNameServiceSysfee, + bc.contracts.NameService.Hash, "addRoot", true, "com") // block #12 + 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 + 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 + require.NoError(t, err) + checkResult(t, res, stackitem.Null{}) + // Compile contract to test `invokescript` RPC call _, _ = newDeployTx(t, bc, priv0ScriptHash, prefix+"invokescript_contract.go", "ContractForInvokescriptTest", nil) } diff --git a/pkg/core/native/name_service.go b/pkg/core/native/name_service.go index 4edb45155b..d39d8e3c4d 100644 --- a/pkg/core/native/name_service.go +++ b/pkg/core/native/name_service.go @@ -15,6 +15,7 @@ import ( "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" @@ -42,17 +43,6 @@ type nameState struct { Admin util.Uint160 } -// RecordType represents name record type. -type RecordType byte - -// Pre-defined record types. -const ( - RecordTypeA RecordType = 1 - RecordTypeCNAME RecordType = 5 - RecordTypeTXT RecordType = 16 - RecordTypeAAAA RecordType = 28 -) - const ( nameServiceID = -10 @@ -448,19 +438,19 @@ func (n *NameService) setRecord(ic *interop.Context, args []stackitem.Item) stac return stackitem.Null{} } -func checkName(rt RecordType, name string) { +func checkName(rt nnsrecords.Type, name string) { var valid bool switch rt { - case RecordTypeA: + 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 RecordTypeCNAME: + case nnsrecords.CNAME: valid = matchName(name) - case RecordTypeTXT: + case nnsrecords.TXT: valid = utf8.RuneCountInString(name) <= 255 - case RecordTypeAAAA: + case nnsrecords.AAAA: valid = ipv6Regex.MatchString(name) && net.ParseIP(name) != nil } @@ -513,7 +503,7 @@ func (n *NameService) resolve(ic *interop.Context, args []stackitem.Item) stacki return stackitem.NewByteArray([]byte(result)) } -func (n *NameService) resolveInternal(ic *interop.Context, name string, t RecordType, redirect int) (string, bool) { +func (n *NameService) resolveInternal(ic *interop.Context, name string, t nnsrecords.Type, redirect int) (string, bool) { if redirect < 0 { panic("invalid redirect") } @@ -521,26 +511,26 @@ func (n *NameService) resolveInternal(ic *interop.Context, name string, t Record if data, ok := records[t]; ok { return data, true } - data, ok := records[RecordTypeCNAME] + 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[RecordType]string { +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[RecordType]string) + res := make(map[nnsrecords.Type]string) d.Seek(n.ID, key, func(k, v []byte) { - rt := RecordType(k[len(k)-1]) + rt := nnsrecords.Type(k[len(k)-1]) res[rt] = string(v) }) return res } -func makeRecordKey(domain, name string, rt RecordType) []byte { +func makeRecordKey(domain, name string, rt nnsrecords.Type) []byte { key := make([]byte, 1+util.Uint160Size+util.Uint160Size+1) key[0] = prefixRecord i := 1 @@ -647,7 +637,7 @@ func toDomain(name string) string { return domain } -func toRecordType(item stackitem.Item) RecordType { +func toRecordType(item stackitem.Item) nnsrecords.Type { bi, err := item.TryInteger() if err != nil || !bi.IsInt64() { panic("invalid record type") @@ -656,8 +646,8 @@ func toRecordType(item stackitem.Item) RecordType { if val > math.MaxUint8 { panic("invalid record type") } - switch rt := RecordType(val); rt { - case RecordTypeA, RecordTypeCNAME, RecordTypeTXT, RecordTypeAAAA: + switch rt := nnsrecords.Type(val); rt { + case nnsrecords.A, nnsrecords.CNAME, nnsrecords.TXT, nnsrecords.AAAA: return rt default: panic("invalid record type") diff --git a/pkg/core/native/name_service_test.go b/pkg/core/native/name_service_test.go index c8fccf0e78..bc741051fb 100644 --- a/pkg/core/native/name_service_test.go +++ b/pkg/core/native/name_service_test.go @@ -3,6 +3,7 @@ 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" @@ -36,45 +37,45 @@ func TestParseDomain(t *testing.T) { func TestNameService_CheckName(t *testing.T) { // tests are got from the C# implementation testCases := []struct { - Type RecordType + Type nnsrecords.Type Name string ShouldFail bool }{ - {Type: RecordTypeA, Name: "0.0.0.0"}, - {Type: RecordTypeA, Name: "10.10.10.10"}, - {Type: RecordTypeA, Name: "255.255.255.255"}, - {Type: RecordTypeA, Name: "192.168.1.1"}, - {Type: RecordTypeA, Name: "1a", ShouldFail: true}, - {Type: RecordTypeA, Name: "256.0.0.0", ShouldFail: true}, - {Type: RecordTypeA, Name: "01.01.01.01", ShouldFail: true}, - {Type: RecordTypeA, Name: "00.0.0.0", ShouldFail: true}, - {Type: RecordTypeA, Name: "0.0.0.-1", ShouldFail: true}, - {Type: RecordTypeA, Name: "0.0.0.0.1", ShouldFail: true}, - {Type: RecordTypeA, Name: "11111111.11111111.11111111.11111111", ShouldFail: true}, - {Type: RecordTypeA, Name: "11111111.11111111.11111111.11111111", ShouldFail: true}, - {Type: RecordTypeA, Name: "ff.ff.ff.ff", ShouldFail: true}, - {Type: RecordTypeA, Name: "0.0.256", ShouldFail: true}, - {Type: RecordTypeA, Name: "0.0.0", ShouldFail: true}, - {Type: RecordTypeA, Name: "0.257", ShouldFail: true}, - {Type: RecordTypeA, Name: "1.1", ShouldFail: true}, - {Type: RecordTypeA, Name: "257", ShouldFail: true}, - {Type: RecordTypeA, Name: "1", ShouldFail: true}, - {Type: RecordTypeAAAA, Name: "2001:db8::8:800:200c:417a"}, - {Type: RecordTypeAAAA, Name: "ff01::101"}, - {Type: RecordTypeAAAA, Name: "::1"}, - {Type: RecordTypeAAAA, Name: "::"}, - {Type: RecordTypeAAAA, Name: "2001:db8:0:0:8:800:200c:417a"}, - {Type: RecordTypeAAAA, Name: "ff01:0:0:0:0:0:0:101"}, - {Type: RecordTypeAAAA, Name: "0:0:0:0:0:0:0:1"}, - {Type: RecordTypeAAAA, Name: "0:0:0:0:0:0:0:0"}, - {Type: RecordTypeAAAA, Name: "2001:DB8::8:800:200C:417A", ShouldFail: true}, - {Type: RecordTypeAAAA, Name: "FF01::101", ShouldFail: true}, - {Type: RecordTypeAAAA, Name: "fF01::101", ShouldFail: true}, - {Type: RecordTypeAAAA, Name: "2001:DB8:0:0:8:800:200C:417A", ShouldFail: true}, - {Type: RecordTypeAAAA, Name: "FF01:0:0:0:0:0:0:101", ShouldFail: true}, - {Type: RecordTypeAAAA, Name: "::ffff:1.01.1.01", ShouldFail: true}, - {Type: RecordTypeAAAA, Name: "2001:DB8:0:0:8:800:200C:4Z", ShouldFail: true}, - {Type: RecordTypeAAAA, Name: "::13.1.68.3", ShouldFail: true}, + {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 { diff --git a/pkg/core/native/nnsrecords/nnsrecords.go b/pkg/core/native/nnsrecords/nnsrecords.go new file mode 100644 index 0000000000..958f3f1bb3 --- /dev/null +++ b/pkg/core/native/nnsrecords/nnsrecords.go @@ -0,0 +1,12 @@ +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 +) diff --git a/pkg/core/native_name_service_test.go b/pkg/core/native_name_service_test.go index e79e559d4b..8085444543 100644 --- a/pkg/core/native_name_service_test.go +++ b/pkg/core/native_name_service_test.go @@ -7,6 +7,7 @@ import ( "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" @@ -67,7 +68,7 @@ func TestExpiration(t *testing.T) { true, "first.com", acc.Contract.ScriptHash()) testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, - "setRecord", stackitem.Null{}, "first.com", int64(native.RecordTypeTXT), "sometext") + "setRecord", stackitem.Null{}, "first.com", int64(nnsrecords.TXT), "sometext") b1 := bc.topBlock.Load().(*block.Block) tx, err := prepareContractMethodInvokeGeneric(bc, defaultRegisterSysfee, bc.contracts.NameService.Hash, @@ -108,7 +109,7 @@ func TestExpiration(t *testing.T) { checkResult(t, &aer[0], stackitem.NewBool(false)) tx, err = prepareContractMethodInvokeGeneric(bc, defaultNameServiceSysfee, bc.contracts.NameService.Hash, - "getRecord", acc, "first.com", int64(native.RecordTypeTXT)) + "getRecord", acc, "first.com", int64(nnsrecords.TXT)) require.NoError(t, err) b5 := newBlockCustom(bc.GetConfig(), func(b *block.Block) { b.Index = b4.Index + 1 @@ -182,36 +183,36 @@ func TestSetGetRecord(t *testing.T) { testNameServiceInvoke(t, bc, "addRoot", stackitem.Null{}, "com") t.Run("set before register", func(t *testing.T) { - testNameServiceInvoke(t, bc, "setRecord", nil, "neo.com", int64(native.RecordTypeTXT), "sometext") + 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(native.RecordTypeA), "not.an.ip.address") + 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(native.RecordTypeA), "1.2.3.4") + "neo.com", int64(nnsrecords.A), "1.2.3.4") }) - testNameServiceInvoke(t, bc, "getRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeA)) - testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeA), "1.2.3.4") - testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(native.RecordTypeA)) - testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeA), "1.2.3.4") - testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(native.RecordTypeA)) - testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeAAAA), "2001:0000:1f1f:0000:0000:0100:11a0:addf") - testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeCNAME), "nspcc.ru") - testNameServiceInvoke(t, bc, "setRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeTXT), "sometext") + 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(native.RecordTypeCNAME)) + "neo.com", int64(nnsrecords.CNAME)) }) - testNameServiceInvoke(t, bc, "getRecord", "nspcc.ru", "neo.com", int64(native.RecordTypeCNAME)) - testNameServiceInvoke(t, bc, "deleteRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeCNAME)) - testNameServiceInvoke(t, bc, "getRecord", stackitem.Null{}, "neo.com", int64(native.RecordTypeCNAME)) - testNameServiceInvoke(t, bc, "getRecord", "1.2.3.4", "neo.com", int64(native.RecordTypeA)) + 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) { @@ -239,20 +240,20 @@ func TestSetAdmin(t *testing.T) { t.Run("set and delete by admin", func(t *testing.T) { testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "setRecord", stackitem.Null{}, - "neo.com", int64(native.RecordTypeTXT), "sometext") + "neo.com", int64(nnsrecords.TXT), "sometext") testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, guest, "deleteRecord", nil, - "neo.com", int64(native.RecordTypeTXT)) + "neo.com", int64(nnsrecords.TXT)) testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, admin, "deleteRecord", stackitem.Null{}, - "neo.com", int64(native.RecordTypeTXT)) + "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(native.RecordTypeTXT), "sometext") + "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(native.RecordTypeTXT)) + "neo.com", int64(nnsrecords.TXT)) }) } @@ -267,7 +268,7 @@ func TestTransfer(t *testing.T) { testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "register", true, "neo.com", from.PrivateKey().GetScriptHash()) testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, from, "setRecord", stackitem.Null{}, - "neo.com", int64(native.RecordTypeA), "1.2.3.4") + "neo.com", int64(nnsrecords.A), "1.2.3.4") testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, from, "transfer", nil, to.Contract.ScriptHash().BytesBE(), []byte("not.exists")) testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, true, "transfer", @@ -355,23 +356,23 @@ func TestResolve(t *testing.T) { testNameServiceInvokeAux(t, bc, defaultRegisterSysfee, acc, "register", true, "neo.com", acc.PrivateKey().GetScriptHash()) testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{}, - "neo.com", int64(native.RecordTypeA), "1.2.3.4") + "neo.com", int64(nnsrecords.A), "1.2.3.4") testNameServiceInvokeAux(t, bc, defaultNameServiceSysfee, acc, "setRecord", stackitem.Null{}, - "neo.com", int64(native.RecordTypeCNAME), "alias.com") + "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(native.RecordTypeTXT), "sometxt") + "alias.com", int64(nnsrecords.TXT), "sometxt") testNameServiceInvoke(t, bc, "resolve", "1.2.3.4", - "neo.com", int64(native.RecordTypeA)) + "neo.com", int64(nnsrecords.A)) testNameServiceInvoke(t, bc, "resolve", "alias.com", - "neo.com", int64(native.RecordTypeCNAME)) + "neo.com", int64(nnsrecords.CNAME)) testNameServiceInvoke(t, bc, "resolve", "sometxt", - "neo.com", int64(native.RecordTypeTXT)) + "neo.com", int64(nnsrecords.TXT)) testNameServiceInvoke(t, bc, "resolve", stackitem.Null{}, - "neo.com", int64(native.RecordTypeAAAA)) + "neo.com", int64(nnsrecords.AAAA)) } const ( diff --git a/pkg/rpc/client/helper.go b/pkg/rpc/client/helper.go new file mode 100644 index 0000000000..74f02d8cb1 --- /dev/null +++ b/pkg/rpc/client/helper.go @@ -0,0 +1,97 @@ +package client + +import ( + "crypto/elliptic" + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// getInvocationError returns an error in case of bad VM state or empty stack. +func getInvocationError(result *result.Invoke) error { + if result.State != "HALT" { + return fmt.Errorf("invocation failed: %s", result.FaultException) + } + if len(result.Stack) == 0 { + return errors.New("result stack is empty") + } + return nil +} + +// topBoolFromStack returns the top boolean value from stack. +func topBoolFromStack(st []stackitem.Item) (bool, error) { + index := len(st) - 1 // top stack element is last in the array + result, ok := st[index].Value().(bool) + if !ok { + return false, fmt.Errorf("invalid stack item type: %s", st[index].Type()) + } + return result, nil +} + +// topIntFromStack returns the top integer value from stack. +func topIntFromStack(st []stackitem.Item) (int64, error) { + index := len(st) - 1 // top stack element is last in the array + bi, err := st[index].TryInteger() + if err != nil { + return 0, err + } + return bi.Int64(), nil +} + +// topPublicKeysFromStack returns the top array of public keys from stack. +func topPublicKeysFromStack(st []stackitem.Item) (keys.PublicKeys, error) { + index := len(st) - 1 // top stack element is last in the array + var ( + pks keys.PublicKeys + err error + ) + items, ok := st[index].Value().([]stackitem.Item) + if !ok { + return nil, fmt.Errorf("invalid stack item type: %s", st[index].Type()) + } + pks = make(keys.PublicKeys, len(items)) + for i, item := range items { + val, ok := item.Value().([]byte) + if !ok { + return nil, fmt.Errorf("invalid array element #%d: %s", i, item.Type()) + } + pks[i], err = keys.NewPublicKeyFromBytes(val, elliptic.P256()) + if err != nil { + return nil, err + } + } + return pks, nil +} + +// top string from stack returns the top string from stack. +func topStringFromStack(st []stackitem.Item) (string, error) { + index := len(st) - 1 // top stack element is last in the array + bs, err := st[index].TryBytes() + if err != nil { + return "", err + } + return string(bs), nil +} + +// topUint160FromStack returns the top util.Uint160 from stack. +func topUint160FromStack(st []stackitem.Item) (util.Uint160, error) { + index := len(st) - 1 // top stack element is last in the array + bs, err := st[index].TryBytes() + if err != nil { + return util.Uint160{}, err + } + return util.Uint160DecodeBytesBE(bs) +} + +// topMapFromStack returns the top stackitem.Map from stack. +func topMapFromStack(st []stackitem.Item) (*stackitem.Map, error) { + index := len(st) - 1 // top stack element is last in the array + if t := st[index].Type(); t != stackitem.MapT { + return nil, fmt.Errorf("invalid return stackitem type: %s", t.String()) + } + return st[index].(*stackitem.Map), nil +} diff --git a/pkg/rpc/client/native.go b/pkg/rpc/client/native.go index 8839103543..8794af8620 100644 --- a/pkg/rpc/client/native.go +++ b/pkg/rpc/client/native.go @@ -3,14 +3,14 @@ package client // Various non-policy things from native contracts. import ( - "crypto/elliptic" + "errors" "fmt" "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/vm/stackitem" ) // GetOraclePrice invokes `getPrice` method on a native Oracle contract. @@ -66,27 +66,53 @@ func (c *Client) GetDesignatedByRole(role noderoles.Role, index uint32) (keys.Pu return topPublicKeysFromStack(result.Stack) } -// topPublicKeysFromStack returns the top array of public keys from stack. -func topPublicKeysFromStack(st []stackitem.Item) (keys.PublicKeys, error) { - index := len(st) - 1 // top stack element is last in the array - var ( - pks keys.PublicKeys - err error - ) - items, ok := st[index].Value().([]stackitem.Item) - if !ok { - return nil, fmt.Errorf("invalid stack item type: %s", st[index].Type()) +// NNSResolve invokes `resolve` method on a native NameService contract. +func (c *Client) NNSResolve(name string, typ nnsrecords.Type) (string, error) { + if typ == nnsrecords.CNAME { + return "", errors.New("can't resolve CNAME record type") } - pks = make(keys.PublicKeys, len(items)) - for i, item := range items { - val, ok := item.Value().([]byte) - if !ok { - return nil, fmt.Errorf("invalid array element #%d: %s", i, item.Type()) - } - pks[i], err = keys.NewPublicKeyFromBytes(val, elliptic.P256()) - if err != nil { - return nil, err - } + 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{ + { + Type: smartcontract.StringType, + Value: name, + }, + { + Type: smartcontract.IntegerType, + Value: int64(typ), + }, + }, nil) + if err != nil { + return "", err + } + err = getInvocationError(result) + if err != nil { + return "", fmt.Errorf("`resolve`: %w", err) + } + 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{ + { + Type: smartcontract.StringType, + Value: name, + }, + }, nil) + if err != nil { + return false, err + } + err = getInvocationError(result) + if err != nil { + return false, fmt.Errorf("`isAvailable`: %w", err) } - return pks, nil + return topBoolFromStack(result.Stack) } diff --git a/pkg/rpc/client/nep.go b/pkg/rpc/client/nep.go new file mode 100644 index 0000000000..092669eb16 --- /dev/null +++ b/pkg/rpc/client/nep.go @@ -0,0 +1,72 @@ +package client + +import ( + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// nepDecimals invokes `decimals` NEP* method on a specified contract. +func (c *Client) nepDecimals(tokenHash util.Uint160) (int64, error) { + result, err := c.InvokeFunction(tokenHash, "decimals", []smartcontract.Parameter{}, nil) + if err != nil { + return 0, err + } + err = getInvocationError(result) + if err != nil { + return 0, err + } + + return topIntFromStack(result.Stack) +} + +// nepSymbol invokes `symbol` NEP* method on a specified contract. +func (c *Client) nepSymbol(tokenHash util.Uint160) (string, error) { + result, err := c.InvokeFunction(tokenHash, "symbol", []smartcontract.Parameter{}, nil) + if err != nil { + return "", err + } + err = getInvocationError(result) + if err != nil { + return "", err + } + + return topStringFromStack(result.Stack) +} + +// nepTotalSupply invokes `totalSupply` NEP* method on a specified contract. +func (c *Client) nepTotalSupply(tokenHash util.Uint160) (int64, error) { + result, err := c.InvokeFunction(tokenHash, "totalSupply", []smartcontract.Parameter{}, nil) + if err != nil { + return 0, err + } + err = getInvocationError(result) + if err != nil { + return 0, err + } + + return topIntFromStack(result.Stack) +} + +// nepBalanceOf invokes `balanceOf` NEP* method on a specified contract. +func (c *Client) nepBalanceOf(tokenHash, acc util.Uint160, tokenID *string) (int64, error) { + params := []smartcontract.Parameter{{ + Type: smartcontract.Hash160Type, + Value: acc, + }} + if tokenID != nil { + params = append(params, smartcontract.Parameter{ + Type: smartcontract.StringType, + Value: *tokenID, + }) + } + result, err := c.InvokeFunction(tokenHash, "balanceOf", params, nil) + if err != nil { + return 0, err + } + err = getInvocationError(result) + if err != nil { + return 0, err + } + + return topIntFromStack(result.Stack) +} diff --git a/pkg/rpc/client/nep11.go b/pkg/rpc/client/nep11.go new file mode 100644 index 0000000000..f313815b3b --- /dev/null +++ b/pkg/rpc/client/nep11.go @@ -0,0 +1,165 @@ +package client + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "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/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" +) + +// NEP11Decimals invokes `decimals` NEP11 method on a specified contract. +func (c *Client) NEP11Decimals(tokenHash util.Uint160) (int64, error) { + return c.nepDecimals(tokenHash) +} + +// NEP11Symbol invokes `symbol` NEP11 method on a specified contract. +func (c *Client) NEP11Symbol(tokenHash util.Uint160) (string, error) { + return c.nepSymbol(tokenHash) +} + +// NEP11TotalSupply invokes `totalSupply` NEP11 method on a specified contract. +func (c *Client) NEP11TotalSupply(tokenHash util.Uint160) (int64, error) { + return c.nepTotalSupply(tokenHash) +} + +// NEP11BalanceOf invokes `balanceOf` NEP11 method on a specified contract. +func (c *Client) NEP11BalanceOf(tokenHash, owner util.Uint160) (int64, error) { + return c.nepBalanceOf(tokenHash, owner, nil) +} + +// TransferNEP11 creates an invocation transaction that invokes 'transfer' method +// on a given token to move the whole NEP11 token with the specified token ID to +// given account and sends it to the network returning just a hash of it. +func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160, + tokenHash util.Uint160, tokenID string, gas int64) (util.Uint256, error) { + if !c.initDone { + return util.Uint256{}, errNetworkNotInitialized + } + tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, to, tokenID) + if err != nil { + return util.Uint256{}, err + } + + if err := acc.SignTx(c.GetNetwork(), tx); err != nil { + return util.Uint256{}, fmt.Errorf("can't sign NEP11 transfer tx: %w", err) + } + + return c.SendRawTransaction(tx) +} + +// createNEP11TransferTx is an internal helper for TransferNEP11 and +// TransferNEP11D which creates an invocation transaction for the +// 'transfer' method of a given contract (token) to move the whole (or the +// specified amount of) NEP11 token with the specified token ID to given account +// and returns it. The returned transaction is not signed. +// `args` for TransferNEP11: to util.Uint160, tokenID string; +// `args` for TransferNEP11D: from, to util.Uint160, amount int64, tokenID string. +func (c *Client) createNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160, + gas int64, args ...interface{}) (*transaction.Transaction, error) { + w := io.NewBufBinWriter() + emit.AppCall(w.BinWriter, tokenHash, "transfer", callflag.All, args...) + emit.Opcodes(w.BinWriter, opcode.ASSERT) + if w.Err != nil { + return nil, fmt.Errorf("failed to create NEP11 transfer script: %w", w.Err) + } + from, err := address.StringToUint160(acc.Address) + if err != nil { + return nil, fmt.Errorf("bad account address: %w", err) + } + return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, []SignerAccount{{ + Signer: transaction.Signer{ + Account: from, + Scopes: transaction.CalledByEntry, + }, + Account: acc, + }}) +} + +// Non-divisible NFT methods section start. + +// NEP11NDOwnerOf invokes `ownerOf` non-devisible NEP11 method with the +// specified token ID on a specified contract. +func (c *Client) NEP11NDOwnerOf(tokenHash util.Uint160, tokenID string) (util.Uint160, error) { + result, err := c.InvokeFunction(tokenHash, "ownerOf", []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: tokenID, + }, + }, nil) + if err != nil { + return util.Uint160{}, err + } + err = getInvocationError(result) + if err != nil { + return util.Uint160{}, err + } + + return topUint160FromStack(result.Stack) +} + +// Non-divisible NFT methods section end. + +// Divisible NFT methods section start. + +// TransferNEP11D creates an invocation transaction that invokes 'transfer' +// method on a given token to move specified amount of divisible NEP11 assets +// (in FixedN format using contract's number of decimals) to given account and +// sends it to the network returning just a hash of it. +func (c *Client) TransferNEP11D(acc *wallet.Account, to util.Uint160, + tokenHash util.Uint160, amount int64, tokenID string, gas int64) (util.Uint256, error) { + if !c.initDone { + return util.Uint256{}, errNetworkNotInitialized + } + from, err := address.StringToUint160(acc.Address) + if err != nil { + return util.Uint256{}, fmt.Errorf("bad account address: %w", err) + } + tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, acc.Address, from, to, amount, tokenID) + if err != nil { + return util.Uint256{}, err + } + + if err := acc.SignTx(c.GetNetwork(), tx); err != nil { + return util.Uint256{}, fmt.Errorf("can't sign NEP11 divisible transfer tx: %w", err) + } + + return c.SendRawTransaction(tx) +} + +// NEP11DBalanceOf invokes `balanceOf` divisible NEP11 method on a +// specified contract. +func (c *Client) NEP11DBalanceOf(tokenHash, owner util.Uint160, tokenID string) (int64, error) { + return c.nepBalanceOf(tokenHash, owner, &tokenID) +} + +// Divisible NFT methods section end. + +// Optional NFT methods section start. + +// NEP11Properties invokes `properties` optional NEP11 method on a +// specified contract. +func (c *Client) NEP11Properties(tokenHash util.Uint160, tokenID string) (*stackitem.Map, error) { + result, err := c.InvokeFunction(tokenHash, "properties", []smartcontract.Parameter{{ + Type: smartcontract.StringType, + Value: tokenID, + }}, nil) + if err != nil { + return nil, err + } + err = getInvocationError(result) + if err != nil { + return nil, err + } + + return topMapFromStack(result.Stack) +} + +// Optional NFT methods section end. diff --git a/pkg/rpc/client/nep17.go b/pkg/rpc/client/nep17.go index f9ce80a7fd..51d7b5ed73 100644 --- a/pkg/rpc/client/nep17.go +++ b/pkg/rpc/client/nep17.go @@ -1,19 +1,15 @@ package client import ( - "errors" "fmt" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" - "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "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" ) @@ -33,61 +29,22 @@ type SignerAccount struct { // NEP17Decimals invokes `decimals` NEP17 method on a specified contract. func (c *Client) NEP17Decimals(tokenHash util.Uint160) (int64, error) { - result, err := c.InvokeFunction(tokenHash, "decimals", []smartcontract.Parameter{}, nil) - if err != nil { - return 0, err - } - err = getInvocationError(result) - if err != nil { - return 0, fmt.Errorf("failed to get NEP17 decimals: %w", err) - } - - return topIntFromStack(result.Stack) + return c.nepDecimals(tokenHash) } // NEP17Symbol invokes `symbol` NEP17 method on a specified contract. func (c *Client) NEP17Symbol(tokenHash util.Uint160) (string, error) { - result, err := c.InvokeFunction(tokenHash, "symbol", []smartcontract.Parameter{}, nil) - if err != nil { - return "", err - } - err = getInvocationError(result) - if err != nil { - return "", fmt.Errorf("failed to get NEP17 symbol: %w", err) - } - - return topStringFromStack(result.Stack) + return c.nepSymbol(tokenHash) } // NEP17TotalSupply invokes `totalSupply` NEP17 method on a specified contract. func (c *Client) NEP17TotalSupply(tokenHash util.Uint160) (int64, error) { - result, err := c.InvokeFunction(tokenHash, "totalSupply", []smartcontract.Parameter{}, nil) - if err != nil { - return 0, err - } - err = getInvocationError(result) - if err != nil { - return 0, fmt.Errorf("failed to get NEP17 total supply: %w", err) - } - - return topIntFromStack(result.Stack) + return c.nepTotalSupply(tokenHash) } // NEP17BalanceOf invokes `balanceOf` NEP17 method on a specified contract. func (c *Client) NEP17BalanceOf(tokenHash, acc util.Uint160) (int64, error) { - result, err := c.InvokeFunction(tokenHash, "balanceOf", []smartcontract.Parameter{{ - Type: smartcontract.Hash160Type, - Value: acc, - }}, nil) - if err != nil { - return 0, err - } - err = getInvocationError(result) - if err != nil { - return 0, fmt.Errorf("failed to get NEP17 balance: %w", err) - } - - return topIntFromStack(result.Stack) + return c.nepBalanceOf(tokenHash, acc, nil) } // NEP17TokenInfo returns full NEP17 token info. @@ -143,13 +100,9 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, reci if w.Err != nil { return nil, fmt.Errorf("failed to create transfer script: %w", w.Err) } - accAddr, err := address.StringToUint160(acc.Address) - if err != nil { - return nil, fmt.Errorf("bad account address: %v", err) - } return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, []SignerAccount{{ Signer: transaction.Signer{ - Account: accAddr, + Account: from, Scopes: transaction.CalledByEntry, }, Account: acc, @@ -176,9 +129,6 @@ func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee, sysFee = result.GasConsumed } - if !c.initDone { - return nil, errNetworkNotInitialized - } tx := transaction.New(script, sysFee) tx.Signers = signers @@ -200,6 +150,10 @@ func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee, // using contract's number of decimals) to given account with data specified and // sends it to the network returning just a hash of it. func (c *Client) TransferNEP17(acc *wallet.Account, to util.Uint160, token util.Uint160, amount int64, gas int64, data interface{}) (util.Uint256, error) { + if !c.initDone { + return util.Uint256{}, errNetworkNotInitialized + } + tx, err := c.CreateNEP17TransferTx(acc, to, token, amount, gas, data) if err != nil { return util.Uint256{}, err @@ -214,6 +168,10 @@ func (c *Client) TransferNEP17(acc *wallet.Account, to util.Uint160, token util. // MultiTransferNEP17 is similar to TransferNEP17, buf allows to have multiple recipients. func (c *Client) MultiTransferNEP17(acc *wallet.Account, gas int64, recipients []TransferTarget, data []interface{}) (util.Uint256, error) { + if !c.initDone { + return util.Uint256{}, errNetworkNotInitialized + } + tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients, data) if err != nil { return util.Uint256{}, err @@ -225,32 +183,3 @@ func (c *Client) MultiTransferNEP17(acc *wallet.Account, gas int64, recipients [ return c.SendRawTransaction(tx) } - -func topIntFromStack(st []stackitem.Item) (int64, error) { - index := len(st) - 1 // top stack element is last in the array - bi, err := st[index].TryInteger() - if err != nil { - return 0, err - } - return bi.Int64(), nil -} - -func topStringFromStack(st []stackitem.Item) (string, error) { - index := len(st) - 1 // top stack element is last in the array - bs, err := st[index].TryBytes() - if err != nil { - return "", err - } - return string(bs), nil -} - -// getInvocationError returns an error in case of bad VM state or empty stack. -func getInvocationError(result *result.Invoke) error { - if result.State != "HALT" { - return fmt.Errorf("invocation failed: %s", result.FaultException) - } - if len(result.Stack) == 0 { - return errors.New("result stack is empty") - } - return nil -} diff --git a/pkg/rpc/client/policy.go b/pkg/rpc/client/policy.go index 674d7314e4..e8644a0258 100644 --- a/pkg/rpc/client/policy.go +++ b/pkg/rpc/client/policy.go @@ -6,7 +6,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) // GetFeePerByte invokes `getFeePerByte` method on a native Policy contract. @@ -80,13 +79,3 @@ func (c *Client) IsBlocked(hash util.Uint160) (bool, error) { } return topBoolFromStack(result.Stack) } - -// topBoolFromStack returns the top boolean value from stack -func topBoolFromStack(st []stackitem.Item) (bool, error) { - index := len(st) - 1 // top stack element is last in the array - result, ok := st[index].Value().(bool) - if !ok { - return false, fmt.Errorf("invalid stack item type: %s", st[index].Type()) - } - return result, nil -} diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index 41305f3322..51c3fd6fe9 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -8,6 +8,7 @@ import ( "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" @@ -19,6 +20,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "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" ) @@ -476,3 +478,92 @@ func TestClient_GetNativeContracts(t *testing.T) { require.NoError(t, err) require.Equal(t, chain.GetNatives(), cs) } + +func TestClient_NEP11(t *testing.T) { + chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) + defer chain.Close() + defer rpcSrv.Shutdown() + + c, err := client.New(context.Background(), httpSrv.URL, client.Options{}) + require.NoError(t, err) + require.NoError(t, c.Init()) + + h, err := chain.GetNativeContractScriptHash(nativenames.NameService) + require.NoError(t, err) + acc := testchain.PrivateKeyByID(0).GetScriptHash() + + t.Run("Decimals", func(t *testing.T) { + d, err := c.NEP11Decimals(h) + require.NoError(t, err) + require.EqualValues(t, 0, d) // non-divisible + }) + t.Run("TotalSupply", func(t *testing.T) { + s, err := c.NEP11TotalSupply(h) + require.NoError(t, err) + require.EqualValues(t, 1, s) // the only `neo.com` of acc0 + }) + t.Run("Symbol", func(t *testing.T) { + sym, err := c.NEP11Symbol(h) + require.NoError(t, err) + require.Equal(t, "NNS", sym) + }) + t.Run("BalanceOf", func(t *testing.T) { + b, err := c.NEP11BalanceOf(h, acc) + require.NoError(t, err) + require.EqualValues(t, 1, b) + }) + t.Run("OwnerOf", func(t *testing.T) { + b, err := c.NEP11NDOwnerOf(h, "neo.com") + require.NoError(t, err) + require.EqualValues(t, acc, b) + }) + 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 + 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 + require.EqualValues(t, expected, p) + }) + t.Run("Transfer", func(t *testing.T) { + _, err := c.TransferNEP11(wallet.NewAccountFromPrivateKey(testchain.PrivateKeyByID(0)), testchain.PrivateKeyByID(1).GetScriptHash(), h, "neo.com", 0) + require.NoError(t, err) + }) +} + +func TestClient_NNS(t *testing.T) { + chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) + defer chain.Close() + defer rpcSrv.Shutdown() + + c, err := client.New(context.Background(), httpSrv.URL, client.Options{}) + require.NoError(t, err) + require.NoError(t, c.Init()) + + t.Run("NNSIsAvailable, false", func(t *testing.T) { + b, err := c.NNSIsAvailable("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") + 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) + 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) + require.Error(t, err) + }) + t.Run("NNSResolve, forbidden", func(t *testing.T) { + _, err := c.NNSResolve("neogo.com", nnsrecords.CNAME) + require.Error(t, err) + }) +} diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index c29c4cdd3f..3e3ecd01b2 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -60,7 +60,7 @@ type rpcTestCase struct { } const testContractHash = "c6ca2347bb84b99807221365c900ec069a265e7c" -const deploymentTxHash = "fdd4f9252cde778010d14e9710efeeb80796fd38d778e9943c1d5bb2dc656c99" +const deploymentTxHash = "26692315f71f4790263160c0f570828be77c6927493ae6657a6e5a6a09229eb9" const genesisBlockHash = "5b60644c6c6f58faca72c70689d7ed1f40c2e795772bd0de5a88e983ad55080c" const verifyContractHash = "5bb4bac40e961e334ba7bd36d2496010f67e246e" @@ -654,7 +654,7 @@ var rpcTestCases = map[string][]rpcTestCase{ require.True(t, ok) expected := result.UnclaimedGas{ Address: testchain.MultisigScriptHash(), - Unclaimed: *big.NewInt(5000), + Unclaimed: *big.NewInt(7000), } assert.Equal(t, expected, *actual) }, @@ -1415,7 +1415,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, 11, actual.Confirmations) + assert.Equal(t, 15, actual.Confirmations) assert.Equal(t, TXHash, actual.Transaction.Hash()) }) @@ -1533,12 +1533,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{6, 7, 8, 9}, []int{1, 2}) }) + 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("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{3, 4}, []int{0}) }) - t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{6}, []int{1}) }) - t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{5, 6}, []int{1}) }) - t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{7, 8}, []int{2}) }) + 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}) }) }) } @@ -1639,8 +1639,8 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "68992456820", - LastUpdated: 10, + Amount: "67960042780", + LastUpdated: 14, }}, Address: testchain.PrivateKeyByID(0).GetScriptHash().StringLE(), } @@ -1649,7 +1649,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}, []int{0, 1, 2, 3, 4, 5, 6}) + 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}) } func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) { @@ -1658,6 +1658,19 @@ 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 + 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 + require.NoError(t, err) + require.Equal(t, 1, len(blockRegisterDomain.Transactions)) + txRegisterDomain := blockRegisterDomain.Transactions[0] + + blockGASBounty2, err := e.chain.GetBlock(e.chain.GetHeaderHash(12)) // size of committee = 6 + require.NoError(t, err) + 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)) @@ -1677,7 +1690,7 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc require.NoError(t, err) require.Equal(t, 1, len(blockSendRubles.Transactions)) txSendRubles := blockSendRubles.Transactions[0] - blockGASBounty := blockSendRubles // index 6 = size of committee + blockGASBounty1 := blockSendRubles // index 6 = size of committee blockReceiveRubles, err := e.chain.GetBlock(e.chain.GetHeaderHash(5)) require.NoError(t, err) @@ -1716,6 +1729,22 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc // duplicate the Server method. expected := result.NEP17Transfers{ Sent: []result.NEP17Transfer{ + { + Timestamp: blockSetRecord.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: big.NewInt(txSetRecord.SystemFee + txSetRecord.NetworkFee).String(), + Index: 14, + TxHash: blockSetRecord.Hash(), + }, + { + Timestamp: blockRegisterDomain.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: big.NewInt(txRegisterDomain.SystemFee + txRegisterDomain.NetworkFee).String(), + Index: 13, + TxHash: blockRegisterDomain.Hash(), + }, { Timestamp: blockDeploy3.Timestamp, Asset: e.chain.UtilityTokenHash(), @@ -1818,13 +1847,22 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc }, Received: []result.NEP17Transfer{ { - Timestamp: blockGASBounty.Timestamp, + Timestamp: blockGASBounty2.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", + Amount: "50000000", + Index: 12, + NotifyIndex: 0, + TxHash: blockGASBounty2.Hash(), + }, + { + Timestamp: blockGASBounty1.Timestamp, Asset: e.chain.UtilityTokenHash(), Address: "", Amount: "50000000", Index: 6, NotifyIndex: 0, - TxHash: blockGASBounty.Hash(), + TxHash: blockGASBounty1.Hash(), }, { Timestamp: blockReceiveRubles.Timestamp, diff --git a/pkg/rpc/server/testdata/testblocks.acc b/pkg/rpc/server/testdata/testblocks.acc index fd8c3bc97b..a5e2a6d209 100644 Binary files a/pkg/rpc/server/testdata/testblocks.acc and b/pkg/rpc/server/testdata/testblocks.acc differ diff --git a/pkg/vm/emit/emit.go b/pkg/vm/emit/emit.go index 00f7c1b44a..55949f8cb8 100644 --- a/pkg/vm/emit/emit.go +++ b/pkg/vm/emit/emit.go @@ -94,7 +94,7 @@ func Array(w *io.BinWriter, es ...interface{}) { Bool(w, e) default: if es[i] != nil { - w.Err = errors.New("unsupported type") + w.Err = fmt.Errorf("unsupported type: %v", e) return } Opcodes(w, opcode.PUSHNULL)