Skip to content

Commit 9618b62

Browse files
authored
Merge pull request #17 from twostack/txbuilder-api
TransactionBuilder Signature API Completion
2 parents be70d9b + 9ce257d commit 9618b62

File tree

4 files changed

+243
-8
lines changed

4 files changed

+243
-8
lines changed

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
#Release 1.6.1
2+
### TransactionBuilder Signature API Completion
3+
4+
Version 1.6.0 introduces the new API for passing a
5+
TransactionSigner to the TransactionBuilder.spendFromTransaction()
6+
method.
7+
8+
This update completes the shape of that API by doing the same for :
9+
* TransactionBuilder.spendFromOutpoint()
10+
* TransactionBuilder.spendFromOutput()
11+
* TransactionBuilder.spendFromUtxoMap()
12+
113
#Release 1.6.0
214
### TransactionBuilder Signature generation refactor
315

build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
group 'org.twostack'
9-
version '1.6.0'
9+
version '1.6.1'
1010

1111
repositories {
1212
mavenCentral()

src/main/java/org/twostack/bitcoin4j/transaction/TransactionBuilder.java

+88-2
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,52 @@ public TransactionOutpoint getOutpoint() {
9090
}
9191
}
9292

93-
/*
93+
94+
/**
95+
utxoMap is expected to have :
96+
97+
{
98+
"transactionId" : [String],
99+
"satoshis", [BigInteger],
100+
"sequenceNumber", [long],
101+
"outputIndex", [int],
102+
"scriptPubKey", [String]
103+
}
104+
*/
105+
public TransactionBuilder spendFromUtxoMap(TransactionSigner signer, Map<String, Object> utxoMap, @Nullable UnlockingScriptBuilder unlocker){
106+
107+
String transactionId = (String) utxoMap.get("transactionId");
108+
109+
int outputIndex = (int ) utxoMap.get("outputIndex");
110+
long sequenceNumber = (long) utxoMap.get("sequenceNumber");
111+
112+
TransactionOutpoint outpoint = new TransactionOutpoint();
113+
outpoint.setOutputIndex(outputIndex);
114+
outpoint.setLockingScript(Script.fromAsmString((String)utxoMap.get("scriptPubKey")));
115+
outpoint.setSatoshis((BigInteger)utxoMap.get("satoshis"));
116+
outpoint.setTransactionId(transactionId);
117+
118+
this.signerMap.put(transactionId, new SignerDto(signer, outpoint));
119+
120+
if (unlocker == null){
121+
unlocker = new DefaultUnlockBuilder();
122+
}
123+
124+
TransactionInput input = new TransactionInput(
125+
HEX.decode((String)utxoMap.get("transactionId")),
126+
outputIndex,
127+
sequenceNumber,
128+
unlocker
129+
);
130+
131+
spendingMap.put((String) utxoMap.get("transactionId"), (BigInteger) utxoMap.get("satoshis"));
132+
133+
inputs.add(input);
134+
135+
return this;
136+
}
137+
138+
/**
94139
utxoMap is expected to have :
95140
96141
{
@@ -172,7 +217,9 @@ public TransactionBuilder spendFromTransaction(Transaction txn, int outputIndex,
172217

173218
}
174219

175-
public TransactionBuilder spendFromOutpoint(TransactionOutpoint outpoint, long sequenceNumber, UnlockingScriptBuilder unlocker) {
220+
public TransactionBuilder spendFromOutpoint(TransactionSigner signer, TransactionOutpoint outpoint, long sequenceNumber, UnlockingScriptBuilder unlocker) {
221+
222+
this.signerMap.put(outpoint.getTransactionId(), new SignerDto(signer, outpoint));
176223

177224
TransactionInput input = new TransactionInput(
178225
HEX.decode(outpoint.getTransactionId()),
@@ -188,6 +235,21 @@ public TransactionBuilder spendFromOutpoint(TransactionOutpoint outpoint, long s
188235
}
189236

190237

238+
public TransactionBuilder spendFromOutpoint(TransactionOutpoint outpoint, long sequenceNumber, UnlockingScriptBuilder unlocker) {
239+
240+
TransactionInput input = new TransactionInput(
241+
HEX.decode(outpoint.getTransactionId()),
242+
outpoint.getOutputIndex(),
243+
sequenceNumber,
244+
unlocker
245+
);
246+
247+
spendingMap.put(outpoint.getTransactionId(), outpoint.getSatoshis());
248+
249+
inputs.add(input);
250+
return this;
251+
}
252+
191253
public TransactionBuilder spendFromOutput(String utxoTxnId, int outputIndex, BigInteger amount, long sequenceNumber, UnlockingScriptBuilder unlocker) {
192254

193255
TransactionInput input = new TransactionInput(
@@ -203,6 +265,30 @@ public TransactionBuilder spendFromOutput(String utxoTxnId, int outputIndex, Big
203265
return this;
204266
}
205267

268+
269+
public TransactionBuilder spendFromOutput(TransactionSigner signer, String utxoTxnId, int outputIndex, BigInteger amount, long sequenceNumber, UnlockingScriptBuilder unlocker) {
270+
271+
TransactionOutpoint outpoint = new TransactionOutpoint();
272+
outpoint.setOutputIndex(outputIndex);
273+
outpoint.setLockingScript(unlocker.getUnlockingScript());
274+
outpoint.setSatoshis(amount);
275+
outpoint.setTransactionId(utxoTxnId);
276+
277+
this.signerMap.put(utxoTxnId, new SignerDto(signer, outpoint));
278+
279+
TransactionInput input = new TransactionInput(
280+
HEX.decode(utxoTxnId),
281+
outputIndex,
282+
sequenceNumber,
283+
unlocker
284+
);
285+
286+
spendingMap.put(utxoTxnId, amount);
287+
288+
inputs.add(input);
289+
return this;
290+
}
291+
206292
public TransactionBuilder spendTo(LockingScriptBuilder locker, BigInteger satoshis) throws TransactionException{
207293

208294
int satoshiCompare = satoshis.compareTo(BigInteger.ZERO);

src/test/java/org/twostack/bitcoin4j/transaction/TransactionBuilderTest.java

+142-5
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,12 @@
1212
import org.twostack.bitcoin4j.exception.SignatureDecodeException;
1313
import org.twostack.bitcoin4j.exception.TransactionException;
1414
import org.twostack.bitcoin4j.params.NetworkAddressType;
15-
import org.twostack.bitcoin4j.script.Script;
16-
import org.twostack.bitcoin4j.script.ScriptError;
1715

1816
import java.io.IOException;
1917
import java.io.InputStreamReader;
2018
import java.math.BigInteger;
2119
import java.nio.charset.StandardCharsets;
22-
import java.util.Set;
23-
24-
import static org.twostack.bitcoin4j.utils.TestUtil.parseVerifyFlags;
20+
import java.util.HashMap;
2521

2622
public class TransactionBuilderTest {
2723

@@ -67,4 +63,145 @@ public void processAndSignMultiInput() throws IOException, InvalidKeyException,
6763

6864
}
6965

66+
@Test
67+
public void builderCanSpendFromOutput() throws InvalidKeyException, IOException {
68+
69+
//This WIF is for a private key that actually has testnet coins on TESTNET
70+
//The transactions in multi_input.json are UTXOs that exist(ed) on TESTNET
71+
// at time of writing this test, and can be viewed on TESTNET using a block explorer
72+
String wif = "cRTUuWgPdp7tJPrn1Xeq196eZa4ZCpg8n3cgDJsJmgDHBZ8x9fpv";
73+
PrivateKey privateKey = PrivateKey.fromWIF(wif);
74+
75+
JsonNode json = new ObjectMapper().readTree(
76+
new InputStreamReader(getClass().getResourceAsStream("multi_input.json"),
77+
StandardCharsets.UTF_8)
78+
);
79+
80+
//build one large transaction that spends all the inputs
81+
TransactionBuilder builder = new TransactionBuilder();
82+
for (JsonNode utxoInfo : json) {
83+
84+
Integer fundingOutputIndex = utxoInfo.get("tx_pos").asInt();
85+
String rawTxHex = utxoInfo.get("raw_tx").asText();
86+
BigInteger amount = BigInteger.valueOf(utxoInfo.get("value").asInt());
87+
88+
Transaction fundingTx = Transaction.fromHex(rawTxHex);
89+
UnlockingScriptBuilder unlocker = new P2PKHUnlockBuilder(privateKey.getPublicKey());
90+
TransactionSigner signer = new TransactionSigner(SigHashType.ALL.value | SigHashType.FORKID.value, privateKey);
91+
92+
builder.spendFromOutput(signer, fundingTx.getTransactionId(), fundingOutputIndex, amount, TransactionInput.MAX_SEQ_NUMBER, unlocker);
93+
94+
}
95+
96+
Address recipientAddress = Address.fromKey(NetworkAddressType.TEST_PKH, privateKey.getPublicKey());
97+
98+
99+
Assertions.assertThatCode(() -> {
100+
Transaction broadcastTx = builder.withFeePerKb(512)
101+
.spendTo(new P2PKHLockBuilder(recipientAddress), BigInteger.valueOf(100000))
102+
.sendChangeTo(recipientAddress)
103+
.build(true);
104+
}).doesNotThrowAnyException();
105+
106+
}
107+
108+
109+
@Test
110+
public void builderCanSpendFromOutpoint() throws InvalidKeyException, IOException {
111+
112+
//This WIF is for a private key that actually has testnet coins on TESTNET
113+
//The transactions in multi_input.json are UTXOs that exist(ed) on TESTNET
114+
// at time of writing this test, and can be viewed on TESTNET using a block explorer
115+
String wif = "cRTUuWgPdp7tJPrn1Xeq196eZa4ZCpg8n3cgDJsJmgDHBZ8x9fpv";
116+
PrivateKey privateKey = PrivateKey.fromWIF(wif);
117+
118+
JsonNode json = new ObjectMapper().readTree(
119+
new InputStreamReader(getClass().getResourceAsStream("multi_input.json"),
120+
StandardCharsets.UTF_8)
121+
);
122+
123+
//build one large transaction that spends all the inputs
124+
TransactionBuilder builder = new TransactionBuilder();
125+
for (JsonNode utxoInfo : json) {
126+
127+
Integer fundingOutputIndex = utxoInfo.get("tx_pos").asInt();
128+
String rawTxHex = utxoInfo.get("raw_tx").asText();
129+
BigInteger amount = BigInteger.valueOf(utxoInfo.get("value").asInt());
130+
131+
Transaction fundingTx = Transaction.fromHex(rawTxHex);
132+
133+
TransactionOutpoint outpoint = new TransactionOutpoint();
134+
outpoint.setTransactionId(fundingTx.getTransactionId());
135+
outpoint.setSatoshis(amount);
136+
outpoint.setOutputIndex(fundingOutputIndex);
137+
outpoint.setLockingScript(fundingTx.getOutputs().get(fundingOutputIndex).getScript());
138+
139+
UnlockingScriptBuilder unlocker = new P2PKHUnlockBuilder(privateKey.getPublicKey());
140+
141+
TransactionSigner signer = new TransactionSigner(SigHashType.ALL.value | SigHashType.FORKID.value, privateKey);
142+
143+
builder.spendFromOutpoint(signer, outpoint, TransactionInput.MAX_SEQ_NUMBER, unlocker);
144+
145+
}
146+
147+
Address recipientAddress = Address.fromKey(NetworkAddressType.TEST_PKH, privateKey.getPublicKey());
148+
149+
150+
Assertions.assertThatCode(() -> {
151+
Transaction broadcastTx = builder.withFeePerKb(512)
152+
.spendTo(new P2PKHLockBuilder(recipientAddress), BigInteger.valueOf(100000))
153+
.sendChangeTo(recipientAddress)
154+
.build(true);
155+
}).doesNotThrowAnyException();
156+
}
157+
158+
@Test
159+
public void builderCanSpendFromUtxoMap() throws InvalidKeyException, IOException {
160+
161+
162+
//This WIF is for a private key that actually has testnet coins on TESTNET
163+
//The transactions in multi_input.json are UTXOs that exist(ed) on TESTNET
164+
// at time of writing this test, and can be viewed on TESTNET using a block explorer
165+
String wif = "cRTUuWgPdp7tJPrn1Xeq196eZa4ZCpg8n3cgDJsJmgDHBZ8x9fpv";
166+
PrivateKey privateKey = PrivateKey.fromWIF(wif);
167+
168+
JsonNode json = new ObjectMapper().readTree(
169+
new InputStreamReader(getClass().getResourceAsStream("multi_input.json"),
170+
StandardCharsets.UTF_8)
171+
);
172+
173+
//build one large transaction that spends all the inputs
174+
TransactionBuilder builder = new TransactionBuilder();
175+
for (JsonNode utxoInfo : json) {
176+
177+
Integer fundingOutputIndex = utxoInfo.get("tx_pos").asInt();
178+
String rawTxHex = utxoInfo.get("raw_tx").asText();
179+
BigInteger amount = BigInteger.valueOf(utxoInfo.get("value").asInt());
180+
181+
Transaction fundingTx = Transaction.fromHex(rawTxHex);
182+
183+
HashMap utxoMap = new HashMap();
184+
utxoMap.put("transactionId", fundingTx.getTransactionId());
185+
utxoMap.put("satoshis", amount);
186+
utxoMap.put("sequenceNumber", TransactionInput.MAX_SEQ_NUMBER);
187+
utxoMap.put("outputIndex", fundingOutputIndex);
188+
utxoMap.put("scriptPubKey", Utils.HEX.encode(fundingTx.getOutputs().get(fundingOutputIndex).serialize()));
189+
190+
UnlockingScriptBuilder unlocker = new P2PKHUnlockBuilder(privateKey.getPublicKey());
191+
192+
TransactionSigner signer = new TransactionSigner(SigHashType.ALL.value | SigHashType.FORKID.value, privateKey);
193+
builder.spendFromUtxoMap(signer, utxoMap, unlocker);
194+
195+
}
196+
197+
Address recipientAddress = Address.fromKey(NetworkAddressType.TEST_PKH, privateKey.getPublicKey());
198+
199+
200+
Assertions.assertThatCode(() -> {
201+
Transaction broadcastTx = builder.withFeePerKb(512)
202+
.spendTo(new P2PKHLockBuilder(recipientAddress), BigInteger.valueOf(100000))
203+
.sendChangeTo(recipientAddress)
204+
.build(true);
205+
}).doesNotThrowAnyException();
206+
}
70207
}

0 commit comments

Comments
 (0)