feat: implement collectibles ownership update timestamp db
This commit is contained in:
parent
daf3fc79bf
commit
b4d5c22050
|
@ -137,6 +137,7 @@ func (c *loadOwnedCollectiblesCommand) Run(parent context.Context) (err error) {
|
|||
|
||||
pageNr := 0
|
||||
cursor := thirdparty.FetchFromStartCursor
|
||||
start := time.Now()
|
||||
|
||||
c.triggerEvent(EventCollectiblesOwnershipUpdateStarted, c.chainID, c.account, "")
|
||||
// Fetch collectibles in chunks
|
||||
|
@ -162,7 +163,7 @@ func (c *loadOwnedCollectiblesCommand) Run(parent context.Context) (err error) {
|
|||
cursor = partialOwnership.NextCursor
|
||||
|
||||
if cursor == thirdparty.FetchFromStartCursor {
|
||||
err = c.ownershipDB.Update(c.chainID, c.account, c.partialOwnership)
|
||||
err = c.ownershipDB.Update(c.chainID, c.account, c.partialOwnership, start.Unix())
|
||||
if err != nil {
|
||||
log.Error("failed updating ownershipDB in loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "error", err)
|
||||
c.err = err
|
||||
|
|
|
@ -11,10 +11,13 @@ import (
|
|||
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
w_common "github.com/status-im/status-go/services/wallet/common"
|
||||
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/sqlite"
|
||||
)
|
||||
|
||||
const InvalidTimestamp = int64(-1)
|
||||
|
||||
type OwnershipDB struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
@ -28,6 +31,9 @@ func NewOwnershipDB(sqlDb *sql.DB) *OwnershipDB {
|
|||
const ownershipColumns = "chain_id, contract_address, token_id, owner_address"
|
||||
const selectOwnershipColumns = "chain_id, contract_address, token_id"
|
||||
|
||||
const ownershipTimestampColumns = "owner_address, chain_id, timestamp"
|
||||
const selectOwnershipTimestampColumns = "timestamp"
|
||||
|
||||
func removeAddressOwnership(creator sqlite.StatementCreator, chainID w_common.ChainID, ownerAddress common.Address) error {
|
||||
deleteOwnership, err := creator.Prepare("DELETE FROM collectibles_ownership_cache WHERE chain_id = ? AND owner_address = ?")
|
||||
if err != nil {
|
||||
|
@ -59,7 +65,19 @@ func insertAddressOwnership(creator sqlite.StatementCreator, ownerAddress common
|
|||
return nil
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) Update(chainID w_common.ChainID, ownerAddress common.Address, collectibles []thirdparty.CollectibleUniqueID) (err error) {
|
||||
func updateAddressOwnershipTimestamp(creator sqlite.StatementCreator, ownerAddress common.Address, chainID w_common.ChainID, timestamp int64) error {
|
||||
updateTimestamp, err := creator.Prepare(fmt.Sprintf(`INSERT OR REPLACE INTO collectibles_ownership_update_timestamps (%s)
|
||||
VALUES (?, ?, ?)`, ownershipTimestampColumns))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = updateTimestamp.Exec(ownerAddress, chainID, timestamp)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) Update(chainID w_common.ChainID, ownerAddress common.Address, collectibles []thirdparty.CollectibleUniqueID, timestamp int64) (err error) {
|
||||
var (
|
||||
tx *sql.Tx
|
||||
)
|
||||
|
@ -87,6 +105,9 @@ func (o *OwnershipDB) Update(chainID w_common.ChainID, ownerAddress common.Addre
|
|||
return err
|
||||
}
|
||||
|
||||
// Update timestamp
|
||||
err = updateAddressOwnershipTimestamp(tx, ownerAddress, chainID, timestamp)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -163,3 +184,29 @@ func (o *OwnershipDB) GetOwnedCollectible(chainID w_common.ChainID, ownerAddress
|
|||
|
||||
return &ids[0], nil
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) GetOwnershipUpdateTimestamp(owner common.Address, chainID walletCommon.ChainID) (int64, error) {
|
||||
query := fmt.Sprintf(`SELECT %s
|
||||
FROM collectibles_ownership_update_timestamps
|
||||
WHERE owner_address = ? AND chain_id = ?`, selectOwnershipTimestampColumns)
|
||||
|
||||
stmt, err := o.db.Prepare(query)
|
||||
if err != nil {
|
||||
return InvalidTimestamp, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
row := stmt.QueryRow(owner, chainID)
|
||||
|
||||
var timestamp int64
|
||||
|
||||
err = row.Scan(×tamp)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return InvalidTimestamp, nil
|
||||
} else if err != nil {
|
||||
return InvalidTimestamp, err
|
||||
}
|
||||
|
||||
return timestamp, nil
|
||||
}
|
||||
|
|
|
@ -46,14 +46,17 @@ func TestUpdateOwnership(t *testing.T) {
|
|||
ownerAddress1 := common.HexToAddress("0x1234")
|
||||
chainID0 := w_common.ChainID(0)
|
||||
ownedListChain0 := generateTestCollectibles(chainID0, 10)
|
||||
timestampChain0 := int64(1234567890)
|
||||
chainID1 := w_common.ChainID(1)
|
||||
ownedListChain1 := generateTestCollectibles(chainID1, 15)
|
||||
timestampChain1 := int64(1234567891)
|
||||
|
||||
ownedList1 := append(ownedListChain0, ownedListChain1...)
|
||||
|
||||
ownerAddress2 := common.HexToAddress("0x5678")
|
||||
chainID2 := w_common.ChainID(2)
|
||||
ownedListChain2 := generateTestCollectibles(chainID2, 20)
|
||||
timestampChain2 := int64(1234567892)
|
||||
|
||||
ownedList2 := ownedListChain2
|
||||
|
||||
|
@ -63,15 +66,55 @@ func TestUpdateOwnership(t *testing.T) {
|
|||
|
||||
var err error
|
||||
|
||||
err = oDB.Update(chainID0, ownerAddress1, ownedListChain0)
|
||||
var loadedTimestamp int64
|
||||
var loadedList []thirdparty.CollectibleUniqueID
|
||||
|
||||
loadedTimestamp, err = oDB.GetOwnershipUpdateTimestamp(ownerAddress1, chainID0)
|
||||
require.NoError(t, err)
|
||||
err = oDB.Update(chainID1, ownerAddress1, ownedListChain1)
|
||||
require.Equal(t, InvalidTimestamp, loadedTimestamp)
|
||||
|
||||
loadedTimestamp, err = oDB.GetOwnershipUpdateTimestamp(ownerAddress1, chainID1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, InvalidTimestamp, loadedTimestamp)
|
||||
|
||||
loadedTimestamp, err = oDB.GetOwnershipUpdateTimestamp(ownerAddress2, chainID2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, InvalidTimestamp, loadedTimestamp)
|
||||
|
||||
err = oDB.Update(chainID0, ownerAddress1, ownedListChain0, timestampChain0)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = oDB.Update(chainID2, ownerAddress2, ownedListChain2)
|
||||
loadedTimestamp, err = oDB.GetOwnershipUpdateTimestamp(ownerAddress1, chainID0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, timestampChain0, loadedTimestamp)
|
||||
|
||||
loadedTimestamp, err = oDB.GetOwnershipUpdateTimestamp(ownerAddress1, chainID1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, InvalidTimestamp, loadedTimestamp)
|
||||
|
||||
loadedTimestamp, err = oDB.GetOwnershipUpdateTimestamp(ownerAddress2, chainID2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, InvalidTimestamp, loadedTimestamp)
|
||||
|
||||
err = oDB.Update(chainID1, ownerAddress1, ownedListChain1, timestampChain1)
|
||||
require.NoError(t, err)
|
||||
|
||||
loadedList, err := oDB.GetOwnedCollectibles([]w_common.ChainID{chainID0}, []common.Address{ownerAddress1}, 0, len(fullList))
|
||||
err = oDB.Update(chainID2, ownerAddress2, ownedListChain2, timestampChain2)
|
||||
require.NoError(t, err)
|
||||
|
||||
loadedTimestamp, err = oDB.GetOwnershipUpdateTimestamp(ownerAddress1, chainID0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, timestampChain0, loadedTimestamp)
|
||||
|
||||
loadedTimestamp, err = oDB.GetOwnershipUpdateTimestamp(ownerAddress1, chainID1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, timestampChain1, loadedTimestamp)
|
||||
|
||||
loadedTimestamp, err = oDB.GetOwnershipUpdateTimestamp(ownerAddress2, chainID2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, timestampChain2, loadedTimestamp)
|
||||
|
||||
loadedList, err = oDB.GetOwnedCollectibles([]w_common.ChainID{chainID0}, []common.Address{ownerAddress1}, 0, len(fullList))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ownedListChain0, loadedList)
|
||||
|
||||
|
@ -109,9 +152,11 @@ func TestLargeTokenID(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
timestamp := int64(1234567890)
|
||||
|
||||
var err error
|
||||
|
||||
err = oDB.Update(chainID, ownerAddress, ownedListChain)
|
||||
err = oDB.Update(chainID, ownerAddress, ownedListChain, timestamp)
|
||||
require.NoError(t, err)
|
||||
|
||||
loadedList, err := oDB.GetOwnedCollectibles([]w_common.ChainID{chainID}, []common.Address{ownerAddress}, 0, len(ownedListChain))
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
// 1691753758_initial.up.sql (5.738kB)
|
||||
// 1692701329_add_collectibles_and_collections_data_cache.up.sql (1.808kB)
|
||||
// 1692701339_add_scope_to_pending.up.sql (576B)
|
||||
// 1694540071_add_collectibles_ownership_update_timestamp.up.sql (349B)
|
||||
// doc.go (74B)
|
||||
|
||||
package migrations
|
||||
|
@ -13,7 +14,6 @@ import (
|
|||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -23,7 +23,7 @@ import (
|
|||
func bindataRead(data []byte, name string) ([]byte, error) {
|
||||
gz, err := gzip.NewReader(bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %q: %v", name, err)
|
||||
return nil, fmt.Errorf("read %q: %w", name, err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
@ -31,7 +31,7 @@ func bindataRead(data []byte, name string) ([]byte, error) {
|
|||
clErr := gz.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %q: %v", name, err)
|
||||
return nil, fmt.Errorf("read %q: %w", name, err)
|
||||
}
|
||||
if clErr != nil {
|
||||
return nil, err
|
||||
|
@ -87,7 +87,7 @@ func _1691753758_initialUpSql() (*asset, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1691753758_initial.up.sql", size: 5738, mode: os.FileMode(0644), modTime: time.Unix(1692726720, 0)}
|
||||
info := bindataFileInfo{name: "1691753758_initial.up.sql", size: 5738, mode: os.FileMode(0644), modTime: time.Unix(1694431782, 0)}
|
||||
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x6b, 0x25, 0x31, 0xc8, 0x27, 0x3, 0x6b, 0x9f, 0x15, 0x42, 0x2f, 0x85, 0xfb, 0xe3, 0x6, 0xea, 0xf7, 0x97, 0x12, 0x56, 0x3c, 0x9a, 0x5b, 0x1a, 0xca, 0xb1, 0x23, 0xfa, 0xcd, 0x57, 0x25, 0x5c}}
|
||||
return a, nil
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ func _1692701329_add_collectibles_and_collections_data_cacheUpSql() (*asset, err
|
|||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1692701329_add_collectibles_and_collections_data_cache.up.sql", size: 1808, mode: os.FileMode(0644), modTime: time.Unix(1692726720, 0)}
|
||||
info := bindataFileInfo{name: "1692701329_add_collectibles_and_collections_data_cache.up.sql", size: 1808, mode: os.FileMode(0644), modTime: time.Unix(1694431782, 0)}
|
||||
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x1, 0x51, 0xf4, 0x2b, 0x92, 0xde, 0x59, 0x65, 0xd8, 0x9b, 0x57, 0xe0, 0xfd, 0x7b, 0x12, 0xb, 0x29, 0x6e, 0x9d, 0xb5, 0x90, 0xe, 0xfa, 0x12, 0x97, 0xd, 0x61, 0x60, 0x7f, 0x32, 0x1d, 0xc3}}
|
||||
return a, nil
|
||||
}
|
||||
|
@ -127,11 +127,31 @@ func _1692701339_add_scope_to_pendingUpSql() (*asset, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1692701339_add_scope_to_pending.up.sql", size: 576, mode: os.FileMode(0644), modTime: time.Unix(1692726720, 0)}
|
||||
info := bindataFileInfo{name: "1692701339_add_scope_to_pending.up.sql", size: 576, mode: os.FileMode(0644), modTime: time.Unix(1694431782, 0)}
|
||||
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x36, 0x8a, 0x5e, 0xe2, 0x63, 0x15, 0x37, 0xba, 0x55, 0x18, 0xf3, 0xcc, 0xe0, 0x5, 0x84, 0xe1, 0x5b, 0xe8, 0x1, 0x32, 0x6b, 0x9f, 0x7d, 0x9f, 0xd9, 0x23, 0x6c, 0xa9, 0xb5, 0xdc, 0xf4, 0x93}}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var __1694540071_add_collectibles_ownership_update_timestampUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x9c\xcf\x41\x4b\x80\x30\x18\xc6\xf1\xfb\x3e\xc5\x7b\x4c\xf0\x1b\x78\x9a\xba\x6c\x20\xaf\xa4\x5b\x78\x1b\xcb\xbd\xe1\x40\xa7\xb8\x45\xf8\xed\x03\x09\xa1\xa0\x43\xdd\xff\x3c\x3c\xbf\xaa\x17\x5c\x09\x50\xbc\x6c\x05\xc8\x47\xc0\x4e\x81\x18\xe5\xa0\x06\x98\xb6\x65\xa1\x29\xf9\xd7\x85\xa2\xd9\x3e\x02\x1d\x71\xf6\xbb\x79\xdf\x9d\x4d\x64\x92\x5f\x29\x26\xbb\xee\x11\x1e\x18\x00\xc0\x55\x18\xeb\xdc\x41\x31\xc2\x0b\xef\xab\x27\xde\x5f\x7b\xa8\xdb\x36\xbf\x9a\x69\xb6\x3e\x18\xef\x40\xe3\x20\x1b\x14\x35\x94\xb2\x91\xa8\x7e\x64\xf7\xf6\xaf\x1d\xcb\x0a\xc6\xbe\xbe\x6b\x94\xcf\x5a\x80\xc4\x5a\x8c\xff\x24\x18\xef\x28\x24\xff\x76\x1a\x0a\xe9\x38\xa1\xc3\x3f\xe8\xbf\xc1\xf3\xdb\x98\x15\xec\x33\x00\x00\xff\xff\x02\x64\xbc\xba\x5d\x01\x00\x00")
|
||||
|
||||
func _1694540071_add_collectibles_ownership_update_timestampUpSqlBytes() ([]byte, error) {
|
||||
return bindataRead(
|
||||
__1694540071_add_collectibles_ownership_update_timestampUpSql,
|
||||
"1694540071_add_collectibles_ownership_update_timestamp.up.sql",
|
||||
)
|
||||
}
|
||||
|
||||
func _1694540071_add_collectibles_ownership_update_timestampUpSql() (*asset, error) {
|
||||
bytes, err := _1694540071_add_collectibles_ownership_update_timestampUpSqlBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "1694540071_add_collectibles_ownership_update_timestamp.up.sql", size: 349, mode: os.FileMode(0644), modTime: time.Unix(1694540373, 0)}
|
||||
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x7f, 0x45, 0xc7, 0xce, 0x79, 0x63, 0xbc, 0x6f, 0x83, 0x5f, 0xe2, 0x3, 0x56, 0xcc, 0x5, 0x2f, 0x85, 0xda, 0x7e, 0xea, 0xf5, 0xd2, 0xac, 0x19, 0xd4, 0xd8, 0x5e, 0xdd, 0xed, 0xe2, 0xa9, 0x97}}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var _docGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xc9\xb1\x0d\xc4\x20\x0c\x05\xd0\x9e\x29\xfe\x02\xd8\xfd\x6d\xe3\x4b\xac\x2f\x44\x82\x09\x78\x7f\xa5\x49\xfd\xa6\x1d\xdd\xe8\xd8\xcf\x55\x8a\x2a\xe3\x47\x1f\xbe\x2c\x1d\x8c\xfa\x6f\xe3\xb4\x34\xd4\xd9\x89\xbb\x71\x59\xb6\x18\x1b\x35\x20\xa2\x9f\x0a\x03\xa2\xe5\x0d\x00\x00\xff\xff\x60\xcd\x06\xbe\x4a\x00\x00\x00")
|
||||
|
||||
func docGoBytes() ([]byte, error) {
|
||||
|
@ -147,7 +167,7 @@ func docGo() (*asset, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
info := bindataFileInfo{name: "doc.go", size: 74, mode: os.FileMode(0644), modTime: time.Unix(1692726720, 0)}
|
||||
info := bindataFileInfo{name: "doc.go", size: 74, mode: os.FileMode(0644), modTime: time.Unix(1694431782, 0)}
|
||||
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xde, 0x7c, 0x28, 0xcd, 0x47, 0xf2, 0xfa, 0x7c, 0x51, 0x2d, 0xd8, 0x38, 0xb, 0xb0, 0x34, 0x9d, 0x4c, 0x62, 0xa, 0x9e, 0x28, 0xc3, 0x31, 0x23, 0xd9, 0xbb, 0x89, 0x9f, 0xa0, 0x89, 0x1f, 0xe8}}
|
||||
return a, nil
|
||||
}
|
||||
|
@ -243,24 +263,27 @@ func AssetNames() []string {
|
|||
|
||||
// _bindata is a table, holding each asset generator, mapped to its name.
|
||||
var _bindata = map[string]func() (*asset, error){
|
||||
"1691753758_initial.up.sql": _1691753758_initialUpSql,
|
||||
|
||||
"1691753758_initial.up.sql": _1691753758_initialUpSql,
|
||||
"1692701329_add_collectibles_and_collections_data_cache.up.sql": _1692701329_add_collectibles_and_collections_data_cacheUpSql,
|
||||
|
||||
"1692701339_add_scope_to_pending.up.sql": _1692701339_add_scope_to_pendingUpSql,
|
||||
|
||||
"1692701339_add_scope_to_pending.up.sql": _1692701339_add_scope_to_pendingUpSql,
|
||||
"1694540071_add_collectibles_ownership_update_timestamp.up.sql": _1694540071_add_collectibles_ownership_update_timestampUpSql,
|
||||
"doc.go": docGo,
|
||||
}
|
||||
|
||||
// AssetDebug is true if the assets were built with the debug flag enabled.
|
||||
const AssetDebug = false
|
||||
|
||||
// AssetDir returns the file names below a certain
|
||||
// directory embedded in the file by go-bindata.
|
||||
// For example if you run go-bindata on data/... and data contains the
|
||||
// following hierarchy:
|
||||
// data/
|
||||
// foo.txt
|
||||
// img/
|
||||
// a.png
|
||||
// b.png
|
||||
//
|
||||
// data/
|
||||
// foo.txt
|
||||
// img/
|
||||
// a.png
|
||||
// b.png
|
||||
//
|
||||
// then AssetDir("data") would return []string{"foo.txt", "img"},
|
||||
// AssetDir("data/img") would return []string{"a.png", "b.png"},
|
||||
// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and
|
||||
|
@ -293,10 +316,11 @@ type bintree struct {
|
|||
}
|
||||
|
||||
var _bintree = &bintree{nil, map[string]*bintree{
|
||||
"1691753758_initial.up.sql": &bintree{_1691753758_initialUpSql, map[string]*bintree{}},
|
||||
"1692701329_add_collectibles_and_collections_data_cache.up.sql": &bintree{_1692701329_add_collectibles_and_collections_data_cacheUpSql, map[string]*bintree{}},
|
||||
"1692701339_add_scope_to_pending.up.sql": &bintree{_1692701339_add_scope_to_pendingUpSql, map[string]*bintree{}},
|
||||
"doc.go": &bintree{docGo, map[string]*bintree{}},
|
||||
"1691753758_initial.up.sql": {_1691753758_initialUpSql, map[string]*bintree{}},
|
||||
"1692701329_add_collectibles_and_collections_data_cache.up.sql": {_1692701329_add_collectibles_and_collections_data_cacheUpSql, map[string]*bintree{}},
|
||||
"1692701339_add_scope_to_pending.up.sql": {_1692701339_add_scope_to_pendingUpSql, map[string]*bintree{}},
|
||||
"1694540071_add_collectibles_ownership_update_timestamp.up.sql": {_1694540071_add_collectibles_ownership_update_timestampUpSql, map[string]*bintree{}},
|
||||
"doc.go": {docGo, map[string]*bintree{}},
|
||||
}}
|
||||
|
||||
// RestoreAsset restores an asset under the given directory.
|
||||
|
@ -313,7 +337,7 @@ func RestoreAsset(dir, name string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
|
||||
err = os.WriteFile(_filePath(dir, name), data, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS collectibles_ownership_update_timestamps (
|
||||
owner_address VARCHAR NOT NULL,
|
||||
chain_id UNSIGNED BIGINT NOT NULL,
|
||||
timestamp UNSIGNED BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS collectibles_ownership_update_timestamps_identify_entry ON collectibles_ownership_update_timestamps (owner_address, chain_id);
|
Loading…
Reference in New Issue