mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Service support for Payments.
Co-authored-by: Alan Evans <alan@signal.org> Co-authored-by: Alex Hart <alex@signal.org> Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
@@ -21,7 +21,6 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
@@ -56,10 +55,13 @@ import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequ
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
|
||||
import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.payments.CurrencyConversions;
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials;
|
||||
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteConfigResponse;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||
@@ -75,7 +77,6 @@ import org.whispersystems.util.Base64;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyStore;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -636,19 +637,31 @@ public class SignalServiceAccountManager {
|
||||
this.pushServiceSocket.pingStorageService();
|
||||
}
|
||||
|
||||
public CurrencyConversions getCurrencyConversions() throws IOException {
|
||||
return this.pushServiceSocket.getCurrencyConversions();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The avatar URL path, if one was written.
|
||||
*/
|
||||
public Optional<String> setVersionedProfile(UUID uuid, ProfileKey profileKey, String name, String about, String aboutEmoji, StreamDetails avatar)
|
||||
public Optional<String> setVersionedProfile(UUID uuid,
|
||||
ProfileKey profileKey,
|
||||
String name,
|
||||
String about,
|
||||
String aboutEmoji,
|
||||
Optional<SignalServiceProtos.PaymentAddress> paymentsAddress,
|
||||
StreamDetails avatar)
|
||||
throws IOException
|
||||
{
|
||||
if (name == null) name = "";
|
||||
|
||||
byte[] ciphertextName = new ProfileCipher(profileKey).encryptName(name.getBytes(StandardCharsets.UTF_8), ProfileCipher.getTargetNameLength(name));
|
||||
byte[] ciphertextAbout = new ProfileCipher(profileKey).encryptName(about.getBytes(StandardCharsets.UTF_8), ProfileCipher.getTargetAboutLength(about));
|
||||
byte[] ciphertextEmoji = new ProfileCipher(profileKey).encryptName(aboutEmoji.getBytes(StandardCharsets.UTF_8), ProfileCipher.EMOJI_PADDED_LENGTH);
|
||||
boolean hasAvatar = avatar != null;
|
||||
ProfileAvatarData profileAvatarData = null;
|
||||
ProfileCipher profileCipher = new ProfileCipher(profileKey);
|
||||
byte[] ciphertextName = profileCipher.encryptString(name, ProfileCipher.getTargetNameLength(name));
|
||||
byte[] ciphertextAbout = profileCipher.encryptString(about, ProfileCipher.getTargetAboutLength(about));
|
||||
byte[] ciphertextEmoji = profileCipher.encryptString(aboutEmoji, ProfileCipher.EMOJI_PADDED_LENGTH);
|
||||
byte[] ciphertextMobileCoinAddress = paymentsAddress.transform(address -> profileCipher.encryptWithLength(address.toByteArray(), ProfileCipher.PAYMENTS_ADDRESS_CONTENT_SIZE)).orNull();
|
||||
boolean hasAvatar = avatar != null;
|
||||
ProfileAvatarData profileAvatarData = null;
|
||||
|
||||
if (hasAvatar) {
|
||||
profileAvatarData = new ProfileAvatarData(avatar.getStream(),
|
||||
@@ -661,6 +674,7 @@ public class SignalServiceAccountManager {
|
||||
ciphertextName,
|
||||
ciphertextAbout,
|
||||
ciphertextEmoji,
|
||||
ciphertextMobileCoinAddress,
|
||||
hasAvatar,
|
||||
profileKey.getCommitment(uuid).serialize()),
|
||||
profileAvatarData);
|
||||
@@ -731,4 +745,9 @@ public class SignalServiceAccountManager {
|
||||
public GroupsV2Api getGroupsV2Api() {
|
||||
return new GroupsV2Api(pushServiceSocket, groupsV2Operations);
|
||||
}
|
||||
|
||||
public AuthCredentials getPaymentsAuthorization() throws IOException {
|
||||
return pushServiceSocket.getPaymentsAuthorization();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMess
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.OutgoingPaymentMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
@@ -57,6 +58,8 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.Uint64RangeException;
|
||||
import org.whispersystems.signalservice.api.util.Uint64Util;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
|
||||
@@ -364,6 +367,8 @@ public class SignalServiceMessageSender {
|
||||
content = createMultiDeviceFetchTypeContent(message.getFetchType().get());
|
||||
} else if (message.getMessageRequestResponse().isPresent()) {
|
||||
content = createMultiDeviceMessageRequestResponseContent(message.getMessageRequestResponse().get());
|
||||
} else if (message.getOutgoingPaymentMessage().isPresent()) {
|
||||
content = createMultiDeviceOutgoingPaymentContent(message.getOutgoingPaymentMessage().get());
|
||||
} else if (message.getKeys().isPresent()) {
|
||||
content = createMultiDeviceSyncKeysContent(message.getKeys().get());
|
||||
} else if (message.getVerified().isPresent()) {
|
||||
@@ -748,6 +753,21 @@ public class SignalServiceMessageSender {
|
||||
builder.setGroupCallUpdate(DataMessage.GroupCallUpdate.newBuilder().setEraId(message.getGroupCallUpdate().get().getEraId()));
|
||||
}
|
||||
|
||||
if (message.getPayment().isPresent()) {
|
||||
SignalServiceDataMessage.Payment payment = message.getPayment().get();
|
||||
|
||||
if (payment.getPaymentNotification().isPresent()) {
|
||||
SignalServiceDataMessage.PaymentNotification paymentNotification = payment.getPaymentNotification().get();
|
||||
DataMessage.Payment.Notification.MobileCoin.Builder mobileCoinPayment = DataMessage.Payment.Notification.MobileCoin.newBuilder().setReceipt(ByteString.copyFrom(paymentNotification.getReceipt()));
|
||||
DataMessage.Payment.Notification.Builder paymentBuilder = DataMessage.Payment.Notification.newBuilder()
|
||||
.setNote(paymentNotification.getNote())
|
||||
.setMobileCoin(mobileCoinPayment);
|
||||
|
||||
builder.setPayment(DataMessage.Payment.newBuilder().setNotification(paymentBuilder));
|
||||
builder.setRequiredProtocolVersion(Math.max(DataMessage.ProtocolVersion.PAYMENTS_VALUE, builder.getRequiredProtocolVersion()));
|
||||
}
|
||||
}
|
||||
|
||||
builder.setTimestamp(message.getTimestamp());
|
||||
|
||||
return enforceMaxContentSize(container.setDataMessage(builder).build().toByteArray());
|
||||
@@ -1095,6 +1115,43 @@ public class SignalServiceMessageSender {
|
||||
return container.setSyncMessage(syncMessage).build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceOutgoingPaymentContent(OutgoingPaymentMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.OutgoingPayment.Builder paymentMessage = SyncMessage.OutgoingPayment.newBuilder();
|
||||
|
||||
if (message.getRecipient().isPresent()) {
|
||||
paymentMessage.setRecipientUuid(message.getRecipient().get().toString());
|
||||
}
|
||||
|
||||
if (message.getNote().isPresent()) {
|
||||
paymentMessage.setNote(message.getNote().get());
|
||||
}
|
||||
|
||||
try {
|
||||
SyncMessage.OutgoingPayment.MobileCoin.Builder mobileCoinBuilder = SyncMessage.OutgoingPayment.MobileCoin.newBuilder();
|
||||
|
||||
if (message.getAddress().isPresent()) {
|
||||
mobileCoinBuilder.setRecipientAddress(ByteString.copyFrom(message.getAddress().get()));
|
||||
}
|
||||
mobileCoinBuilder.setAmountPicoMob(Uint64Util.bigIntegerToUInt64(message.getAmount().toPicoMobBigInteger()))
|
||||
.setFeePicoMob(Uint64Util.bigIntegerToUInt64(message.getFee().toPicoMobBigInteger()))
|
||||
.setReceipt(message.getReceipt())
|
||||
.setLedgerBlockTimestamp(message.getBlockTimestamp())
|
||||
.setLedgerBlockIndex(message.getBlockIndex())
|
||||
.addAllOutputPublicKeys(message.getPublicKeys())
|
||||
.addAllSpentKeyImages(message.getKeyImages());
|
||||
|
||||
paymentMessage.setMobileCoin(mobileCoinBuilder);
|
||||
} catch (Uint64RangeException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
syncMessage.setOutgoingPayment(paymentMessage);
|
||||
|
||||
return container.setSyncMessage(syncMessage).build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createMultiDeviceSyncKeysContent(KeysMessage keysMessage) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
|
||||
@@ -5,11 +5,15 @@ import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
@@ -30,6 +34,10 @@ public class ProfileCipher {
|
||||
public static final int MAX_POSSIBLE_NAME_LENGTH = NAME_PADDED_LENGTH_2;
|
||||
public static final int MAX_POSSIBLE_ABOUT_LENGTH = ABOUT_PADDED_LENGTH_3;
|
||||
public static final int EMOJI_PADDED_LENGTH = 32;
|
||||
public static final int ENCRYPTION_OVERHEAD = 28;
|
||||
|
||||
public static final int PAYMENTS_ADDRESS_BASE64_FIELD_SIZE = 776;
|
||||
public static final int PAYMENTS_ADDRESS_CONTENT_SIZE = PAYMENTS_ADDRESS_BASE64_FIELD_SIZE * 6 / 8 - ProfileCipher.ENCRYPTION_OVERHEAD;
|
||||
|
||||
private final ProfileKey key;
|
||||
|
||||
@@ -37,7 +45,12 @@ public class ProfileCipher {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public byte[] encryptName(byte[] input, int paddedLength) {
|
||||
/**
|
||||
* Encrypts an input and ensures padded length.
|
||||
* <p>
|
||||
* Padded length does not include {@link #ENCRYPTION_OVERHEAD}.
|
||||
*/
|
||||
public byte[] encrypt(byte[] input, int paddedLength) {
|
||||
try {
|
||||
byte[] inputPadded = new byte[paddedLength];
|
||||
|
||||
@@ -52,13 +65,22 @@ public class ProfileCipher {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
|
||||
|
||||
return ByteUtil.combine(nonce, cipher.doFinal(inputPadded));
|
||||
byte[] encryptedPadded = ByteUtil.combine(nonce, cipher.doFinal(inputPadded));
|
||||
|
||||
if (encryptedPadded.length != (paddedLength + ENCRYPTION_OVERHEAD)) {
|
||||
throw new AssertionError(String.format(Locale.US, "Wrong output length %d != padded length %d + %d", encryptedPadded.length, paddedLength, ENCRYPTION_OVERHEAD));
|
||||
}
|
||||
|
||||
return encryptedPadded;
|
||||
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | BadPaddingException | NoSuchPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decryptName(byte[] input) throws InvalidCiphertextException {
|
||||
/**
|
||||
* Returns original data with padding still intact.
|
||||
*/
|
||||
public byte[] decrypt(byte[] input) throws InvalidCiphertextException {
|
||||
try {
|
||||
if (input.length < 12 + 16 + 1) {
|
||||
throw new InvalidCiphertextException("Too short: " + input.length);
|
||||
@@ -70,20 +92,7 @@ public class ProfileCipher {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
|
||||
|
||||
byte[] paddedPlaintext = cipher.doFinal(input, nonce.length, input.length - nonce.length);
|
||||
int plaintextLength = 0;
|
||||
|
||||
for (int i=paddedPlaintext.length-1;i>=0;i--) {
|
||||
if (paddedPlaintext[i] != (byte)0x00) {
|
||||
plaintextLength = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
byte[] plaintext = new byte[plaintextLength];
|
||||
System.arraycopy(paddedPlaintext, 0, plaintext, 0, plaintextLength);
|
||||
|
||||
return plaintext;
|
||||
return cipher.doFinal(input, nonce.length, input.length - nonce.length);
|
||||
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException | BadPaddingException e) {
|
||||
@@ -91,6 +100,70 @@ public class ProfileCipher {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a string's UTF bytes representation.
|
||||
*/
|
||||
public byte[] encryptString(String input, int paddedLength) {
|
||||
return encrypt(input.getBytes(StandardCharsets.UTF_8), paddedLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips 0 char padding from decrypt result.
|
||||
*/
|
||||
public String decryptString(byte[] input) throws InvalidCiphertextException {
|
||||
byte[] paddedPlaintext = decrypt(input);
|
||||
int plaintextLength = 0;
|
||||
|
||||
for (int i = paddedPlaintext.length - 1; i >= 0; i--) {
|
||||
if (paddedPlaintext[i] != (byte) 0x00) {
|
||||
plaintextLength = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
byte[] plaintext = new byte[plaintextLength];
|
||||
System.arraycopy(paddedPlaintext, 0, plaintext, 0, plaintextLength);
|
||||
|
||||
return new String(plaintext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the length, and adds padding.
|
||||
* <p>
|
||||
* encrypt(input.length | input | padding)
|
||||
* <p>
|
||||
* Padded length does not include 28 bytes encryption overhead.
|
||||
*/
|
||||
public byte[] encryptWithLength(byte[] input, int paddedLength) {
|
||||
ByteBuffer content = ByteBuffer.wrap(new byte[input.length + 4]);
|
||||
content.order(ByteOrder.LITTLE_ENDIAN);
|
||||
content.putInt(input.length);
|
||||
content.put(input);
|
||||
return encrypt(content.array(), paddedLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts result from:
|
||||
* <p>
|
||||
* decrypt(encrypt(result.length | result | padding))
|
||||
*/
|
||||
public byte[] decryptWithLength(byte[] input) throws InvalidCiphertextException, IOException {
|
||||
byte[] decrypted = decrypt(input);
|
||||
int maxLength = decrypted.length - 4;
|
||||
ByteBuffer content = ByteBuffer.wrap(decrypted);
|
||||
content.order(ByteOrder.LITTLE_ENDIAN);
|
||||
int contentLength = content.getInt();
|
||||
if (contentLength > maxLength) {
|
||||
throw new IOException("Encoded length exceeds content length");
|
||||
}
|
||||
if (contentLength < 0) {
|
||||
throw new IOException("Encoded length is less than 0");
|
||||
}
|
||||
byte[] result = new byte[contentLength];
|
||||
content.get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean verifyUnidentifiedAccess(byte[] theirUnidentifiedAccessVerifier) {
|
||||
try {
|
||||
if (theirUnidentifiedAccessVerifier == null || theirUnidentifiedAccessVerifier.length == 0) return false;
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.OutgoingPaymentMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
@@ -36,6 +37,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOper
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.payments.Money;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
@@ -364,6 +366,21 @@ public final class SignalServiceContent {
|
||||
groupContext);
|
||||
}
|
||||
|
||||
SignalServiceDataMessage.Payment payment;
|
||||
try {
|
||||
payment = createPayment(content);
|
||||
} catch (InvalidMessageException e) {
|
||||
throw new ProtocolInvalidMessageException(e, metadata.getSender().getIdentifier(), metadata.getSenderDevice());
|
||||
}
|
||||
|
||||
if (content.getRequiredProtocolVersion() > SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT.getNumber()) {
|
||||
throw new UnsupportedDataMessageProtocolVersionException(SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT.getNumber(),
|
||||
content.getRequiredProtocolVersion(),
|
||||
metadata.getSender().getIdentifier(),
|
||||
metadata.getSenderDevice(),
|
||||
groupContext);
|
||||
}
|
||||
|
||||
for (SignalServiceProtos.AttachmentPointer pointer : content.getAttachmentsList()) {
|
||||
attachments.add(createAttachmentPointer(pointer));
|
||||
}
|
||||
@@ -391,7 +408,8 @@ public final class SignalServiceContent {
|
||||
content.getIsViewOnce(),
|
||||
reaction,
|
||||
remoteDelete,
|
||||
groupCallUpdate);
|
||||
groupCallUpdate,
|
||||
payment);
|
||||
}
|
||||
|
||||
private static SignalServiceSyncMessage createSynchronizeMessage(SignalServiceMetadata metadata,
|
||||
@@ -589,6 +607,32 @@ public final class SignalServiceContent {
|
||||
return SignalServiceSyncMessage.forMessageRequestResponse(responseMessage);
|
||||
}
|
||||
|
||||
if (content.hasOutgoingPayment()) {
|
||||
SignalServiceProtos.SyncMessage.OutgoingPayment outgoingPayment = content.getOutgoingPayment();
|
||||
switch (outgoingPayment.getPaymentDetailCase()) {
|
||||
case MOBILECOIN: {
|
||||
SignalServiceProtos.SyncMessage.OutgoingPayment.MobileCoin mobileCoin = outgoingPayment.getMobileCoin();
|
||||
Money.MobileCoin amount = Money.picoMobileCoin(mobileCoin.getAmountPicoMob());
|
||||
Money.MobileCoin fee = Money.picoMobileCoin(mobileCoin.getFeePicoMob());
|
||||
ByteString address = mobileCoin.getRecipientAddress();
|
||||
Optional<UUID> recipient = Optional.fromNullable(UuidUtil.parseOrNull(outgoingPayment.getRecipientUuid()));
|
||||
|
||||
return SignalServiceSyncMessage.forOutgoingPayment(new OutgoingPaymentMessage(recipient,
|
||||
amount,
|
||||
fee,
|
||||
mobileCoin.getReceipt(),
|
||||
mobileCoin.getLedgerBlockIndex(),
|
||||
mobileCoin.getLedgerBlockTimestamp(),
|
||||
address.isEmpty() ? Optional.absent() : Optional.of(address.toByteArray()),
|
||||
Optional.of(outgoingPayment.getNote()),
|
||||
mobileCoin.getOutputPublicKeysList(),
|
||||
mobileCoin.getSpentKeyImagesList()));
|
||||
}
|
||||
default:
|
||||
return SignalServiceSyncMessage.empty();
|
||||
}
|
||||
}
|
||||
|
||||
return SignalServiceSyncMessage.empty();
|
||||
}
|
||||
|
||||
@@ -800,6 +844,33 @@ public final class SignalServiceContent {
|
||||
return new SignalServiceDataMessage.GroupCallUpdate(groupCallUpdate.getEraId());
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.Payment createPayment(SignalServiceProtos.DataMessage content) throws InvalidMessageException {
|
||||
if (!content.hasPayment()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SignalServiceProtos.DataMessage.Payment payment = content.getPayment();
|
||||
|
||||
switch (payment.getItemCase()) {
|
||||
case NOTIFICATION: return new SignalServiceDataMessage.Payment(createPaymentNotification(payment));
|
||||
default : throw new InvalidMessageException("Unknown payment item");
|
||||
}
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.PaymentNotification createPaymentNotification(SignalServiceProtos.DataMessage.Payment content)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
if (!content.hasNotification() ||
|
||||
content.getNotification().getTransactionCase() != SignalServiceProtos.DataMessage.Payment.Notification.TransactionCase.MOBILECOIN)
|
||||
{
|
||||
throw new InvalidMessageException();
|
||||
}
|
||||
|
||||
SignalServiceProtos.DataMessage.Payment.Notification payment = content.getNotification();
|
||||
|
||||
return new SignalServiceDataMessage.PaymentNotification(payment.getMobileCoin().getReceipt().toByteArray(), payment.getNote());
|
||||
}
|
||||
|
||||
private static List<SharedContact> createSharedContacts(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (content.getContactCount() <= 0) return null;
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ public class SignalServiceDataMessage {
|
||||
private final Optional<Reaction> reaction;
|
||||
private final Optional<RemoteDelete> remoteDelete;
|
||||
private final Optional<GroupCallUpdate> groupCallUpdate;
|
||||
private final Optional<Payment> payment;
|
||||
|
||||
/**
|
||||
* Construct a SignalServiceDataMessage.
|
||||
@@ -58,7 +59,8 @@ public class SignalServiceDataMessage {
|
||||
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
|
||||
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
|
||||
List<Mention> mentions, Sticker sticker, boolean viewOnce, Reaction reaction, RemoteDelete remoteDelete,
|
||||
GroupCallUpdate groupCallUpdate)
|
||||
GroupCallUpdate groupCallUpdate,
|
||||
Payment payment)
|
||||
{
|
||||
try {
|
||||
this.group = SignalServiceGroupContext.createOptional(group, groupV2);
|
||||
@@ -79,6 +81,7 @@ public class SignalServiceDataMessage {
|
||||
this.reaction = Optional.fromNullable(reaction);
|
||||
this.remoteDelete = Optional.fromNullable(remoteDelete);
|
||||
this.groupCallUpdate = Optional.fromNullable(groupCallUpdate);
|
||||
this.payment = Optional.fromNullable(payment);
|
||||
|
||||
if (attachments != null && !attachments.isEmpty()) {
|
||||
this.attachments = Optional.of(attachments);
|
||||
@@ -227,6 +230,10 @@ public class SignalServiceDataMessage {
|
||||
return groupCallUpdate;
|
||||
}
|
||||
|
||||
public Optional<Payment> getPayment() {
|
||||
return payment;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||
@@ -249,6 +256,7 @@ public class SignalServiceDataMessage {
|
||||
private Reaction reaction;
|
||||
private RemoteDelete remoteDelete;
|
||||
private GroupCallUpdate groupCallUpdate;
|
||||
private Payment payment;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
@@ -371,13 +379,19 @@ public class SignalServiceDataMessage {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withPayment(Payment payment) {
|
||||
this.payment = payment;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalServiceDataMessage build() {
|
||||
if (timestamp == 0) timestamp = System.currentTimeMillis();
|
||||
return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession,
|
||||
expiresInSeconds, expirationUpdate, profileKey,
|
||||
profileKeyUpdate, quote, sharedContacts, previews,
|
||||
mentions, sticker, viewOnce, reaction, remoteDelete,
|
||||
groupCallUpdate);
|
||||
groupCallUpdate,
|
||||
payment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,4 +604,35 @@ public class SignalServiceDataMessage {
|
||||
return eraId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PaymentNotification {
|
||||
|
||||
private final byte[] receipt;
|
||||
private final String note;
|
||||
|
||||
public PaymentNotification(byte[] receipt, String note) {
|
||||
this.receipt = receipt;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public byte[] getReceipt() {
|
||||
return receipt;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Payment {
|
||||
private final Optional<PaymentNotification> paymentNotification;
|
||||
|
||||
public Payment(PaymentNotification paymentNotification) {
|
||||
this.paymentNotification = Optional.of(paymentNotification);
|
||||
}
|
||||
|
||||
public Optional<PaymentNotification> getPaymentNotification() {
|
||||
return paymentNotification;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.payments.Money;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class OutgoingPaymentMessage {
|
||||
|
||||
private final Optional<UUID> recipient;
|
||||
private final Money.MobileCoin amount;
|
||||
private final Money.MobileCoin fee;
|
||||
private final ByteString receipt;
|
||||
private final long blockIndex;
|
||||
private final long blockTimestamp;
|
||||
private final Optional<byte[]> address;
|
||||
private final Optional<String> note;
|
||||
private final List<ByteString> publicKeys;
|
||||
private final List<ByteString> keyImages;
|
||||
|
||||
public OutgoingPaymentMessage(Optional<UUID> recipient,
|
||||
Money.MobileCoin amount,
|
||||
Money.MobileCoin fee,
|
||||
ByteString receipt,
|
||||
long blockIndex,
|
||||
long blockTimestamp,
|
||||
Optional<byte[]> address,
|
||||
Optional<String> note,
|
||||
List<ByteString> publicKeys,
|
||||
List<ByteString> keyImages)
|
||||
{
|
||||
this.recipient = recipient;
|
||||
this.amount = amount;
|
||||
this.fee = fee;
|
||||
this.receipt = receipt;
|
||||
this.blockIndex = blockIndex;
|
||||
this.blockTimestamp = blockTimestamp;
|
||||
this.address = address;
|
||||
this.note = note;
|
||||
this.publicKeys = publicKeys;
|
||||
this.keyImages = keyImages;
|
||||
}
|
||||
|
||||
public Optional<UUID> getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public Money.MobileCoin getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public ByteString getReceipt() {
|
||||
return receipt;
|
||||
}
|
||||
|
||||
public Money.MobileCoin getFee() {
|
||||
return fee;
|
||||
}
|
||||
|
||||
public long getBlockIndex() {
|
||||
return blockIndex;
|
||||
}
|
||||
|
||||
public long getBlockTimestamp() {
|
||||
return blockTimestamp;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public Optional<String> getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public List<ByteString> getPublicKeys() {
|
||||
return publicKeys;
|
||||
}
|
||||
|
||||
public List<ByteString> getKeyImages() {
|
||||
return keyImages;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/*
|
||||
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
@@ -27,6 +27,7 @@ public class SignalServiceSyncMessage {
|
||||
private final Optional<FetchType> fetchType;
|
||||
private final Optional<KeysMessage> keys;
|
||||
private final Optional<MessageRequestResponseMessage> messageRequestResponse;
|
||||
private final Optional<OutgoingPaymentMessage> outgoingPaymentMessage;
|
||||
|
||||
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
|
||||
Optional<ContactsMessage> contacts,
|
||||
@@ -40,7 +41,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional<List<StickerPackOperationMessage>> stickerPackOperations,
|
||||
Optional<FetchType> fetchType,
|
||||
Optional<KeysMessage> keys,
|
||||
Optional<MessageRequestResponseMessage> messageRequestResponse)
|
||||
Optional<MessageRequestResponseMessage> messageRequestResponse,
|
||||
Optional<OutgoingPaymentMessage> outgoingPaymentMessage)
|
||||
{
|
||||
this.sent = sent;
|
||||
this.contacts = contacts;
|
||||
@@ -55,249 +57,282 @@ public class SignalServiceSyncMessage {
|
||||
this.fetchType = fetchType;
|
||||
this.keys = keys;
|
||||
this.messageRequestResponse = messageRequestResponse;
|
||||
this.outgoingPaymentMessage = outgoingPaymentMessage;
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) {
|
||||
return new SignalServiceSyncMessage(Optional.of(sent),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forContacts(ContactsMessage contacts) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.of(contacts),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forGroups(SignalServiceAttachment groups) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(groups),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forRequest(RequestMessage request) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(request),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forRead(List<ReadMessage> reads) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(reads),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forViewOnceOpen(ViewOnceOpenMessage timerRead) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(timerRead),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forRead(ReadMessage read) {
|
||||
List<ReadMessage> reads = new LinkedList<>();
|
||||
reads.add(read);
|
||||
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(reads),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forVerified(VerifiedMessage verifiedMessage) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(verifiedMessage),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forBlocked(BlockedListMessage blocked) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(blocked),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forConfiguration(ConfigurationMessage configuration) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(configuration),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forStickerPackOperations(List<StickerPackOperationMessage> stickerPackOperations) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(stickerPackOperations),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forFetchLatest(FetchType fetchType) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(fetchType),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forKeys(KeysMessage keys) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(keys),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forMessageRequestResponse(MessageRequestResponseMessage messageRequestResponse) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.of(messageRequestResponse));
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(messageRequestResponse),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forOutgoingPayment(OutgoingPaymentMessage outgoingPaymentMessage) {
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(outgoingPaymentMessage));
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage empty() {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent(),
|
||||
Optional.<MessageRequestResponseMessage>absent());
|
||||
return new SignalServiceSyncMessage(Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
}
|
||||
|
||||
public Optional<SentTranscriptMessage> getSent() {
|
||||
@@ -352,6 +387,10 @@ public class SignalServiceSyncMessage {
|
||||
return messageRequestResponse;
|
||||
}
|
||||
|
||||
public Optional<OutgoingPaymentMessage> getOutgoingPaymentMessage() {
|
||||
return outgoingPaymentMessage;
|
||||
}
|
||||
|
||||
public enum FetchType {
|
||||
LOCAL_PROFILE,
|
||||
STORAGE_MANIFEST
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
public abstract class Currency {
|
||||
|
||||
private Currency() {}
|
||||
|
||||
public abstract String getCurrencyCode();
|
||||
public abstract int getDecimalPrecision();
|
||||
public abstract Formatter getFormatter(FormatterOptions formatterOptions);
|
||||
|
||||
private static class CryptoCurrency extends Currency {
|
||||
|
||||
private final String currencyCode;
|
||||
private final int decimalPrecision;
|
||||
|
||||
CryptoCurrency(String currencyCode, int decimalPrecision) {
|
||||
this.currencyCode = currencyCode;
|
||||
this.decimalPrecision = decimalPrecision;
|
||||
}
|
||||
|
||||
public String getCurrencyCode() {
|
||||
return currencyCode;
|
||||
}
|
||||
|
||||
public int getDecimalPrecision() {
|
||||
return decimalPrecision;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Formatter getFormatter(FormatterOptions formatterOptions) {
|
||||
return Formatter.forMoney(this, formatterOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FiatCurrency extends Currency {
|
||||
|
||||
private final java.util.Currency javaCurrency;
|
||||
|
||||
private FiatCurrency(java.util.Currency javaCurrency) {
|
||||
this.javaCurrency = javaCurrency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrencyCode() {
|
||||
return javaCurrency.getCurrencyCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecimalPrecision() {
|
||||
return javaCurrency.getDefaultFractionDigits();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Formatter getFormatter(FormatterOptions formatterOptions) {
|
||||
return Formatter.forFiat(javaCurrency, formatterOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public static Currency fromJavaCurrency(java.util.Currency javaCurrency) {
|
||||
return new FiatCurrency(javaCurrency);
|
||||
}
|
||||
|
||||
public static Currency fromCodeAndPrecision(String code, int decimalPrecision) {
|
||||
return new CryptoCurrency(code, decimalPrecision);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public final class CurrencyConversion {
|
||||
@JsonProperty
|
||||
private String base;
|
||||
|
||||
@JsonProperty
|
||||
private Map<String, Double> conversions;
|
||||
|
||||
public String getBase() {
|
||||
return base;
|
||||
}
|
||||
|
||||
public Map<String, Double> getConversions() {
|
||||
return conversions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class CurrencyConversions {
|
||||
@JsonProperty
|
||||
private List<CurrencyConversion> currencies;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
|
||||
public List<CurrencyConversion> getCurrencies() {
|
||||
return currencies;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* Formats the given amount to look like a given currency, utilizing the given formatter options.
|
||||
*/
|
||||
public abstract class Formatter {
|
||||
|
||||
protected final FormatterOptions formatterOptions;
|
||||
|
||||
private Formatter(FormatterOptions formatterOptions) {
|
||||
this.formatterOptions = formatterOptions;
|
||||
}
|
||||
|
||||
public abstract String format(BigDecimal amount);
|
||||
|
||||
static Formatter forMoney(Currency currency, FormatterOptions formatterOptions) {
|
||||
return new CryptoFormatter(currency, formatterOptions);
|
||||
}
|
||||
|
||||
static Formatter forFiat(java.util.Currency currency, FormatterOptions formatterOptions) {
|
||||
return new FiatFormatter(currency, formatterOptions);
|
||||
}
|
||||
|
||||
private static final class FiatFormatter extends Formatter {
|
||||
|
||||
private final java.util.Currency javaCurrency;
|
||||
|
||||
private FiatFormatter(java.util.Currency javaCurrency, FormatterOptions formatterOptions) {
|
||||
super(formatterOptions);
|
||||
|
||||
this.javaCurrency = javaCurrency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(BigDecimal amount) {
|
||||
BigDecimal toFormat = formatterOptions.alwaysPositive ? amount.abs() : amount;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
NumberFormat numberFormat;
|
||||
|
||||
if (formatterOptions.withUnit) {
|
||||
numberFormat = NumberFormat.getCurrencyInstance(formatterOptions.locale);
|
||||
numberFormat.setCurrency(javaCurrency);
|
||||
} else {
|
||||
numberFormat = NumberFormat.getNumberInstance(formatterOptions.locale);
|
||||
numberFormat.setMinimumFractionDigits(javaCurrency.getDefaultFractionDigits());
|
||||
}
|
||||
|
||||
numberFormat.setMaximumFractionDigits(Math.min(javaCurrency.getDefaultFractionDigits(),
|
||||
formatterOptions.maximumFractionDigits));
|
||||
|
||||
builder.append(numberFormat.format(toFormat));
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CryptoFormatter extends Formatter {
|
||||
|
||||
private final Currency currency;
|
||||
|
||||
private CryptoFormatter(Currency currency, FormatterOptions formatterOptions) {
|
||||
super(formatterOptions);
|
||||
|
||||
this.currency = currency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String format(BigDecimal amount) {
|
||||
NumberFormat format = NumberFormat.getNumberInstance(formatterOptions.locale);
|
||||
BigDecimal toFormat = formatterOptions.alwaysPositive ? amount.abs() : amount;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
format.setMaximumFractionDigits(Math.min(currency.getDecimalPrecision(), formatterOptions.maximumFractionDigits));
|
||||
|
||||
if (toFormat.signum() == 1 && formatterOptions.alwaysPrefixWithSign) {
|
||||
builder.append("+");
|
||||
}
|
||||
|
||||
builder.append(format.format(toFormat));
|
||||
|
||||
if (formatterOptions.withSpaceBeforeUnit && formatterOptions.withUnit) {
|
||||
builder.append(" ");
|
||||
}
|
||||
|
||||
if (formatterOptions.withUnit) {
|
||||
builder.append(currency.getCurrencyCode());
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public final class FormatterOptions {
|
||||
|
||||
public final Locale locale;
|
||||
public final boolean alwaysPositive;
|
||||
public final boolean alwaysPrefixWithSign;
|
||||
public final boolean withSpaceBeforeUnit;
|
||||
public final boolean withUnit;
|
||||
public final int maximumFractionDigits;
|
||||
|
||||
FormatterOptions(Builder builder) {
|
||||
this.locale = builder.locale;
|
||||
this.alwaysPositive = builder.alwaysPositive;
|
||||
this.alwaysPrefixWithSign = builder.alwaysPrefixWithSign;
|
||||
this.withSpaceBeforeUnit = builder.withSpaceBeforeUnit;
|
||||
this.withUnit = builder.withUnit;
|
||||
this.maximumFractionDigits = builder.maximumFractionDigits;
|
||||
}
|
||||
|
||||
public static FormatterOptions defaults() {
|
||||
return builder().build();
|
||||
}
|
||||
|
||||
public static FormatterOptions defaults(Locale locale) {
|
||||
return builder(locale).build();
|
||||
}
|
||||
|
||||
public static FormatterOptions.Builder builder() {
|
||||
return builder(Locale.getDefault());
|
||||
}
|
||||
|
||||
public static FormatterOptions.Builder builder(Locale locale) {
|
||||
return new Builder(locale);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private final Locale locale;
|
||||
|
||||
private boolean alwaysPositive = false;
|
||||
private boolean alwaysPrefixWithSign = false;
|
||||
private boolean withSpaceBeforeUnit = true;
|
||||
private boolean withUnit = true;
|
||||
private int maximumFractionDigits = Integer.MAX_VALUE;
|
||||
|
||||
private Builder(Locale locale) {
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
public Builder alwaysPositive() {
|
||||
alwaysPositive = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder alwaysPrefixWithSign() {
|
||||
alwaysPrefixWithSign = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withoutSpaceBeforeUnit() {
|
||||
withSpaceBeforeUnit = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withoutUnit() {
|
||||
withUnit = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withMaximumFractionDigits(int maximumFractionDigits) {
|
||||
this.maximumFractionDigits = Math.max(maximumFractionDigits, 0);
|
||||
return this;
|
||||
}
|
||||
|
||||
public FormatterOptions build() {
|
||||
return new FormatterOptions(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import org.whispersystems.signalservice.api.util.Uint64RangeException;
|
||||
import org.whispersystems.signalservice.api.util.Uint64Util;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
|
||||
public abstract class Money {
|
||||
|
||||
/**
|
||||
* @param amount Can be positive or negative. Can exceed 64 bits.
|
||||
* Must not have a decimal scale beyond that which mobile coin allows.
|
||||
*/
|
||||
public static MobileCoin mobileCoin(BigDecimal amount) {
|
||||
return picoMobileCoin(amount.movePointRight(MobileCoin.PRECISION).toBigIntegerExact());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param picoMobileCoin Can be positive or negative. Can exceed 64 bits.
|
||||
*/
|
||||
public static MobileCoin picoMobileCoin(BigInteger picoMobileCoin) {
|
||||
return picoMobileCoin.signum() == 0 ? MobileCoin.ZERO
|
||||
: new MobileCoin(picoMobileCoin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param picoMobileCoinUint64 Treated as unsigned.
|
||||
*/
|
||||
public static MobileCoin picoMobileCoin(long picoMobileCoinUint64) {
|
||||
return picoMobileCoin(Uint64Util.uint64ToBigInteger(picoMobileCoinUint64));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the output of {@link #serialize()}.
|
||||
*
|
||||
* @throws ParseException iff the format is incorrect.
|
||||
*/
|
||||
public static Money parse(String serialized) throws ParseException {
|
||||
if (serialized == null) {
|
||||
throw new ParseException();
|
||||
}
|
||||
String[] split = serialized.split(":");
|
||||
if (split.length != 2) {
|
||||
throw new ParseException();
|
||||
}
|
||||
if (Money.MobileCoin.CURRENCY.getCurrencyCode().equals(split[0])) {
|
||||
return picoMobileCoin(new BigInteger(split[1]));
|
||||
}
|
||||
throw new ParseException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the output of {@link #serialize()}. Asserts that there is no parsing exception.
|
||||
*/
|
||||
public static Money parseOrThrow(String serialized) {
|
||||
try {
|
||||
return parse(serialized);
|
||||
} catch (ParseException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract boolean isPositive();
|
||||
|
||||
public abstract boolean isNegative();
|
||||
|
||||
public abstract Money negate();
|
||||
|
||||
public abstract Money abs();
|
||||
|
||||
public abstract Money add(Money other);
|
||||
|
||||
public abstract Money subtract(Money other);
|
||||
|
||||
public abstract Currency getCurrency();
|
||||
|
||||
public abstract String serializeAmountString();
|
||||
|
||||
public MobileCoin requireMobileCoin() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public final String serialize() {
|
||||
return getCurrency().getCurrencyCode() + ":" + serializeAmountString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given instance of one money type, this will give you the corresponding zero value.
|
||||
*/
|
||||
public abstract Money toZero();
|
||||
|
||||
public static final class MobileCoin extends Money {
|
||||
public static final Comparator<MobileCoin> ASCENDING = (x, y) -> x.amount.compareTo(y.amount);
|
||||
public static final Comparator<MobileCoin> DESCENDING = (x, y) -> y.amount.compareTo(x.amount);
|
||||
|
||||
public static final MobileCoin ZERO = new MobileCoin(BigInteger.ZERO);
|
||||
|
||||
private static final int PRECISION = 12;
|
||||
|
||||
public static final Currency CURRENCY = Currency.fromCodeAndPrecision("MOB", PRECISION);
|
||||
|
||||
private final BigInteger amount;
|
||||
private final BigDecimal amountDecimal;
|
||||
|
||||
private MobileCoin(BigInteger amount) {
|
||||
this.amount = amount;
|
||||
this.amountDecimal = new BigDecimal(amount).movePointLeft(PRECISION).stripTrailingZeros();
|
||||
}
|
||||
|
||||
public static MobileCoin sum(Collection<MobileCoin> values) {
|
||||
switch (values.size()) {
|
||||
case 0:
|
||||
return ZERO;
|
||||
case 1:
|
||||
return values.iterator().next();
|
||||
default: {
|
||||
BigInteger result = ZERO.amount;
|
||||
|
||||
for (MobileCoin value : values) {
|
||||
result = result.add(value.amount);
|
||||
}
|
||||
|
||||
return Money.picoMobileCoin(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPositive() {
|
||||
return amount.signum() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNegative() {
|
||||
return amount.signum() == -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MobileCoin negate() {
|
||||
return new MobileCoin(amount.negate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MobileCoin abs() {
|
||||
if (amount.signum() == -1) {
|
||||
return negate();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money add(Money other) {
|
||||
return new MobileCoin(amount.add(other.requireMobileCoin().amount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money subtract(Money other) {
|
||||
return new MobileCoin(amount.subtract(other.requireMobileCoin().amount));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Currency getCurrency() {
|
||||
return CURRENCY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MobileCoin requireMobileCoin() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money toZero() {
|
||||
return ZERO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof MobileCoin && amount.equals(((MobileCoin) o).amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return amount.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String serializeAmountString() {
|
||||
return toPicoMobBigInteger().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* The value expressed in Mobile coin.
|
||||
*/
|
||||
public String getAmountDecimalString() {
|
||||
return amountDecimal.toString();
|
||||
}
|
||||
|
||||
public boolean greaterThan(MobileCoin other) {
|
||||
return amount.compareTo(other.amount) > 0;
|
||||
}
|
||||
|
||||
public boolean lessThan(MobileCoin other) {
|
||||
return amount.compareTo(other.amount) < 0;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public double toDouble() {
|
||||
return amountDecimal.doubleValue();
|
||||
}
|
||||
|
||||
public BigDecimal toBigDecimal() {
|
||||
return amountDecimal;
|
||||
}
|
||||
|
||||
public BigInteger toPicoMobBigInteger() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public long toPicoMobUint64() throws Uint64RangeException {
|
||||
return Uint64Util.bigIntegerToUInt64(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Formatter formatter) {
|
||||
return formatter.format(amountDecimal);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract boolean equals(Object o);
|
||||
|
||||
@Override
|
||||
public abstract int hashCode();
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return serialize();
|
||||
}
|
||||
|
||||
public abstract String toString(Formatter formatter);
|
||||
|
||||
public final String toString(FormatterOptions formatterOptions){
|
||||
Formatter formatter = getCurrency().getFormatter(formatterOptions);
|
||||
return toString(formatter);
|
||||
}
|
||||
|
||||
public static final class ParseException extends Exception {
|
||||
private ParseException() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import org.whispersystems.signalservice.api.util.Uint64RangeException;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
public final class MoneyProtobufUtil {
|
||||
|
||||
public static SignalServiceProtos.DataMessage.Payment.Amount moneyToPaymentAmount(Money money) {
|
||||
SignalServiceProtos.DataMessage.Payment.Amount.Builder builder = SignalServiceProtos.DataMessage.Payment.Amount.newBuilder();
|
||||
|
||||
if (money instanceof Money.MobileCoin) {
|
||||
try {
|
||||
builder.setMobileCoin(SignalServiceProtos.DataMessage.Payment.Amount.MobileCoin.newBuilder()
|
||||
.setPicoMob(money.requireMobileCoin()
|
||||
.toPicoMobUint64()));
|
||||
} catch (Uint64RangeException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static Money paymentAmountToMoney(SignalServiceProtos.DataMessage.Payment.Amount amount) throws UnsupportedCurrencyException {
|
||||
switch (amount.getAmountCase()) {
|
||||
case MOBILECOIN:
|
||||
return Money.picoMobileCoin(amount.getMobileCoin().getPicoMob());
|
||||
case AMOUNT_NOT_SET:
|
||||
default:
|
||||
throw new UnsupportedCurrencyException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
public final class PaymentsConstants {
|
||||
|
||||
private PaymentsConstants() {}
|
||||
|
||||
public static final int PAYMENTS_ENTROPY_LENGTH = 32;
|
||||
public static final int MNEMONIC_LENGTH = Math.round(PAYMENTS_ENTROPY_LENGTH * 0.75f);
|
||||
public static final int SHORT_FRACTION_LENGTH = 4;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
public final class UnsupportedCurrencyException extends Exception {
|
||||
}
|
||||
@@ -35,6 +35,9 @@ public class SignalServiceProfile {
|
||||
@JsonProperty
|
||||
private String aboutEmoji;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] paymentAddress;
|
||||
|
||||
@JsonProperty
|
||||
private String avatar;
|
||||
|
||||
@@ -76,6 +79,10 @@ public class SignalServiceProfile {
|
||||
return aboutEmoji;
|
||||
}
|
||||
|
||||
public byte[] getPaymentAddress() {
|
||||
return paymentAddress;
|
||||
}
|
||||
|
||||
public String getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ public class SignalServiceProfileWrite {
|
||||
@JsonProperty
|
||||
private byte[] aboutEmoji;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] paymentAddress;
|
||||
|
||||
@JsonProperty
|
||||
private boolean avatar;
|
||||
|
||||
@@ -27,13 +30,14 @@ public class SignalServiceProfileWrite {
|
||||
public SignalServiceProfileWrite(){
|
||||
}
|
||||
|
||||
public SignalServiceProfileWrite(String version, byte[] name, byte[] about, byte[] aboutEmoji, boolean avatar, byte[] commitment) {
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.about = about;
|
||||
this.aboutEmoji = aboutEmoji;
|
||||
this.avatar = avatar;
|
||||
this.commitment = commitment;
|
||||
public SignalServiceProfileWrite(String version, byte[] name, byte[] about, byte[] aboutEmoji, byte[] paymentAddress, boolean avatar, byte[] commitment) {
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.about = about;
|
||||
this.aboutEmoji = aboutEmoji;
|
||||
this.paymentAddress = paymentAddress;
|
||||
this.avatar = avatar;
|
||||
this.commitment = commitment;
|
||||
}
|
||||
|
||||
public boolean hasAvatar() {
|
||||
|
||||
@@ -2,7 +2,9 @@ package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.whispersystems.libsignal.logging.Log;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.payments.PaymentsConstants;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
import org.whispersystems.signalservice.api.util.ProtoUtil;
|
||||
@@ -25,6 +27,7 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||
private final Optional<byte[]> profileKey;
|
||||
private final List<PinnedConversation> pinnedConversations;
|
||||
private final boolean preferContactAvatars;
|
||||
private final Payments payments;
|
||||
|
||||
public SignalAccountRecord(StorageId id, AccountRecord proto) {
|
||||
this.id = id;
|
||||
@@ -37,6 +40,7 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||
this.avatarUrlPath = OptionalUtil.absentIfEmpty(proto.getAvatarUrlPath());
|
||||
this.pinnedConversations = new ArrayList<>(proto.getPinnedConversationsCount());
|
||||
this.preferContactAvatars = proto.getPreferContactAvatars();
|
||||
this.payments = new Payments(proto.getPayments().getEnabled(), OptionalUtil.absentIfEmpty(proto.getPayments().getEntropy()));
|
||||
|
||||
for (AccountRecord.PinnedConversation conversation : proto.getPinnedConversationsList()) {
|
||||
pinnedConversations.add(PinnedConversation.fromRemote(conversation));
|
||||
@@ -112,6 +116,10 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||
return preferContactAvatars;
|
||||
}
|
||||
|
||||
public Payments getPayments() {
|
||||
return payments;
|
||||
}
|
||||
|
||||
AccountRecord toProto() {
|
||||
return proto;
|
||||
}
|
||||
@@ -220,6 +228,31 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Payments {
|
||||
private static final String TAG = Payments.class.getSimpleName();
|
||||
|
||||
private final boolean enabled;
|
||||
private final Optional<byte[]> entropy;
|
||||
|
||||
public Payments(boolean enabled, Optional<byte[]> entropy) {
|
||||
byte[] entropyBytes = entropy.orNull();
|
||||
if (entropyBytes != null && entropyBytes.length != PaymentsConstants.PAYMENTS_ENTROPY_LENGTH) {
|
||||
Log.w(TAG, "Blocked entropy of length " + entropyBytes.length);
|
||||
entropyBytes = null;
|
||||
}
|
||||
this.entropy = Optional.fromNullable(entropyBytes);
|
||||
this.enabled = enabled && this.entropy.isPresent();
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getEntropy() {
|
||||
return entropy;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final StorageId id;
|
||||
private final AccountRecord.Builder builder;
|
||||
@@ -312,6 +345,22 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPayments(boolean enabled, byte[] entropy) {
|
||||
org.whispersystems.signalservice.internal.storage.protos.Payments.Builder paymentsBuilder = org.whispersystems.signalservice.internal.storage.protos.Payments.newBuilder();
|
||||
|
||||
boolean entropyPresent = entropy != null && entropy.length == PaymentsConstants.PAYMENTS_ENTROPY_LENGTH;
|
||||
|
||||
paymentsBuilder.setEnabled(enabled && entropyPresent);
|
||||
|
||||
if (entropyPresent) {
|
||||
paymentsBuilder.setEntropy(ByteString.copyFrom(entropy));
|
||||
}
|
||||
|
||||
builder.setPayments(paymentsBuilder);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalAccountRecord build() {
|
||||
AccountRecord proto = builder.build();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.whispersystems.signalservice.api.util;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -11,6 +12,16 @@ public final class OptionalUtil {
|
||||
private OptionalUtil() {
|
||||
}
|
||||
|
||||
public static <T, R> Optional<R> flatMap(Optional<T> input, Function<T, Optional<R>> flatMapFunction) {
|
||||
Optional<Optional<R>> output = input.transform(flatMapFunction);
|
||||
|
||||
if (output.isPresent()) {
|
||||
return output.get();
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean byteArrayEquals(Optional<byte[]> a, Optional<byte[]> b) {
|
||||
if (a.isPresent() != b.isPresent()) {
|
||||
return false;
|
||||
|
||||
@@ -120,8 +120,10 @@ public final class ProtoUtil {
|
||||
field.setAccessible(true);
|
||||
|
||||
GeneratedMessageLite inner = (GeneratedMessageLite) field.get(proto);
|
||||
innerProtos.add(inner);
|
||||
innerProtos.addAll(getInnerProtos(inner));
|
||||
if (inner != null) {
|
||||
innerProtos.add(inner);
|
||||
innerProtos.addAll(getInnerProtos(inner));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.whispersystems.signalservice.api.util;
|
||||
|
||||
public final class Uint64RangeException extends Exception {
|
||||
Uint64RangeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.whispersystems.signalservice.api.util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import static org.whispersystems.libsignal.util.ByteUtil.longToByteArray;
|
||||
|
||||
public final class Uint64Util {
|
||||
|
||||
private final static BigInteger MAX_UINT64 = uint64ToBigInteger(0xffffffffffffffffL);
|
||||
|
||||
private Uint64Util() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link BigInteger} of the supplied value, treating it as unsigned.
|
||||
*/
|
||||
public static BigInteger uint64ToBigInteger(long uint64) {
|
||||
if (uint64 < 0) {
|
||||
return new BigInteger(1, longToByteArray(uint64));
|
||||
} else {
|
||||
return BigInteger.valueOf(uint64);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a long to be treated as unsigned of the supplied {@link BigInteger}.
|
||||
*
|
||||
* @param value Must be in the range [0..2^64)
|
||||
* @throws Uint64RangeException iff value is outside of range.
|
||||
*/
|
||||
public static long bigIntegerToUInt64(BigInteger value) throws Uint64RangeException {
|
||||
if (value.signum() < 0) {
|
||||
throw new Uint64RangeException("BigInteger out of uint64 range (negative)");
|
||||
}
|
||||
|
||||
if (value.compareTo(MAX_UINT64) > 0) {
|
||||
throw new Uint64RangeException("BigInteger out of uint64 range (> MAX)");
|
||||
}
|
||||
|
||||
return value.longValue();
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,18 @@ public class AuthCredentials {
|
||||
@JsonProperty
|
||||
private String password;
|
||||
|
||||
public static AuthCredentials create(String username, String password) {
|
||||
AuthCredentials authCredentials = new AuthCredentials();
|
||||
authCredentials.username = username;
|
||||
authCredentials.password = password;
|
||||
return authCredentials;
|
||||
}
|
||||
|
||||
public String asBasic() {
|
||||
return Credentials.basic(username, password);
|
||||
}
|
||||
|
||||
public String username() { return username; }
|
||||
|
||||
public String password() { return password; }
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemo
|
||||
import org.whispersystems.signalservice.api.messages.calls.CallingResponse;
|
||||
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||
import org.whispersystems.signalservice.api.payments.CurrencyConversions;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
|
||||
@@ -88,6 +89,7 @@ import org.whispersystems.signalservice.internal.push.exceptions.GroupNotFoundEx
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.PaymentsRegionException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
||||
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
|
||||
@@ -131,7 +133,6 @@ import java.util.UUID;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
@@ -192,6 +193,8 @@ public class PushServiceSocket {
|
||||
private static final String ATTACHMENT_V2_PATH = "/v2/attachments/form/upload";
|
||||
private static final String ATTACHMENT_V3_PATH = "/v3/attachments/form/upload";
|
||||
|
||||
private static final String PAYMENTS_AUTH_PATH = "/v1/payments/auth";
|
||||
|
||||
private static final String PROFILE_PATH = "/v1/profile/%s";
|
||||
private static final String PROFILE_USERNAME_PATH = "/v1/profile/username/%s";
|
||||
|
||||
@@ -216,6 +219,8 @@ public class PushServiceSocket {
|
||||
private static final String GROUPSV2_GROUP_JOIN = "/v1/groups/join/%s";
|
||||
private static final String GROUPSV2_TOKEN = "/v1/groups/token";
|
||||
|
||||
private static final String PAYMENTS_CONVERSIONS = "/v1/payments/conversions";
|
||||
|
||||
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
|
||||
|
||||
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
|
||||
@@ -717,7 +722,12 @@ public class PushServiceSocket {
|
||||
String requestBody = JsonUtil.toJson(signalServiceProfileWrite);
|
||||
ProfileAvatarUploadAttributes formAttributes;
|
||||
|
||||
String response = makeServiceRequest(String.format(PROFILE_PATH, ""), "PUT", requestBody);
|
||||
String response = makeServiceRequest(String.format(PROFILE_PATH, ""),
|
||||
"PUT",
|
||||
requestBody,
|
||||
NO_HEADERS,
|
||||
PaymentsRegionException::responseCodeHandler,
|
||||
Optional.absent());
|
||||
|
||||
if (signalServiceProfileWrite.hasAvatar() && profileAvatar != null) {
|
||||
try {
|
||||
@@ -784,10 +794,14 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
private String getCredentials(String authPath) throws IOException {
|
||||
private AuthCredentials getAuthCredentials(String authPath) throws IOException {
|
||||
String response = makeServiceRequest(authPath, "GET", null, NO_HEADERS);
|
||||
AuthCredentials token = JsonUtil.fromJson(response, AuthCredentials.class);
|
||||
return token.asBasic();
|
||||
return token;
|
||||
}
|
||||
|
||||
private String getCredentials(String authPath) throws IOException {
|
||||
return getAuthCredentials(authPath).asBasic();
|
||||
}
|
||||
|
||||
public String getContactDiscoveryAuthorization() throws IOException {
|
||||
@@ -798,6 +812,10 @@ public class PushServiceSocket {
|
||||
return getCredentials(KBS_AUTH_PATH);
|
||||
}
|
||||
|
||||
public AuthCredentials getPaymentsAuthorization() throws IOException {
|
||||
return getAuthCredentials(PAYMENTS_AUTH_PATH);
|
||||
}
|
||||
|
||||
public TokenResponse getKeyBackupServiceToken(String authorizationToken, String enclaveName)
|
||||
throws IOException
|
||||
{
|
||||
@@ -2133,6 +2151,18 @@ public class PushServiceSocket {
|
||||
return GroupExternalCredential.parseFrom(readBodyBytes(response));
|
||||
}
|
||||
|
||||
public CurrencyConversions getCurrencyConversions()
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
|
||||
{
|
||||
String response = makeServiceRequest(PAYMENTS_CONVERSIONS, "GET", null);
|
||||
try {
|
||||
return JsonUtil.fromJson(response, CurrencyConversions.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new MalformedResponseException("Unable to parse entity", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GroupHistory {
|
||||
private final GroupChanges groupChanges;
|
||||
private final Optional<ContentRange> contentRange;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.whispersystems.signalservice.internal.push.exceptions;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
public final class PaymentsRegionException extends NonSuccessfulResponseCodeException {
|
||||
public PaymentsRegionException(int code) {
|
||||
super(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Promotes a 403 to this exception type.
|
||||
*/
|
||||
public static void responseCodeHandler(int responseCode) throws PaymentsRegionException {
|
||||
if (responseCode == 403) {
|
||||
throw new PaymentsRegionException(responseCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,6 +238,45 @@ message DataMessage {
|
||||
optional string eraId = 1;
|
||||
}
|
||||
|
||||
message Payment {
|
||||
|
||||
message Address {
|
||||
message MobileCoin {
|
||||
optional bytes address = 1;
|
||||
}
|
||||
|
||||
oneof Address {
|
||||
MobileCoin mobileCoin = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message Amount {
|
||||
message MobileCoin {
|
||||
optional uint64 picoMob = 1;
|
||||
}
|
||||
|
||||
oneof Amount {
|
||||
MobileCoin mobileCoin = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message Notification {
|
||||
message MobileCoin {
|
||||
optional bytes receipt = 1;
|
||||
}
|
||||
|
||||
oneof Transaction {
|
||||
MobileCoin mobileCoin = 1;
|
||||
}
|
||||
|
||||
optional string note = 2;
|
||||
}
|
||||
|
||||
oneof Item {
|
||||
Notification notification = 1;
|
||||
}
|
||||
}
|
||||
|
||||
enum ProtocolVersion {
|
||||
option allow_alias = true;
|
||||
|
||||
@@ -248,7 +287,8 @@ message DataMessage {
|
||||
REACTIONS = 4;
|
||||
CDN_SELECTOR_ATTACHMENTS = 5;
|
||||
MENTIONS = 6;
|
||||
CURRENT = 6;
|
||||
PAYMENTS = 7;
|
||||
CURRENT = 7;
|
||||
}
|
||||
|
||||
optional string body = 1;
|
||||
@@ -269,7 +309,7 @@ message DataMessage {
|
||||
optional Delete delete = 17;
|
||||
repeated BodyRange bodyRanges = 18;
|
||||
optional GroupCallUpdate groupCallUpdate = 19;
|
||||
reserved /* future use */ 20;
|
||||
optional Payment payment = 20;
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
@@ -418,6 +458,28 @@ message SyncMessage {
|
||||
optional Type type = 4;
|
||||
}
|
||||
|
||||
message OutgoingPayment {
|
||||
message MobileCoin {
|
||||
optional bytes recipientAddress = 1;
|
||||
// @required
|
||||
optional uint64 amountPicoMob = 2;
|
||||
// @required
|
||||
optional uint64 feePicoMob = 3;
|
||||
optional bytes receipt = 4;
|
||||
optional uint64 ledgerBlockTimestamp = 5;
|
||||
// @required
|
||||
optional uint64 ledgerBlockIndex = 6;
|
||||
repeated bytes spentKeyImages = 7;
|
||||
repeated bytes outputPublicKeys = 8;
|
||||
}
|
||||
optional string recipientUuid = 1;
|
||||
optional string note = 2;
|
||||
|
||||
oneof paymentDetail {
|
||||
MobileCoin mobileCoin = 3;
|
||||
}
|
||||
}
|
||||
|
||||
optional Sent sent = 1;
|
||||
optional Contacts contacts = 2;
|
||||
optional Groups groups = 3;
|
||||
@@ -432,6 +494,7 @@ message SyncMessage {
|
||||
optional FetchLatest fetchLatest = 12;
|
||||
optional Keys keys = 13;
|
||||
optional MessageRequestResponse messageRequestResponse = 14;
|
||||
optional OutgoingPayment outgoingPayment = 15;
|
||||
}
|
||||
|
||||
message AttachmentPointer {
|
||||
@@ -530,3 +593,14 @@ message GroupDetails {
|
||||
optional uint32 inboxPosition = 10;
|
||||
optional bool archived = 11;
|
||||
}
|
||||
|
||||
message PaymentAddress {
|
||||
oneof Address {
|
||||
MobileCoinAddress mobileCoinAddress = 1;
|
||||
}
|
||||
|
||||
message MobileCoinAddress {
|
||||
optional bytes address = 1;
|
||||
optional bytes signature = 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,11 @@ message GroupV2Record {
|
||||
bool markedUnread = 5;
|
||||
}
|
||||
|
||||
message Payments {
|
||||
bool enabled = 1;
|
||||
bytes entropy = 2;
|
||||
}
|
||||
|
||||
message AccountRecord {
|
||||
|
||||
enum PhoneNumberSharingMode {
|
||||
@@ -135,4 +140,5 @@ message AccountRecord {
|
||||
bool unlistedPhoneNumber = 13;
|
||||
repeated PinnedConversation pinnedConversations = 14;
|
||||
bool preferContactAvatars = 15;
|
||||
Payments payments = 16;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Security;
|
||||
|
||||
public class ProfileCipherTest extends TestCase {
|
||||
@@ -23,18 +22,18 @@ public class ProfileCipherTest extends TestCase {
|
||||
public void testEncryptDecrypt() throws InvalidCiphertextException, InvalidInputException {
|
||||
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
|
||||
ProfileCipher cipher = new ProfileCipher(key);
|
||||
byte[] name = cipher.encryptName("Clement\0Duval".getBytes(), 53);
|
||||
byte[] plaintext = cipher.decryptName(name);
|
||||
assertEquals(new String(plaintext), "Clement\0Duval");
|
||||
byte[] name = cipher.encrypt("Clement\0Duval".getBytes(), 53);
|
||||
String plaintext = cipher.decryptString(name);
|
||||
assertEquals(plaintext, "Clement\0Duval");
|
||||
}
|
||||
|
||||
public void testEmpty() throws Exception {
|
||||
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
|
||||
ProfileCipher cipher = new ProfileCipher(key);
|
||||
byte[] name = cipher.encryptName("".getBytes(), 26);
|
||||
byte[] plaintext = cipher.decryptName(name);
|
||||
byte[] name = cipher.encrypt("".getBytes(), 26);
|
||||
String plaintext = cipher.decryptString(name);
|
||||
|
||||
assertEquals(plaintext.length, 0);
|
||||
assertEquals(plaintext.length(), 0);
|
||||
}
|
||||
|
||||
public void testStreams() throws Exception {
|
||||
@@ -64,7 +63,7 @@ public class ProfileCipherTest extends TestCase {
|
||||
public void testEncryptLengthBucket1() throws InvalidInputException {
|
||||
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
|
||||
ProfileCipher cipher = new ProfileCipher(key);
|
||||
byte[] name = cipher.encryptName("Peter\0Parker".getBytes(), 53);
|
||||
byte[] name = cipher.encrypt("Peter\0Parker".getBytes(), 53);
|
||||
|
||||
String encoded = Base64.encodeBytes(name);
|
||||
|
||||
@@ -74,7 +73,7 @@ public class ProfileCipherTest extends TestCase {
|
||||
public void testEncryptLengthBucket2() throws InvalidInputException {
|
||||
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
|
||||
ProfileCipher cipher = new ProfileCipher(key);
|
||||
byte[] name = cipher.encryptName("Peter\0Parker".getBytes(), 257);
|
||||
byte[] name = cipher.encrypt("Peter\0Parker".getBytes(), 257);
|
||||
|
||||
String encoded = Base64.encodeBytes(name);
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Currency;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class FiatFormatterTest {
|
||||
|
||||
private static final Currency javaCurrency = Currency.getInstance("USD");
|
||||
|
||||
@Test
|
||||
public void givenAFiatCurrency_whenIFormatWithDefaultOptions_thenIExpectADefaultFormattedString() {
|
||||
// GIVEN
|
||||
Formatter formatter = Formatter.forFiat(javaCurrency, FormatterOptions.defaults(Locale.US));
|
||||
|
||||
// WHEN
|
||||
String result = formatter.format(BigDecimal.valueOf(100));
|
||||
|
||||
// THEN
|
||||
assertEquals("$100.00", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenANegative_whenIFormatWithAlwaysPositive_thenIExpectPositive() {
|
||||
// GIVEN
|
||||
FormatterOptions options = FormatterOptions.builder(Locale.US).alwaysPositive().build();
|
||||
Formatter formatter = Formatter.forFiat(javaCurrency, options);
|
||||
|
||||
// WHEN
|
||||
String result = formatter.format(BigDecimal.valueOf(-100L));
|
||||
|
||||
// THEN
|
||||
assertEquals("$100.00", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenALargeFiatCurrency_whenIFormatWithDefaultOptions_thenIExpectGrouping() {
|
||||
// GIVEN
|
||||
Formatter formatter = Formatter.forFiat(javaCurrency, FormatterOptions.defaults(Locale.US));
|
||||
|
||||
// WHEN
|
||||
String result = formatter.format(BigDecimal.valueOf(1000L));
|
||||
|
||||
// THEN
|
||||
assertEquals("$1,000.00", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAFiatCurrency_whenIFormatWithoutUnit_thenIExpectAStringWithoutUnit() {
|
||||
// GIVEN
|
||||
Formatter formatter = Formatter.forFiat(javaCurrency, FormatterOptions.builder(Locale.US).withoutUnit().build());
|
||||
|
||||
// WHEN
|
||||
String result = formatter.format(BigDecimal.ONE);
|
||||
|
||||
// THEN
|
||||
assertEquals("1.00", result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class MobileCoinFormatterTest {
|
||||
|
||||
private static final Currency currency = Money.MobileCoin.CURRENCY;
|
||||
|
||||
@Test
|
||||
public void givenAMoneyCurrency_whenIFormatWithDefaultOptions_thenIExpectADefaultFormattedString() {
|
||||
// GIVEN
|
||||
Formatter formatter = Formatter.forMoney(currency, FormatterOptions.defaults(Locale.US));
|
||||
|
||||
// WHEN
|
||||
String result = formatter.format(BigDecimal.ONE);
|
||||
|
||||
// THEN
|
||||
assertEquals("1 MOB", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenALargeMoneyCurrency_whenIFormatWithDefaultOptions_thenIExpectGrouping() {
|
||||
// GIVEN
|
||||
Formatter formatter = Formatter.forMoney(currency, FormatterOptions.defaults(Locale.US));
|
||||
|
||||
// WHEN
|
||||
String result = formatter.format(BigDecimal.valueOf(-1000L));
|
||||
|
||||
// THEN
|
||||
assertEquals("-1,000 MOB", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenANegative_whenIFormatWithAlwaysPositive_thenIExpectPositive() {
|
||||
// GIVEN
|
||||
FormatterOptions options = FormatterOptions.builder(Locale.US).alwaysPositive().build();
|
||||
Formatter formatter = Formatter.forMoney(currency, options);
|
||||
|
||||
// WHEN
|
||||
String result = formatter.format(BigDecimal.valueOf(-100L));
|
||||
|
||||
// THEN
|
||||
assertEquals("100 MOB", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAnAmount_whenIFormatWithoutSpaceBeforeUnit_thenIExpectNoSpaceBeforeUnit() {
|
||||
// GIVEN
|
||||
FormatterOptions options = FormatterOptions.builder(Locale.US).withoutSpaceBeforeUnit().build();
|
||||
Formatter formatter = Formatter.forMoney(currency, options);
|
||||
|
||||
// WHEN
|
||||
String result = formatter.format(BigDecimal.valueOf(100L));
|
||||
|
||||
// THEN
|
||||
assertEquals("100MOB", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAnAmount_whenIFormatWithoutUnit_thenIExpectNoSpaceBeforeUnit() {
|
||||
// GIVEN
|
||||
FormatterOptions options = FormatterOptions.builder(Locale.US).withoutUnit().build();
|
||||
Formatter formatter = Formatter.forMoney(currency, options);
|
||||
|
||||
// WHEN
|
||||
String result = formatter.format(BigDecimal.valueOf(100L));
|
||||
|
||||
// THEN
|
||||
assertEquals("100", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAnAmount_whenIFormatWithAlwaysPrefixSign_thenIExpectSignOnPositiveValues() {
|
||||
// GIVEN
|
||||
FormatterOptions options = FormatterOptions.builder(Locale.US).alwaysPrefixWithSign().build();
|
||||
Formatter formatter = Formatter.forMoney(currency, options);
|
||||
|
||||
// WHEN
|
||||
String result = formatter.format(BigDecimal.valueOf(100L));
|
||||
|
||||
// THEN
|
||||
assertEquals("+100 MOB", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAMoneyCurrency_whenIToStringWithDefaultOptions_thenIExpectADefaultFormattedString() {
|
||||
// GIVEN
|
||||
FormatterOptions options = FormatterOptions.defaults(Locale.US);
|
||||
|
||||
// WHEN
|
||||
String result = Money.mobileCoin(BigDecimal.ONE).toString(options);
|
||||
|
||||
// THEN
|
||||
assertEquals("1 MOB", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAnAmount_whenIToStringWithAlwaysPrefixSign_thenIExpectSignOnPositiveValues() {
|
||||
// GIVEN
|
||||
FormatterOptions options = FormatterOptions.builder(Locale.US).alwaysPrefixWithSign().build();
|
||||
|
||||
// WHEN
|
||||
String result = Money.mobileCoin(BigDecimal.ONE).toString(options);
|
||||
|
||||
// THEN
|
||||
assertEquals("+1 MOB", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAnAmount_whenIToStringWithMaximumFractionalDigitsOf4_thenIExpectRoundingAndTruncating() {
|
||||
// GIVEN
|
||||
FormatterOptions options = FormatterOptions.builder(Locale.US).withMaximumFractionDigits(4).build();
|
||||
|
||||
// WHEN
|
||||
String result = Money.mobileCoin(BigDecimal.valueOf(1.1234567)).toString(options);
|
||||
|
||||
// THEN
|
||||
assertEquals("1.1235 MOB", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAnAmount_whenIToStringWithMaximumFractionalDigitsOf5_thenIExpectToSee5Places() {
|
||||
// GIVEN
|
||||
FormatterOptions options = FormatterOptions.builder(Locale.US).withMaximumFractionDigits(5).build();
|
||||
|
||||
// WHEN
|
||||
String result = Money.mobileCoin(BigDecimal.valueOf(1.1234507)).toString(options);
|
||||
|
||||
// THEN
|
||||
assertEquals("1.12345 MOB", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAnAmount_whenIToStringWithMaximumFractionalDigitsOf5ButFewerActual_thenIExpectToSeeFewerPlaces() {
|
||||
// GIVEN
|
||||
FormatterOptions options = FormatterOptions.builder(Locale.US).withMaximumFractionDigits(5).build();
|
||||
|
||||
// WHEN
|
||||
String result = Money.mobileCoin(BigDecimal.valueOf(1.120003)).toString(options);
|
||||
|
||||
// THEN
|
||||
assertEquals("1.12 MOB", result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.signalservice.api.util.Uint64RangeException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class MoneyTest_MobileCoin {
|
||||
|
||||
@Test
|
||||
public void create_zero() {
|
||||
Money mobileCoin = Money.mobileCoin(BigDecimal.ZERO);
|
||||
|
||||
assertFalse(mobileCoin.isPositive());
|
||||
assertFalse(mobileCoin.isNegative());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_positive() {
|
||||
Money mobileCoin = Money.mobileCoin(BigDecimal.ONE);
|
||||
|
||||
assertTrue(mobileCoin.isPositive());
|
||||
assertFalse(mobileCoin.isNegative());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_negative() {
|
||||
Money mobileCoin = Money.mobileCoin(BigDecimal.ONE.negate());
|
||||
|
||||
assertFalse(mobileCoin.isPositive());
|
||||
assertTrue(mobileCoin.isNegative());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toString_format() {
|
||||
Money.MobileCoin mobileCoin = Money.mobileCoin(BigDecimal.valueOf(-1000.32456));
|
||||
|
||||
assertEquals("MOB:-1000324560000000", mobileCoin.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toAmountString_format() {
|
||||
Money.MobileCoin mobileCoin = Money.mobileCoin(BigDecimal.valueOf(-1000.32456));
|
||||
|
||||
assertEquals("-1000.32456", mobileCoin.getAmountDecimalString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void currency() {
|
||||
Money mobileCoin = Money.mobileCoin(BigDecimal.valueOf(-1000.32456));
|
||||
|
||||
assertEquals("MOB", mobileCoin.getCurrency().getCurrencyCode());
|
||||
assertEquals(12, mobileCoin.getCurrency().getDecimalPrecision());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equality() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money mobileCoin10 = Money.mobileCoin(BigDecimal.ONE);
|
||||
|
||||
assertEquals(mobileCoin1, mobileCoin10);
|
||||
assertEquals(mobileCoin1.hashCode(), mobileCoin10.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inequality() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money mobileCoin10 = Money.mobileCoin(BigDecimal.TEN);
|
||||
|
||||
assertNotEquals(mobileCoin1, mobileCoin10);
|
||||
assertNotEquals(mobileCoin1.hashCode(), mobileCoin10.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negate() {
|
||||
Money money1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money moneyNegative1 = Money.mobileCoin(BigDecimal.ONE.negate());
|
||||
Money negated = money1.negate();
|
||||
|
||||
assertEquals(moneyNegative1, negated);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void abs() {
|
||||
Money money0 = Money.mobileCoin(BigDecimal.ZERO);
|
||||
Money money1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money moneyNegative1 = Money.mobileCoin(BigDecimal.ONE.negate());
|
||||
Money absOfZero = money0.abs();
|
||||
Money absOfPositive = money1.abs();
|
||||
Money absOfNegative = moneyNegative1.abs();
|
||||
|
||||
assertSame(money0, absOfZero);
|
||||
assertSame(money1, absOfPositive);
|
||||
assertEquals(money1, absOfNegative);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void require_cast() {
|
||||
Money money = Money.mobileCoin(BigDecimal.ONE.negate());
|
||||
Money.MobileCoin mobileCoin = money.requireMobileCoin();
|
||||
|
||||
assertSame(money, mobileCoin);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serialize_negative() {
|
||||
Money.MobileCoin mobileCoin = Money.mobileCoin(BigDecimal.valueOf(-1000.32456));
|
||||
|
||||
assertEquals("MOB:-1000324560000000", mobileCoin.serialize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse_negative() throws Money.ParseException {
|
||||
Money original = Money.mobileCoin(BigDecimal.valueOf(-1000.32456));
|
||||
String serialized = original.serialize();
|
||||
|
||||
Money deserialized = Money.parse(serialized);
|
||||
|
||||
assertEquals(original, deserialized);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseOrThrow() {
|
||||
Money original = Money.mobileCoin(BigDecimal.valueOf(-123.6323));
|
||||
String serialized = original.serialize();
|
||||
|
||||
Money deserialized = Money.parseOrThrow(serialized);
|
||||
|
||||
assertEquals(original, deserialized);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parse_zero() {
|
||||
Money value = Money.parseOrThrow("MOB:0000000000000000");
|
||||
|
||||
assertSame(Money.MobileCoin.ZERO, value);
|
||||
}
|
||||
|
||||
@Test(expected = Money.ParseException.class)
|
||||
public void parse_fail_empty() throws Money.ParseException {
|
||||
Money.parse("");
|
||||
}
|
||||
|
||||
@Test(expected = Money.ParseException.class)
|
||||
public void parse_fail_null() throws Money.ParseException {
|
||||
Money.parse(null);
|
||||
}
|
||||
|
||||
@Test(expected = Money.ParseException.class)
|
||||
public void parse_fail_unknown_currency() throws Money.ParseException {
|
||||
Money.parse("XYZ:123");
|
||||
}
|
||||
|
||||
@Test(expected = Money.ParseException.class)
|
||||
public void parse_fail_no_value() throws Money.ParseException {
|
||||
Money.parse("MOB");
|
||||
}
|
||||
|
||||
@Test(expected = Money.ParseException.class)
|
||||
public void parse_fail_too_many_parts() throws Money.ParseException {
|
||||
Money.parse("MOB:1:2");
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void parseOrThrowOrThrow_fail_empty() {
|
||||
Money.parseOrThrow("");
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void parseOrThrowOrThrow_fail_null() {
|
||||
Money.parseOrThrow(null);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void parseOrThrowOrThrow_fail_unknown_currency() {
|
||||
Money.parseOrThrow("XYZ:123");
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void parseOrThrowOrThrow_fail_no_value() {
|
||||
Money.parseOrThrow("MOB");
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void parseOrThrowOrThrow_fail_too_many_parts() {
|
||||
Money.parseOrThrow("MOB:1:2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void from_big_integer_picoMobileCoin() {
|
||||
Money.MobileCoin mobileCoin1 = Money.mobileCoin(new BigDecimal("352324.325232123456"));
|
||||
Money.MobileCoin mobileCoin2 = Money.picoMobileCoin(BigInteger.valueOf(352324325232123456L));
|
||||
|
||||
assertEquals(mobileCoin1, mobileCoin2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void from_big_integer_picoMobileCoin_zero() {
|
||||
Money.MobileCoin mobileCoin = Money.picoMobileCoin(BigInteger.ZERO);
|
||||
|
||||
assertSame(Money.MobileCoin.ZERO, mobileCoin);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void from_very_large_big_integer_picoMobileCoin() {
|
||||
Money.MobileCoin mobileCoin1 = Money.mobileCoin(new BigDecimal("352324.325232123456"));
|
||||
Money.MobileCoin mobileCoin2 = Money.picoMobileCoin(BigInteger.valueOf(352324325232123456L));
|
||||
|
||||
assertEquals(mobileCoin1, mobileCoin2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void to_picoMob_bigInteger() {
|
||||
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.valueOf(21324.325232));
|
||||
|
||||
BigInteger bigInteger = mobileCoin1.toPicoMobBigInteger();
|
||||
|
||||
assertEquals(BigInteger.valueOf(21324325232000000L), bigInteger);
|
||||
}
|
||||
|
||||
@Test(expected = ArithmeticException.class)
|
||||
public void precision_loss_on_creation() {
|
||||
Money.mobileCoin(new BigDecimal("10376293.0000000000001"));
|
||||
}
|
||||
|
||||
@Test(expected = ArithmeticException.class)
|
||||
public void precision_loss_on_creation_negative() {
|
||||
Money.mobileCoin(new BigDecimal("-10376293.0000000000001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void from_picoMob() {
|
||||
Money.MobileCoin mobileCoin1 = Money.picoMobileCoin(1234567890987654321L);
|
||||
|
||||
assertEquals("1234567.890987654321", mobileCoin1.getAmountDecimalString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void to_picoMob() throws Uint64RangeException {
|
||||
Money.MobileCoin mobileCoin1 = Money.picoMobileCoin(1234567890987654321L);
|
||||
|
||||
assertEquals(1234567890987654321L, mobileCoin1.toPicoMobUint64());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void from_large_picoMob() {
|
||||
Money.MobileCoin mobileCoin1 = Money.picoMobileCoin(0x9000000000000000L);
|
||||
|
||||
assertEquals("10376293.541461622784", mobileCoin1.getAmountDecimalString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void from_large_negative() {
|
||||
Money.MobileCoin mobileCoin1 = Money.mobileCoin(new BigDecimal("-1234567.890987654321"));
|
||||
|
||||
assertEquals("-1234567.890987654321", mobileCoin1.getAmountDecimalString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void to_large_picoMob() throws Uint64RangeException {
|
||||
Money.MobileCoin mobileCoin1 = Money.picoMobileCoin(0x9000000000000000L);
|
||||
|
||||
assertEquals(0x9000000000000000L, mobileCoin1.toPicoMobUint64());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void from_maximum_picoMob() {
|
||||
Money.MobileCoin mobileCoin = Money.picoMobileCoin(0xffffffffffffffffL);
|
||||
|
||||
assertEquals("18446744073709551615", mobileCoin.serializeAmountString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void from_maximum_picoMob_and_back() throws Uint64RangeException {
|
||||
Money.MobileCoin mobileCoin = Money.picoMobileCoin(0xffffffffffffffffL);
|
||||
|
||||
assertEquals(0xffffffffffffffffL, mobileCoin.toPicoMobUint64());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void large_mobile_coin_value_exceeding_64_bits() {
|
||||
Money.MobileCoin mobileCoin = Money.mobileCoin(new BigDecimal("18446744.073709551616"));
|
||||
|
||||
assertEquals("18446744073709551616", mobileCoin.serializeAmountString());
|
||||
}
|
||||
|
||||
@Test(expected = Uint64RangeException.class)
|
||||
public void large_mobile_coin_value_exceeding_64_bits_toPicoMobUint64_failure() throws Uint64RangeException {
|
||||
Money.MobileCoin mobileCoin = Money.mobileCoin(new BigDecimal("18446744.073709551616"));
|
||||
|
||||
mobileCoin.toPicoMobUint64();
|
||||
}
|
||||
|
||||
@Test(expected = Uint64RangeException.class)
|
||||
public void negative_to_pico_mob_uint64() throws Uint64RangeException {
|
||||
Money.MobileCoin mobileCoin1 = Money.mobileCoin(new BigDecimal("-1"));
|
||||
|
||||
mobileCoin1.toPicoMobUint64();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void greater_than() {
|
||||
assertTrue(mobileCoin2(2).greaterThan(mobileCoin2(1)));
|
||||
assertTrue(mobileCoin2(-1).greaterThan(mobileCoin2(-2)));
|
||||
assertFalse(mobileCoin2(2).greaterThan(mobileCoin2(2)));
|
||||
assertFalse(mobileCoin2(1).greaterThan(mobileCoin2(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void less_than() {
|
||||
assertTrue(mobileCoin2(1).lessThan(mobileCoin2(2)));
|
||||
assertTrue(mobileCoin2(-2).lessThan(mobileCoin2(-1)));
|
||||
assertFalse(mobileCoin2(2).lessThan(mobileCoin2(2)));
|
||||
assertFalse(mobileCoin2(2).lessThan(mobileCoin2(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zero_constant() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ZERO);
|
||||
Money mobileCoin2 = Money.MobileCoin.ZERO;
|
||||
|
||||
assertEquals(mobileCoin1, mobileCoin2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void to_zero() {
|
||||
Money mobileCoin = Money.mobileCoin(BigDecimal.ONE);
|
||||
|
||||
assertSame(Money.MobileCoin.ZERO, mobileCoin.toZero());
|
||||
}
|
||||
|
||||
private static Money.MobileCoin mobileCoin2(double value) {
|
||||
return Money.mobileCoin(BigDecimal.valueOf(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.signalservice.api.util.Uint64RangeException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class MoneyTest_MobileCoin_add {
|
||||
|
||||
@Test
|
||||
public void add_0() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ZERO);
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ZERO);
|
||||
|
||||
Money sum = mobileCoin1.add(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.ZERO), sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void add_1_rhs() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ZERO);
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
|
||||
|
||||
Money sum = mobileCoin1.add(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.ONE), sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void add_1_lhs() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ZERO);
|
||||
|
||||
Money sum = mobileCoin1.add(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.ONE), sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void add_2() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
|
||||
|
||||
Money sum = mobileCoin1.add(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.valueOf(2)), sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void add_fraction() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(2.2));
|
||||
|
||||
Money sum = mobileCoin1.add(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.valueOf(3.2)), sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void add_negative_fraction() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.valueOf(-5.2));
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
|
||||
|
||||
Money sum = mobileCoin1.add(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.valueOf(-4.2)), sum);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public final class MoneyTest_MobileCoin_comparators {
|
||||
|
||||
@Test
|
||||
public void sort_ascending() {
|
||||
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money.MobileCoin mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(2));
|
||||
List<Money.MobileCoin> list = asList(mobileCoin2, mobileCoin1);
|
||||
list.sort(Money.MobileCoin.ASCENDING);
|
||||
|
||||
assertEquals(asList(mobileCoin1, mobileCoin2), list);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sort_descending() {
|
||||
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money.MobileCoin mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(2));
|
||||
List<Money.MobileCoin> list = asList(mobileCoin1, mobileCoin2);
|
||||
list.sort(Money.MobileCoin.DESCENDING);
|
||||
|
||||
assertEquals(asList(mobileCoin2, mobileCoin1), list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public final class MoneyTest_MobileCoin_subtract {
|
||||
|
||||
@Test
|
||||
public void subtract_0() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ZERO);
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ZERO);
|
||||
|
||||
Money sum = mobileCoin1.subtract(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.ZERO), sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subtract_1_rhs() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ZERO);
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
|
||||
|
||||
Money sum = mobileCoin1.subtract(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.ONE.negate()), sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subtract_1_lhs() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ZERO);
|
||||
|
||||
Money sum = mobileCoin1.subtract(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.ONE), sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subtract_2() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
|
||||
|
||||
Money sum = mobileCoin1.subtract(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.ZERO), sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subtract_fraction() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.valueOf(2.2));
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.ONE);
|
||||
|
||||
Money sum = mobileCoin1.subtract(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.valueOf(1.2)), sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subtract_negative_fraction() {
|
||||
Money mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(-5.2));
|
||||
|
||||
Money sum = mobileCoin1.subtract(mobileCoin2);
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.valueOf(6.2)), sum);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.whispersystems.signalservice.api.payments;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public final class MoneyTest_MobileCoin_sum {
|
||||
|
||||
@Test
|
||||
public void sum_empty_list() {
|
||||
Money sum = Money.MobileCoin.sum(emptyList());
|
||||
|
||||
assertSame(Money.MobileCoin.ZERO, sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sum_1() {
|
||||
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
|
||||
Money sum = Money.MobileCoin.sum(singletonList(mobileCoin1));
|
||||
|
||||
assertSame(mobileCoin1, sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sum_2() {
|
||||
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money.MobileCoin mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(2));
|
||||
|
||||
Money sum = Money.MobileCoin.sum(asList(mobileCoin1, mobileCoin2));
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.valueOf(3)), sum);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sum_negatives() {
|
||||
Money.MobileCoin mobileCoin1 = Money.mobileCoin(BigDecimal.ONE);
|
||||
Money.MobileCoin mobileCoin2 = Money.mobileCoin(BigDecimal.valueOf(-2));
|
||||
|
||||
Money sum = Money.MobileCoin.sum(asList(mobileCoin1, mobileCoin2));
|
||||
|
||||
assertEquals(Money.mobileCoin(BigDecimal.valueOf(-1)), sum);
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,18 @@ public class ProtoUtilTest {
|
||||
assertTrue(ProtoUtil.hasUnknownFields(personWithUnknowns));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hasUnknownFields_nullInnerMessage() throws InvalidProtocolBufferException {
|
||||
TestPersonWithNewMessage person = TestPersonWithNewMessage.newBuilder()
|
||||
.setName("Peter Parker")
|
||||
.setAge(23)
|
||||
.build();
|
||||
|
||||
TestPerson personWithUnknowns = TestPerson.parseFrom(person.toByteArray());
|
||||
|
||||
assertFalse(ProtoUtil.hasUnknownFields(personWithUnknowns));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void combineWithUnknownFields_noUnknowns() throws InvalidProtocolBufferException {
|
||||
TestPerson personWithUnknowns = TestPerson.newBuilder()
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.whispersystems.signalservice.api.util;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.whispersystems.signalservice.api.util.Uint64Util.uint64ToBigInteger;
|
||||
import static org.whispersystems.signalservice.api.util.Uint64Util.bigIntegerToUInt64;
|
||||
|
||||
public final class Uint64UtilTest {
|
||||
|
||||
@Test
|
||||
public void long_zero_to_bigInteger() {
|
||||
BigInteger bigInteger = uint64ToBigInteger(0);
|
||||
|
||||
assertEquals("0", bigInteger.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void long_to_bigInteger() {
|
||||
BigInteger bigInteger = uint64ToBigInteger(12345L);
|
||||
|
||||
assertEquals("12345", bigInteger.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bigInteger_zero_to_long() throws Uint64RangeException {
|
||||
long uint64 = bigIntegerToUInt64(BigInteger.ZERO);
|
||||
|
||||
assertEquals(0, uint64);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void first_uint64_value_to_bigInteger() {
|
||||
BigInteger bigInteger = uint64ToBigInteger(0x8000000000000000L);
|
||||
|
||||
assertEquals("9223372036854775808", bigInteger.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bigInteger_to_first_uint64_value() throws Uint64RangeException {
|
||||
long uint64 = bigIntegerToUInt64(new BigInteger("9223372036854775808"));
|
||||
|
||||
assertEquals(0x8000000000000000L, uint64);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void large_uint64_value_to_bigInteger() {
|
||||
BigInteger bigInteger = uint64ToBigInteger(0xa523f21e412c14d2L);
|
||||
|
||||
assertEquals("11899620852199331026", bigInteger.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bigInteger_to_large_uint64_value() throws Uint64RangeException {
|
||||
long uint64 = bigIntegerToUInt64(new BigInteger("11899620852199331026"));
|
||||
|
||||
assertEquals(0xa523f21e412c14d2L, uint64);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void largest_uint64_value_to_bigInteger() {
|
||||
BigInteger bigInteger = uint64ToBigInteger(0xffffffffffffffffL);
|
||||
|
||||
assertEquals("18446744073709551615", bigInteger.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bigInteger_to_largest_uint64_value() throws Uint64RangeException {
|
||||
long uint64 = bigIntegerToUInt64(new BigInteger("18446744073709551615"));
|
||||
|
||||
assertEquals(0xffffffffffffffffL, uint64);
|
||||
}
|
||||
|
||||
@Test(expected = Uint64RangeException.class)
|
||||
public void too_big_by_one() throws Uint64RangeException {
|
||||
bigIntegerToUInt64(new BigInteger("18446744073709551616"));
|
||||
}
|
||||
|
||||
@Test(expected = Uint64RangeException.class)
|
||||
public void too_small_by_one() throws Uint64RangeException {
|
||||
bigIntegerToUInt64(new BigInteger("-1"));
|
||||
}
|
||||
|
||||
@Test(expected = Uint64RangeException.class)
|
||||
public void too_big_by_a_lot() throws Uint64RangeException {
|
||||
bigIntegerToUInt64(new BigInteger("1844674407370955161623"));
|
||||
}
|
||||
|
||||
@Test(expected = Uint64RangeException.class)
|
||||
public void too_small_by_a_lot() throws Uint64RangeException {
|
||||
bigIntegerToUInt64(new BigInteger("-1844674407370955161623"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user