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:
Android Team
2021-04-06 12:59:37 -03:00
committed by Alan Evans
parent dd38dd9cae
commit c42023855b
38 changed files with 2366 additions and 236 deletions

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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() {
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
package org.whispersystems.signalservice.api.payments;
public final class UnsupportedCurrencyException extends Exception {
}

View File

@@ -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;
}

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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;

View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,7 @@
package org.whispersystems.signalservice.api.util;
public final class Uint64RangeException extends Exception {
Uint64RangeException(String message) {
super(message);
}
}

View File

@@ -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();
}
}

View File

@@ -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; }
}

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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()

View File

@@ -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"));
}
}