| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| /** | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.bitcoin.protocols.payments.recurring; | ||
|
|
||
| import com.google.bitcoin.store.RecurringPaymentProtobufSerializer; | ||
| import com.google.common.base.Function; | ||
| import com.google.common.base.Predicate; | ||
| import com.google.common.collect.Iterables; | ||
| import com.google.common.collect.Lists; | ||
| import com.google.common.io.Files; | ||
| import org.bitcoin.protocols.payments.Protos; | ||
|
|
||
| import java.io.File; | ||
| import java.io.FileInputStream; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.math.BigInteger; | ||
| import java.util.List; | ||
|
|
||
| public class Subscriptions { | ||
|
|
||
| private static String SUBSCRIPTIONS_FILE_NAME = "subscriptions"; | ||
|
|
||
| private File walletDirectory; | ||
| private RecurringPaymentProtobufSerializer serializer; | ||
| private List<org.bitcoin.protocols.subscriptions.Protos.Subscription> subscriptions; | ||
|
|
||
| public static void storeContract(Protos.PaymentDetails paymentDetails, File walletDirectory) throws IOException { | ||
| File f = new File(walletDirectory, SUBSCRIPTIONS_FILE_NAME); | ||
| f.createNewFile(); | ||
|
|
||
| RecurringPaymentProtobufSerializer recurringPaymentProtobufSerializer = new RecurringPaymentProtobufSerializer(); | ||
| recurringPaymentProtobufSerializer.storeContract(paymentDetails, f); | ||
| } | ||
|
|
||
| public Subscriptions(File walletDirectory) { | ||
| this.walletDirectory = walletDirectory; | ||
| this.serializer = new RecurringPaymentProtobufSerializer(); | ||
| } | ||
|
|
||
| public void load() throws IOException { | ||
| File f = new File(walletDirectory, SUBSCRIPTIONS_FILE_NAME); | ||
| Files.touch(f); | ||
|
|
||
| InputStream in = new FileInputStream(f); | ||
| try { | ||
| this.subscriptions = serializer.loadSubscriptions(in); | ||
| } finally { | ||
| in.close(); | ||
| } | ||
| } | ||
|
|
||
| public List<Protos.PaymentDetails> getAllContracts() { | ||
| return Lists.transform(subscriptions, new Function<org.bitcoin.protocols.subscriptions.Protos.Subscription, Protos.PaymentDetails>() { | ||
| @Override | ||
| public Protos.PaymentDetails apply(org.bitcoin.protocols.subscriptions.Protos.Subscription input) { | ||
| return input.getContract(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| public BigInteger getPaidAmountForPeriod(Protos.PaymentDetails contract) { | ||
| final String key = serializer.getUniqueKeyForContract(contract); | ||
| org.bitcoin.protocols.subscriptions.Protos.Subscription subscription = Iterables.tryFind(subscriptions, new Predicate<org.bitcoin.protocols.subscriptions.Protos.Subscription>() { | ||
| @Override | ||
| public boolean apply(org.bitcoin.protocols.subscriptions.Protos.Subscription input) { | ||
| return key.equals(serializer.getUniqueKeyForContract(input.getContract())); | ||
| } | ||
| }).get(); | ||
|
|
||
| long paidAmountForPeriod = 0; | ||
| for (Protos.PaymentDetails pastPaymentForPeriod : subscription.getPaymentsForPeriodList()) { | ||
| for (Protos.Output output : pastPaymentForPeriod.getOutputsList()) { | ||
| paidAmountForPeriod += output.getAmount(); | ||
| } | ||
| } | ||
|
|
||
| return BigInteger.valueOf(paidAmountForPeriod); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| /** | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.bitcoin.store; | ||
|
|
||
| import com.google.common.annotations.VisibleForTesting; | ||
| import com.google.common.collect.Lists; | ||
| import org.bitcoin.protocols.subscriptions.Protos; | ||
|
|
||
| import java.io.File; | ||
| import java.io.FileInputStream; | ||
| import java.io.FileOutputStream; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.io.OutputStream; | ||
| import java.util.List; | ||
|
|
||
| public class RecurringPaymentProtobufSerializer { | ||
|
|
||
| // New subscription | ||
| public void storeContract(org.bitcoin.protocols.payments.Protos.PaymentDetails contract, File subscriptionsFile) throws IOException { | ||
| // We are recreating the list of recurring payments in a temp file that we will atomically move upon completion | ||
| File temp = File.createTempFile(subscriptionsFile.getName(), ".tmp", subscriptionsFile.getAbsoluteFile().getParentFile()); | ||
|
|
||
| final FileInputStream input = new FileInputStream(subscriptionsFile); | ||
| List<Protos.Subscription> allSubscriptions = loadSubscriptions(input); | ||
|
|
||
| List<Protos.Subscription> allUpdatedPaymentDetails = Lists.newLinkedList(); | ||
| Protos.Subscription newSubscriptionForContract = Protos.Subscription.newBuilder().setContract(contract).build(); | ||
| allUpdatedPaymentDetails.add(newSubscriptionForContract); | ||
|
|
||
| String keyForNewContract = getUniqueKeyForContract(contract); | ||
| for (Protos.Subscription subscription : allSubscriptions) { | ||
| String key = getUniqueKeyForContract(subscription.getContract()); | ||
|
|
||
| // Consider only subscriptions for contracts other than the one we're adding or updating | ||
| if (!key.equals(keyForNewContract)) { | ||
| allUpdatedPaymentDetails.add(subscription); | ||
| } | ||
| } | ||
|
|
||
| final FileOutputStream output = new FileOutputStream(temp); | ||
| try { | ||
| writePaymentDetails(allUpdatedPaymentDetails, output); | ||
|
|
||
| // Final rename | ||
| if (!temp.renameTo(subscriptionsFile)) { | ||
| throw new IOException("Final rename failed"); | ||
| } | ||
| } finally { | ||
| try { | ||
| input.close(); | ||
| } finally { | ||
| output.close(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public List<Protos.Subscription> loadSubscriptions(InputStream input) throws IOException { | ||
| List<Protos.Subscription> result = Lists.newLinkedList(); | ||
| while (input.available() > 0) { | ||
| result.add(Protos.Subscription.parseDelimitedFrom(input)); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| public String getUniqueKeyForContract(org.bitcoin.protocols.payments.Protos.PaymentDetails contract) { | ||
| return contract.getPaymentUrl() + contract.getTime(); | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| void writePaymentDetails(List<Protos.Subscription> subscriptions, OutputStream output) throws IOException { | ||
| for (Protos.Subscription subscription : subscriptions) { | ||
| subscription.writeDelimitedTo(output); | ||
| } | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /** | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| // Used only to store state | ||
|
|
||
| package payments; | ||
|
|
||
| option java_package = "org.bitcoin.protocols.subscriptions"; | ||
| option java_outer_classname = "Protos"; | ||
|
|
||
| import "paymentrequest.proto"; | ||
|
|
||
| message Subscription { | ||
| required PaymentDetails contract = 1; | ||
| repeated PaymentDetails payments_for_period = 2; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| /** | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.bitcoin.store; | ||
|
|
||
| import com.google.bitcoin.core.ECKey; | ||
| import com.google.bitcoin.core.NetworkParameters; | ||
| import com.google.bitcoin.core.Transaction; | ||
| import com.google.bitcoin.core.TransactionOutput; | ||
| import com.google.bitcoin.core.Utils; | ||
| import com.google.bitcoin.params.TestNet3Params; | ||
| import com.google.bitcoin.protocols.payments.recurring.Subscriptions; | ||
| import com.google.common.collect.ImmutableList; | ||
| import com.google.common.collect.Lists; | ||
| import com.google.protobuf.ByteString; | ||
| import org.bitcoin.protocols.payments.Protos; | ||
| import org.junit.Assert; | ||
| import org.junit.Before; | ||
| import org.junit.Test; | ||
|
|
||
| import java.io.ByteArrayInputStream; | ||
| import java.io.ByteArrayOutputStream; | ||
| import java.math.BigInteger; | ||
| import java.util.List; | ||
|
|
||
| public class RecurringPaymentProtobufSerializerTest { | ||
|
|
||
| private static final NetworkParameters params = TestNet3Params.get(); | ||
| private static final String simplePaymentUrl = "http://a.simple.url.com/"; | ||
| private static final String paymentRequestMemo = "send coinz noa plz kthx"; | ||
| private static final ByteString merchantData = ByteString.copyFromUtf8("merchant data"); | ||
| private static final long time = System.currentTimeMillis() / 1000L; | ||
| private ECKey serverKey; | ||
| private Transaction tx; | ||
| private TransactionOutput outputToMe; | ||
| BigInteger nanoCoins = Utils.toNanoCoins(1, 0); | ||
|
|
||
| @Before | ||
| public void setUp() throws Exception { | ||
| serverKey = new ECKey(); | ||
| tx = new Transaction(params); | ||
| outputToMe = new TransactionOutput(params, tx, nanoCoins, serverKey); | ||
| tx.addOutput(outputToMe); | ||
| } | ||
|
|
||
| @Test | ||
| public void testOnePaymentDetails() throws Exception { | ||
|
|
||
| Protos.Output.Builder outputBuilder = Protos.Output.newBuilder() | ||
| .setAmount(nanoCoins.longValue()) | ||
| .setScript(ByteString.copyFrom(outputToMe.getScriptBytes())); | ||
| Protos.RecurringPaymentDetails recurringPaymentDetails = Protos.RecurringPaymentDetails.newBuilder() | ||
| .setPollingUrl(simplePaymentUrl) | ||
| .setMaxPaymentPerPeriod(1000) | ||
| .setPaymentFrequencyType(Protos.PaymentFrequencyType.ANNUAL) | ||
| .build(); | ||
| Protos.PaymentDetails paymentDetails = Protos.PaymentDetails.newBuilder() | ||
| .setNetwork("test") | ||
| .setTime(time - 10) | ||
| .setExpires(time - 1) | ||
| .setPaymentUrl(simplePaymentUrl) | ||
| .addOutputs(outputBuilder) | ||
| .setMemo(paymentRequestMemo) | ||
| .setMerchantData(merchantData) | ||
| .setSerializedRecurringPaymentDetails(recurringPaymentDetails.toByteString()) | ||
| .build(); | ||
|
|
||
| RecurringPaymentProtobufSerializer serializer = new RecurringPaymentProtobufSerializer(); | ||
| ByteArrayOutputStream output = new ByteArrayOutputStream(); | ||
|
|
||
| org.bitcoin.protocols.subscriptions.Protos.Subscription subscription = org.bitcoin.protocols.subscriptions.Protos.Subscription.newBuilder() | ||
| .setContract(paymentDetails) | ||
| .build(); | ||
| serializer.writePaymentDetails(ImmutableList.of(subscription), output); | ||
|
|
||
| List<org.bitcoin.protocols.subscriptions.Protos.Subscription> result = serializer.loadSubscriptions(new ByteArrayInputStream(output.toByteArray())); | ||
| Assert.assertEquals(1, result.size()); | ||
| Assert.assertEquals(result.get(0).getContract(), paymentDetails); | ||
| Assert.assertEquals(0, result.get(0).getPaymentsForPeriodList().size()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testMultiplePaymentDetails() throws Exception { | ||
| List<org.bitcoin.protocols.subscriptions.Protos.Subscription> allSubscriptions = Lists.newArrayList(); | ||
| for (int i = 0 ; i < 12; i++) { | ||
| Protos.Output.Builder outputBuilder = Protos.Output.newBuilder() | ||
| .setAmount(nanoCoins.longValue()) | ||
| .setScript(ByteString.copyFrom(outputToMe.getScriptBytes())); | ||
| Protos.RecurringPaymentDetails recurringPaymentDetails = Protos.RecurringPaymentDetails.newBuilder() | ||
| .setPollingUrl(simplePaymentUrl) | ||
| .setMaxPaymentPerPeriod(1000) | ||
| .setPaymentFrequencyType(Protos.PaymentFrequencyType.ANNUAL) | ||
| .build(); | ||
| Protos.PaymentDetails paymentDetails = Protos.PaymentDetails.newBuilder() | ||
| .setNetwork("test") | ||
| .setTime(time) | ||
| .setExpires(time - i) | ||
| .setPaymentUrl(simplePaymentUrl) | ||
| .addOutputs(outputBuilder) | ||
| .setMemo(paymentRequestMemo) | ||
| .setMerchantData(merchantData) | ||
| .setSerializedRecurringPaymentDetails(recurringPaymentDetails.toByteString()) | ||
| .build(); | ||
|
|
||
| org.bitcoin.protocols.subscriptions.Protos.Subscription subscription = org.bitcoin.protocols.subscriptions.Protos.Subscription.newBuilder() | ||
| .setContract(paymentDetails) | ||
| .build(); | ||
| allSubscriptions.add(subscription); | ||
| } | ||
|
|
||
| RecurringPaymentProtobufSerializer serializer = new RecurringPaymentProtobufSerializer(); | ||
| ByteArrayOutputStream output = new ByteArrayOutputStream(); | ||
| serializer.writePaymentDetails(allSubscriptions, output); | ||
|
|
||
| List<org.bitcoin.protocols.subscriptions.Protos.Subscription> result = serializer.loadSubscriptions(new ByteArrayInputStream(output.toByteArray())); | ||
| Assert.assertEquals(12, result.size()); | ||
| for (int i = 0; i < 12; i++) { | ||
| Assert.assertEquals(time - i, result.get(i).getContract().getExpires()); | ||
| Assert.assertEquals(0, result.get(i).getPaymentsForPeriodList().size()); | ||
| } | ||
| } | ||
| } |