feat(collectibles): Mint collectibles (ERC-721):
Add testing smart contract and go api. Add collectibles service. Issue #3051
This commit is contained in:
parent
51f99a2631
commit
8acc46f758
|
@ -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
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -63,6 +63,7 @@ const (
|
|||
SetPubKey PendingTrxType = "SetPubKey"
|
||||
BuyStickerPack PendingTrxType = "BuyStickerPack"
|
||||
WalletTransfer PendingTrxType = "WalletTransfer"
|
||||
CollectibleDeployment PendingTrxType = "CollectibleDeployment"
|
||||
)
|
||||
|
||||
type PendingTransaction struct {
|
||||
|
|
Loading…
Reference in New Issue