feat(collectibles): Mint collectibles (ERC-721):

Add testing smart contract and go api.
Add collectibles service.

Issue #3051
This commit is contained in:
Michal Iskierko 2023-01-12 16:17:21 +01:00 committed by Michał Iskierko
parent 51f99a2631
commit 8acc46f758
13 changed files with 1893 additions and 47 deletions

View File

@ -1 +1 @@
0.130.3
0.131.0

View File

@ -0,0 +1,122 @@
// SPDX-License-Identifier: Mozilla Public License 2.0
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract CollectibleV1 is
Context,
ERC721Enumerable,
Ownable
{
using Counters for Counters.Counter;
// State variables
Counters.Counter private _tokenIdTracker;
/**
* If we want unlimited total supply we should set maxSupply to 2^256-1.
*/
uint256 public maxSupply;
/**
* If set to true, the contract owner can burn any token.
*/
bool public remoteBurnable;
/**
* If set to false it acts as a soulbound token.
*/
bool public transferable;
string public baseTokenURI;
constructor(
string memory _name,
string memory _symbol,
uint256 _maxSupply,
bool _remoteBurnable,
bool _transferable,
string memory _baseTokenURI
) ERC721(_name, _symbol) {
maxSupply = _maxSupply;
remoteBurnable = _remoteBurnable;
transferable = _transferable;
baseTokenURI = _baseTokenURI;
}
// Events
// External functions
/**
* @dev Creates a new token for each address in `addresses`. Its token ID will be automatically
* assigned (and available on the emitted {IERC721-Transfer} event), and the token
* URI autogenerated based on the base URI passed at construction.
*
*/
function mintTo(address[] memory addresses) external onlyOwner {
// We cannot just use totalSupply() to create the new tokenId because tokens
// can be burned so we use a separate counter.
require(_tokenIdTracker.current() + addresses.length < maxSupply, "MAX_SUPPLY_REACHED");
for (uint256 i = 0; i < addresses.length; i++) {
_safeMint(addresses[i], _tokenIdTracker.current(), "");
_tokenIdTracker.increment();
}
}
// Public functions
/**
* @notice remoteBurn allows the owner to burn a token
* @param tokenId The token ID to be burned
*/
function remoteBurn(uint256 tokenId) public onlyOwner {
require(remoteBurnable, "NOT_REMOTE_BURNABLE");
_burn(tokenId);
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
// Internal functions
/**
* @notice
* @dev
*/
function _baseURI() internal view virtual override returns (string memory) {
return baseTokenURI;
}
/**
* @notice
* @dev
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 firstTokenId,
uint256 batchSize
) internal virtual override(ERC721Enumerable) {
super._beforeTokenTransfer(from, to, firstTokenId, batchSize);
}
// Private functions
}

File diff suppressed because one or more lines are too long

View File

@ -35,6 +35,7 @@ import (
appmetricsservice "github.com/status-im/status-go/services/appmetrics"
"github.com/status-im/status-go/services/browsers"
"github.com/status-im/status-go/services/chat"
"github.com/status-im/status-go/services/collectibles"
"github.com/status-im/status-go/services/ens"
"github.com/status-im/status-go/services/gif"
localnotifications "github.com/status-im/status-go/services/local-notifications"
@ -120,6 +121,7 @@ type StatusNode struct {
wakuV2Srvc *wakuv2.Waku
wakuV2ExtSrvc *wakuv2ext.Service
ensSrvc *ens.Service
collectiblesSrvc *collectibles.Service
gifSrvc *gif.Service
stickersSrvc *stickers.Service
chatSrvc *chat.Service
@ -462,6 +464,7 @@ func (n *StatusNode) stop() error {
n.wakuV2Srvc = nil
n.wakuV2ExtSrvc = nil
n.ensSrvc = nil
n.collectiblesSrvc = nil
n.stickersSrvc = nil
n.publicMethods = make(map[string]bool)

View File

@ -25,6 +25,7 @@ import (
appmetricsservice "github.com/status-im/status-go/services/appmetrics"
"github.com/status-im/status-go/services/browsers"
"github.com/status-im/status-go/services/chat"
"github.com/status-im/status-go/services/collectibles"
"github.com/status-im/status-go/services/ens"
"github.com/status-im/status-go/services/ext"
"github.com/status-im/status-go/services/gif"
@ -75,6 +76,7 @@ func (b *StatusNode) initServices(config *params.NodeConfig, mediaServer *server
services = append(services, b.personalService())
services = append(services, b.statusPublicService())
services = append(services, b.ensService())
services = append(services, b.collectiblesService())
services = append(services, b.stickersService(accDB))
services = append(services, b.updatesService())
services = appendIf(config.EnableNTPSync, services, b.timeSource())
@ -400,6 +402,13 @@ func (b *StatusNode) ensService() *ens.Service {
return b.ensSrvc
}
func (b *StatusNode) collectiblesService() *collectibles.Service {
if b.collectiblesSrvc == nil {
b.collectiblesSrvc = collectibles.NewService(b.rpcClient, b.gethAccountManager, b.config)
}
return b.collectiblesSrvc
}
func (b *StatusNode) stickersService(accountDB *accounts.Database) *stickers.Service {
if b.stickersSrvc == nil {
b.stickersSrvc = stickers.NewService(accountDB, b.rpcClient, b.gethAccountManager, b.rpcFiltersSrvc, b.config, b.downloader, b.httpServer)

View File

@ -0,0 +1,101 @@
package collectibles
import (
"context"
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/contracts/collectibles"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/utils"
"github.com/status-im/status-go/transactions"
)
func NewAPI(rpcClient *rpc.Client, accountsManager *account.GethManager, config *params.NodeConfig) *API {
return &API{
RPCClient: rpcClient,
accountsManager: accountsManager,
config: config,
}
}
type API struct {
RPCClient *rpc.Client
accountsManager *account.GethManager
config *params.NodeConfig
}
type DeploymentDetails struct {
ContractAddress string `json:"contractAddress"`
TransactionHash string `json:"transactionHash"`
}
const maxSupply = 999999999
type DeploymentParameters struct {
Name string `json:"name"`
Symbol string `json:"symbol"`
Supply int `json:"supply"`
InfiniteSupply bool `json:"infiniteSupply"`
Transferable bool `json:"transferable"`
RemoteSelfDestruct bool `json:"remoteSelfDestruct"`
TokenURI string `json:"tokenUri"`
}
func (d *DeploymentParameters) GetSupply() *big.Int {
if d.InfiniteSupply {
return d.GetInfiniteSupply()
}
return big.NewInt(int64(d.Supply))
}
// infinite supply for ERC721 is 2^256-1
func (d *DeploymentParameters) GetInfiniteSupply() *big.Int {
max := new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil)
max.Sub(max, big.NewInt(1))
return max
}
func (d *DeploymentParameters) Validate() error {
if len(d.Name) <= 0 {
return errors.New("empty collectible name")
}
if len(d.Symbol) <= 0 {
return errors.New("empty collectible symbol")
}
if !d.InfiniteSupply && (d.Supply < 0 || d.Supply > maxSupply) {
return fmt.Errorf("wrong supply value: %v", d.Supply)
}
return nil
}
func (api *API) Deploy(ctx context.Context, chainID uint64, deploymentParameters DeploymentParameters, txArgs transactions.SendTxArgs, password string) (DeploymentDetails, error) {
err := deploymentParameters.Validate()
if err != nil {
return DeploymentDetails{}, err
}
transactOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.accountsManager, api.config.KeyStoreDir, txArgs.From, password))
ethClient, err := api.RPCClient.EthClient(chainID)
if err != nil {
log.Error(err.Error())
return DeploymentDetails{}, err
}
address, tx, _, err := collectibles.DeployCollectibles(transactOpts, ethClient, deploymentParameters.Name,
deploymentParameters.Symbol, deploymentParameters.GetSupply(),
deploymentParameters.RemoteSelfDestruct, deploymentParameters.Transferable,
deploymentParameters.TokenURI)
if err != nil {
log.Error(err.Error())
return DeploymentDetails{}, err
}
return DeploymentDetails{address.Hex(), tx.Hash().Hex()}, nil
}

View File

@ -0,0 +1,70 @@
package collectibles
import (
"math/big"
"testing"
"github.com/stretchr/testify/require"
)
func TestDeploymentParameters(t *testing.T) {
var testCases = []struct {
name string
parameters DeploymentParameters
isError bool
}{
{
name: "emptyName",
parameters: DeploymentParameters{"", "SYMBOL", 123, false, false, false, ""},
isError: true,
},
{
name: "emptySymbol",
parameters: DeploymentParameters{"NAME", "", 123, false, false, false, ""},
isError: true,
},
{
name: "negativeSupply",
parameters: DeploymentParameters{"NAME", "SYM", -123, false, false, false, ""},
isError: true,
},
{
name: "zeroSupply",
parameters: DeploymentParameters{"NAME", "SYM", 0, false, false, false, ""},
isError: false,
},
{
name: "negativeSupplyAndInfinite",
parameters: DeploymentParameters{"NAME", "SYM", -123, true, false, false, ""},
isError: false,
},
{
name: "supplyGreaterThanMax",
parameters: DeploymentParameters{"NAME", "SYM", maxSupply + 1, false, false, false, ""},
isError: true,
},
{
name: "supplyIsMax",
parameters: DeploymentParameters{"NAME", "SYM", maxSupply, false, false, false, ""},
isError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.parameters.Validate()
if tc.isError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
notInfiniteSupplyParams := DeploymentParameters{"NAME", "SYM", 123, false, false, false, ""}
requiredSupply := big.NewInt(123)
require.Equal(t, notInfiniteSupplyParams.GetSupply(), requiredSupply)
infiniteSupplyParams := DeploymentParameters{"NAME", "SYM", 123, true, false, false, ""}
requiredSupply = infiniteSupplyParams.GetInfiniteSupply()
require.Equal(t, infiniteSupplyParams.GetSupply(), requiredSupply)
}

View File

@ -0,0 +1,48 @@
package collectibles
import (
"github.com/ethereum/go-ethereum/p2p"
ethRpc "github.com/ethereum/go-ethereum/rpc"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
)
// Collectibles service
type Service struct {
api *API
}
// Returns a new Collectibles Service.
func NewService(rpcClient *rpc.Client, accountsManager *account.GethManager, config *params.NodeConfig) *Service {
return &Service{
NewAPI(rpcClient, accountsManager, config),
}
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []ethRpc.API {
return []ethRpc.API{
{
Namespace: "collectibles",
Version: "0.1.0",
Service: s.api,
Public: true,
},
}
}
// Start is run when a service is started.
func (s *Service) Start() error {
return nil
}
// Stop is run when a service is stopped.
func (s *Service) Stop() error {
return nil
}

View File

@ -23,7 +23,6 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/contracts"
@ -34,6 +33,7 @@ import (
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/rpcfilters"
"github.com/status-im/status-go/services/utils"
"github.com/status-im/status-go/transactions"
)
@ -310,17 +310,6 @@ func (api *API) Price(ctx context.Context, chainID uint64) (string, error) {
return fmt.Sprintf("%x", price), nil
}
func (api *API) getSigner(chainID uint64, from types.Address, password string) bind.SignerFn {
return func(addr common.Address, tx *ethTypes.Transaction) (*ethTypes.Transaction, error) {
selectedAccount, err := api.accountsManager.VerifyAccountPassword(api.config.KeyStoreDir, from.Hex(), password)
if err != nil {
return nil, err
}
s := ethTypes.NewLondonSigner(new(big.Int).SetUint64(chainID))
return ethTypes.SignTx(tx, s, selectedAccount.PrivateKey)
}
}
func (api *API) Release(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, password string, username string) (string, error) {
registryAddr, err := api.usernameRegistrarAddr(ctx, chainID)
if err != nil {
@ -332,7 +321,7 @@ func (api *API) Release(ctx context.Context, chainID uint64, txArgs transactions
return "", err
}
txOpts := txArgs.ToTransactOpts(api.getSigner(chainID, txArgs.From, password))
txOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.accountsManager, api.config.KeyStoreDir, txArgs.From, password))
tx, err := registrar.Release(txOpts, usernameToLabel(username))
if err != nil {
return "", err
@ -411,7 +400,7 @@ func (api *API) Register(ctx context.Context, chainID uint64, txArgs transaction
return "", err
}
txOpts := txArgs.ToTransactOpts(api.getSigner(chainID, txArgs.From, password))
txOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.accountsManager, api.config.KeyStoreDir, txArgs.From, password))
tx, err := snt.ApproveAndCall(
txOpts,
registryAddr,
@ -523,7 +512,7 @@ func (api *API) SetPubKey(ctx context.Context, chainID uint64, txArgs transactio
}
x, y := extractCoordinates(pubkey)
txOpts := txArgs.ToTransactOpts(api.getSigner(chainID, txArgs.From, password))
txOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.accountsManager, api.config.KeyStoreDir, txArgs.From, password))
tx, err := resolver.SetPubkey(txOpts, nameHash(username), x, y)
if err != nil {
return "", err

View File

@ -11,25 +11,14 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/status-im/status-go/contracts/snt"
"github.com/status-im/status-go/contracts/stickers"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/services/utils"
"github.com/status-im/status-go/services/wallet/bigint"
"github.com/status-im/status-go/transactions"
)
func (api *API) getSigner(chainID uint64, from types.Address, password string) bind.SignerFn {
return func(addr common.Address, tx *ethTypes.Transaction) (*ethTypes.Transaction, error) {
selectedAccount, err := api.accountsManager.VerifyAccountPassword(api.keyStoreDir, from.Hex(), password)
if err != nil {
return nil, err
}
s := ethTypes.NewLondonSigner(new(big.Int).SetUint64(chainID))
return ethTypes.SignTx(tx, s, selectedAccount.PrivateKey)
}
}
func (api *API) Buy(ctx context.Context, chainID uint64, txArgs transactions.SendTxArgs, packID *bigint.BigInt, password string) (string, error) {
snt, err := api.contractMaker.NewSNT(chainID)
if err != nil {
@ -63,7 +52,7 @@ func (api *API) Buy(ctx context.Context, chainID uint64, txArgs transactions.Sen
return "", err
}
txOpts := txArgs.ToTransactOpts(api.getSigner(chainID, txArgs.From, password))
txOpts := txArgs.ToTransactOpts(utils.GetSigner(chainID, api.accountsManager, api.keyStoreDir, txArgs.From, password))
tx, err := snt.ApproveAndCall(
txOpts,
stickerMarketAddress,

View File

@ -1,13 +0,0 @@
package services
import "github.com/ethereum/go-ethereum/rpc"
// APIByNamespace retrieve an api by its namespace or returns nil.
func APIByNamespace(apis []rpc.API, namespace string) interface{} {
for _, api := range apis {
if api.Namespace == namespace {
return api.Service
}
}
return nil
}

22
services/utils/utils.go Normal file
View File

@ -0,0 +1,22 @@
package utils
import (
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/types"
)
func GetSigner(chainID uint64, accountsManager *account.GethManager, keyStoreDir string, from types.Address, password string) bind.SignerFn {
return func(addr common.Address, tx *ethTypes.Transaction) (*ethTypes.Transaction, error) {
selectedAccount, err := accountsManager.VerifyAccountPassword(keyStoreDir, from.Hex(), password)
if err != nil {
return nil, err
}
s := ethTypes.NewLondonSigner(new(big.Int).SetUint64(chainID))
return ethTypes.SignTx(tx, s, selectedAccount.PrivateKey)
}
}

View File

@ -58,11 +58,12 @@ type MultiTransactionResult struct {
type PendingTrxType string
const (
RegisterENS PendingTrxType = "RegisterENS"
ReleaseENS PendingTrxType = "ReleaseENS"
SetPubKey PendingTrxType = "SetPubKey"
BuyStickerPack PendingTrxType = "BuyStickerPack"
WalletTransfer PendingTrxType = "WalletTransfer"
RegisterENS PendingTrxType = "RegisterENS"
ReleaseENS PendingTrxType = "ReleaseENS"
SetPubKey PendingTrxType = "SetPubKey"
BuyStickerPack PendingTrxType = "BuyStickerPack"
WalletTransfer PendingTrxType = "WalletTransfer"
CollectibleDeployment PendingTrxType = "CollectibleDeployment"
)
type PendingTransaction struct {