From 5ae936fc0732b48f3469f5a2ced1049b5f0a9c43 Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Tue, 10 Dec 2024 11:56:24 +0100 Subject: [PATCH] chore(community)_: duplicated community tokens package --- api/geth_backend.go | 1 + node/get_status_node.go | 2 + node/status_node_services.go | 9 + services/communitytokensv2/api.go | 501 ++++++++++++ services/communitytokensv2/api_test.go | 31 + .../communitytokensdatabase/database.go | 66 ++ .../communitytokensdatabase/database_test.go | 121 +++ services/communitytokensv2/estimations.go | 443 +++++++++++ services/communitytokensv2/manager.go | 210 +++++ services/communitytokensv2/service.go | 742 ++++++++++++++++++ services/communitytokensv2/token_instances.go | 176 +++++ services/communitytokensv2/version.go | 7 + 12 files changed, 2309 insertions(+) create mode 100644 services/communitytokensv2/api.go create mode 100644 services/communitytokensv2/api_test.go create mode 100644 services/communitytokensv2/communitytokensdatabase/database.go create mode 100644 services/communitytokensv2/communitytokensdatabase/database_test.go create mode 100644 services/communitytokensv2/estimations.go create mode 100644 services/communitytokensv2/manager.go create mode 100644 services/communitytokensv2/service.go create mode 100644 services/communitytokensv2/token_instances.go create mode 100644 services/communitytokensv2/version.go diff --git a/api/geth_backend.go b/api/geth_backend.go index b0580a0deac..268ca66d406 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -2709,6 +2709,7 @@ func (b *GethStatusBackend) injectAccountsIntoWakuService(w types.WakuKeyManager b.statusNode.ChatService(accDB).Init(messenger) b.statusNode.EnsService().Init(messenger.SyncEnsNamesWithDispatchMessage) b.statusNode.CommunityTokensService().Init(messenger) + b.statusNode.CommunityTokensServiceV2().Init(messenger) } return nil diff --git a/node/get_status_node.go b/node/get_status_node.go index bc5b6ec25bb..5f590c89c1e 100644 --- a/node/get_status_node.go +++ b/node/get_status_node.go @@ -38,6 +38,7 @@ import ( "github.com/status-im/status-go/services/browsers" "github.com/status-im/status-go/services/chat" "github.com/status-im/status-go/services/communitytokens" + "github.com/status-im/status-go/services/communitytokensv2" "github.com/status-im/status-go/services/connector" "github.com/status-im/status-go/services/ens" "github.com/status-im/status-go/services/eth" @@ -129,6 +130,7 @@ type StatusNode struct { wakuV2ExtSrvc *wakuv2ext.Service ensSrvc *ens.Service communityTokensSrvc *communitytokens.Service + communityTokensSrvcV2 *communitytokensv2.Service gifSrvc *gif.Service stickersSrvc *stickers.Service chatSrvc *chat.Service diff --git a/node/status_node_services.go b/node/status_node_services.go index cbd527ad0cd..3851f7894e0 100644 --- a/node/status_node_services.go +++ b/node/status_node_services.go @@ -40,6 +40,7 @@ import ( "github.com/status-im/status-go/services/browsers" "github.com/status-im/status-go/services/chat" "github.com/status-im/status-go/services/communitytokens" + "github.com/status-im/status-go/services/communitytokensv2" "github.com/status-im/status-go/services/connector" "github.com/status-im/status-go/services/ens" "github.com/status-im/status-go/services/eth" @@ -97,6 +98,7 @@ func (b *StatusNode) initServices(config *params.NodeConfig, mediaServer *server services = append(services, b.pendingTrackerService(&b.walletFeed)) services = append(services, b.ensService(b.timeSourceNow())) services = append(services, b.CommunityTokensService()) + services = append(services, b.CommunityTokensServiceV2()) services = append(services, b.stickersService(accDB)) services = append(services, b.updatesService()) services = appendIf(b.appDB != nil && b.multiaccountsDB != nil, services, b.accountsService(&b.accountsFeed, accDB, mediaServer)) @@ -509,6 +511,13 @@ func (b *StatusNode) CommunityTokensService() *communitytokens.Service { return b.communityTokensSrvc } +func (b *StatusNode) CommunityTokensServiceV2() *communitytokensv2.Service { + if b.communityTokensSrvcV2 == nil { + b.communityTokensSrvcV2 = communitytokensv2.NewService(b.rpcClient, b.gethAccountManager, b.pendingTracker, b.config, b.appDB, &b.walletFeed, b.transactor) + } + return b.communityTokensSrvcV2 +} + func (b *StatusNode) stickersService(accountDB *accounts.Database) *stickers.Service { if b.stickersSrvc == nil { b.stickersSrvc = stickers.NewService(accountDB, b.rpcClient, b.gethAccountManager, b.config, b.downloader, b.httpServer, b.pendingTracker) diff --git a/services/communitytokensv2/api.go b/services/communitytokensv2/api.go new file mode 100644 index 00000000000..d962a27b472 --- /dev/null +++ b/services/communitytokensv2/api.go @@ -0,0 +1,501 @@ +package communitytokensv2 + +import ( + "context" + "fmt" + "math/big" + + "go.uber.org/zap" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + "github.com/status-im/status-go/contracts/community-tokens/assets" + "github.com/status-im/status-go/contracts/community-tokens/collectibles" + communitytokendeployer "github.com/status-im/status-go/contracts/community-tokens/deployer" + "github.com/status-im/status-go/contracts/community-tokens/ownertoken" + communityownertokenregistry "github.com/status-im/status-go/contracts/community-tokens/registry" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/logutils" + "github.com/status-im/status-go/protocol/communities/token" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/services/utils" + "github.com/status-im/status-go/services/wallet/bigint" + wcommon "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/services/wallet/requests" + "github.com/status-im/status-go/services/wallet/wallettypes" + "github.com/status-im/status-go/transactions" +) + +func NewAPI(s *Service) *API { + return &API{ + s: s, + } +} + +type API struct { + s *Service +} + +func (api *API) DeployCollectibles(ctx context.Context, chainID uint64, deploymentParameters requests.DeploymentParameters, txArgs wallettypes.SendTxArgs, password string) (requests.DeploymentDetails, error) { + err := deploymentParameters.Validate(false) + if err != nil { + return requests.DeploymentDetails{}, err + } + transactOpts := txArgs.ToTransactOpts(utils.VerifyPasswordAndGetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password)) + + ethClient, err := api.s.manager.rpcClient.EthClient(chainID) + if err != nil { + logutils.ZapLogger().Error(err.Error()) + return requests.DeploymentDetails{}, err + } + address, tx, _, err := collectibles.DeployCollectibles(transactOpts, ethClient, deploymentParameters.Name, + deploymentParameters.Symbol, deploymentParameters.GetSupply(), + deploymentParameters.RemoteSelfDestruct, deploymentParameters.Transferable, + deploymentParameters.TokenURI, common.HexToAddress(deploymentParameters.OwnerTokenAddress), + common.HexToAddress(deploymentParameters.MasterTokenAddress)) + if err != nil { + logutils.ZapLogger().Error(err.Error()) + return requests.DeploymentDetails{}, err + } + + err = api.s.pendingTracker.TrackPendingTransaction( + wcommon.ChainID(chainID), + tx.Hash(), + common.Address(txArgs.From), + address, + transactions.DeployCommunityToken, + transactions.Keep, + "", + ) + if err != nil { + logutils.ZapLogger().Error("TrackPendingTransaction error", zap.Error(err)) + return requests.DeploymentDetails{}, err + } + + savedCommunityToken, err := api.s.CreateCommunityTokenAndSave(int(chainID), deploymentParameters, txArgs.From.Hex(), address.Hex(), + protobuf.CommunityTokenType_ERC721, token.CommunityLevel, tx.Hash().Hex()) + if err != nil { + return requests.DeploymentDetails{}, err + } + + return requests.DeploymentDetails{ + ContractAddress: address.Hex(), + TransactionHash: tx.Hash().Hex(), + CommunityToken: savedCommunityToken}, nil +} + +func decodeSignature(sig []byte) (r [32]byte, s [32]byte, v uint8, err error) { + if len(sig) != crypto.SignatureLength { + return [32]byte{}, [32]byte{}, 0, fmt.Errorf("wrong size for signature: got %d, want %d", len(sig), crypto.SignatureLength) + } + copy(r[:], sig[:32]) + copy(s[:], sig[32:64]) + v = sig[64] + 27 + return r, s, v, nil +} + +func prepareDeploymentSignatureStruct(signature string, communityID string, addressFrom common.Address) (communitytokendeployer.CommunityTokenDeployerDeploymentSignature, error) { + r, s, v, err := decodeSignature(common.FromHex(signature)) + if err != nil { + return communitytokendeployer.CommunityTokenDeployerDeploymentSignature{}, err + } + communityEthAddress, err := convert33BytesPubKeyToEthAddress(communityID) + if err != nil { + return communitytokendeployer.CommunityTokenDeployerDeploymentSignature{}, err + } + communitySignature := communitytokendeployer.CommunityTokenDeployerDeploymentSignature{ + V: v, + R: r, + S: s, + Deployer: addressFrom, + Signer: communityEthAddress, + } + return communitySignature, nil +} + +func (api *API) DeployOwnerToken(ctx context.Context, chainID uint64, + ownerTokenParameters requests.DeploymentParameters, masterTokenParameters requests.DeploymentParameters, + signerPubKey string, txArgs wallettypes.SendTxArgs, password string) (requests.DeploymentDetails, error) { + err := ownerTokenParameters.Validate(false) + if err != nil { + return requests.DeploymentDetails{}, err + } + + if len(signerPubKey) <= 0 { + return requests.DeploymentDetails{}, fmt.Errorf("signerPubKey is empty") + } + + err = masterTokenParameters.Validate(false) + if err != nil { + return requests.DeploymentDetails{}, err + } + + transactOpts := txArgs.ToTransactOpts(utils.VerifyPasswordAndGetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password)) + + deployerContractInst, err := api.NewCommunityTokenDeployerInstance(chainID) + if err != nil { + return requests.DeploymentDetails{}, err + } + + ownerTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{ + Name: ownerTokenParameters.Name, + Symbol: ownerTokenParameters.Symbol, + BaseURI: ownerTokenParameters.TokenURI, + } + + masterTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{ + Name: masterTokenParameters.Name, + Symbol: masterTokenParameters.Symbol, + BaseURI: masterTokenParameters.TokenURI, + } + + signature, err := api.s.Messenger.CreateCommunityTokenDeploymentSignature(context.Background(), chainID, txArgs.From.Hex(), ownerTokenParameters.CommunityID) + if err != nil { + return requests.DeploymentDetails{}, err + } + + communitySignature, err := prepareDeploymentSignatureStruct(types.HexBytes(signature).String(), ownerTokenParameters.CommunityID, common.Address(txArgs.From)) + if err != nil { + return requests.DeploymentDetails{}, err + } + + logutils.ZapLogger().Debug("Prepare deployment", zap.Any("signature", communitySignature)) + + tx, err := deployerContractInst.Deploy(transactOpts, ownerTokenConfig, masterTokenConfig, communitySignature, common.FromHex(signerPubKey)) + + if err != nil { + logutils.ZapLogger().Error(err.Error()) + return requests.DeploymentDetails{}, err + } + + logutils.ZapLogger().Debug("Contract deployed", zap.Stringer("hash", tx.Hash())) + + err = api.s.pendingTracker.TrackPendingTransaction( + wcommon.ChainID(chainID), + tx.Hash(), + common.Address(txArgs.From), + common.Address{}, + transactions.DeployOwnerToken, + transactions.Keep, + "", + ) + if err != nil { + logutils.ZapLogger().Error("TrackPendingTransaction error", zap.Error(err)) + return requests.DeploymentDetails{}, err + } + + savedOwnerToken, err := api.s.CreateCommunityTokenAndSave(int(chainID), ownerTokenParameters, txArgs.From.Hex(), + api.s.TemporaryOwnerContractAddress(tx.Hash().Hex()), protobuf.CommunityTokenType_ERC721, token.OwnerLevel, tx.Hash().Hex()) + if err != nil { + return requests.DeploymentDetails{}, err + } + savedMasterToken, err := api.s.CreateCommunityTokenAndSave(int(chainID), masterTokenParameters, txArgs.From.Hex(), + api.s.TemporaryMasterContractAddress(tx.Hash().Hex()), protobuf.CommunityTokenType_ERC721, token.MasterLevel, tx.Hash().Hex()) + if err != nil { + return requests.DeploymentDetails{}, err + } + + return requests.DeploymentDetails{ + ContractAddress: "", + TransactionHash: tx.Hash().Hex(), + OwnerToken: savedOwnerToken, + MasterToken: savedMasterToken}, nil +} + +// recovery function which starts transaction tracking again +func (api *API) ReTrackOwnerTokenDeploymentTransaction(ctx context.Context, chainID uint64, contractAddress string) error { + return api.s.ReTrackOwnerTokenDeploymentTransaction(ctx, chainID, contractAddress) +} + +func (api *API) DeployAssets(ctx context.Context, chainID uint64, deploymentParameters requests.DeploymentParameters, txArgs wallettypes.SendTxArgs, password string) (requests.DeploymentDetails, error) { + + err := deploymentParameters.Validate(true) + if err != nil { + return requests.DeploymentDetails{}, err + } + + transactOpts := txArgs.ToTransactOpts(utils.VerifyPasswordAndGetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password)) + + ethClient, err := api.s.manager.rpcClient.EthClient(chainID) + if err != nil { + logutils.ZapLogger().Error(err.Error()) + return requests.DeploymentDetails{}, err + } + + const decimals = 18 + address, tx, _, err := assets.DeployAssets(transactOpts, ethClient, deploymentParameters.Name, + deploymentParameters.Symbol, decimals, deploymentParameters.GetSupply(), + deploymentParameters.TokenURI, + common.HexToAddress(deploymentParameters.OwnerTokenAddress), + common.HexToAddress(deploymentParameters.MasterTokenAddress)) + if err != nil { + logutils.ZapLogger().Error(err.Error()) + return requests.DeploymentDetails{}, err + } + + err = api.s.pendingTracker.TrackPendingTransaction( + wcommon.ChainID(chainID), + tx.Hash(), + common.Address(txArgs.From), + address, + transactions.DeployCommunityToken, + transactions.Keep, + "", + ) + if err != nil { + logutils.ZapLogger().Error("TrackPendingTransaction error", zap.Error(err)) + return requests.DeploymentDetails{}, err + } + + savedCommunityToken, err := api.s.CreateCommunityTokenAndSave(int(chainID), deploymentParameters, txArgs.From.Hex(), address.Hex(), + protobuf.CommunityTokenType_ERC20, token.CommunityLevel, tx.Hash().Hex()) + if err != nil { + return requests.DeploymentDetails{}, err + } + + return requests.DeploymentDetails{ + ContractAddress: address.Hex(), + TransactionHash: tx.Hash().Hex(), + CommunityToken: savedCommunityToken}, nil +} + +func (api *API) DeployCollectiblesEstimate(ctx context.Context, chainID uint64, fromAddress string) (*CommunityTokenFees, error) { + return api.s.deployCollectiblesEstimate(ctx, chainID, fromAddress) +} + +func (api *API) DeployAssetsEstimate(ctx context.Context, chainID uint64, fromAddress string) (*CommunityTokenFees, error) { + return api.s.deployAssetsEstimate(ctx, chainID, fromAddress) +} + +func (api *API) DeployOwnerTokenEstimate(ctx context.Context, chainID uint64, fromAddress string, + ownerTokenParameters requests.DeploymentParameters, masterTokenParameters requests.DeploymentParameters, + communityID string, signerPubKey string) (*CommunityTokenFees, error) { + return api.s.deployOwnerTokenEstimate(ctx, chainID, fromAddress, ownerTokenParameters, masterTokenParameters, communityID, signerPubKey) +} + +func (api *API) EstimateMintTokens(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, walletAddresses []string, amount *bigint.BigInt) (*CommunityTokenFees, error) { + return api.s.mintTokensEstimate(ctx, chainID, contractAddress, fromAddress, walletAddresses, amount) +} + +// This is only ERC721 function +func (api *API) EstimateRemoteBurn(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, tokenIds []*bigint.BigInt) (*CommunityTokenFees, error) { + return api.s.remoteBurnEstimate(ctx, chainID, contractAddress, fromAddress, tokenIds) +} + +func (api *API) EstimateBurn(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, burnAmount *bigint.BigInt) (*CommunityTokenFees, error) { + return api.s.burnEstimate(ctx, chainID, contractAddress, fromAddress, burnAmount) +} + +func (api *API) EstimateSetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, newSignerPubKey string) (*CommunityTokenFees, error) { + return api.s.setSignerPubKeyEstimate(ctx, chainID, contractAddress, fromAddress, newSignerPubKey) +} + +func (api *API) NewOwnerTokenInstance(chainID uint64, contractAddress string) (*ownertoken.OwnerToken, error) { + return api.s.NewOwnerTokenInstance(chainID, contractAddress) +} + +func (api *API) NewCommunityTokenDeployerInstance(chainID uint64) (*communitytokendeployer.CommunityTokenDeployer, error) { + return api.s.manager.NewCommunityTokenDeployerInstance(chainID) +} + +func (api *API) NewCommunityOwnerTokenRegistryInstance(chainID uint64, contractAddress string) (*communityownertokenregistry.CommunityOwnerTokenRegistry, error) { + return api.s.NewCommunityOwnerTokenRegistryInstance(chainID, contractAddress) +} + +func (api *API) NewCollectiblesInstance(chainID uint64, contractAddress string) (*collectibles.Collectibles, error) { + return api.s.manager.NewCollectiblesInstance(chainID, contractAddress) +} + +func (api *API) NewAssetsInstance(chainID uint64, contractAddress string) (*assets.Assets, error) { + return api.s.manager.NewAssetsInstance(chainID, contractAddress) +} + +// Universal minting function for every type of token. +func (api *API) MintTokens(ctx context.Context, chainID uint64, contractAddress string, txArgs wallettypes.SendTxArgs, password string, walletAddresses []string, amount *bigint.BigInt) (string, error) { + + err := api.s.ValidateWalletsAndAmounts(walletAddresses, amount) + if err != nil { + return "", err + } + + transactOpts := txArgs.ToTransactOpts(utils.VerifyPasswordAndGetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password)) + + contractInst, err := NewTokenInstance(api.s, chainID, contractAddress) + if err != nil { + return "", err + } + + tx, err := contractInst.Mint(transactOpts, walletAddresses, amount) + if err != nil { + return "", err + } + + err = api.s.pendingTracker.TrackPendingTransaction( + wcommon.ChainID(chainID), + tx.Hash(), + common.Address(txArgs.From), + common.HexToAddress(contractAddress), + transactions.AirdropCommunityToken, + transactions.Keep, + "", + ) + if err != nil { + logutils.ZapLogger().Error("TrackPendingTransaction error", zap.Error(err)) + return "", err + } + + return tx.Hash().Hex(), nil +} + +// This is only ERC721 function +func (api *API) RemoteDestructedAmount(ctx context.Context, chainID uint64, contractAddress string) (*bigint.BigInt, error) { + callOpts := &bind.CallOpts{Context: ctx, Pending: false} + contractInst, err := api.NewCollectiblesInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + + // total supply = airdropped only (w/o burnt) + totalSupply, err := contractInst.TotalSupply(callOpts) + if err != nil { + return nil, err + } + + // minted = all created tokens (airdropped and remotely destructed) + mintedCount, err := contractInst.MintedCount(callOpts) + if err != nil { + return nil, err + } + + var res = new(big.Int) + res.Sub(mintedCount, totalSupply) + + return &bigint.BigInt{Int: res}, nil +} + +// This is only ERC721 function +func (api *API) RemoteBurn(ctx context.Context, chainID uint64, contractAddress string, txArgs wallettypes.SendTxArgs, password string, tokenIds []*bigint.BigInt, additionalData string) (string, error) { + err := api.s.validateTokens(tokenIds) + if err != nil { + return "", err + } + + transactOpts := txArgs.ToTransactOpts(utils.VerifyPasswordAndGetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password)) + + var tempTokenIds []*big.Int + for _, v := range tokenIds { + tempTokenIds = append(tempTokenIds, v.Int) + } + + contractInst, err := NewTokenInstance(api.s, chainID, contractAddress) + if err != nil { + return "", err + } + + tx, err := contractInst.RemoteBurn(transactOpts, tempTokenIds) + if err != nil { + return "", err + } + + err = api.s.pendingTracker.TrackPendingTransaction( + wcommon.ChainID(chainID), + tx.Hash(), + common.Address(txArgs.From), + common.HexToAddress(contractAddress), + transactions.RemoteDestructCollectible, + transactions.Keep, + additionalData, + ) + if err != nil { + logutils.ZapLogger().Error("TrackPendingTransaction error", zap.Error(err)) + return "", err + } + + return tx.Hash().Hex(), nil +} + +func (api *API) GetCollectiblesContractInstance(chainID uint64, contractAddress string) (*collectibles.Collectibles, error) { + return api.s.manager.GetCollectiblesContractInstance(chainID, contractAddress) +} + +func (api *API) GetAssetContractInstance(chainID uint64, contractAddress string) (*assets.Assets, error) { + return api.s.manager.GetAssetContractInstance(chainID, contractAddress) +} + +func (api *API) RemainingSupply(ctx context.Context, chainID uint64, contractAddress string) (*bigint.BigInt, error) { + return api.s.remainingSupply(ctx, chainID, contractAddress) +} + +func (api *API) Burn(ctx context.Context, chainID uint64, contractAddress string, txArgs wallettypes.SendTxArgs, password string, burnAmount *bigint.BigInt) (string, error) { + err := api.s.validateBurnAmount(ctx, burnAmount, chainID, contractAddress) + if err != nil { + return "", err + } + + transactOpts := txArgs.ToTransactOpts(utils.VerifyPasswordAndGetSigner(chainID, api.s.accountsManager, api.s.config.KeyStoreDir, txArgs.From, password)) + + newMaxSupply, err := api.s.prepareNewMaxSupply(ctx, chainID, contractAddress, burnAmount) + if err != nil { + return "", err + } + + contractInst, err := NewTokenInstance(api.s, chainID, contractAddress) + if err != nil { + return "", err + } + + tx, err := contractInst.SetMaxSupply(transactOpts, newMaxSupply) + if err != nil { + return "", err + } + + err = api.s.pendingTracker.TrackPendingTransaction( + wcommon.ChainID(chainID), + tx.Hash(), + common.Address(txArgs.From), + common.HexToAddress(contractAddress), + transactions.BurnCommunityToken, + transactions.Keep, + "", + ) + if err != nil { + logutils.ZapLogger().Error("TrackPendingTransaction error", zap.Error(err)) + return "", err + } + + return tx.Hash().Hex(), nil +} + +// Gets signer public key from smart contract with a given chainId and address +func (api *API) GetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string) (string, error) { + return api.s.GetSignerPubKey(ctx, chainID, contractAddress) +} + +// Gets signer public key directly from deployer contract +func (api *API) SafeGetSignerPubKey(ctx context.Context, chainID uint64, communityID string) (string, error) { + return api.s.SafeGetSignerPubKey(ctx, chainID, communityID) +} + +// Gets owner token contract address from deployer contract +func (api *API) SafeGetOwnerTokenAddress(ctx context.Context, chainID uint64, communityID string) (string, error) { + return api.s.SafeGetOwnerTokenAddress(ctx, chainID, communityID) +} + +func (api *API) SetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string, txArgs wallettypes.SendTxArgs, password string, newSignerPubKey string) (string, error) { + return api.s.SetSignerPubKey(ctx, chainID, contractAddress, txArgs, password, newSignerPubKey) +} + +func (api *API) OwnerTokenOwnerAddress(ctx context.Context, chainID uint64, contractAddress string) (string, error) { + callOpts := &bind.CallOpts{Context: ctx, Pending: false} + contractInst, err := api.NewOwnerTokenInstance(chainID, contractAddress) + if err != nil { + return "", err + } + ownerAddress, err := contractInst.OwnerOf(callOpts, big.NewInt(0)) + if err != nil { + return "", err + } + return ownerAddress.Hex(), nil +} diff --git a/services/communitytokensv2/api_test.go b/services/communitytokensv2/api_test.go new file mode 100644 index 00000000000..5b34bef6e44 --- /dev/null +++ b/services/communitytokensv2/api_test.go @@ -0,0 +1,31 @@ +package communitytokensv2 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" +) + +func TestTypedDataHash(t *testing.T) { + sigHash := common.Hex2Bytes("dd91c30357aafeb2792b5f0facbd83995943c1ea113a906ebbeb58bfeb27dfc2") + domainSep := common.Hex2Bytes("4a672b5a08e88d37f7239165a0c9e03a01196587d52c638c0c99cbee5ba527c8") + contractAddr := "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + signer := "0x54e3922e97e334905fb489be7c5df1f83cb1ce58" + deployer := "0x7c8999dC9a822c1f0Df42023113EDB4FDd543266" + goodHashResult := "0xccbb375343347491706cf4b43796f7b96ccc89c9e191a8b78679daeba1684ec7" + + typedHash, err := typedStructuredDataHash(domainSep, signer, deployer, contractAddr, 420) + require.NoError(t, err, "creating typed structured data hash") + require.Equal(t, goodHashResult, typedHash.String()) + + customTypedHash := customTypedStructuredDataHash(domainSep, sigHash, signer, deployer) + require.Equal(t, goodHashResult, customTypedHash.String()) +} + +func TestCompressedKeyToEthAddress(t *testing.T) { + ethAddr, err := convert33BytesPubKeyToEthAddress("0x02bcbe39785b55a22383f82ac631ea7500e204627369c4ea01d9296af0ea573f57") + require.NoError(t, err, "converting pub key to address") + require.Equal(t, "0x0A1ec0002dDB927B03049F1aD8D589aBEA4Ba4b3", ethAddr.Hex()) +} diff --git a/services/communitytokensv2/communitytokensdatabase/database.go b/services/communitytokensv2/communitytokensdatabase/database.go new file mode 100644 index 00000000000..8da07e5f0d6 --- /dev/null +++ b/services/communitytokensv2/communitytokensdatabase/database.go @@ -0,0 +1,66 @@ +package communitytokensdatabase + +import ( + "database/sql" + "fmt" + + "github.com/status-im/status-go/protocol/communities/token" + "github.com/status-im/status-go/protocol/protobuf" +) + +type Database struct { + db *sql.DB +} + +func NewCommunityTokensDatabase(db *sql.DB) *Database { + return &Database{db: db} +} + +func (db *Database) GetTokenType(chainID uint64, contractAddress string) (protobuf.CommunityTokenType, error) { + var result = protobuf.CommunityTokenType_UNKNOWN_TOKEN_TYPE + rows, err := db.db.Query(`SELECT type FROM community_tokens WHERE chain_id=? AND address=? LIMIT 1`, chainID, contractAddress) + if err != nil { + return result, err + } + defer rows.Close() + + if rows.Next() { + err := rows.Scan(&result) + return result, err + } + return result, fmt.Errorf("can't find token: chainId %v, contractAddress %v", chainID, contractAddress) +} + +func (db *Database) GetTokenPrivilegesLevel(chainID uint64, contractAddress string) (token.PrivilegesLevel, error) { + var result = token.CommunityLevel + rows, err := db.db.Query(`SELECT privileges_level FROM community_tokens WHERE chain_id=? AND address=? LIMIT 1`, chainID, contractAddress) + if err != nil { + return result, err + } + defer rows.Close() + + if rows.Next() { + err := rows.Scan(&result) + return result, err + } + return result, fmt.Errorf("can't find privileges level: chainId %v, contractAddress %v", chainID, contractAddress) +} + +func (db *Database) GetCommunityERC20Metadata() ([]*token.CommunityToken, error) { + rows, err := db.db.Query(`SELECT community_id, address, name, symbol, chain_id FROM community_tokens WHERE type = ?`, protobuf.CommunityTokenType_ERC20) + if err != nil { + return nil, err + } + defer rows.Close() + + var result []*token.CommunityToken + for rows.Next() { + token := token.CommunityToken{} + err := rows.Scan(&token.CommunityID, &token.Address, &token.Name, &token.Symbol, &token.ChainID) + if err != nil { + return nil, err + } + result = append(result, &token) + } + return result, rows.Err() +} diff --git a/services/communitytokensv2/communitytokensdatabase/database_test.go b/services/communitytokensv2/communitytokensdatabase/database_test.go new file mode 100644 index 00000000000..94c5f7de8f5 --- /dev/null +++ b/services/communitytokensv2/communitytokensdatabase/database_test.go @@ -0,0 +1,121 @@ +package communitytokensdatabase + +import ( + "database/sql" + "math/big" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/status-im/status-go/appdatabase" + "github.com/status-im/status-go/protocol/communities/token" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/protocol/sqlite" + "github.com/status-im/status-go/services/wallet/bigint" + "github.com/status-im/status-go/t/helpers" +) + +func TestDatabaseSuite(t *testing.T) { + suite.Run(t, new(DatabaseSuite)) +} + +type DatabaseSuite struct { + suite.Suite + + db *Database +} + +func (s *DatabaseSuite) addCommunityToken(db *sql.DB, token *token.CommunityToken) error { + _, err := db.Exec(`INSERT INTO community_tokens (community_id, address, type, name, symbol, description, supply_str, + infinite_supply, transferable, remote_self_destruct, chain_id, deploy_state, image_base64, decimals, deployer, privileges_level) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, token.CommunityID, token.Address, token.TokenType, token.Name, + token.Symbol, token.Description, token.Supply.String(), token.InfiniteSupply, token.Transferable, token.RemoteSelfDestruct, + token.ChainID, token.DeployState, token.Base64Image, token.Decimals, token.Deployer, token.PrivilegesLevel) + return err +} + +func (s *DatabaseSuite) setupDatabase(db *sql.DB) error { + token721 := &token.CommunityToken{ + CommunityID: "123", + TokenType: protobuf.CommunityTokenType_ERC721, + Address: "0x123", + Name: "StatusToken", + Symbol: "STT", + Description: "desc", + Supply: &bigint.BigInt{Int: big.NewInt(123)}, + InfiniteSupply: false, + Transferable: true, + RemoteSelfDestruct: true, + ChainID: 1, + DeployState: token.InProgress, + Base64Image: "ABCD", + Deployer: "0xDEP1", + PrivilegesLevel: token.OwnerLevel, + } + + token20 := &token.CommunityToken{ + CommunityID: "345", + TokenType: protobuf.CommunityTokenType_ERC20, + Address: "0x345", + Name: "StatusToken", + Symbol: "STT", + Description: "desc", + Supply: &bigint.BigInt{Int: big.NewInt(345)}, + InfiniteSupply: false, + Transferable: true, + RemoteSelfDestruct: true, + ChainID: 2, + DeployState: token.Failed, + Base64Image: "QWERTY", + Decimals: 21, + Deployer: "0xDEP2", + PrivilegesLevel: token.CommunityLevel, + } + + err := s.addCommunityToken(db, token721) + if err != nil { + return err + } + return s.addCommunityToken(db, token20) +} + +func (s *DatabaseSuite) SetupTest() { + s.db = nil + + db, err := helpers.SetupTestMemorySQLDB(appdatabase.DbInitializer{}) + s.NoError(err, "creating sqlite db instance") + + err = sqlite.Migrate(db) + s.NoError(err, "protocol migrate") + + s.db = &Database{db: db} + + err = s.setupDatabase(db) + s.NoError(err, "setting up database") +} + +func (s *DatabaseSuite) TestGetTokenType() { + contractType, err := s.db.GetTokenType(1, "0x123") + s.Require().NoError(err) + s.Equal(contractType, protobuf.CommunityTokenType_ERC721) + + contractType, err = s.db.GetTokenType(2, "0x345") + s.Require().NoError(err) + s.Equal(contractType, protobuf.CommunityTokenType_ERC20) + + _, err = s.db.GetTokenType(10, "0x777") + s.Require().Error(err) +} + +func (s *DatabaseSuite) TestGetPrivilegesLevel() { + privLevel, err := s.db.GetTokenPrivilegesLevel(1, "0x123") + s.Require().NoError(err) + s.Equal(privLevel, token.OwnerLevel) + + privLevel, err = s.db.GetTokenPrivilegesLevel(2, "0x345") + s.Require().NoError(err) + s.Equal(privLevel, token.CommunityLevel) + + _, err = s.db.GetTokenType(10, "0x777") + s.Require().Error(err) +} diff --git a/services/communitytokensv2/estimations.go b/services/communitytokensv2/estimations.go new file mode 100644 index 00000000000..8e257f6416a --- /dev/null +++ b/services/communitytokensv2/estimations.go @@ -0,0 +1,443 @@ +package communitytokens + +import ( + "context" + "fmt" + "math/big" + "strings" + + "go.uber.org/zap" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/params" + "github.com/status-im/status-go/contracts/community-tokens/assets" + "github.com/status-im/status-go/contracts/community-tokens/collectibles" + communitytokendeployer "github.com/status-im/status-go/contracts/community-tokens/deployer" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/logutils" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/services/wallet/bigint" + "github.com/status-im/status-go/services/wallet/requests" + "github.com/status-im/status-go/services/wallet/router/fees" + "github.com/status-im/status-go/services/wallet/wallettypes" +) + +type CommunityTokenFees struct { + GasUnits uint64 `json:"gasUnits"` + SuggestedFees *fees.SuggestedFeesGwei `json:"suggestedFees"` +} + +func weiToGwei(val *big.Int) *big.Float { + result := new(big.Float) + result.SetInt(val) + + unit := new(big.Int) + unit.SetInt64(params.GWei) + + return result.Quo(result, new(big.Float).SetInt(unit)) +} + +func gweiToWei(val *big.Float) *big.Int { + res, _ := new(big.Float).Mul(val, big.NewFloat(1000000000)).Int(nil) + return res +} + +func (s *Service) deployOwnerTokenEstimate(ctx context.Context, chainID uint64, fromAddress string, + ownerTokenParameters requests.DeploymentParameters, masterTokenParameters requests.DeploymentParameters, + communityID string, signerPubKey string) (*CommunityTokenFees, error) { + + gasUnits, err := s.deployOwnerTokenGasUnits(ctx, chainID, fromAddress, ownerTokenParameters, masterTokenParameters, + communityID, signerPubKey) + if err != nil { + return nil, err + } + + deployerAddress, err := communitytokendeployer.ContractAddress(chainID) + if err != nil { + return nil, err + } + + return s.prepareCommunityTokenFees(ctx, common.HexToAddress(fromAddress), &deployerAddress, gasUnits, chainID) +} + +func (s *Service) deployCollectiblesEstimate(ctx context.Context, chainID uint64, fromAddress string) (*CommunityTokenFees, error) { + gasUnits, err := s.deployCollectiblesGasUnits(ctx, chainID, fromAddress) + if err != nil { + return nil, err + } + return s.prepareCommunityTokenFees(ctx, common.HexToAddress(fromAddress), nil, gasUnits, chainID) +} + +func (s *Service) deployAssetsEstimate(ctx context.Context, chainID uint64, fromAddress string) (*CommunityTokenFees, error) { + gasUnits, err := s.deployAssetsGasUnits(ctx, chainID, fromAddress) + if err != nil { + return nil, err + } + return s.prepareCommunityTokenFees(ctx, common.HexToAddress(fromAddress), nil, gasUnits, chainID) +} + +func (s *Service) mintTokensEstimate(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, walletAddresses []string, amount *bigint.BigInt) (*CommunityTokenFees, error) { + gasUnits, err := s.mintTokensGasUnits(ctx, chainID, contractAddress, fromAddress, walletAddresses, amount) + if err != nil { + return nil, err + } + toAddress := common.HexToAddress(contractAddress) + return s.prepareCommunityTokenFees(ctx, common.HexToAddress(fromAddress), &toAddress, gasUnits, chainID) +} + +func (s *Service) remoteBurnEstimate(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, tokenIds []*bigint.BigInt) (*CommunityTokenFees, error) { + gasUnits, err := s.remoteBurnGasUnits(ctx, chainID, contractAddress, fromAddress, tokenIds) + if err != nil { + return nil, err + } + toAddress := common.HexToAddress(contractAddress) + return s.prepareCommunityTokenFees(ctx, common.HexToAddress(fromAddress), &toAddress, gasUnits, chainID) +} + +func (s *Service) burnEstimate(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, burnAmount *bigint.BigInt) (*CommunityTokenFees, error) { + gasUnits, err := s.burnGasUnits(ctx, chainID, contractAddress, fromAddress, burnAmount) + if err != nil { + return nil, err + } + toAddress := common.HexToAddress(contractAddress) + return s.prepareCommunityTokenFees(ctx, common.HexToAddress(fromAddress), &toAddress, gasUnits, chainID) +} + +func (s *Service) setSignerPubKeyEstimate(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, newSignerPubKey string) (*CommunityTokenFees, error) { + gasUnits, err := s.setSignerPubKeyGasUnits(ctx, chainID, contractAddress, fromAddress, newSignerPubKey) + if err != nil { + return nil, err + } + toAddress := common.HexToAddress(contractAddress) + return s.prepareCommunityTokenFees(ctx, common.HexToAddress(fromAddress), &toAddress, gasUnits, chainID) +} + +func (s *Service) setSignerPubKeyGasUnits(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, newSignerPubKey string) (uint64, error) { + if len(newSignerPubKey) <= 0 { + return 0, fmt.Errorf("signerPubKey is empty") + } + + contractInst, err := s.NewOwnerTokenInstance(chainID, contractAddress) + if err != nil { + return 0, err + } + ownerTokenInstance := &OwnerTokenInstance{instance: contractInst} + + return s.estimateMethodForTokenInstance(ctx, ownerTokenInstance, chainID, contractAddress, fromAddress, "setSignerPublicKey", common.FromHex(newSignerPubKey)) +} + +func (s *Service) burnGasUnits(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, burnAmount *bigint.BigInt) (uint64, error) { + err := s.validateBurnAmount(ctx, burnAmount, chainID, contractAddress) + if err != nil { + return 0, err + } + + newMaxSupply, err := s.prepareNewMaxSupply(ctx, chainID, contractAddress, burnAmount) + if err != nil { + return 0, err + } + + return s.estimateMethod(ctx, chainID, contractAddress, fromAddress, "setMaxSupply", newMaxSupply) +} + +func (s *Service) remoteBurnGasUnits(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, tokenIds []*bigint.BigInt) (uint64, error) { + err := s.validateTokens(tokenIds) + if err != nil { + return 0, err + } + + var tempTokenIds []*big.Int + for _, v := range tokenIds { + tempTokenIds = append(tempTokenIds, v.Int) + } + + return s.estimateMethod(ctx, chainID, contractAddress, fromAddress, "remoteBurn", tempTokenIds) +} + +func (s *Service) deployOwnerTokenGasUnits(ctx context.Context, chainID uint64, fromAddress string, + ownerTokenParameters requests.DeploymentParameters, masterTokenParameters requests.DeploymentParameters, + communityID string, signerPubKey string) (uint64, error) { + ethClient, err := s.manager.rpcClient.EthClient(chainID) + if err != nil { + logutils.ZapLogger().Error(err.Error()) + return 0, err + } + + deployerAddress, err := communitytokendeployer.ContractAddress(chainID) + if err != nil { + return 0, err + } + + deployerABI, err := abi.JSON(strings.NewReader(communitytokendeployer.CommunityTokenDeployerABI)) + if err != nil { + return 0, err + } + + ownerTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{ + Name: ownerTokenParameters.Name, + Symbol: ownerTokenParameters.Symbol, + BaseURI: ownerTokenParameters.TokenURI, + } + + masterTokenConfig := communitytokendeployer.CommunityTokenDeployerTokenConfig{ + Name: masterTokenParameters.Name, + Symbol: masterTokenParameters.Symbol, + BaseURI: masterTokenParameters.TokenURI, + } + + signature, err := s.Messenger.CreateCommunityTokenDeploymentSignature(ctx, chainID, fromAddress, communityID) + if err != nil { + return 0, err + } + + communitySignature, err := prepareDeploymentSignatureStruct(types.HexBytes(signature).String(), communityID, common.HexToAddress(fromAddress)) + if err != nil { + return 0, err + } + + data, err := deployerABI.Pack("deploy", ownerTokenConfig, masterTokenConfig, communitySignature, common.FromHex(signerPubKey)) + + if err != nil { + return 0, err + } + + toAddr := deployerAddress + fromAddr := common.HexToAddress(fromAddress) + + callMsg := ethereum.CallMsg{ + From: fromAddr, + To: &toAddr, + Value: big.NewInt(0), + Data: data, + } + + estimate, err := ethClient.EstimateGas(ctx, callMsg) + if err != nil { + return 0, err + } + + finalEstimation := estimate + uint64(float32(estimate)*0.1) + logutils.ZapLogger().Debug("Owner token deployment estimation", zap.Uint64("gas", finalEstimation)) + return finalEstimation, nil +} + +func (s *Service) deployCollectiblesGasUnits(ctx context.Context, chainID uint64, fromAddress string) (uint64, error) { + ethClient, err := s.manager.rpcClient.EthClient(chainID) + if err != nil { + logutils.ZapLogger().Error(err.Error()) + return 0, err + } + + collectiblesABI, err := abi.JSON(strings.NewReader(collectibles.CollectiblesABI)) + if err != nil { + return 0, err + } + + // use random parameters, they will not have impact on deployment results + data, err := collectiblesABI.Pack("" /*constructor name is empty*/, "name", "SYMBOL", big.NewInt(20), true, false, "tokenUri", + common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"), common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110")) + if err != nil { + return 0, err + } + + callMsg := ethereum.CallMsg{ + From: common.HexToAddress(fromAddress), + To: nil, + Value: big.NewInt(0), + Data: append(common.FromHex(collectibles.CollectiblesBin), data...), + } + estimate, err := ethClient.EstimateGas(ctx, callMsg) + if err != nil { + return 0, err + } + + finalEstimation := estimate + uint64(float32(estimate)*0.1) + logutils.ZapLogger().Debug("Collectibles deployment estimation", zap.Uint64("gas", finalEstimation)) + return finalEstimation, nil +} + +func (s *Service) deployAssetsGasUnits(ctx context.Context, chainID uint64, fromAddress string) (uint64, error) { + ethClient, err := s.manager.rpcClient.EthClient(chainID) + if err != nil { + logutils.ZapLogger().Error(err.Error()) + return 0, err + } + + assetsABI, err := abi.JSON(strings.NewReader(assets.AssetsABI)) + if err != nil { + return 0, err + } + + // use random parameters, they will not have impact on deployment results + data, err := assetsABI.Pack("" /*constructor name is empty*/, "name", "SYMBOL", uint8(18), big.NewInt(20), "tokenUri", + common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"), common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110")) + if err != nil { + return 0, err + } + + callMsg := ethereum.CallMsg{ + From: common.HexToAddress(fromAddress), + To: nil, + Value: big.NewInt(0), + Data: append(common.FromHex(assets.AssetsBin), data...), + } + estimate, err := ethClient.EstimateGas(ctx, callMsg) + if err != nil { + return 0, err + } + + finalEstimation := estimate + uint64(float32(estimate)*0.1) + logutils.ZapLogger().Debug("Assets deployment estimation: ", zap.Uint64("gas", finalEstimation)) + return finalEstimation, nil +} + +// if we want to mint 2 tokens to addresses ["a", "b"] we need to mint +// twice to every address - we need to send to smart contract table ["a", "a", "b", "b"] +func multiplyWalletAddresses(amount *bigint.BigInt, contractAddresses []string) []string { + var totalAddresses []string + for i := big.NewInt(1); i.Cmp(amount.Int) <= 0; { + totalAddresses = append(totalAddresses, contractAddresses...) + i.Add(i, big.NewInt(1)) + } + return totalAddresses +} + +func prepareMintCollectiblesData(walletAddresses []string, amount *bigint.BigInt) []common.Address { + totalAddresses := multiplyWalletAddresses(amount, walletAddresses) + var usersAddresses = []common.Address{} + for _, k := range totalAddresses { + usersAddresses = append(usersAddresses, common.HexToAddress(k)) + } + return usersAddresses +} + +func prepareMintAssetsData(walletAddresses []string, amount *bigint.BigInt) ([]common.Address, []*big.Int) { + var usersAddresses = []common.Address{} + var amountsList = []*big.Int{} + for _, k := range walletAddresses { + usersAddresses = append(usersAddresses, common.HexToAddress(k)) + amountsList = append(amountsList, amount.Int) + } + return usersAddresses, amountsList +} + +func (s *Service) mintCollectiblesGasUnits(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, walletAddresses []string, amount *bigint.BigInt) (uint64, error) { + err := s.ValidateWalletsAndAmounts(walletAddresses, amount) + if err != nil { + return 0, err + } + usersAddresses := prepareMintCollectiblesData(walletAddresses, amount) + return s.estimateMethod(ctx, chainID, contractAddress, fromAddress, "mintTo", usersAddresses) +} + +func (s *Service) mintAssetsGasUnits(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, walletAddresses []string, amount *bigint.BigInt) (uint64, error) { + err := s.ValidateWalletsAndAmounts(walletAddresses, amount) + if err != nil { + return 0, err + } + usersAddresses, amountsList := prepareMintAssetsData(walletAddresses, amount) + return s.estimateMethod(ctx, chainID, contractAddress, fromAddress, "mintTo", usersAddresses, amountsList) +} + +func (s *Service) mintTokensGasUnits(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, walletAddresses []string, amount *bigint.BigInt) (uint64, error) { + tokenType, err := s.db.GetTokenType(chainID, contractAddress) + if err != nil { + return 0, err + } + + switch tokenType { + case protobuf.CommunityTokenType_ERC721: + return s.mintCollectiblesGasUnits(ctx, chainID, contractAddress, fromAddress, walletAddresses, amount) + case protobuf.CommunityTokenType_ERC20: + return s.mintAssetsGasUnits(ctx, chainID, contractAddress, fromAddress, walletAddresses, amount) + default: + return 0, fmt.Errorf("unknown token type: %v", tokenType) + } +} + +func (s *Service) prepareCommunityTokenFees(ctx context.Context, from common.Address, to *common.Address, gasUnits uint64, chainID uint64) (*CommunityTokenFees, error) { + suggestedFees, err := s.feeManager.SuggestedFeesGwei(ctx, chainID) + if err != nil { + return nil, err + } + + txArgs := s.suggestedFeesToSendTxArgs(from, to, gasUnits, suggestedFees) + + l1Fee, err := s.estimateL1Fee(ctx, chainID, txArgs) + if err == nil { + suggestedFees.L1GasFee = weiToGwei(big.NewInt(int64(l1Fee))) + } + return &CommunityTokenFees{ + GasUnits: gasUnits, + SuggestedFees: suggestedFees, + }, nil +} + +func (s *Service) suggestedFeesToSendTxArgs(from common.Address, to *common.Address, gas uint64, suggestedFees *fees.SuggestedFeesGwei) wallettypes.SendTxArgs { + sendArgs := wallettypes.SendTxArgs{} + sendArgs.From = types.Address(from) + sendArgs.To = (*types.Address)(to) + sendArgs.Gas = (*hexutil.Uint64)(&gas) + if suggestedFees.EIP1559Enabled { + sendArgs.MaxPriorityFeePerGas = (*hexutil.Big)(gweiToWei(suggestedFees.MaxPriorityFeePerGas)) + sendArgs.MaxFeePerGas = (*hexutil.Big)(gweiToWei(suggestedFees.MaxFeePerGasMedium)) + } else { + sendArgs.GasPrice = (*hexutil.Big)(gweiToWei(suggestedFees.GasPrice)) + } + return sendArgs +} + +func (s *Service) estimateL1Fee(ctx context.Context, chainID uint64, sendArgs wallettypes.SendTxArgs) (uint64, error) { + transaction, _, err := s.transactor.ValidateAndBuildTransaction(chainID, sendArgs, -1) + if err != nil { + return 0, err + } + + data, err := transaction.MarshalBinary() + if err != nil { + return 0, err + } + + return s.feeManager.GetL1Fee(ctx, chainID, data) +} + +func (s *Service) estimateMethodForTokenInstance(ctx context.Context, contractInstance TokenInstance, chainID uint64, contractAddress string, fromAddress string, methodName string, args ...interface{}) (uint64, error) { + ethClient, err := s.manager.rpcClient.EthClient(chainID) + if err != nil { + logutils.ZapLogger().Error(err.Error()) + return 0, err + } + + data, err := contractInstance.PackMethod(ctx, methodName, args...) + + if err != nil { + return 0, err + } + + toAddr := common.HexToAddress(contractAddress) + fromAddr := common.HexToAddress(fromAddress) + + callMsg := ethereum.CallMsg{ + From: fromAddr, + To: &toAddr, + Value: big.NewInt(0), + Data: data, + } + estimate, err := ethClient.EstimateGas(ctx, callMsg) + + if err != nil { + return 0, err + } + return estimate + uint64(float32(estimate)*0.1), nil +} + +func (s *Service) estimateMethod(ctx context.Context, chainID uint64, contractAddress string, fromAddress string, methodName string, args ...interface{}) (uint64, error) { + contractInst, err := NewTokenInstance(s, chainID, contractAddress) + if err != nil { + return 0, err + } + return s.estimateMethodForTokenInstance(ctx, contractInst, chainID, contractAddress, fromAddress, methodName, args...) +} diff --git a/services/communitytokensv2/manager.go b/services/communitytokensv2/manager.go new file mode 100644 index 00000000000..b2dd7d5d4fa --- /dev/null +++ b/services/communitytokensv2/manager.go @@ -0,0 +1,210 @@ +package communitytokensv2 + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/status-im/status-go/contracts/community-tokens/assets" + "github.com/status-im/status-go/contracts/community-tokens/collectibles" + communitytokendeployer "github.com/status-im/status-go/contracts/community-tokens/deployer" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol/communities" + "github.com/status-im/status-go/rpc" + "github.com/status-im/status-go/services/wallet/bigint" + "github.com/status-im/status-go/services/wallet/requests" +) + +type Manager struct { + rpcClient *rpc.Client +} + +func NewManager(rpcClient *rpc.Client) *Manager { + return &Manager{ + rpcClient: rpcClient, + } +} + +func (m *Manager) NewCollectiblesInstance(chainID uint64, contractAddress string) (*collectibles.Collectibles, error) { + backend, err := m.rpcClient.EthClient(chainID) + if err != nil { + return nil, err + } + return collectibles.NewCollectibles(common.HexToAddress(contractAddress), backend) +} + +func (m *Manager) NewCommunityTokenDeployerInstance(chainID uint64) (*communitytokendeployer.CommunityTokenDeployer, error) { + backend, err := m.rpcClient.EthClient(chainID) + if err != nil { + return nil, err + } + deployerAddr, err := communitytokendeployer.ContractAddress(chainID) + if err != nil { + return nil, err + } + return communitytokendeployer.NewCommunityTokenDeployer(deployerAddr, backend) +} + +func (m *Manager) GetCollectiblesContractInstance(chainID uint64, contractAddress string) (*collectibles.Collectibles, error) { + contractInst, err := m.NewCollectiblesInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + return contractInst, nil +} + +func (m *Manager) NewAssetsInstance(chainID uint64, contractAddress string) (*assets.Assets, error) { + backend, err := m.rpcClient.EthClient(chainID) + if err != nil { + return nil, err + } + return assets.NewAssets(common.HexToAddress(contractAddress), backend) +} + +func (m *Manager) GetAssetContractInstance(chainID uint64, contractAddress string) (*assets.Assets, error) { + contractInst, err := m.NewAssetsInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + return contractInst, nil +} + +func (m *Manager) GetCollectibleContractData(chainID uint64, contractAddress string) (*communities.CollectibleContractData, error) { + callOpts := &bind.CallOpts{Context: context.Background(), Pending: false} + + contract, err := m.GetCollectiblesContractInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + totalSupply, err := contract.MaxSupply(callOpts) + if err != nil { + return nil, err + } + transferable, err := contract.Transferable(callOpts) + if err != nil { + return nil, err + } + remoteBurnable, err := contract.RemoteBurnable(callOpts) + if err != nil { + return nil, err + } + + return &communities.CollectibleContractData{ + TotalSupply: &bigint.BigInt{Int: totalSupply}, + Transferable: transferable, + RemoteBurnable: remoteBurnable, + InfiniteSupply: requests.GetInfiniteSupply().Cmp(totalSupply) == 0, + }, nil +} + +func (m *Manager) GetAssetContractData(chainID uint64, contractAddress string) (*communities.AssetContractData, error) { + callOpts := &bind.CallOpts{Context: context.Background(), Pending: false} + contract, err := m.GetAssetContractInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + totalSupply, err := contract.MaxSupply(callOpts) + if err != nil { + return nil, err + } + + return &communities.AssetContractData{ + TotalSupply: &bigint.BigInt{Int: totalSupply}, + InfiniteSupply: requests.GetInfiniteSupply().Cmp(totalSupply) == 0, + }, nil +} + +func convert33BytesPubKeyToEthAddress(pubKey string) (common.Address, error) { + decoded, err := types.DecodeHex(pubKey) + if err != nil { + return common.Address{}, err + } + communityPubKey, err := crypto.DecompressPubkey(decoded) + if err != nil { + return common.Address{}, err + } + return common.Address(crypto.PubkeyToAddress(*communityPubKey)), nil +} + +// Simpler version of hashing typed structured data alternative to typedStructuredDataHash. Keeping this for reference. +func customTypedStructuredDataHash(domainSeparator []byte, signatureTypedHash []byte, signer string, deployer string) types.Hash { + // every field should be 32 bytes, eth address is 20 bytes so padding should be added + emptyOffset := [12]byte{} + hashedEncoded := crypto.Keccak256Hash(signatureTypedHash, emptyOffset[:], common.HexToAddress(signer).Bytes(), + emptyOffset[:], common.HexToAddress(deployer).Bytes()) + rawData := []byte(fmt.Sprintf("\x19\x01%s%s", domainSeparator, hashedEncoded.Bytes())) + return crypto.Keccak256Hash(rawData) +} + +// Returns a typed structured hash according to https://eips.ethereum.org/EIPS/eip-712 +// Domain separator from smart contract is used. +func typedStructuredDataHash(domainSeparator []byte, signer string, addressFrom string, deployerContractAddress string, chainID uint64) (types.Hash, error) { + myTypedData := apitypes.TypedData{ + Types: apitypes.Types{ + "Deploy": []apitypes.Type{ + {Name: "signer", Type: "address"}, + {Name: "deployer", Type: "address"}, + }, + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, + }, + PrimaryType: "Deploy", + // Domain field should be here to keep correct structure but + // domainSeparator from smart contract is used. + Domain: apitypes.TypedDataDomain{ + Name: "CommunityTokenDeployer", // name from Deployer smart contract + Version: "1", // version from Deployer smart contract + ChainId: math.NewHexOrDecimal256(int64(chainID)), + VerifyingContract: deployerContractAddress, + }, + Message: apitypes.TypedDataMessage{ + "signer": signer, + "deployer": addressFrom, + }, + } + + typedDataHash, err := myTypedData.HashStruct(myTypedData.PrimaryType, myTypedData.Message) + if err != nil { + return types.Hash{}, err + } + rawData := []byte(fmt.Sprintf("\x19\x01%s%s", domainSeparator, string(typedDataHash))) + return crypto.Keccak256Hash(rawData), nil +} + +// Creates +func (m *Manager) DeploymentSignatureDigest(chainID uint64, addressFrom string, communityID string) ([]byte, error) { + callOpts := &bind.CallOpts{Pending: false} + communityEthAddr, err := convert33BytesPubKeyToEthAddress(communityID) + if err != nil { + return nil, err + } + + deployerAddr, err := communitytokendeployer.ContractAddress(chainID) + if err != nil { + return nil, err + } + deployerContractInst, err := m.NewCommunityTokenDeployerInstance(chainID) + if err != nil { + return nil, err + } + + domainSeparator, err := deployerContractInst.DOMAINSEPARATOR(callOpts) + if err != nil { + return nil, err + } + + structedHash, err := typedStructuredDataHash(domainSeparator[:], communityEthAddr.Hex(), addressFrom, deployerAddr.Hex(), chainID) + if err != nil { + return nil, err + } + + return structedHash.Bytes(), nil +} diff --git a/services/communitytokensv2/service.go b/services/communitytokensv2/service.go new file mode 100644 index 00000000000..ed8c02c8b84 --- /dev/null +++ b/services/communitytokensv2/service.go @@ -0,0 +1,742 @@ +package communitytokensv2 + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "math/big" + "strings" + + "github.com/pkg/errors" + "go.uber.org/zap" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/event" + "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/contracts/community-tokens/mastertoken" + "github.com/status-im/status-go/contracts/community-tokens/ownertoken" + communityownertokenregistry "github.com/status-im/status-go/contracts/community-tokens/registry" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/logutils" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/protocol" + "github.com/status-im/status-go/protocol/communities" + "github.com/status-im/status-go/protocol/communities/token" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/rpc" + "github.com/status-im/status-go/services/communitytokens/communitytokensdatabase" + "github.com/status-im/status-go/services/utils" + "github.com/status-im/status-go/services/wallet/bigint" + wcommon "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/services/wallet/requests" + "github.com/status-im/status-go/services/wallet/router/fees" + "github.com/status-im/status-go/services/wallet/walletevent" + "github.com/status-im/status-go/services/wallet/wallettypes" + "github.com/status-im/status-go/signal" + "github.com/status-im/status-go/transactions" +) + +// Collectibles service +type Service struct { + manager *Manager + accountsManager *account.GethManager + pendingTracker *transactions.PendingTxTracker + config *params.NodeConfig + db *communitytokensdatabase.Database + Messenger *protocol.Messenger + walletFeed *event.Feed + walletWatcher *walletevent.Watcher + transactor *transactions.Transactor + feeManager *fees.FeeManager +} + +// Returns a new Collectibles Service. +func NewService(rpcClient *rpc.Client, accountsManager *account.GethManager, pendingTracker *transactions.PendingTxTracker, + config *params.NodeConfig, appDb *sql.DB, walletFeed *event.Feed, transactor *transactions.Transactor) *Service { + return &Service{ + manager: &Manager{rpcClient: rpcClient}, + accountsManager: accountsManager, + pendingTracker: pendingTracker, + config: config, + db: communitytokensdatabase.NewCommunityTokensDatabase(appDb), + walletFeed: walletFeed, + transactor: transactor, + feeManager: &fees.FeeManager{RPCClient: rpcClient}, + } +} + +// 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: "communitytokensv2", + Version: "0.1.0", + Service: NewAPI(s), + Public: true, + }, + } +} + +// Start is run when a service is started. +func (s *Service) Start() error { + + s.walletWatcher = walletevent.NewWatcher(s.walletFeed, s.handleWalletEvent) + s.walletWatcher.Start() + + return nil +} + +func (s *Service) handleWalletEvent(event walletevent.Event) { + if event.Type == transactions.EventPendingTransactionStatusChanged { + var p transactions.StatusChangedPayload + err := json.Unmarshal([]byte(event.Message), &p) + if err != nil { + logutils.ZapLogger().Error(errors.Wrap(err, fmt.Sprintf("can't parse transaction message %v\n", event.Message)).Error()) + return + } + if p.Status == transactions.Pending { + return + } + pendingTransaction, err := s.pendingTracker.GetPendingEntry(p.ChainID, p.Hash) + if err != nil { + logutils.ZapLogger().Error(errors.Wrap(err, fmt.Sprintf("no pending transaction with hash %v on chain %v\n", p.Hash, p.ChainID)).Error()) + return + } + + var communityToken, ownerToken, masterToken *token.CommunityToken = &token.CommunityToken{}, &token.CommunityToken{}, &token.CommunityToken{} + var tokenErr error + switch pendingTransaction.Type { + case transactions.DeployCommunityToken: + communityToken, tokenErr = s.handleDeployCommunityToken(p.Status, pendingTransaction) + case transactions.AirdropCommunityToken: + communityToken, tokenErr = s.handleAirdropCommunityToken(p.Status, pendingTransaction) + case transactions.RemoteDestructCollectible: + communityToken, tokenErr = s.handleRemoteDestructCollectible(p.Status, pendingTransaction) + case transactions.BurnCommunityToken: + communityToken, tokenErr = s.handleBurnCommunityToken(p.Status, pendingTransaction) + case transactions.DeployOwnerToken: + ownerToken, masterToken, tokenErr = s.handleDeployOwnerToken(p.Status, pendingTransaction) + case transactions.SetSignerPublicKey: + communityToken, tokenErr = s.handleSetSignerPubKey(p.Status, pendingTransaction) + default: + return + } + + err = s.pendingTracker.Delete(context.Background(), p.ChainID, p.Hash) + if err != nil { + logutils.ZapLogger().Error(errors.Wrap(err, fmt.Sprintf("can't delete pending transaction with hash %v on chain %v\n", p.Hash, p.ChainID)).Error()) + } + + errorStr := "" + if tokenErr != nil { + errorStr = tokenErr.Error() + } + + signal.SendCommunityTokenTransactionStatusSignal(string(pendingTransaction.Type), p.Status == transactions.Success, pendingTransaction.Hash, + communityToken, ownerToken, masterToken, errorStr) + } +} + +func (s *Service) handleAirdropCommunityToken(status string, pendingTransaction *transactions.PendingTransaction) (*token.CommunityToken, error) { + communityToken, err := s.Messenger.GetCommunityTokenByChainAndAddress(int(pendingTransaction.ChainID), pendingTransaction.To.String()) + if communityToken == nil { + return nil, fmt.Errorf("token does not exist in database: chainId=%v, address=%v", pendingTransaction.ChainID, pendingTransaction.To.String()) + } else { + publishErr := s.publishTokenActionToPrivilegedMembers(communityToken.CommunityID, uint64(communityToken.ChainID), + communityToken.Address, protobuf.CommunityTokenAction_AIRDROP) + if publishErr != nil { + logutils.ZapLogger().Warn("can't publish airdrop action") + } + } + return communityToken, err +} + +func (s *Service) handleRemoteDestructCollectible(status string, pendingTransaction *transactions.PendingTransaction) (*token.CommunityToken, error) { + communityToken, err := s.Messenger.GetCommunityTokenByChainAndAddress(int(pendingTransaction.ChainID), pendingTransaction.To.String()) + if communityToken == nil { + return nil, fmt.Errorf("token does not exist in database: chainId=%v, address=%v", pendingTransaction.ChainID, pendingTransaction.To.String()) + } else { + publishErr := s.publishTokenActionToPrivilegedMembers(communityToken.CommunityID, uint64(communityToken.ChainID), + communityToken.Address, protobuf.CommunityTokenAction_REMOTE_DESTRUCT) + if publishErr != nil { + logutils.ZapLogger().Warn("can't publish remote destruct action") + } + } + return communityToken, err +} + +func (s *Service) handleBurnCommunityToken(status string, pendingTransaction *transactions.PendingTransaction) (*token.CommunityToken, error) { + if status == transactions.Success { + // get new max supply and update database + newMaxSupply, err := s.maxSupply(context.Background(), uint64(pendingTransaction.ChainID), pendingTransaction.To.String()) + if err != nil { + return nil, err + } + err = s.Messenger.UpdateCommunityTokenSupply(int(pendingTransaction.ChainID), pendingTransaction.To.String(), &bigint.BigInt{Int: newMaxSupply}) + if err != nil { + return nil, err + } + } + + communityToken, err := s.Messenger.GetCommunityTokenByChainAndAddress(int(pendingTransaction.ChainID), pendingTransaction.To.String()) + + if communityToken == nil { + return nil, fmt.Errorf("token does not exist in database: chainId=%v, address=%v", pendingTransaction.ChainID, pendingTransaction.To.String()) + } else { + publishErr := s.publishTokenActionToPrivilegedMembers(communityToken.CommunityID, uint64(communityToken.ChainID), + communityToken.Address, protobuf.CommunityTokenAction_BURN) + if publishErr != nil { + logutils.ZapLogger().Warn("can't publish burn action") + } + } + return communityToken, err +} + +func (s *Service) handleDeployOwnerToken(status string, pendingTransaction *transactions.PendingTransaction) (*token.CommunityToken, *token.CommunityToken, error) { + newMasterAddress, err := s.GetMasterTokenContractAddressFromHash(context.Background(), uint64(pendingTransaction.ChainID), pendingTransaction.Hash.Hex()) + if err != nil { + return nil, nil, err + } + newOwnerAddress, err := s.GetOwnerTokenContractAddressFromHash(context.Background(), uint64(pendingTransaction.ChainID), pendingTransaction.Hash.Hex()) + if err != nil { + return nil, nil, err + } + + err = s.Messenger.UpdateCommunityTokenAddress(int(pendingTransaction.ChainID), s.TemporaryOwnerContractAddress(pendingTransaction.Hash.Hex()), newOwnerAddress) + if err != nil { + return nil, nil, err + } + err = s.Messenger.UpdateCommunityTokenAddress(int(pendingTransaction.ChainID), s.TemporaryMasterContractAddress(pendingTransaction.Hash.Hex()), newMasterAddress) + if err != nil { + return nil, nil, err + } + + ownerToken, err := s.updateStateAndAddTokenToCommunityDescription(status, int(pendingTransaction.ChainID), newOwnerAddress) + if err != nil { + return nil, nil, err + } + + masterToken, err := s.updateStateAndAddTokenToCommunityDescription(status, int(pendingTransaction.ChainID), newMasterAddress) + if err != nil { + return nil, nil, err + } + + return ownerToken, masterToken, nil +} + +func (s *Service) updateStateAndAddTokenToCommunityDescription(status string, chainID int, address string) (*token.CommunityToken, error) { + tokenToUpdate, err := s.Messenger.GetCommunityTokenByChainAndAddress(chainID, address) + if err != nil { + return nil, err + } + if tokenToUpdate == nil { + return nil, fmt.Errorf("token does not exist in database: chainID=%v, address=%v", chainID, address) + } + + if status == transactions.Success { + err := s.Messenger.UpdateCommunityTokenState(chainID, address, token.Deployed) + if err != nil { + return nil, err + } + err = s.Messenger.AddCommunityToken(tokenToUpdate.CommunityID, chainID, address) + if err != nil { + return nil, err + } + } else { + err := s.Messenger.UpdateCommunityTokenState(chainID, address, token.Failed) + if err != nil { + return nil, err + } + } + return s.Messenger.GetCommunityTokenByChainAndAddress(chainID, address) +} + +func (s *Service) handleDeployCommunityToken(status string, pendingTransaction *transactions.PendingTransaction) (*token.CommunityToken, error) { + return s.updateStateAndAddTokenToCommunityDescription(status, int(pendingTransaction.ChainID), pendingTransaction.To.String()) +} + +func (s *Service) handleSetSignerPubKey(status string, pendingTransaction *transactions.PendingTransaction) (*token.CommunityToken, error) { + + communityToken, err := s.Messenger.GetCommunityTokenByChainAndAddress(int(pendingTransaction.ChainID), pendingTransaction.To.String()) + if err != nil { + return nil, err + } + if communityToken == nil { + return nil, fmt.Errorf("token does not exist in database: chainId=%v, address=%v", pendingTransaction.ChainID, pendingTransaction.To.String()) + } + + if status == transactions.Success { + _, err := s.Messenger.PromoteSelfToControlNode(types.FromHex(communityToken.CommunityID)) + if err != nil { + return nil, err + } + } + return communityToken, err +} + +// Stop is run when a service is stopped. +func (s *Service) Stop() error { + s.walletWatcher.Stop() + return nil +} + +func (s *Service) Init(messenger *protocol.Messenger) { + s.Messenger = messenger +} + +func (s *Service) NewCommunityOwnerTokenRegistryInstance(chainID uint64, contractAddress string) (*communityownertokenregistry.CommunityOwnerTokenRegistry, error) { + backend, err := s.manager.rpcClient.EthClient(chainID) + if err != nil { + return nil, err + } + return communityownertokenregistry.NewCommunityOwnerTokenRegistry(common.HexToAddress(contractAddress), backend) +} + +func (s *Service) NewOwnerTokenInstance(chainID uint64, contractAddress string) (*ownertoken.OwnerToken, error) { + + backend, err := s.manager.rpcClient.EthClient(chainID) + if err != nil { + return nil, err + } + return ownertoken.NewOwnerToken(common.HexToAddress(contractAddress), backend) + +} + +func (s *Service) NewMasterTokenInstance(chainID uint64, contractAddress string) (*mastertoken.MasterToken, error) { + backend, err := s.manager.rpcClient.EthClient(chainID) + if err != nil { + return nil, err + } + return mastertoken.NewMasterToken(common.HexToAddress(contractAddress), backend) +} + +func (s *Service) validateTokens(tokenIds []*bigint.BigInt) error { + if len(tokenIds) == 0 { + return errors.New("token list is empty") + } + return nil +} + +func (s *Service) validateBurnAmount(ctx context.Context, burnAmount *bigint.BigInt, chainID uint64, contractAddress string) error { + if burnAmount.Cmp(big.NewInt(0)) <= 0 { + return errors.New("burnAmount is less than 0") + } + remainingSupply, err := s.remainingSupply(ctx, chainID, contractAddress) + if err != nil { + return err + } + if burnAmount.Cmp(remainingSupply.Int) > 1 { + return errors.New("burnAmount is bigger than remaining amount") + } + return nil +} + +func (s *Service) remainingSupply(ctx context.Context, chainID uint64, contractAddress string) (*bigint.BigInt, error) { + tokenType, err := s.db.GetTokenType(chainID, contractAddress) + if err != nil { + return nil, err + } + switch tokenType { + case protobuf.CommunityTokenType_ERC721: + return s.remainingCollectiblesSupply(ctx, chainID, contractAddress) + case protobuf.CommunityTokenType_ERC20: + return s.remainingAssetsSupply(ctx, chainID, contractAddress) + default: + return nil, fmt.Errorf("unknown token type: %v", tokenType) + } +} + +func (s *Service) prepareNewMaxSupply(ctx context.Context, chainID uint64, contractAddress string, burnAmount *bigint.BigInt) (*big.Int, error) { + maxSupply, err := s.maxSupply(ctx, chainID, contractAddress) + if err != nil { + return nil, err + } + var newMaxSupply = new(big.Int) + newMaxSupply.Sub(maxSupply, burnAmount.Int) + return newMaxSupply, nil +} + +// RemainingSupply = MaxSupply - MintedCount +func (s *Service) remainingCollectiblesSupply(ctx context.Context, chainID uint64, contractAddress string) (*bigint.BigInt, error) { + callOpts := &bind.CallOpts{Context: ctx, Pending: false} + contractInst, err := s.manager.NewCollectiblesInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + maxSupply, err := contractInst.MaxSupply(callOpts) + if err != nil { + return nil, err + } + mintedCount, err := contractInst.MintedCount(callOpts) + if err != nil { + return nil, err + } + var res = new(big.Int) + res.Sub(maxSupply, mintedCount) + return &bigint.BigInt{Int: res}, nil +} + +// RemainingSupply = MaxSupply - TotalSupply +func (s *Service) remainingAssetsSupply(ctx context.Context, chainID uint64, contractAddress string) (*bigint.BigInt, error) { + callOpts := &bind.CallOpts{Context: ctx, Pending: false} + contractInst, err := s.manager.NewAssetsInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + maxSupply, err := contractInst.MaxSupply(callOpts) + if err != nil { + return nil, err + } + totalSupply, err := contractInst.TotalSupply(callOpts) + if err != nil { + return nil, err + } + var res = new(big.Int) + res.Sub(maxSupply, totalSupply) + return &bigint.BigInt{Int: res}, nil +} + +func (s *Service) ValidateWalletsAndAmounts(walletAddresses []string, amount *bigint.BigInt) error { + if len(walletAddresses) == 0 { + return errors.New("wallet addresses list is empty") + } + if amount.Cmp(big.NewInt(0)) <= 0 { + return errors.New("amount is <= 0") + } + return nil +} + +func (s *Service) GetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string) (string, error) { + + callOpts := &bind.CallOpts{Context: ctx, Pending: false} + contractInst, err := s.NewOwnerTokenInstance(chainID, contractAddress) + if err != nil { + return "", err + } + signerPubKey, err := contractInst.SignerPublicKey(callOpts) + if err != nil { + return "", err + } + + return types.ToHex(signerPubKey), nil +} + +func (s *Service) SafeGetSignerPubKey(ctx context.Context, chainID uint64, communityID string) (string, error) { + // 1. Get Owner Token contract address from deployer contract - SafeGetOwnerTokenAddress() + ownerTokenAddr, err := s.SafeGetOwnerTokenAddress(ctx, chainID, communityID) + if err != nil { + return "", err + } + // 2. Get Signer from owner token contract - GetSignerPubKey() + return s.GetSignerPubKey(ctx, chainID, ownerTokenAddr) +} + +func (s *Service) SafeGetOwnerTokenAddress(ctx context.Context, chainID uint64, communityID string) (string, error) { + callOpts := &bind.CallOpts{Context: ctx, Pending: false} + deployerContractInst, err := s.manager.NewCommunityTokenDeployerInstance(chainID) + if err != nil { + return "", err + } + registryAddr, err := deployerContractInst.DeploymentRegistry(callOpts) + if err != nil { + return "", err + } + registryContractInst, err := s.NewCommunityOwnerTokenRegistryInstance(chainID, registryAddr.Hex()) + if err != nil { + return "", err + } + communityEthAddress, err := convert33BytesPubKeyToEthAddress(communityID) + if err != nil { + return "", err + } + ownerTokenAddress, err := registryContractInst.GetEntry(callOpts, communityEthAddress) + + return ownerTokenAddress.Hex(), err +} + +func (s *Service) GetCollectibleContractData(chainID uint64, contractAddress string) (*communities.CollectibleContractData, error) { + return s.manager.GetCollectibleContractData(chainID, contractAddress) +} + +func (s *Service) GetAssetContractData(chainID uint64, contractAddress string) (*communities.AssetContractData, error) { + return s.manager.GetAssetContractData(chainID, contractAddress) +} + +func (s *Service) DeploymentSignatureDigest(chainID uint64, addressFrom string, communityID string) ([]byte, error) { + return s.manager.DeploymentSignatureDigest(chainID, addressFrom, communityID) +} + +func (s *Service) ProcessCommunityTokenAction(message *protobuf.CommunityTokenAction) error { + communityToken, err := s.Messenger.GetCommunityTokenByChainAndAddress(int(message.ChainId), message.ContractAddress) + if err != nil { + return err + } + if communityToken == nil { + return fmt.Errorf("can't find community token in database: chain %v, address %v", message.ChainId, message.ContractAddress) + } + + if message.ActionType == protobuf.CommunityTokenAction_BURN { + // get new max supply and update database + newMaxSupply, err := s.maxSupply(context.Background(), uint64(communityToken.ChainID), communityToken.Address) + if err != nil { + return nil + } + err = s.Messenger.UpdateCommunityTokenSupply(communityToken.ChainID, communityToken.Address, &bigint.BigInt{Int: newMaxSupply}) + if err != nil { + return err + } + communityToken, _ = s.Messenger.GetCommunityTokenByChainAndAddress(int(message.ChainId), message.ContractAddress) + } + + signal.SendCommunityTokenActionSignal(communityToken, message.ActionType) + + return nil +} + +func (s *Service) SetSignerPubKey(ctx context.Context, chainID uint64, contractAddress string, txArgs wallettypes.SendTxArgs, password string, newSignerPubKey string) (string, error) { + + if len(newSignerPubKey) <= 0 { + return "", fmt.Errorf("signerPubKey is empty") + } + + transactOpts := txArgs.ToTransactOpts(utils.VerifyPasswordAndGetSigner(chainID, s.accountsManager, s.config.KeyStoreDir, txArgs.From, password)) + + contractInst, err := s.NewOwnerTokenInstance(chainID, contractAddress) + if err != nil { + return "", err + } + + tx, err := contractInst.SetSignerPublicKey(transactOpts, common.FromHex(newSignerPubKey)) + if err != nil { + return "", err + } + + err = s.pendingTracker.TrackPendingTransaction( + wcommon.ChainID(chainID), + tx.Hash(), + common.Address(txArgs.From), + common.HexToAddress(contractAddress), + transactions.SetSignerPublicKey, + transactions.Keep, + "", + ) + if err != nil { + logutils.ZapLogger().Error("TrackPendingTransaction error", zap.Error(err)) + return "", err + } + + return tx.Hash().Hex(), nil +} + +func (s *Service) maxSupplyCollectibles(ctx context.Context, chainID uint64, contractAddress string) (*big.Int, error) { + callOpts := &bind.CallOpts{Context: ctx, Pending: false} + contractInst, err := s.manager.NewCollectiblesInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + return contractInst.MaxSupply(callOpts) +} + +func (s *Service) maxSupplyAssets(ctx context.Context, chainID uint64, contractAddress string) (*big.Int, error) { + callOpts := &bind.CallOpts{Context: ctx, Pending: false} + contractInst, err := s.manager.NewAssetsInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + return contractInst.MaxSupply(callOpts) +} + +func (s *Service) maxSupply(ctx context.Context, chainID uint64, contractAddress string) (*big.Int, error) { + tokenType, err := s.db.GetTokenType(chainID, contractAddress) + if err != nil { + return nil, err + } + + switch tokenType { + case protobuf.CommunityTokenType_ERC721: + return s.maxSupplyCollectibles(ctx, chainID, contractAddress) + case protobuf.CommunityTokenType_ERC20: + return s.maxSupplyAssets(ctx, chainID, contractAddress) + default: + return nil, fmt.Errorf("unknown token type: %v", tokenType) + } +} + +func (s *Service) CreateCommunityTokenAndSave(chainID int, deploymentParameters requests.DeploymentParameters, + deployerAddress string, contractAddress string, tokenType protobuf.CommunityTokenType, privilegesLevel token.PrivilegesLevel, transactionHash string) (*token.CommunityToken, error) { + + contractVersion := "" + if privilegesLevel == token.CommunityLevel { + contractVersion = s.currentVersion() + } + + tokenToSave := &token.CommunityToken{ + TokenType: tokenType, + CommunityID: deploymentParameters.CommunityID, + Address: contractAddress, + Name: deploymentParameters.Name, + Symbol: deploymentParameters.Symbol, + Description: deploymentParameters.Description, + Supply: &bigint.BigInt{Int: deploymentParameters.GetSupply()}, + InfiniteSupply: deploymentParameters.InfiniteSupply, + Transferable: deploymentParameters.Transferable, + RemoteSelfDestruct: deploymentParameters.RemoteSelfDestruct, + ChainID: chainID, + DeployState: token.InProgress, + Decimals: deploymentParameters.Decimals, + Deployer: deployerAddress, + PrivilegesLevel: privilegesLevel, + Base64Image: deploymentParameters.Base64Image, + TransactionHash: transactionHash, + Version: contractVersion, + } + + return s.Messenger.SaveCommunityToken(tokenToSave, deploymentParameters.CroppedImage) +} + +const ( + MasterSuffix = "-master" + OwnerSuffix = "-owner" +) + +func (s *Service) TemporaryMasterContractAddress(hash string) string { + return hash + MasterSuffix +} + +func (s *Service) TemporaryOwnerContractAddress(hash string) string { + return hash + OwnerSuffix +} + +func (s *Service) HashFromTemporaryContractAddress(address string) string { + if strings.HasSuffix(address, OwnerSuffix) { + return strings.TrimSuffix(address, OwnerSuffix) + } else if strings.HasSuffix(address, MasterSuffix) { + return strings.TrimSuffix(address, MasterSuffix) + } + return "" +} + +func (s *Service) GetMasterTokenContractAddressFromHash(ctx context.Context, chainID uint64, txHash string) (string, error) { + ethClient, err := s.manager.rpcClient.EthClient(chainID) + if err != nil { + return "", err + } + + receipt, err := ethClient.TransactionReceipt(ctx, common.HexToHash(txHash)) + if err != nil { + return "", err + } + + deployerContractInst, err := s.manager.NewCommunityTokenDeployerInstance(chainID) + if err != nil { + return "", err + } + + logMasterTokenCreatedSig := []byte("DeployMasterToken(address)") + logMasterTokenCreatedSigHash := crypto.Keccak256Hash(logMasterTokenCreatedSig) + + for _, vLog := range receipt.Logs { + if vLog.Topics[0].Hex() == logMasterTokenCreatedSigHash.Hex() { + event, err := deployerContractInst.ParseDeployMasterToken(*vLog) + if err != nil { + return "", err + } + return event.Arg0.Hex(), nil + } + } + return "", fmt.Errorf("can't find master token address in transaction: %v", txHash) +} + +func (s *Service) GetOwnerTokenContractAddressFromHash(ctx context.Context, chainID uint64, txHash string) (string, error) { + ethClient, err := s.manager.rpcClient.EthClient(chainID) + if err != nil { + return "", err + } + + receipt, err := ethClient.TransactionReceipt(ctx, common.HexToHash(txHash)) + if err != nil { + return "", err + } + + deployerContractInst, err := s.manager.NewCommunityTokenDeployerInstance(chainID) + if err != nil { + return "", err + } + + logOwnerTokenCreatedSig := []byte("DeployOwnerToken(address)") + logOwnerTokenCreatedSigHash := crypto.Keccak256Hash(logOwnerTokenCreatedSig) + + for _, vLog := range receipt.Logs { + if vLog.Topics[0].Hex() == logOwnerTokenCreatedSigHash.Hex() { + event, err := deployerContractInst.ParseDeployOwnerToken(*vLog) + if err != nil { + return "", err + } + return event.Arg0.Hex(), nil + } + } + return "", fmt.Errorf("can't find owner token address in transaction: %v", txHash) +} + +func (s *Service) ReTrackOwnerTokenDeploymentTransaction(ctx context.Context, chainID uint64, contractAddress string) error { + communityToken, err := s.Messenger.GetCommunityTokenByChainAndAddress(int(chainID), contractAddress) + if err != nil { + return err + } + if communityToken == nil { + return fmt.Errorf("can't find token with address %v on chain %v", contractAddress, chainID) + } + if communityToken.DeployState != token.InProgress { + return fmt.Errorf("token with address %v on chain %v is not in progress", contractAddress, chainID) + } + + hashString := communityToken.TransactionHash + if hashString == "" && (communityToken.PrivilegesLevel == token.OwnerLevel || communityToken.PrivilegesLevel == token.MasterLevel) { + hashString = s.HashFromTemporaryContractAddress(communityToken.Address) + } + + if hashString == "" { + return fmt.Errorf("can't find transaction hash for token with address %v on chain %v", contractAddress, chainID) + } + + transactionType := transactions.DeployCommunityToken + if communityToken.PrivilegesLevel == token.OwnerLevel || communityToken.PrivilegesLevel == token.MasterLevel { + transactionType = transactions.DeployOwnerToken + } + + _, err = s.pendingTracker.GetPendingEntry(wcommon.ChainID(chainID), common.HexToHash(hashString)) + if errors.Is(err, sql.ErrNoRows) { + // start only if no pending transaction in database + err = s.pendingTracker.TrackPendingTransaction( + wcommon.ChainID(chainID), + common.HexToHash(hashString), + common.HexToAddress(communityToken.Deployer), + common.Address{}, + transactionType, + transactions.Keep, + "", + ) + logutils.ZapLogger().Debug("retracking pending transaction", zap.String("hashId", hashString)) + } else { + logutils.ZapLogger().Debug("pending transaction already tracked", zap.String("hashId", hashString)) + } + return err +} + +func (s *Service) publishTokenActionToPrivilegedMembers(communityID string, chainID uint64, contractAddress string, actionType protobuf.CommunityTokenAction_ActionType) error { + decodedCommunityID, err := types.DecodeHex(communityID) + if err != nil { + return err + } + return s.Messenger.PublishTokenActionToPrivilegedMembers(decodedCommunityID, chainID, contractAddress, actionType) +} diff --git a/services/communitytokensv2/token_instances.go b/services/communitytokensv2/token_instances.go new file mode 100644 index 00000000000..247a49557be --- /dev/null +++ b/services/communitytokensv2/token_instances.go @@ -0,0 +1,176 @@ +package communitytokens + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" + "github.com/status-im/status-go/contracts/community-tokens/assets" + "github.com/status-im/status-go/contracts/community-tokens/collectibles" + "github.com/status-im/status-go/contracts/community-tokens/mastertoken" + "github.com/status-im/status-go/contracts/community-tokens/ownertoken" + "github.com/status-im/status-go/protocol/communities/token" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/services/wallet/bigint" +) + +type TokenInstance interface { + RemoteBurn(*bind.TransactOpts, []*big.Int) (*types.Transaction, error) + Mint(*bind.TransactOpts, []string, *bigint.BigInt) (*types.Transaction, error) + SetMaxSupply(*bind.TransactOpts, *big.Int) (*types.Transaction, error) + PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error) +} + +// Owner Token +type OwnerTokenInstance struct { + TokenInstance + instance *ownertoken.OwnerToken +} + +func (t OwnerTokenInstance) RemoteBurn(transactOpts *bind.TransactOpts, tokenIds []*big.Int) (*types.Transaction, error) { + return nil, fmt.Errorf("remote destruction for owner token not implemented") +} + +func (t OwnerTokenInstance) Mint(transactOpts *bind.TransactOpts, walletAddresses []string, amount *bigint.BigInt) (*types.Transaction, error) { + return nil, fmt.Errorf("minting for owner token not implemented") +} + +func (t OwnerTokenInstance) SetMaxSupply(transactOpts *bind.TransactOpts, maxSupply *big.Int) (*types.Transaction, error) { + return nil, fmt.Errorf("setting max supply for owner token not implemented") +} + +func (t OwnerTokenInstance) PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error) { + ownerTokenABI, err := abi.JSON(strings.NewReader(ownertoken.OwnerTokenABI)) + if err != nil { + return []byte{}, err + } + return ownerTokenABI.Pack(methodName, args...) +} + +// Master Token +type MasterTokenInstance struct { + TokenInstance + instance *mastertoken.MasterToken +} + +func (t MasterTokenInstance) RemoteBurn(transactOpts *bind.TransactOpts, tokenIds []*big.Int) (*types.Transaction, error) { + return t.instance.RemoteBurn(transactOpts, tokenIds) +} + +func (t MasterTokenInstance) Mint(transactOpts *bind.TransactOpts, walletAddresses []string, amount *bigint.BigInt) (*types.Transaction, error) { + usersAddresses := prepareMintCollectiblesData(walletAddresses, amount) + return t.instance.MintTo(transactOpts, usersAddresses) +} + +func (t MasterTokenInstance) SetMaxSupply(transactOpts *bind.TransactOpts, maxSupply *big.Int) (*types.Transaction, error) { + return t.instance.SetMaxSupply(transactOpts, maxSupply) +} + +func (t MasterTokenInstance) PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error) { + masterTokenABI, err := abi.JSON(strings.NewReader(mastertoken.MasterTokenABI)) + if err != nil { + return []byte{}, err + } + return masterTokenABI.Pack(methodName, args...) +} + +// Collectible +type CollectibleInstance struct { + TokenInstance + instance *collectibles.Collectibles +} + +func (t CollectibleInstance) RemoteBurn(transactOpts *bind.TransactOpts, tokenIds []*big.Int) (*types.Transaction, error) { + return t.instance.RemoteBurn(transactOpts, tokenIds) +} + +func (t CollectibleInstance) Mint(transactOpts *bind.TransactOpts, walletAddresses []string, amount *bigint.BigInt) (*types.Transaction, error) { + usersAddresses := prepareMintCollectiblesData(walletAddresses, amount) + return t.instance.MintTo(transactOpts, usersAddresses) +} + +func (t CollectibleInstance) SetMaxSupply(transactOpts *bind.TransactOpts, maxSupply *big.Int) (*types.Transaction, error) { + return t.instance.SetMaxSupply(transactOpts, maxSupply) +} + +func (t CollectibleInstance) PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error) { + collectiblesABI, err := abi.JSON(strings.NewReader(collectibles.CollectiblesABI)) + if err != nil { + return []byte{}, err + } + return collectiblesABI.Pack(methodName, args...) +} + +// Asset +type AssetInstance struct { + TokenInstance + instance *assets.Assets +} + +func (t AssetInstance) RemoteBurn(transactOpts *bind.TransactOpts, tokenIds []*big.Int) (*types.Transaction, error) { + return nil, fmt.Errorf("remote destruction for assets not implemented") +} + +// The amount should be in smallest denomination of the asset (like wei) with decimal = 18, eg. +// if we want to mint 2.34 of the token, then amount should be 234{16 zeros}. +func (t AssetInstance) Mint(transactOpts *bind.TransactOpts, walletAddresses []string, amount *bigint.BigInt) (*types.Transaction, error) { + usersAddresses, amountsList := prepareMintAssetsData(walletAddresses, amount) + return t.instance.MintTo(transactOpts, usersAddresses, amountsList) +} + +func (t AssetInstance) SetMaxSupply(transactOpts *bind.TransactOpts, maxSupply *big.Int) (*types.Transaction, error) { + return t.instance.SetMaxSupply(transactOpts, maxSupply) +} + +func (t AssetInstance) PackMethod(ctx context.Context, methodName string, args ...interface{}) ([]byte, error) { + assetsABI, err := abi.JSON(strings.NewReader(assets.AssetsABI)) + if err != nil { + return []byte{}, err + } + return assetsABI.Pack(methodName, args...) +} + +// creator + +func NewTokenInstance(s *Service, chainID uint64, contractAddress string) (TokenInstance, error) { + tokenType, err := s.db.GetTokenType(chainID, contractAddress) + if err != nil { + return nil, err + } + privLevel, err := s.db.GetTokenPrivilegesLevel(chainID, contractAddress) + if err != nil { + return nil, err + } + switch { + case privLevel == token.OwnerLevel: + contractInst, err := s.NewOwnerTokenInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + return &OwnerTokenInstance{instance: contractInst}, nil + case privLevel == token.MasterLevel: + contractInst, err := s.NewMasterTokenInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + return &MasterTokenInstance{instance: contractInst}, nil + case tokenType == protobuf.CommunityTokenType_ERC721: + contractInst, err := s.manager.NewCollectiblesInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + return &CollectibleInstance{instance: contractInst}, nil + case tokenType == protobuf.CommunityTokenType_ERC20: + contractInst, err := s.manager.NewAssetsInstance(chainID, contractAddress) + if err != nil { + return nil, err + } + return &AssetInstance{instance: contractInst}, nil + } + + return nil, fmt.Errorf("unknown type of contract: chain=%v, address=%v", chainID, contractAddress) +} diff --git a/services/communitytokensv2/version.go b/services/communitytokensv2/version.go new file mode 100644 index 00000000000..d140ef361ed --- /dev/null +++ b/services/communitytokensv2/version.go @@ -0,0 +1,7 @@ +package communitytokensv2 + +const currentVersion = "1.0.0" + +func (s *Service) currentVersion() string { + return currentVersion +}