2021-10-01 19:24:05 +00:00
|
|
|
package tests
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"math/big"
|
2021-10-26 11:28:14 +00:00
|
|
|
"path"
|
2022-08-18 12:17:16 +00:00
|
|
|
"strings"
|
2021-10-01 19:24:05 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2023-03-07 11:06:21 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
2021-10-01 19:24:05 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
|
2024-09-06 13:37:35 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
2024-01-29 11:01:37 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
2021-10-26 11:28:14 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/neotest"
|
2024-01-29 11:01:37 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
2021-10-01 19:24:05 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
|
|
|
const nnsPath = "../nns"
|
|
|
|
|
2021-10-04 13:18:05 +00:00
|
|
|
const msPerYear = 365 * 24 * time.Hour / time.Millisecond
|
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
func newNNSInvoker(t *testing.T, addRoot bool) *neotest.ContractInvoker {
|
|
|
|
e := newExecutor(t)
|
|
|
|
ctr := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
|
|
|
|
e.DeployContract(t, ctr, nil)
|
|
|
|
|
|
|
|
c := e.CommitteeInvoker(ctr.Hash)
|
|
|
|
if addRoot {
|
2022-08-18 12:17:16 +00:00
|
|
|
// Set expiration big enough to pass all tests.
|
|
|
|
refresh, retry, expire, ttl := int64(101), int64(102), int64(msPerYear/1000*100), int64(104)
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, true, "register",
|
|
|
|
"com", c.CommitteeHash,
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-26 11:28:14 +00:00
|
|
|
}
|
|
|
|
return c
|
|
|
|
}
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
func TestNNSGeneric(t *testing.T) {
|
|
|
|
c := newNNSInvoker(t, false)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, "NNS", "symbol")
|
|
|
|
c.Invoke(t, 0, "decimals")
|
|
|
|
c.Invoke(t, 0, "totalSupply")
|
2021-10-01 19:24:05 +00:00
|
|
|
}
|
|
|
|
|
2021-10-04 10:26:23 +00:00
|
|
|
func TestNNSRegisterTLD(t *testing.T) {
|
2021-10-26 11:28:14 +00:00
|
|
|
c := newNNSInvoker(t, false)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-04 10:26:23 +00:00
|
|
|
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
|
2021-10-26 11:28:14 +00:00
|
|
|
|
|
|
|
c.InvokeFail(t, "invalid domain name format", "register",
|
|
|
|
"0com", c.CommitteeHash,
|
2023-05-17 07:51:39 +00:00
|
|
|
"email@frostfs.info", refresh, retry, expire, ttl)
|
2024-06-18 13:50:09 +00:00
|
|
|
c.InvokeFail(t, "invalid fragment '0com'", "register",
|
|
|
|
"0com", c.CommitteeHash,
|
|
|
|
"email@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
acc := c.NewAccount(t)
|
|
|
|
cAcc := c.WithSigners(acc)
|
|
|
|
cAcc.InvokeFail(t, "not witnessed by committee", "register",
|
|
|
|
"com", acc.ScriptHash(),
|
2023-05-17 07:51:39 +00:00
|
|
|
"email@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2024-01-25 13:20:33 +00:00
|
|
|
t.Run("size checks", func(t *testing.T) {
|
|
|
|
c.Invoke(t, true, "register",
|
|
|
|
"ns", c.CommitteeHash,
|
|
|
|
"email@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
|
|
|
|
c.InvokeFail(t, "invalid domain name format", "register",
|
|
|
|
"x", c.CommitteeHash,
|
|
|
|
"email@frostfs.info", refresh, retry, expire, ttl)
|
2024-06-18 13:50:09 +00:00
|
|
|
c.InvokeFail(t, "domain name too short", "register",
|
|
|
|
"x", c.CommitteeHash,
|
|
|
|
"email@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
c.InvokeFail(t, "domain name too long", "register",
|
|
|
|
getTooLongDomainName(255), c.CommitteeHash,
|
|
|
|
"email@frostfs.info", refresh, retry, expire, ttl)
|
2024-01-25 13:20:33 +00:00
|
|
|
})
|
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, true, "register",
|
|
|
|
"com", c.CommitteeHash,
|
2023-05-17 07:51:39 +00:00
|
|
|
"email@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
c.InvokeFail(t, "TLD already exists", "register",
|
|
|
|
"com", c.CommitteeHash,
|
2023-05-17 07:51:39 +00:00
|
|
|
"email@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-01 19:24:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestNNSRegister(t *testing.T) {
|
2021-10-26 11:28:14 +00:00
|
|
|
c := newNNSInvoker(t, false)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
accTop := c.NewAccount(t)
|
2021-10-04 10:26:23 +00:00
|
|
|
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
|
2021-10-26 11:28:14 +00:00
|
|
|
c1 := c.WithSigners(c.Committee, accTop)
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"com", accTop.ScriptHash(),
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
acc := c.NewAccount(t)
|
|
|
|
c2 := c.WithSigners(c.Committee, acc)
|
|
|
|
c2.InvokeFail(t, "not witnessed by admin", "register",
|
|
|
|
"testdomain.com", acc.ScriptHash(),
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-04 11:47:42 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
c3 := c.WithSigners(accTop, acc)
|
2021-11-26 14:05:06 +00:00
|
|
|
t.Run("domain names with hyphen", func(t *testing.T) {
|
|
|
|
c3.InvokeFail(t, "invalid domain name format", "register",
|
|
|
|
"-testdomain.com", acc.ScriptHash(),
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2024-06-18 13:50:09 +00:00
|
|
|
c3.InvokeFail(t, "invalid fragment '-testdomain'", "register",
|
|
|
|
"-testdomain.com", acc.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
|
2021-11-26 14:05:06 +00:00
|
|
|
c3.InvokeFail(t, "invalid domain name format", "register",
|
|
|
|
"testdomain-.com", acc.ScriptHash(),
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2024-06-18 13:50:09 +00:00
|
|
|
c3.InvokeFail(t, "invalid fragment 'testdomain-'", "register",
|
|
|
|
"testdomain-.com", acc.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
|
2021-11-26 14:05:06 +00:00
|
|
|
c3.Invoke(t, true, "register",
|
|
|
|
"test-domain.com", acc.ScriptHash(),
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-11-26 14:05:06 +00:00
|
|
|
})
|
2024-09-06 13:37:35 +00:00
|
|
|
expected := stackitem.NewArray([]stackitem.Item{
|
|
|
|
stackitem.NewByteArray([]byte("testdomain.com")),
|
|
|
|
})
|
|
|
|
tx := c3.Invoke(t, true, "register",
|
2021-10-26 11:28:14 +00:00
|
|
|
"testdomain.com", acc.ScriptHash(),
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2024-09-06 13:37:35 +00:00
|
|
|
c.CheckTxNotificationEvent(t, tx, -1, state.NotificationEvent{ScriptHash: c.Hash, Name: "RegisterDomain", Item: expected})
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
b := c.TopBlock(t)
|
2024-09-06 13:37:35 +00:00
|
|
|
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewBuffer(
|
2023-05-17 07:51:39 +00:00
|
|
|
[]byte(fmt.Sprintf("testdomain.com myemail@frostfs.info %d %d %d %d %d",
|
2021-10-26 11:28:14 +00:00
|
|
|
b.Timestamp, refresh, retry, expire, ttl)))})
|
|
|
|
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.SOA))
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
cAcc := c.WithSigners(acc)
|
2024-09-06 13:37:35 +00:00
|
|
|
|
|
|
|
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")),
|
|
|
|
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))})
|
|
|
|
tx = cAcc.Invoke(t, stackitem.Null{}, "addRecord",
|
2021-10-01 19:24:05 +00:00
|
|
|
"testdomain.com", int64(nns.TXT), "first TXT record")
|
2024-09-06 13:37:35 +00:00
|
|
|
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "AddRecord", Item: expected})
|
|
|
|
|
2021-11-17 14:10:07 +00:00
|
|
|
cAcc.InvokeFail(t, "record already exists", "addRecord",
|
|
|
|
"testdomain.com", int64(nns.TXT), "first TXT record")
|
2021-10-26 11:28:14 +00:00
|
|
|
cAcc.Invoke(t, stackitem.Null{}, "addRecord",
|
2021-10-01 19:24:05 +00:00
|
|
|
"testdomain.com", int64(nns.TXT), "second TXT record")
|
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
expected = stackitem.NewArray([]stackitem.Item{
|
2021-10-01 19:24:05 +00:00
|
|
|
stackitem.NewByteArray([]byte("first TXT record")),
|
2023-11-07 12:00:02 +00:00
|
|
|
stackitem.NewByteArray([]byte("second TXT record")),
|
|
|
|
})
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2024-09-06 13:37:35 +00:00
|
|
|
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")),
|
|
|
|
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))})
|
|
|
|
tx = cAcc.Invoke(t, stackitem.Null{}, "setRecord",
|
2021-10-01 19:24:05 +00:00
|
|
|
"testdomain.com", int64(nns.TXT), int64(0), "replaced first")
|
2024-09-06 13:37:35 +00:00
|
|
|
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "AddRecord", Item: expected})
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
expected = stackitem.NewArray([]stackitem.Item{
|
2021-10-01 19:24:05 +00:00
|
|
|
stackitem.NewByteArray([]byte("replaced first")),
|
2023-11-07 12:00:02 +00:00
|
|
|
stackitem.NewByteArray([]byte("second TXT record")),
|
|
|
|
})
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
|
2024-09-06 13:37:35 +00:00
|
|
|
|
|
|
|
tx = cAcc.Invoke(t, stackitem.Null{}, "deleteRecords", "testdomain.com", int64(nns.TXT))
|
|
|
|
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")),
|
|
|
|
stackitem.NewBigInteger(big.NewInt(int64(nns.TXT)))})
|
|
|
|
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteRecords", Item: expected})
|
|
|
|
|
|
|
|
c.Invoke(t, stackitem.Null{}, "getRecords", "testdomain.com", int64(nns.TXT))
|
|
|
|
|
2024-09-27 14:50:26 +00:00
|
|
|
cAcc.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"testdomain.com", int64(nns.TXT), "rec1")
|
|
|
|
|
|
|
|
cAcc.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"testdomain.com", int64(nns.TXT), "rec2")
|
|
|
|
|
|
|
|
cAcc.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"testdomain.com", int64(nns.TXT), "rec3")
|
|
|
|
|
|
|
|
cAcc.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"testdomain.com", int64(nns.TXT), "rec4")
|
|
|
|
|
|
|
|
cAcc.Invoke(t, false, "deleteRecord",
|
|
|
|
"testdomain.com", int64(nns.TXT), "rec9999")
|
|
|
|
|
|
|
|
cAcc.Invoke(t, true, "deleteRecord",
|
|
|
|
"testdomain.com", int64(nns.TXT), "rec1")
|
|
|
|
|
|
|
|
expected = stackitem.NewArray([]stackitem.Item{
|
|
|
|
stackitem.NewByteArray([]byte("rec2")),
|
|
|
|
stackitem.NewByteArray([]byte("rec3")),
|
|
|
|
stackitem.NewByteArray([]byte("rec4")),
|
|
|
|
})
|
|
|
|
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
|
|
|
|
|
|
|
|
cAcc.Invoke(t, true, "deleteRecord",
|
|
|
|
"testdomain.com", int64(nns.TXT), "rec4")
|
|
|
|
|
|
|
|
cAcc.Invoke(t, true, "deleteRecord",
|
|
|
|
"testdomain.com", int64(nns.TXT), "rec2")
|
|
|
|
|
|
|
|
expected = stackitem.NewArray([]stackitem.Item{
|
|
|
|
stackitem.NewByteArray([]byte("rec3")),
|
|
|
|
})
|
|
|
|
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
|
|
|
|
|
|
|
|
cAcc.Invoke(t, true, "deleteRecord",
|
|
|
|
"testdomain.com", int64(nns.TXT), "rec3")
|
|
|
|
|
|
|
|
c.Invoke(t, stackitem.Null{}, "getRecords", "testdomain.com", int64(nns.TXT))
|
|
|
|
|
2024-09-06 13:37:35 +00:00
|
|
|
tx = cAcc.Invoke(t, stackitem.Null{}, "deleteDomain", "testdomain.com")
|
|
|
|
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com")),
|
|
|
|
stackitem.NewBigInteger(big.NewInt(int64(nns.CNAME)))})
|
|
|
|
c.CheckTxNotificationEvent(t, tx, 0, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteRecords", Item: expected})
|
|
|
|
|
|
|
|
expected = stackitem.NewArray([]stackitem.Item{stackitem.NewByteArray([]byte("testdomain.com"))})
|
|
|
|
c.CheckTxNotificationEvent(t, tx, 4, state.NotificationEvent{ScriptHash: c.Hash, Name: "DeleteDomain", Item: expected})
|
|
|
|
|
|
|
|
c.InvokeFail(t, "token not found", "getRecords", "testdomain.com", int64(nns.SOA))
|
2021-10-01 19:24:05 +00:00
|
|
|
}
|
|
|
|
|
2024-09-27 14:51:13 +00:00
|
|
|
func TestDeleteDomain(t *testing.T) {
|
|
|
|
c := newNNSInvoker(t, false)
|
|
|
|
|
|
|
|
acc1 := c.NewAccount(t)
|
|
|
|
c1 := c.WithSigners(c.Committee, acc1)
|
|
|
|
|
|
|
|
acc2 := c.NewAccount(t)
|
|
|
|
c2 := c.WithSigners(c.Committee, acc2)
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"com", acc1.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL)
|
|
|
|
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"testdomain.com", acc1.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL)
|
|
|
|
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"domik.testdomain.com", acc1.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL)
|
|
|
|
|
|
|
|
c1.InvokeFail(t, "domain not found", "deleteDomain", "ru")
|
|
|
|
c1.InvokeFail(t, "can't delete a domain that has subdomains", "deleteDomain", "testdomain.com")
|
|
|
|
c1.Invoke(t, stackitem.Null{}, "deleteDomain", "domik.testdomain.com")
|
|
|
|
c1.Invoke(t, stackitem.Null{}, "deleteDomain", "testdomain.com")
|
|
|
|
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"cn", acc1.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL)
|
|
|
|
|
|
|
|
c2.InvokeFail(t, "not witnessed by admin", "deleteDomain", "cn")
|
|
|
|
|
|
|
|
c1.Invoke(t, stackitem.Null{}, "deleteDomain", "cn")
|
|
|
|
|
|
|
|
c2.Invoke(t, true, "register",
|
|
|
|
"cn", acc2.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", defaultRefresh, defaultRetry, defaultExpire, defaultTTL)
|
|
|
|
}
|
|
|
|
|
2024-08-16 13:38:54 +00:00
|
|
|
func TestGlobalDomain(t *testing.T) {
|
|
|
|
c := newNNSInvoker(t, false)
|
|
|
|
|
|
|
|
accTop := c.NewAccount(t)
|
|
|
|
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
|
|
|
|
c1 := c.WithSigners(c.Committee, accTop)
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"com", accTop.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"testdomain.com", accTop.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"globaldomain.com", accTop.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"domik.testdomain.com", accTop.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
c1.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"domik.testdomain.com", int64(nns.TXT), "CID")
|
|
|
|
|
|
|
|
c.Invoke(t, true, "isAvailable", "domik.globaldomain.com")
|
|
|
|
|
|
|
|
c1.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"testdomain.com", int64(nns.TXT), nns.Cnametgt+"=globaldomain.com")
|
|
|
|
c.Invoke(t, true, "isAvailable", "dom.testdomain.com")
|
|
|
|
|
|
|
|
c1.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"domik.testdomain.com", int64(nns.TXT), "random txt record")
|
|
|
|
c.Invoke(t, true, "isAvailable", "domik.globaldomain.com")
|
|
|
|
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"dom.testdomain.com", accTop.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
|
|
|
|
c1.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"dom.testdomain.com", int64(nns.TXT), "CID")
|
|
|
|
|
|
|
|
c.InvokeFail(t, "global domain is already taken", "isAvailable", "dom.testdomain.com")
|
|
|
|
}
|
2021-11-22 13:50:15 +00:00
|
|
|
func TestTLDRecord(t *testing.T) {
|
|
|
|
c := newNNSInvoker(t, true)
|
|
|
|
c.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"com", int64(nns.A), "1.2.3.4")
|
|
|
|
|
|
|
|
result := []stackitem.Item{stackitem.NewByteArray([]byte("1.2.3.4"))}
|
|
|
|
c.Invoke(t, result, "resolve", "com", int64(nns.A))
|
|
|
|
}
|
|
|
|
|
2021-11-19 13:35:58 +00:00
|
|
|
func TestNNSRegisterMulti(t *testing.T) {
|
|
|
|
c := newNNSInvoker(t, true)
|
|
|
|
|
2023-11-07 12:18:48 +00:00
|
|
|
newArgs := func(domain string, account neotest.Signer) []any {
|
|
|
|
return []any{
|
2021-11-19 13:35:58 +00:00
|
|
|
domain, account.ScriptHash(), "doesnt@matter.com",
|
|
|
|
int64(101), int64(102), int64(103), int64(104),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
acc := c.NewAccount(t)
|
|
|
|
cBoth := c.WithSigners(c.Committee, acc)
|
|
|
|
args := newArgs("neo.com", acc)
|
|
|
|
cBoth.Invoke(t, true, "register", args...)
|
|
|
|
|
|
|
|
c1 := c.WithSigners(acc)
|
|
|
|
t.Run("parent domain is missing", func(t *testing.T) {
|
2024-04-15 23:56:31 +00:00
|
|
|
msg := "domain does not exist or is expired: fs.neo.com"
|
2021-11-19 13:35:58 +00:00
|
|
|
args[0] = "testnet.fs.neo.com"
|
|
|
|
c1.InvokeFail(t, msg, "register", args...)
|
|
|
|
})
|
|
|
|
|
|
|
|
args[0] = "fs.neo.com"
|
|
|
|
c1.Invoke(t, true, "register", args...)
|
|
|
|
|
|
|
|
args[0] = "testnet.fs.neo.com"
|
|
|
|
c1.Invoke(t, true, "register", args...)
|
|
|
|
|
|
|
|
acc2 := c.NewAccount(t)
|
|
|
|
c2 := c.WithSigners(c.Committee, acc2)
|
|
|
|
args = newArgs("mainnet.fs.neo.com", acc2)
|
|
|
|
c2.InvokeFail(t, "not witnessed by admin", "register", args...)
|
|
|
|
|
2021-11-22 09:42:28 +00:00
|
|
|
c1.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"something.mainnet.fs.neo.com", int64(nns.A), "1.2.3.4")
|
|
|
|
c1.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"another.fs.neo.com", int64(nns.A), "4.3.2.1")
|
|
|
|
|
2021-11-19 13:35:58 +00:00
|
|
|
c2 = c.WithSigners(acc, acc2)
|
2021-11-22 09:42:28 +00:00
|
|
|
c2.InvokeFail(t, "parent domain has conflicting records: something.mainnet.fs.neo.com",
|
|
|
|
"register", args...)
|
|
|
|
|
|
|
|
c1.Invoke(t, stackitem.Null{}, "deleteRecords",
|
|
|
|
"something.mainnet.fs.neo.com", int64(nns.A))
|
2021-11-19 13:35:58 +00:00
|
|
|
c2.Invoke(t, true, "register", args...)
|
|
|
|
|
|
|
|
c2 = c.WithSigners(acc2)
|
|
|
|
c2.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"cdn.mainnet.fs.neo.com", int64(nns.A), "166.15.14.13")
|
|
|
|
result := stackitem.NewArray([]stackitem.Item{
|
2023-11-07 12:00:02 +00:00
|
|
|
stackitem.NewByteArray([]byte("166.15.14.13")),
|
|
|
|
})
|
2021-11-19 13:35:58 +00:00
|
|
|
c2.Invoke(t, result, "resolve", "cdn.mainnet.fs.neo.com", int64(nns.A))
|
|
|
|
}
|
|
|
|
|
2021-10-01 19:24:05 +00:00
|
|
|
func TestNNSUpdateSOA(t *testing.T) {
|
2021-10-26 11:28:14 +00:00
|
|
|
c := newNNSInvoker(t, true)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-04 10:26:23 +00:00
|
|
|
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, true, "register",
|
|
|
|
"testdomain.com", c.CommitteeHash,
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
|
|
|
refresh *= 2
|
|
|
|
retry *= 2
|
|
|
|
expire *= 2
|
|
|
|
ttl *= 2
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, stackitem.Null{}, "updateSOA",
|
2023-05-17 07:51:39 +00:00
|
|
|
"testdomain.com", "newemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
b := c.TopBlock(t)
|
|
|
|
expected := stackitem.NewArray([]stackitem.Item{stackitem.NewBuffer(
|
2023-05-17 07:51:39 +00:00
|
|
|
[]byte(fmt.Sprintf("testdomain.com newemail@frostfs.info %d %d %d %d %d",
|
2021-10-26 11:28:14 +00:00
|
|
|
b.Timestamp, refresh, retry, expire, ttl)))})
|
|
|
|
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.SOA))
|
2021-10-01 19:24:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestNNSGetAllRecords(t *testing.T) {
|
2021-10-26 11:28:14 +00:00
|
|
|
c := newNNSInvoker(t, true)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-04 10:26:23 +00:00
|
|
|
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, true, "register",
|
|
|
|
"testdomain.com", c.CommitteeHash,
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, stackitem.Null{}, "addRecord", "testdomain.com", int64(nns.TXT), "first TXT record")
|
|
|
|
c.Invoke(t, stackitem.Null{}, "addRecord", "testdomain.com", int64(nns.A), "1.2.3.4")
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
b := c.TopBlock(t)
|
2023-05-17 07:51:39 +00:00
|
|
|
expSOA := fmt.Sprintf("testdomain.com myemail@frostfs.info %d %d %d %d %d",
|
2021-10-01 19:24:05 +00:00
|
|
|
b.Timestamp, refresh, retry, expire, ttl)
|
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
s, err := c.TestInvoke(t, "getAllRecords", "testdomain.com")
|
2021-10-01 19:24:05 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
iter := s.Pop().Value().(*storage.Iterator)
|
2021-10-01 19:24:05 +00:00
|
|
|
require.True(t, iter.Next())
|
|
|
|
require.Equal(t, stackitem.NewStruct([]stackitem.Item{
|
|
|
|
stackitem.Make("testdomain.com"), stackitem.Make(int64(nns.A)),
|
|
|
|
stackitem.Make("1.2.3.4"), stackitem.Make(new(big.Int)),
|
|
|
|
}), iter.Value())
|
|
|
|
|
|
|
|
require.True(t, iter.Next())
|
|
|
|
require.Equal(t, stackitem.NewStruct([]stackitem.Item{
|
|
|
|
stackitem.Make("testdomain.com"), stackitem.Make(int64(nns.SOA)),
|
|
|
|
stackitem.NewBuffer([]byte(expSOA)), stackitem.Make(new(big.Int)),
|
|
|
|
}), iter.Value())
|
|
|
|
|
|
|
|
require.True(t, iter.Next())
|
|
|
|
require.Equal(t, stackitem.NewStruct([]stackitem.Item{
|
|
|
|
stackitem.Make("testdomain.com"), stackitem.Make(int64(nns.TXT)),
|
|
|
|
stackitem.Make("first TXT record"), stackitem.Make(new(big.Int)),
|
|
|
|
}), iter.Value())
|
|
|
|
|
|
|
|
require.False(t, iter.Next())
|
|
|
|
}
|
|
|
|
|
2021-10-04 13:18:05 +00:00
|
|
|
func TestExpiration(t *testing.T) {
|
2021-10-26 11:28:14 +00:00
|
|
|
c := newNNSInvoker(t, true)
|
2021-10-04 13:18:05 +00:00
|
|
|
|
2022-08-18 12:17:16 +00:00
|
|
|
refresh, retry, expire, ttl := int64(101), int64(102), int64(msPerYear/1000*10), int64(104)
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, true, "register",
|
|
|
|
"testdomain.com", c.CommitteeHash,
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-04 13:18:05 +00:00
|
|
|
|
2022-08-18 12:17:16 +00:00
|
|
|
checkProperties := func(t *testing.T, expiration uint64) {
|
|
|
|
expected := stackitem.NewMapWithValue([]stackitem.MapElement{
|
|
|
|
{Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")},
|
2023-11-07 12:00:02 +00:00
|
|
|
{Key: stackitem.Make("expiration"), Value: stackitem.Make(expiration)},
|
|
|
|
})
|
2022-08-18 12:17:16 +00:00
|
|
|
s, err := c.TestInvoke(t, "properties", "testdomain.com")
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, expected.Value(), s.Top().Item().Value())
|
|
|
|
}
|
|
|
|
|
|
|
|
top := c.TopBlock(t)
|
|
|
|
expiration := top.Timestamp + uint64(expire*1000)
|
|
|
|
checkProperties(t, expiration)
|
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
b := c.NewUnsignedBlock(t)
|
2022-08-18 12:17:16 +00:00
|
|
|
b.Timestamp = expiration - 2 // test invoke is done with +1 timestamp
|
2021-10-26 11:28:14 +00:00
|
|
|
require.NoError(t, c.Chain.AddBlock(c.SignBlock(b)))
|
2022-08-18 12:17:16 +00:00
|
|
|
checkProperties(t, expiration)
|
|
|
|
|
|
|
|
b = c.NewUnsignedBlock(t)
|
|
|
|
b.Timestamp = expiration - 1
|
|
|
|
require.NoError(t, c.Chain.AddBlock(c.SignBlock(b)))
|
|
|
|
|
|
|
|
_, err := c.TestInvoke(t, "properties", "testdomain.com")
|
|
|
|
require.Error(t, err)
|
|
|
|
require.True(t, strings.Contains(err.Error(), "name has expired"))
|
2021-10-04 13:18:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
c.InvokeFail(t, "name has expired", "getAllRecords", "testdomain.com")
|
|
|
|
c.InvokeFail(t, "name has expired", "ownerOf", "testdomain.com")
|
2021-10-04 13:18:05 +00:00
|
|
|
}
|
|
|
|
|
2021-10-01 19:24:05 +00:00
|
|
|
func TestNNSSetAdmin(t *testing.T) {
|
2021-10-26 11:28:14 +00:00
|
|
|
c := newNNSInvoker(t, true)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-04 10:26:23 +00:00
|
|
|
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, true, "register",
|
|
|
|
"testdomain.com", c.CommitteeHash,
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
acc := c.NewAccount(t)
|
|
|
|
cAcc := c.WithSigners(acc)
|
|
|
|
cAcc.InvokeFail(t, "not witnessed by admin", "addRecord",
|
2021-10-01 19:24:05 +00:00
|
|
|
"testdomain.com", int64(nns.TXT), "won't be added")
|
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
c1 := c.WithSigners(c.Committee, acc)
|
|
|
|
c1.Invoke(t, stackitem.Null{}, "setAdmin", "testdomain.com", acc.ScriptHash())
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
cAcc.Invoke(t, stackitem.Null{}, "addRecord",
|
2021-10-01 19:24:05 +00:00
|
|
|
"testdomain.com", int64(nns.TXT), "will be added")
|
|
|
|
}
|
|
|
|
|
2021-10-04 10:26:23 +00:00
|
|
|
func TestNNSIsAvailable(t *testing.T) {
|
2021-10-26 11:28:14 +00:00
|
|
|
c := newNNSInvoker(t, false)
|
2021-10-04 10:26:23 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, true, "isAvailable", "com")
|
|
|
|
c.InvokeFail(t, "TLD not found", "isAvailable", "domain.com")
|
2021-10-04 10:26:23 +00:00
|
|
|
|
|
|
|
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, true, "register",
|
|
|
|
"com", c.CommitteeHash,
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-04 10:26:23 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, false, "isAvailable", "com")
|
|
|
|
c.Invoke(t, true, "isAvailable", "domain.com")
|
2021-10-04 10:26:23 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
acc := c.NewAccount(t)
|
|
|
|
c1 := c.WithSigners(c.Committee, acc)
|
2024-04-15 23:56:31 +00:00
|
|
|
|
|
|
|
c1.InvokeFail(t, "domain does not exist or is expired: domain.com", "isAvailable", "dom.domain.com")
|
2021-10-26 11:28:14 +00:00
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"domain.com", acc.ScriptHash(),
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-04 10:26:23 +00:00
|
|
|
|
2024-08-16 13:38:54 +00:00
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"globaldomain.com", acc.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, false, "isAvailable", "domain.com")
|
2023-12-12 17:22:33 +00:00
|
|
|
|
|
|
|
c.Invoke(t, true, "isAvailable", "dom.domain.com")
|
2024-04-15 23:56:31 +00:00
|
|
|
c.InvokeFail(t, "domain does not exist or is expired: dom.domain.com", "isAvailable", "dom.dom.domain.com")
|
2023-12-12 17:22:33 +00:00
|
|
|
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"dom.domain.com", acc.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
c.Invoke(t, false, "isAvailable", "dom.domain.com")
|
|
|
|
c.Invoke(t, true, "isAvailable", "dom.dom.domain.com")
|
2024-06-18 13:50:09 +00:00
|
|
|
|
2024-08-16 13:38:54 +00:00
|
|
|
c1.Invoke(t, stackitem.Null{}, "addRecord",
|
|
|
|
"dom.domain.com", int64(nns.TXT), nns.Cnametgt+"=globaldomain.com")
|
|
|
|
c.Invoke(t, true, "isAvailable", "dom.dom.domain.com")
|
|
|
|
|
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"dom.globaldomain.com", acc.ScriptHash(),
|
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
|
|
|
|
|
|
|
c.InvokeFail(t, "global domain is already taken", "isAvailable", "dom.dom.domain.com")
|
|
|
|
|
2024-06-18 13:50:09 +00:00
|
|
|
c.InvokeFail(t, "domain name too long", "isAvailable", getTooLongDomainName(255))
|
2021-10-04 10:26:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestNNSRenew(t *testing.T) {
|
2021-10-26 11:28:14 +00:00
|
|
|
c := newNNSInvoker(t, true)
|
2021-10-04 10:26:23 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
acc := c.NewAccount(t)
|
|
|
|
c1 := c.WithSigners(c.Committee, acc)
|
2021-10-01 19:24:05 +00:00
|
|
|
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
|
2021-10-26 11:28:14 +00:00
|
|
|
c1.Invoke(t, true, "register",
|
|
|
|
"testdomain.com", c.CommitteeHash,
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-04 10:26:23 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
const msPerYear = 365 * 24 * time.Hour / time.Millisecond
|
|
|
|
b := c.TopBlock(t)
|
2022-08-18 12:17:16 +00:00
|
|
|
ts := b.Timestamp + uint64(expire*1000) + uint64(msPerYear)
|
2021-10-01 19:24:05 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
cAcc := c.WithSigners(acc)
|
2022-08-23 09:06:05 +00:00
|
|
|
cAcc.InvokeFail(t, "not witnessed by admin", "renew", "testdomain.com")
|
|
|
|
c1.Invoke(t, ts, "renew", "testdomain.com")
|
2021-10-26 11:28:14 +00:00
|
|
|
expected := stackitem.NewMapWithValue([]stackitem.MapElement{
|
2022-03-28 06:48:56 +00:00
|
|
|
{Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")},
|
2023-11-07 12:00:02 +00:00
|
|
|
{Key: stackitem.Make("expiration"), Value: stackitem.Make(ts)},
|
|
|
|
})
|
2021-10-26 11:28:14 +00:00
|
|
|
cAcc.Invoke(t, expected, "properties", "testdomain.com")
|
2024-06-18 13:50:09 +00:00
|
|
|
c.InvokeFail(t, "domain name too long", "renew", getTooLongDomainName(255))
|
2021-10-01 19:24:05 +00:00
|
|
|
}
|
2021-10-16 09:47:52 +00:00
|
|
|
|
|
|
|
func TestNNSResolve(t *testing.T) {
|
2021-10-26 11:28:14 +00:00
|
|
|
c := newNNSInvoker(t, true)
|
2021-10-16 09:47:52 +00:00
|
|
|
|
|
|
|
refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, true, "register",
|
|
|
|
"test.com", c.CommitteeHash,
|
2023-05-17 07:51:39 +00:00
|
|
|
"myemail@frostfs.info", refresh, retry, expire, ttl)
|
2021-10-16 09:47:52 +00:00
|
|
|
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, stackitem.Null{}, "addRecord",
|
2021-10-16 09:47:52 +00:00
|
|
|
"test.com", int64(nns.TXT), "expected result")
|
|
|
|
|
|
|
|
records := stackitem.NewArray([]stackitem.Item{stackitem.Make("expected result")})
|
2021-10-26 11:28:14 +00:00
|
|
|
c.Invoke(t, records, "resolve", "test.com", int64(nns.TXT))
|
|
|
|
c.Invoke(t, records, "resolve", "test.com.", int64(nns.TXT))
|
|
|
|
c.InvokeFail(t, "invalid domain name format", "resolve", "test.com..", int64(nns.TXT))
|
2021-10-16 09:47:52 +00:00
|
|
|
}
|
2024-01-29 11:01:37 +00:00
|
|
|
|
|
|
|
func TestNNSAndProxy(t *testing.T) {
|
|
|
|
c := newNNSInvoker(t, false)
|
|
|
|
proxyHash := deployProxyContract(t, c.Executor)
|
|
|
|
proxySigner := neotest.NewContractSigner(proxyHash, func(*transaction.Transaction) []any { return nil })
|
|
|
|
|
|
|
|
g := c.NewInvoker(gas.Hash, c.Validator)
|
|
|
|
g.Invoke(t, true, "transfer",
|
|
|
|
c.Validator.ScriptHash(), proxyHash, 100_0000_0000, nil)
|
|
|
|
|
|
|
|
cc := c.WithSigners(proxySigner, c.Committee)
|
|
|
|
cc.Invoke(t, true, "register", "ns", proxyHash,
|
|
|
|
"ops@frostfs.info", 100, 100, 100, 100)
|
|
|
|
|
|
|
|
checkBalance := func(t *testing.T, owner util.Uint160, balance int64) {
|
|
|
|
s, err := cc.TestInvoke(t, "balanceOf", owner)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, s.Len())
|
|
|
|
require.Equal(t, int64(balance), s.Pop().BigInt().Int64())
|
|
|
|
}
|
|
|
|
|
|
|
|
checkBalance(t, proxyHash, 1)
|
|
|
|
checkBalance(t, c.CommitteeHash, 0)
|
|
|
|
|
|
|
|
t.Run("ensure domain is not lost", func(t *testing.T) {
|
|
|
|
cc.Invoke(t, true, "transfer", c.CommitteeHash, "ns", nil)
|
|
|
|
|
|
|
|
checkBalance(t, proxyHash, 0)
|
|
|
|
checkBalance(t, c.CommitteeHash, 1)
|
|
|
|
})
|
|
|
|
}
|
2024-06-18 13:50:09 +00:00
|
|
|
|
|
|
|
func getTooLongDomainName(max int) (res string) {
|
|
|
|
for len(res) < max {
|
|
|
|
res += "dom."
|
|
|
|
}
|
|
|
|
res += "com"
|
|
|
|
return res
|
|
|
|
}
|