Add support for user name for Revolut accounts

If a user has an existing account with phone number or email as
account ID we show a popup at startup where we require that he sets the
user name. This popup has no close button so he is forced to enter a
value. If there are multiple account multiple popups will be shown.

To not break signed accounts we keep accountId as internal id used for signing.
Old accounts get a popup to add the new required field userName but accountId is
left unchanged. Newly created accounts fill accountId with the value of userName.
In the UI we only use userName.

Input validation does only check for length (5-100 chars). Not sure what
are the requirements at Revolut. Can be changes easily if anyone gets
the specs.
This commit is contained in:
chimp1984 2020-08-30 12:58:31 -05:00
parent 51e66d5763
commit 6c60e1739d
No known key found for this signature in database
GPG key ID: 9801B4EC591F90E3
11 changed files with 220 additions and 104 deletions

View file

@ -96,6 +96,7 @@ public class BisqHeadlessApp implements HeadlessApp {
bisqSetup.setVoteResultExceptionHandler(voteResultException -> log.warn("voteResultException={}", voteResultException.toString()));
bisqSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage));
bisqSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler"));
bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList));
//TODO move to bisqSetup
corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files));

View file

@ -44,6 +44,7 @@ import bisq.core.notifications.alerts.market.MarketAlerts;
import bisq.core.notifications.alerts.price.PriceAlert;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.RevolutAccount;
import bisq.core.payment.TradeLimits;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.fee.FeeService;
@ -221,6 +222,9 @@ public class BisqSetup {
@Setter
@Nullable
private Runnable showPopupIfInvalidBtcConfigHandler;
@Setter
@Nullable
private Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler;
@Getter
final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false);
@ -824,6 +828,8 @@ public class BisqSetup {
priceAlert.onAllServicesInitialized();
marketAlerts.onAllServicesInitialized();
user.onAllServicesInitialized(revolutAccountsUpdateHandler);
allBasicServicesInitialized = true;
}

View file

@ -36,11 +36,15 @@ public final class RevolutAccount extends PaymentAccount {
return new RevolutAccountPayload(paymentMethod.getId(), id);
}
public void setAccountId(String accountId) {
((RevolutAccountPayload) paymentAccountPayload).setAccountId(accountId);
public void setUserName(String userName) {
((RevolutAccountPayload) paymentAccountPayload).setUserName(userName);
}
public String getAccountId() {
return ((RevolutAccountPayload) paymentAccountPayload).getAccountId();
public String getUserName() {
return ((RevolutAccountPayload) paymentAccountPayload).getUserName();
}
public boolean userNameNotSet() {
return ((RevolutAccountPayload) paymentAccountPayload).userNameNotSet();
}
}

View file

@ -19,26 +19,36 @@ package bisq.core.payment.payload;
import bisq.core.locale.Res;
import bisq.common.proto.ProtoUtil;
import com.google.protobuf.Message;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true)
@ToString
@Setter
@Getter
@Slf4j
public final class RevolutAccountPayload extends PaymentAccountPayload {
private String accountId = "";
// Not used anymore from outside. Only used as internal Id to not break existing account witness objects
private String accountId = null;
// Was added in 1.3.8
// To not break signed accounts we keep accountId as internal id used for signing.
// Old accounts get a popup to add the new required field userName but accountId is
// left unchanged. Newly created accounts fill accountId with the value of userName.
// In the UI we only use userName.
@Nullable
private String userName = null;
public RevolutAccountPayload(String paymentMethod, String id) {
super(paymentMethod, id);
@ -52,6 +62,7 @@ public final class RevolutAccountPayload extends PaymentAccountPayload {
private RevolutAccountPayload(String paymentMethod,
String id,
String accountId,
@Nullable String userName,
long maxTradePeriod,
Map<String, String> excludeFromJsonDataMap) {
super(paymentMethod,
@ -60,20 +71,23 @@ public final class RevolutAccountPayload extends PaymentAccountPayload {
excludeFromJsonDataMap);
this.accountId = accountId;
this.userName = userName;
}
@Override
public Message toProtoMessage() {
return getPaymentAccountPayloadBuilder()
.setRevolutAccountPayload(protobuf.RevolutAccountPayload.newBuilder()
.setAccountId(accountId))
.build();
protobuf.RevolutAccountPayload.Builder revolutBuilder = protobuf.RevolutAccountPayload.newBuilder().setAccountId(accountId);
Optional.ofNullable(userName).ifPresent(revolutBuilder::setUserName);
return getPaymentAccountPayloadBuilder().setRevolutAccountPayload(revolutBuilder).build();
}
public static RevolutAccountPayload fromProto(protobuf.PaymentAccountPayload proto) {
protobuf.RevolutAccountPayload revolutAccountPayload = proto.getRevolutAccountPayload();
return new RevolutAccountPayload(proto.getPaymentMethodId(),
proto.getId(),
proto.getRevolutAccountPayload().getAccountId(),
revolutAccountPayload.getAccountId(),
ProtoUtil.stringOrNullFromProto(revolutAccountPayload.getUserName()),
proto.getMaxTradePeriod(),
new HashMap<>(proto.getExcludeFromJsonDataMap()));
}
@ -85,7 +99,7 @@ public final class RevolutAccountPayload extends PaymentAccountPayload {
@Override
public String getPaymentDetails() {
return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account") + " " + accountId;
return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + userName;
}
@Override
@ -95,6 +109,26 @@ public final class RevolutAccountPayload extends PaymentAccountPayload {
@Override
public byte[] getAgeWitnessInputData() {
return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8));
// getAgeWitnessInputData is called at new account creation when accountId is null, we use empty string as
// it has been the case before
String input = this.accountId == null ? "" : this.accountId;
return super.getAgeWitnessInputData(input.getBytes(StandardCharsets.UTF_8));
}
public void setUserName(@Nullable String userName) {
this.userName = userName;
// We only set accountId to userName for new accounts. Existing accounts have accountId set with email
// or phone nr. and we keep that to not break account signing.
if (accountId == null) {
accountId = userName;
}
}
public String getUserName() {
return userName != null ? userName : accountId;
}
public boolean userNameNotSet() {
return userName == null;
}
}

View file

@ -24,6 +24,7 @@ import bisq.core.locale.TradeCurrency;
import bisq.core.notifications.alerts.market.MarketAlertFilter;
import bisq.core.notifications.alerts.price.PriceAlertFilter;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.RevolutAccount;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.core.support.dispute.mediation.mediator.Mediator;
import bisq.core.support.dispute.refund.refundagent.RefundAgent;
@ -50,6 +51,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
@ -126,6 +128,16 @@ public class User implements PersistedDataHost {
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized(@Nullable Consumer<List<RevolutAccount>> resultHandler) {
if (resultHandler != null) {
resultHandler.accept(paymentAccountsAsObservable.stream()
.filter(paymentAccount -> paymentAccount instanceof RevolutAccount)
.map(paymentAccount -> (RevolutAccount) paymentAccount)
.filter(RevolutAccount::userNameNotSet)
.collect(Collectors.toList()));
}
}
@Nullable
public Arbitrator getAcceptedArbitratorByAddress(NodeAddress nodeAddress) {
final List<Arbitrator> acceptedArbitrators = userPayload.getAcceptedArbitrators();

View file

@ -2987,6 +2987,7 @@ seed.restore.error=An error occurred when restoring the wallets with seed words.
payment.account=Account
payment.account.no=Account no.
payment.account.name=Account name
payment.account.userName=User name
payment.account.owner=Account owner full name
payment.account.fullName=Full name (first, middle, last)
payment.account.state=State/Province/Region
@ -3021,8 +3022,6 @@ payment.cashApp.cashTag=$Cashtag
payment.moneyBeam.accountId=Email or phone no.
payment.venmo.venmoUserName=Venmo username
payment.popmoney.accountId=Email or phone no.
payment.revolut.email=Email
payment.revolut.phoneNr=Registered phone no.
payment.promptPay.promptPayId=Citizen ID/Tax ID or phone no.
payment.supportedCurrencies=Supported currencies
payment.limitations=Limitations
@ -3126,8 +3125,12 @@ payment.limits.info.withSigning=To limit chargeback risk, Bisq sets per-trade li
payment.cashDeposit.info=Please confirm your bank allows you to send cash deposits into other peoples' accounts. \
For example, Bank of America and Wells Fargo no longer allow such deposits.
payment.revolut.info=Please be sure that the phone number you used for your Revolut account is registered at Revolut \
otherwise the BTC buyer cannot send you the funds.
payment.revolut.info=Revolut requires the 'User name' as account ID not the phone number or email as it was the case in the past.
payment.account.revolut.addUserNameInfo={0}\n\
Your existing Revolut account ({1}) does not has set the ''User name''.\n\
Please enter your Revolut ''User name'' to update your account data.\n\
This will not affect your account age signing status.
payment.revolut.addUserNameInfo.headLine=Update Revolut account
payment.usPostalMoneyOrder.info=Trading using US Postal Money Orders (USPMO) on Bisq requires that you understand the following:\n\
\n\

View file

@ -23,8 +23,6 @@ import bisq.desktop.util.Layout;
import bisq.desktop.util.validation.RevolutValidator;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.locale.Country;
import bisq.core.locale.CountryUtil;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
import bisq.core.payment.PaymentAccount;
@ -34,46 +32,28 @@ import bisq.core.payment.payload.RevolutAccountPayload;
import bisq.core.util.coin.CoinFormatter;
import bisq.core.util.validation.InputValidator;
import com.jfoenix.controls.JFXComboBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.collections.FXCollections;
import javafx.util.StringConverter;
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane;
import static bisq.desktop.util.FormBuilder.addTopLabelTextField;
import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox;
public class RevolutForm extends PaymentMethodForm {
private final RevolutAccount account;
private RevolutValidator validator;
private InputTextField accountIdInputTextField;
private Country selectedCountry;
private InputTextField userNameInputTextField;
public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload) {
String accountId = ((RevolutAccountPayload) paymentAccountPayload).getAccountId();
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, getTitle(accountId), accountId);
String userName = ((RevolutAccountPayload) paymentAccountPayload).getUserName();
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.userName"), userName);
return gridRow;
}
private static String getTitle(String accountId) {
// From 0.9.4 on we only allow phone nr. as with emails we got too many disputes as users used an email which was
// not registered at Revolut. It seems that phone numbers need to be registered at least we have no reports from
// arbitrators with such cases. Thought email is still supported for backward compatibility.
// We might still get emails from users who have registered when email was supported
return accountId.contains("@") ? Res.get("payment.revolut.email") : Res.get("payment.revolut.phoneNr");
}
public RevolutForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService,
RevolutValidator revolutValidator, InputValidator inputValidator, GridPane gridPane,
int gridRow, CoinFormatter formatter) {
@ -86,63 +66,16 @@ public class RevolutForm extends PaymentMethodForm {
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
// country selection is added only to prevent anymore email id input and
// solely to validate the given phone number
ComboBox<Country> countryComboBox = addCountrySelection();
setCountryComboBoxAction(countryComboBox);
countryComboBox.setItems(FXCollections.observableArrayList(CountryUtil.getAllRevolutCountries()));
accountIdInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.revolut.phoneNr"));
accountIdInputTextField.setValidator(validator);
accountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
account.setAccountId(newValue.trim());
userNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.userName"));
userNameInputTextField.setValidator(validator);
userNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
account.setUserName(newValue.trim());
updateFromInputs();
});
addCurrenciesGrid(true);
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
//set default country as selected
selectedCountry = CountryUtil.getDefaultCountry();
if (CountryUtil.getAllRevolutCountries().contains(selectedCountry)) {
countryComboBox.getSelectionModel().select(selectedCountry);
}
}
ComboBox<Country> addCountrySelection() {
HBox hBox = new HBox();
hBox.setSpacing(5);
ComboBox<Country> countryComboBox = new JFXComboBox<>();
hBox.getChildren().add(countryComboBox);
addTopLabelWithVBox(gridPane, ++gridRow, Res.get("payment.bank.country"), hBox, 0);
countryComboBox.setPromptText(Res.get("payment.select.bank.country"));
countryComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(Country country) {
return country.name + " (" + country.code + ")";
}
@Override
public Country fromString(String s) {
return null;
}
});
return countryComboBox;
}
void setCountryComboBoxAction(ComboBox<Country> countryComboBox) {
countryComboBox.setOnAction(e -> {
selectedCountry = countryComboBox.getSelectionModel().getSelectedItem();
updateFromInputs();
accountIdInputTextField.resetValidation();
accountIdInputTextField.validate();
accountIdInputTextField.requestFocus();
countryComboBox.requestFocus();
});
}
private void addCurrenciesGrid(boolean isEditable) {
@ -161,18 +94,18 @@ public class RevolutForm extends PaymentMethodForm {
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(accountIdInputTextField.getText());
setAccountNameWithString(userNameInputTextField.getText());
}
@Override
public void addFormForDisplayAccount() {
gridRowFrom = gridRow;
addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.name"),
addTopLabelTextField(gridPane, gridRow, Res.get("payment.account.userName"),
account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE);
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(account.getPaymentMethod().getId()));
String accountId = account.getAccountId();
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, getTitle(accountId), accountId).second;
String userName = account.getUserName();
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.userName"), userName).second;
field.setMouseTransparent(false);
addLimitations(true);
addCurrenciesGrid(false);
@ -181,7 +114,7 @@ public class RevolutForm extends PaymentMethodForm {
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& validator.validate(account.getAccountId(), selectedCountry.code).isValid
&& validator.validate(account.getUserName()).isValid
&& account.getTradeCurrencies().size() > 0);
}
}

View file

@ -28,6 +28,7 @@ import bisq.desktop.main.overlays.windows.DisplayAlertMessageWindow;
import bisq.desktop.main.overlays.windows.NewTradeProtocolLaunchWindow;
import bisq.desktop.main.overlays.windows.TacWindow;
import bisq.desktop.main.overlays.windows.TorNetworkSettingsWindow;
import bisq.desktop.main.overlays.windows.UpdateRevolutAccountWindow;
import bisq.desktop.main.overlays.windows.WalletPasswordWindow;
import bisq.desktop.main.overlays.windows.downloadupdate.DisplayUpdateDownloadWindow;
import bisq.desktop.main.presentation.AccountPresentation;
@ -49,6 +50,7 @@ import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
import bisq.core.payment.AliPayAccount;
import bisq.core.payment.CryptoCurrencyAccount;
import bisq.core.payment.RevolutAccount;
import bisq.core.presentation.BalancePresentation;
import bisq.core.presentation.SupportTicketsPresentation;
import bisq.core.presentation.TradePresentation;
@ -87,12 +89,15 @@ import javafx.beans.property.StringProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@ -300,10 +305,11 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
.useReportBugButton()
.show()));
bisqSetup.setDisplayTorNetworkSettingsHandler(show -> {
if (show)
if (show) {
torNetworkSettingsWindow.show();
else
} else if (torNetworkSettingsWindow.isDisplayed()) {
torNetworkSettingsWindow.hide();
}
});
bisqSetup.setSpvFileCorruptedHandler(msg -> new Popup().warning(msg)
.actionButtonText(Res.get("settings.net.reSyncSPVChainButton"))
@ -374,6 +380,12 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
bisqSetup.setShowPopupIfInvalidBtcConfigHandler(this::showPopupIfInvalidBtcConfig);
bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> {
// We copy the array as we will mutate it later
showRevolutAccountUpdateWindow(new ArrayList<>(revolutAccountList));
});
corruptedDatabaseFilesHandler.getCorruptedDatabaseFiles().ifPresent(files -> new Popup()
.warning(Res.get("popup.warning.incompatibleDB", files.toString(), config.appDataDir))
.useShutDownButton()
@ -403,6 +415,17 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
bisqSetup.setFilterWarningHandler(warning -> new Popup().warning(warning).show());
}
private void showRevolutAccountUpdateWindow(List<RevolutAccount> revolutAccountList) {
if (!revolutAccountList.isEmpty()) {
RevolutAccount revolutAccount = revolutAccountList.get(0);
revolutAccountList.remove(0);
new UpdateRevolutAccountWindow(revolutAccount, user).onClose(() -> {
// We delay a bit in case we have multiple account for better UX
UserThread.runAfter(() -> showRevolutAccountUpdateWindow(revolutAccountList), 300, TimeUnit.MILLISECONDS);
}).show();
}
}
private void setupP2PNumPeersWatcher() {
p2PService.getNumConnectedPeers().addListener((observable, oldValue, newValue) -> {
int numPeers = (int) newValue;

View file

@ -0,0 +1,96 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.overlays.windows;
import bisq.desktop.components.InputTextField;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.util.Layout;
import bisq.desktop.util.validation.RevolutValidator;
import bisq.core.locale.Res;
import bisq.core.payment.RevolutAccount;
import bisq.core.user.User;
import javafx.scene.Scene;
import static bisq.desktop.util.FormBuilder.addInputTextField;
import static bisq.desktop.util.FormBuilder.addLabel;
public class UpdateRevolutAccountWindow extends Overlay<UpdateRevolutAccountWindow> {
private final RevolutValidator revolutValidator;
private final RevolutAccount revolutAccount;
private final User user;
private InputTextField userNameInputTextField;
public UpdateRevolutAccountWindow(RevolutAccount revolutAccount, User user) {
super();
this.revolutAccount = revolutAccount;
this.user = user;
type = Type.Attention;
hideCloseButton = true;
revolutValidator = new RevolutValidator();
actionButtonText = Res.get("shared.save");
}
@Override
protected void setupKeyHandler(Scene scene) {
// We do not support enter or escape here
}
@Override
public void show() {
if (headLine == null)
headLine = Res.get("payment.revolut.addUserNameInfo.headLine");
width = 868;
createGridPane();
addHeadLine();
addContent();
addButtons();
applyStyles();
display();
}
private void addContent() {
addLabel(gridPane, ++rowIndex, Res.get("payment.account.revolut.addUserNameInfo", Res.get("payment.revolut.info"), revolutAccount.getAccountName()));
userNameInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("payment.account.userName"), Layout.COMPACT_FIRST_ROW_DISTANCE);
userNameInputTextField.setValidator(revolutValidator);
userNameInputTextField.textProperty().addListener((observable, oldValue, newValue) ->
actionButton.setDisable(!revolutValidator.validate(newValue).isValid));
}
@Override
protected void addButtons() {
super.addButtons();
// We do not allow close in case the userName is not correctly added so we
// overwrote the default handler
actionButton.setOnAction(event -> {
String userName = userNameInputTextField.getText();
if (revolutValidator.validate(userName).isValid) {
revolutAccount.setUserName(userName);
user.persist();
closeHandlerOptional.ifPresent(Runnable::run);
hide();
}
});
actionButton.setDisable(true);
}
}

View file

@ -17,10 +17,13 @@
package bisq.desktop.util.validation;
public final class RevolutValidator extends PhoneNumberValidator {
public final class RevolutValidator extends LengthValidator {
public RevolutValidator() {
// Not sure what are requirements for Revolut user names
super(5, 100);
}
public ValidationResult validate(String input, String code) {
super.setIsoCountryCode(code);
public ValidationResult validate(String input) {
return super.validate(input);
}

View file

@ -1085,6 +1085,7 @@ message PopmoneyAccountPayload {
message RevolutAccountPayload {
string account_id = 1;
string user_name = 2;
}
message PerfectMoneyAccountPayload {