mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-10 05:47:59 -05:00
Accounts-V2: Cleanup password entry and disable export (#6778)
* Cleanup prompts * Merge branch 'master' of github.com:prysmaticlabs/prysm into cleanup-accounts-v2 * Fix * add todo * Merge branch 'master' of github.com:prysmaticlabs/prysm into cleanup-accounts-v2 * Clean * Merge refs/heads/master into cleanup-accounts-v2 * Merge refs/heads/master into cleanup-accounts-v2 * Change with error * Merge branch 'cleanup-accounts-v2' of github.com:prysmaticlabs/prysm into cleanup-accounts-v2 * Merge refs/heads/master into cleanup-accounts-v2 * cleanup and remove deposit tx file * display deposit data
This commit is contained in:
@@ -84,7 +84,7 @@ func PasswordPrompt(promptText string, validateFunc func(string) error) (string,
|
||||
var responseValid bool
|
||||
var response string
|
||||
for !responseValid {
|
||||
fmt.Printf("%s:\n", au.Bold(promptText))
|
||||
fmt.Printf("\n%s: ", au.Bold(promptText))
|
||||
bytePassword, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -50,7 +50,6 @@ go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"accounts_create_test.go",
|
||||
"accounts_export_test.go",
|
||||
"accounts_import_test.go",
|
||||
"accounts_list_test.go",
|
||||
"consts_test.go",
|
||||
|
||||
@@ -29,6 +29,7 @@ func CreateAccount(cliCtx *cli.Context) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not initialize keymanager")
|
||||
}
|
||||
log.Info("Creating a new account...")
|
||||
switch wallet.KeymanagerKind() {
|
||||
case v2keymanager.Remote:
|
||||
return errors.New("cannot create a new account for a remote keymanager")
|
||||
|
||||
@@ -2,7 +2,6 @@ package v2
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -24,45 +23,8 @@ const archiveFilename = "backup.zip"
|
||||
|
||||
// ExportAccount creates a zip archive of the selected accounts to be used in the future for importing accounts.
|
||||
func ExportAccount(cliCtx *cli.Context) error {
|
||||
outputDir, err := inputDirectory(cliCtx, exportDirPromptText, flags.BackupDirFlag)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse output directory")
|
||||
}
|
||||
wallet, err := OpenWallet(cliCtx)
|
||||
if errors.Is(err, ErrNoWalletFound) {
|
||||
return errors.Wrap(err, "nothing to export, no wallet found")
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "could not open wallet")
|
||||
}
|
||||
keymanager, err := wallet.InitializeKeymanager(context.Background(), true /* skip mnemonic confirm */)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not initialize keymanager")
|
||||
}
|
||||
km, ok := keymanager.(*direct.Keymanager)
|
||||
if !ok {
|
||||
return errors.New("can only export accounts for a non-HD wallet")
|
||||
}
|
||||
allAccounts, err := km.ValidatingAccountNames()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not get account names")
|
||||
}
|
||||
accounts, err := selectAccounts(cliCtx, allAccounts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not select accounts")
|
||||
}
|
||||
if len(accounts) == 0 {
|
||||
return errors.New("no accounts to export")
|
||||
}
|
||||
|
||||
if err := wallet.zipAccounts(accounts, outputDir); err != nil {
|
||||
return errors.Wrap(err, "could not export accounts")
|
||||
}
|
||||
|
||||
if err := logAccountsExported(wallet, km, accounts); err != nil {
|
||||
return errors.Wrap(err, "could not log out exported accounts")
|
||||
}
|
||||
|
||||
return nil
|
||||
// TODO(#6777): Re-enable export command.
|
||||
return errors.New("this feature is unimplemented")
|
||||
}
|
||||
|
||||
func selectAccounts(cliCtx *cli.Context, accounts []string) ([]string, error) {
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/shared/testutil"
|
||||
"github.com/prysmaticlabs/prysm/shared/testutil/require"
|
||||
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
|
||||
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct"
|
||||
)
|
||||
|
||||
func TestZipAndUnzip(t *testing.T) {
|
||||
t.Skip("skipping until exporting is implemented")
|
||||
walletDir, passwordsDir, _ := setupWalletAndPasswordsDir(t)
|
||||
randPath, err := rand.Int(rand.Reader, big.NewInt(1000000))
|
||||
require.NoError(t, err, "Could not generate random file path")
|
||||
exportDir := filepath.Join(testutil.TempDir(), fmt.Sprintf("/%d", randPath), "export")
|
||||
importDir := filepath.Join(testutil.TempDir(), fmt.Sprintf("/%d", randPath), "import")
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, os.RemoveAll(exportDir), "Failed to remove directory")
|
||||
require.NoError(t, os.RemoveAll(importDir), "Failed to remove directory")
|
||||
})
|
||||
cliCtx := setupWalletCtx(t, &testWalletConfig{
|
||||
walletDir: walletDir,
|
||||
passwordsDir: passwordsDir,
|
||||
exportDir: exportDir,
|
||||
keymanagerKind: v2keymanager.Direct,
|
||||
})
|
||||
wallet, err := NewWallet(cliCtx, v2keymanager.Direct)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, wallet.SaveWallet())
|
||||
ctx := context.Background()
|
||||
keymanagerCfg := direct.DefaultConfig()
|
||||
keymanagerCfg.AccountPasswordsDirectory = passwordsDir
|
||||
keymanager, err := direct.NewKeymanager(
|
||||
ctx,
|
||||
wallet,
|
||||
keymanagerCfg,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
_, err = keymanager.CreateAccount(ctx, password)
|
||||
require.NoError(t, err)
|
||||
|
||||
accounts, err := keymanager.ValidatingAccountNames()
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(accounts) == 0 {
|
||||
t.Fatal("Expected more accounts, received 0")
|
||||
}
|
||||
err = wallet.zipAccounts(accounts, exportDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
if _, err := os.Stat(filepath.Join(exportDir, archiveFilename)); os.IsNotExist(err) {
|
||||
t.Fatal("Expected file to exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExport_Noninteractive(t *testing.T) {
|
||||
walletDir, passwordsDir, _ := setupWalletAndPasswordsDir(t)
|
||||
randPath, err := rand.Int(rand.Reader, big.NewInt(1000000))
|
||||
require.NoError(t, err, "Could not generate random file path")
|
||||
exportDir := filepath.Join(testutil.TempDir(), fmt.Sprintf("/%d", randPath), "export")
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, os.RemoveAll(exportDir), "Failed to remove directory")
|
||||
})
|
||||
accounts := "all"
|
||||
cliCtx := setupWalletCtx(t, &testWalletConfig{
|
||||
walletDir: walletDir,
|
||||
passwordsDir: passwordsDir,
|
||||
exportDir: exportDir,
|
||||
accountsToExport: accounts,
|
||||
keymanagerKind: v2keymanager.Direct,
|
||||
})
|
||||
wallet, err := NewWallet(cliCtx, v2keymanager.Direct)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, wallet.SaveWallet())
|
||||
ctx := context.Background()
|
||||
keymanagerCfg := direct.DefaultConfig()
|
||||
keymanagerCfg.AccountPasswordsDirectory = passwordsDir
|
||||
encodedCfg, err := direct.MarshalConfigFile(ctx, keymanagerCfg)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, wallet.WriteKeymanagerConfigToDisk(ctx, encodedCfg))
|
||||
keymanager, err := direct.NewKeymanager(
|
||||
ctx,
|
||||
wallet,
|
||||
keymanagerCfg,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
_, err = keymanager.CreateAccount(ctx, password)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, ExportAccount(cliCtx))
|
||||
if _, err := os.Stat(filepath.Join(exportDir, archiveFilename)); os.IsNotExist(err) {
|
||||
t.Fatal("Expected file to exist")
|
||||
}
|
||||
}
|
||||
@@ -110,17 +110,17 @@ func listDirectKeymanagerAccounts(
|
||||
if !showDepositData {
|
||||
continue
|
||||
}
|
||||
enc, err := wallet.ReadFileAtPath(ctx, accountNames[i], direct.DepositTransactionFileName)
|
||||
enc, err := wallet.ReadFileAtPath(ctx, accountNames[i], direct.DepositDataFileName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not read file for account: %s", direct.DepositTransactionFileName)
|
||||
return errors.Wrapf(err, "could not read file for account: %s", direct.DepositDataFileName)
|
||||
}
|
||||
fmt.Printf(
|
||||
"%s %s\n",
|
||||
"(deposit tx file)",
|
||||
filepath.Join(wallet.AccountsDir(), accountNames[i], direct.DepositTransactionFileName),
|
||||
"(deposit_data.ssz file)",
|
||||
filepath.Join(wallet.AccountsDir(), accountNames[i], direct.DepositDataFileName),
|
||||
)
|
||||
fmt.Printf(`
|
||||
======================Deposit Transaction Data=====================
|
||||
======================SSZ Deposit Data=====================
|
||||
|
||||
%#x
|
||||
|
||||
@@ -195,17 +195,17 @@ func listDerivedKeymanagerAccounts(
|
||||
if !showDepositData {
|
||||
continue
|
||||
}
|
||||
enc, err := wallet.ReadFileAtPath(ctx, withdrawalKeyPath, derived.DepositTransactionFileName)
|
||||
enc, err := wallet.ReadFileAtPath(ctx, withdrawalKeyPath, derived.DepositDataFileName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not read file for account: %s", direct.DepositTransactionFileName)
|
||||
return errors.Wrapf(err, "could not read file for account: %s", direct.DepositDataFileName)
|
||||
}
|
||||
fmt.Printf(
|
||||
"%s %s\n",
|
||||
"(deposit tx file)",
|
||||
filepath.Join(wallet.AccountsDir(), withdrawalKeyPath, derived.DepositTransactionFileName),
|
||||
filepath.Join(wallet.AccountsDir(), withdrawalKeyPath, derived.DepositDataFileName),
|
||||
)
|
||||
fmt.Printf(`
|
||||
======================Deposit Transaction Data=====================
|
||||
======================SSZ Deposit Data=====================
|
||||
|
||||
%#x
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestListAccounts_DirectKeymanager(t *testing.T) {
|
||||
for i := 0; i < numAccounts; i++ {
|
||||
accountName, err := keymanager.CreateAccount(ctx, "hello world")
|
||||
require.NoError(t, err)
|
||||
depositData, err := wallet.ReadFileAtPath(ctx, accountName, direct.DepositTransactionFileName)
|
||||
depositData, err := wallet.ReadFileAtPath(ctx, accountName, direct.DepositDataFileName)
|
||||
require.NoError(t, err)
|
||||
depositDataForAccounts[i] = depositData
|
||||
keystoreFileName, err := wallet.FileNameAtPath(ctx, accountName, direct.KeystoreFileName)
|
||||
@@ -161,7 +161,7 @@ func TestListAccounts_DerivedKeymanager(t *testing.T) {
|
||||
_, err := keymanager.CreateAccount(ctx, false /*logAccountInfo*/)
|
||||
require.NoError(t, err)
|
||||
withdrawalKeyPath := fmt.Sprintf(derived.WithdrawalKeyDerivationPathTemplate, i)
|
||||
depositData, err := wallet.ReadFileAtPath(ctx, withdrawalKeyPath, direct.DepositTransactionFileName)
|
||||
depositData, err := wallet.ReadFileAtPath(ctx, withdrawalKeyPath, direct.DepositDataFileName)
|
||||
require.NoError(t, err)
|
||||
depositDataForAccounts[i] = depositData
|
||||
unixTimestamp, err := wallet.ReadFileAtPath(ctx, withdrawalKeyPath, direct.TimestampFileName)
|
||||
|
||||
@@ -27,7 +27,7 @@ const (
|
||||
confirmPasswordPromptText = "Confirm password"
|
||||
walletPasswordPromptText = "Wallet password"
|
||||
newAccountPasswordPromptText = "New account password"
|
||||
passwordForAccountPromptText = "Enter password for account with public key %#x"
|
||||
passwordForAccountPromptText = "Enter password for account with public key %s"
|
||||
)
|
||||
|
||||
type passwordConfirm int
|
||||
|
||||
@@ -148,9 +148,11 @@ func OpenWallet(cliCtx *cli.Context) (*Wallet, error) {
|
||||
}
|
||||
walletPath := filepath.Join(walletDir, keymanagerKind.String())
|
||||
w := &Wallet{
|
||||
walletDir: walletDir,
|
||||
accountsPath: walletPath,
|
||||
keymanagerKind: keymanagerKind,
|
||||
}
|
||||
log.Infof("%s %s", au.BrightMagenta("(wallet directory)"), w.walletDir)
|
||||
if keymanagerKind == v2keymanager.Derived {
|
||||
walletPassword, err := inputPassword(
|
||||
cliCtx,
|
||||
@@ -176,7 +178,7 @@ func OpenWallet(cliCtx *cli.Context) (*Wallet, error) {
|
||||
au := aurora.NewAurora(true)
|
||||
log.Infof("%s %s", au.BrightMagenta("(account passwords path)"), w.passwordsDir)
|
||||
}
|
||||
log.Info("Successfully opened wallet")
|
||||
log.Debug("Successfully opened wallet")
|
||||
return w, nil
|
||||
}
|
||||
|
||||
@@ -427,6 +429,7 @@ func (w *Wallet) enterPasswordForAccount(cliCtx *cli.Context, accountName string
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
pubKeyStr := fmt.Sprintf("%#x", bytesutil.Trunc(pubKey))
|
||||
attemptingPassword := true
|
||||
// Loop asking for the password until the user enters it correctly.
|
||||
for attemptingPassword {
|
||||
@@ -434,21 +437,22 @@ func (w *Wallet) enterPasswordForAccount(cliCtx *cli.Context, accountName string
|
||||
password, err = inputWeakPassword(
|
||||
cliCtx,
|
||||
flags.AccountPasswordFileFlag,
|
||||
fmt.Sprintf(passwordForAccountPromptText, bytesutil.Trunc(pubKey)),
|
||||
fmt.Sprintf(passwordForAccountPromptText, au.BrightGreen(pubKeyStr)),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not input password")
|
||||
}
|
||||
err = w.checkPasswordForAccount(accountName, password)
|
||||
if err != nil && strings.Contains(err.Error(), "invalid checksum") {
|
||||
fmt.Println(au.Red("Incorrect password entered, please try again"))
|
||||
fmt.Print(au.Red("X").Bold())
|
||||
fmt.Print(au.Red("\nIncorrect password entered, please try again"))
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attemptingPassword = false
|
||||
fmt.Print(au.Green("✔️\n").Bold())
|
||||
}
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -73,7 +73,7 @@ func RecoverWallet(cliCtx *cli.Context) error {
|
||||
}
|
||||
}
|
||||
log.WithField("wallet-path", wallet.AccountsDir()).Infof(
|
||||
"Successfully recovered HD wallet with %d accounts. Please use accounts-v2 list to view details for your accounts.",
|
||||
"Successfully recovered HD wallet with %d accounts. Please use accounts-v2 list to view details for your accounts",
|
||||
numAccounts,
|
||||
)
|
||||
return nil
|
||||
|
||||
@@ -47,9 +47,6 @@ const (
|
||||
// keys for Prysm eth2 validators. According to EIP-2334, the format is as follows:
|
||||
// m / purpose / coin_type / account_index / withdrawal_key / validating_key
|
||||
ValidatingKeyDerivationPathTemplate = "m/12381/3600/%d/0/0"
|
||||
// DepositTransactionFileName for the encoded, eth1 raw deposit tx data
|
||||
// for a validator account.
|
||||
DepositTransactionFileName = "deposit_transaction.rlp"
|
||||
// DepositDataFileName for the raw, ssz-encoded deposit data object.
|
||||
DepositDataFileName = "deposit_data.ssz"
|
||||
// EncryptedSeedFileName for persisting a wallet's seed when using a derived keymanager.
|
||||
@@ -308,21 +305,11 @@ func (dr *Keymanager) CreateAccount(ctx context.Context, logAccountInfo bool) (s
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tx, depositData, err := depositutil.GenerateDepositTransaction(blsValidatingKey, blsWithdrawalKey)
|
||||
_, depositData, err := depositutil.GenerateDepositTransaction(blsValidatingKey, blsWithdrawalKey)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could not generate deposit transaction data")
|
||||
}
|
||||
|
||||
if logAccountInfo {
|
||||
// Log the deposit transaction data to the user.
|
||||
depositutil.LogDepositTransaction(log, tx)
|
||||
}
|
||||
|
||||
// We write the raw deposit transaction as an .rlp encoded file.
|
||||
if err := dr.wallet.WriteFileAtPath(ctx, withdrawalKeyPath, DepositTransactionFileName, tx.Data()); err != nil {
|
||||
return "", errors.Wrapf(err, "could not write for account %s: %s", withdrawalKeyPath, DepositTransactionFileName)
|
||||
}
|
||||
|
||||
// We write the ssz-encoded deposit data to disk as a .ssz file.
|
||||
encodedDepositData, err := ssz.Marshal(depositData)
|
||||
if err != nil {
|
||||
@@ -332,6 +319,16 @@ func (dr *Keymanager) CreateAccount(ctx context.Context, logAccountInfo bool) (s
|
||||
return "", errors.Wrapf(err, "could not write for account %s: %s", withdrawalKeyPath, encodedDepositData)
|
||||
}
|
||||
|
||||
if logAccountInfo {
|
||||
// Log the deposit transaction data to the user.
|
||||
fmt.Printf(`
|
||||
========================SSZ Deposit Data===============================
|
||||
|
||||
%#x
|
||||
|
||||
===================================================================`, encodedDepositData)
|
||||
}
|
||||
|
||||
// Finally, write the account creation timestamps as a files.
|
||||
createdAt := roughtime.Now().Unix()
|
||||
createdAtStr := strconv.FormatInt(createdAt, 10)
|
||||
|
||||
@@ -32,9 +32,6 @@ import (
|
||||
var log = logrus.WithField("prefix", "direct-keymanager-v2")
|
||||
|
||||
const (
|
||||
// DepositTransactionFileName for the encoded, eth1 raw deposit tx data
|
||||
// for a validator account.
|
||||
DepositTransactionFileName = "deposit_transaction.rlp"
|
||||
// TimestampFileName stores a timestamp for account creation as a
|
||||
// file for a direct keymanager account.
|
||||
TimestampFileName = "created_at.txt"
|
||||
@@ -43,8 +40,9 @@ const (
|
||||
// KeystoreFileNameFormat exposes the filename the keystore should be formatted in.
|
||||
KeystoreFileNameFormat = "keystore-%d.json"
|
||||
// PasswordFileSuffix for passwords persisted as text to disk.
|
||||
PasswordFileSuffix = ".pass"
|
||||
depositDataFileName = "deposit_data.ssz"
|
||||
PasswordFileSuffix = ".pass"
|
||||
// DepositDataFileName for the ssz-encoded deposit.
|
||||
DepositDataFileName = "deposit_data.ssz"
|
||||
eipVersion = "EIP-2335"
|
||||
)
|
||||
|
||||
@@ -180,28 +178,28 @@ func (dr *Keymanager) CreateAccount(ctx context.Context, password string) (strin
|
||||
|
||||
// Upon confirmation of the withdrawal key, proceed to display
|
||||
// and write associated deposit data to disk.
|
||||
tx, depositData, err := depositutil.GenerateDepositTransaction(validatingKey, withdrawalKey)
|
||||
_, depositData, err := depositutil.GenerateDepositTransaction(validatingKey, withdrawalKey)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could not generate deposit transaction data")
|
||||
}
|
||||
|
||||
// Log the deposit transaction data to the user.
|
||||
depositutil.LogDepositTransaction(log, tx)
|
||||
|
||||
// We write the raw deposit transaction as an .rlp encoded file.
|
||||
if err := dr.wallet.WriteFileAtPath(ctx, accountName, DepositTransactionFileName, tx.Data()); err != nil {
|
||||
return "", errors.Wrapf(err, "could not write for account %s: %s", accountName, DepositTransactionFileName)
|
||||
}
|
||||
|
||||
// We write the ssz-encoded deposit data to disk as a .ssz file.
|
||||
encodedDepositData, err := ssz.Marshal(depositData)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could not marshal deposit data")
|
||||
}
|
||||
if err := dr.wallet.WriteFileAtPath(ctx, accountName, depositDataFileName, encodedDepositData); err != nil {
|
||||
if err := dr.wallet.WriteFileAtPath(ctx, accountName, DepositDataFileName, encodedDepositData); err != nil {
|
||||
return "", errors.Wrapf(err, "could not write for account %s: %s", accountName, encodedDepositData)
|
||||
}
|
||||
|
||||
// Log the deposit transaction data to the user.
|
||||
fmt.Printf(`
|
||||
========================SSZ Deposit Data===============================
|
||||
|
||||
%#x
|
||||
|
||||
===================================================================`, encodedDepositData)
|
||||
|
||||
// Write the encoded keystore to disk with the timestamp appended
|
||||
createdAt := roughtime.Now().Unix()
|
||||
if err := dr.wallet.WriteFileAtPath(ctx, accountName, fmt.Sprintf(KeystoreFileNameFormat, createdAt), encoded); err != nil {
|
||||
|
||||
@@ -60,8 +60,8 @@ func TestDirectKeymanager_CreateAccount(t *testing.T) {
|
||||
// Decode the deposit_data.ssz file and confirm
|
||||
// the public key matches the public key from the
|
||||
// account's decrypted keystore.
|
||||
encodedDepositData, ok := wallet.Files[accountName][depositDataFileName]
|
||||
require.Equal(t, true, ok, "Expected to have stored %s in wallet", depositDataFileName)
|
||||
encodedDepositData, ok := wallet.Files[accountName][DepositDataFileName]
|
||||
require.Equal(t, true, ok, "Expected to have stored %s in wallet", DepositDataFileName)
|
||||
depositData := ðpb.Deposit_Data{}
|
||||
require.NoError(t, ssz.Unmarshal(encodedDepositData, depositData))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user